diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 542242adc6..b7c0aa5c42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,8 +34,8 @@ springboot2 = "2.7.18" springboot3 = "3.5.0" springboot4 = "4.0.0" # Android -targetSdk = "34" -compileSdk = "34" +targetSdk = "36" +compileSdk = "36" minSdk = "21" spotless = "7.0.4" gummyBears = "0.12.0" @@ -80,7 +80,9 @@ androidx-annotation = { module = "androidx.annotation:annotation", version = "1. androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.8.2" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidxCompose" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "androidxCompose" } -androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.4.0" } +androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version="1.7.8" } +androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version="1.7.8" } androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" } # Note: don't change without testing forwards compatibility androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.5.0" } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java index 196c9f3220..43ed3422cd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AppComponentsBreadcrumbsIntegration.java @@ -98,6 +98,7 @@ public void onConfigurationChanged(@NotNull Configuration newConfig) { executeInBackground(() -> captureConfigurationChangedBreadcrumb(now, newConfig)); } + @SuppressWarnings("deprecation") @Override public void onLowMemory() { // we do this in onTrimMemory below already, this is legacy API (14 or below) diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index c2ea72485c..2360350a03 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -142,6 +142,8 @@ dependencies { implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation.layout) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.core) + implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.recyclerview) implementation(libs.androidx.browser) diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index beca2363f8..102ae34a06 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -1,207 +1,257 @@ + xmlns:tools="http://schemas.android.com/tools"> - - + + - + - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - + android:name=".MyApplication" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:networkSecurityConfig="@xml/network" + android:roundIcon="@mipmap/ic_launcher_round" + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning, UnusedAttribute"> + + + android:name=".MainActivity" + android:exported="true" + android:theme="@style/AppTheme.Main"> + + - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + - - + - - + - - + - - + - - + - - + - - + - + + + + + + + - - - + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java deleted file mode 100644 index 1d47a3f27f..0000000000 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ /dev/null @@ -1,397 +0,0 @@ -package io.sentry.samples.android; - -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.os.Bundle; -import android.os.Handler; -import android.widget.Toast; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import io.sentry.Attachment; -import io.sentry.ISpan; -import io.sentry.MeasurementUnit; -import io.sentry.Sentry; -import io.sentry.SentryLogLevel; -import io.sentry.UpdateStatus; -import io.sentry.instrumentation.file.SentryFileOutputStream; -import io.sentry.protocol.Feedback; -import io.sentry.protocol.User; -import io.sentry.samples.android.compose.ComposeActivity; -import io.sentry.samples.android.databinding.ActivityMainBinding; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import timber.log.Timber; - -public class MainActivity extends AppCompatActivity { - - private int crashCount = 0; - private int screenLoadCount = 0; - - final Object mutex = new Object(); - - @Override - @SuppressWarnings("deprecation") - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - SharedState.INSTANCE.setOrientationChange( - getIntent().getBooleanExtra("isOrientationChange", false)); - final ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); - - final File imageFile = getApplicationContext().getFileStreamPath("sentry.png"); - try (final InputStream inputStream = - getApplicationContext().getResources().openRawResource(R.raw.sentry); - FileOutputStream outputStream = new FileOutputStream(imageFile)) { - final byte[] bytes = new byte[1024]; - while (inputStream.read(bytes) != -1) { - // To keep the sample code simple this happens on the main thread. Don't do this in a - // real app. - outputStream.write(bytes); - } - outputStream.flush(); - } catch (IOException e) { - Sentry.captureException(e); - } - - final Attachment image = new Attachment(imageFile.getAbsolutePath(), "sentry.png", "image/png"); - Sentry.configureScope( - scope -> { - scope.addAttachment(image); - }); - - binding.crashFromJava.setOnClickListener( - view -> { - throw new RuntimeException("Uncaught Exception from Java."); - }); - - binding.sendMessage.setOnClickListener(view -> Sentry.captureMessage("Some message.")); - - binding.sendUserFeedback.setOnClickListener( - view -> { - Feedback feedback = - new Feedback("It broke on Android. I don't know why, but this happens."); - feedback.setContactEmail("john@me.com"); - feedback.setName("John Me"); - Sentry.captureFeedback(feedback); - }); - - binding.addAttachment.setOnClickListener( - view -> { - String fileName = Calendar.getInstance().getTimeInMillis() + "_file.txt"; - File file = getApplication().getFileStreamPath(fileName); - try (final FileOutputStream fos = - SentryFileOutputStream.Factory.create(new FileOutputStream(file), file)) { - FileChannel channel = fos.getChannel(); - channel.write(java.nio.ByteBuffer.wrap("Hello, World!".getBytes())); - } catch (IOException e) { - Sentry.captureException(e); - } - - Sentry.configureScope( - scope -> { - String json = "{ \"number\": 10 }"; - Attachment attachment = new Attachment(json.getBytes(), "log.json"); - scope.addAttachment(attachment); - scope.addAttachment(new Attachment(file.getPath())); - }); - }); - - binding.captureException.setOnClickListener( - view -> - Sentry.captureException( - new Exception(new Exception(new Exception("Some exception."))))); - - binding.breadcrumb.setOnClickListener( - view -> { - Sentry.addBreadcrumb("Breadcrumb"); - Sentry.setExtra("extra", "extra"); - Sentry.setFingerprint(Collections.singletonList("fingerprint")); - Sentry.setTransaction("transaction"); - Sentry.captureException(new Exception("Some exception with scope.")); - }); - - binding.unsetUser.setOnClickListener( - view -> { - Sentry.setTag("user_set", "null"); - Sentry.setUser(null); - }); - - binding.setUser.setOnClickListener( - view -> { - Sentry.setTag("user_set", "instance"); - User user = new User(); - user.setUsername("username_from_java"); - // works with some null properties? - // user.setId("id_from_java"); - user.setEmail("email_from_java"); - // Use the client's IP address - user.setIpAddress("{{auto}}"); - Sentry.setUser(user); - }); - - binding.outOfMemory.setOnClickListener( - view -> { - final CountDownLatch latch = new CountDownLatch(1); - for (int i = 0; i < 20; i++) { - new Thread( - () -> { - final List data = new ArrayList<>(); - try { - latch.await(); - for (int j = 0; j < 1_000_000; j++) { - data.add(new String(new byte[1024 * 8])); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - }) - .start(); - } - - latch.countDown(); - }); - - binding.stackOverflow.setOnClickListener(view -> stackOverflow()); - - binding.nativeCrash.setOnClickListener(view -> NativeSample.crash()); - - binding.nativeCapture.setOnClickListener(view -> NativeSample.message()); - - binding.anr.setOnClickListener( - view -> { - // Try cause ANR by blocking for 10 seconds. - // By default the SDK sends an event if blocked by at least 5 seconds. - // Keep clicking on the ANR button till you've gotten the "App. isn''t responding" dialog, - // then either click on Wait or Close, at this point you should have seen an event on - // Sentry. - // NOTE: By default it doesn't raise if the debugger is attached. That can also be - // configured. - new Thread( - new Runnable() { - @Override - public void run() { - synchronized (mutex) { - while (true) { - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - } - }) - .start(); - - new Handler() - .postDelayed( - new Runnable() { - @Override - public void run() { - synchronized (mutex) { - // Shouldn't happen - throw new IllegalStateException(); - } - } - }, - 1000); - }); - - binding.nativeAnr.setOnClickListener( - view -> { - new Thread( - new Runnable() { - @Override - public void run() { - NativeSample.freezeMysteriously(mutex); - } - }) - .start(); - - new Handler() - .postDelayed( - new Runnable() { - @Override - public void run() { - synchronized (mutex) { - // Shouldn't happen - throw new IllegalStateException(); - } - } - }, - 1000); - }); - - binding.openSecondActivity.setOnClickListener( - view -> { - // finishing so its completely destroyed - finish(); - startActivity(new Intent(this, SecondActivity.class)); - }); - - binding.openSampleFragment.setOnClickListener( - view -> SampleFragment.newInstance().show(getSupportFragmentManager(), null)); - - binding.openThirdFragment.setOnClickListener( - view -> startActivity(new Intent(this, ThirdActivityFragment.class))); - - binding.openGesturesActivity.setOnClickListener( - view -> startActivity(new Intent(this, GesturesActivity.class))); - - binding.testTimberIntegration.setOnClickListener( - view -> { - crashCount++; - Timber.i("Some info here"); - Timber.e( - new RuntimeException("Uncaught Exception from Java."), - "Something wrong happened %d times", - crashCount); - }); - - binding.openPermissionsActivity.setOnClickListener( - view -> { - startActivity(new Intent(this, PermissionsActivity.class)); - }); - - binding.openComposeActivity.setOnClickListener( - view -> { - startActivity(new Intent(this, ComposeActivity.class)); - }); - - binding.openProfilingActivity.setOnClickListener( - view -> { - startActivity(new Intent(this, ProfilingActivity.class)); - }); - - binding.openCustomTabsActivity.setOnClickListener( - view -> { - startActivity(new Intent(this, CustomTabsActivity.class)); - }); - - binding.openFrameDataForSpans.setOnClickListener( - view -> startActivity(new Intent(this, FrameDataForSpansActivity.class))); - - binding.throwInCoroutine.setOnClickListener( - view -> { - CoroutinesUtil.INSTANCE.throwInCoroutine(); - }); - - binding.showDialog.setOnClickListener( - view -> { - new AlertDialog.Builder(MainActivity.this) - .setTitle("Example Title") - .setMessage("Example Message") - .setPositiveButton( - "Close", - (dialog, which) -> { - if (SharedState.INSTANCE.isOrientationChange()) { - int currentOrientation = getResources().getConfiguration().orientation; - if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); - } else if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { - setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); - } - } else { - dialog.dismiss(); - } - }) - .show(); - }); - - binding.enableReplayDebugMode.setOnClickListener( - view -> { - Sentry.replay().enableDebugMaskingOverlay(); - }); - - binding.checkForUpdate.setOnClickListener( - view -> { - Toast.makeText(this, "Checking for updates...", Toast.LENGTH_SHORT).show(); - Future future = Sentry.distribution().checkForUpdate(); - // In production, convert this to use your preferred async library (RxJava, Coroutines, - // etc.) - // This sample uses raw threads and Future.get() for simplicity - // Process result on background thread, then update UI - new Thread( - () -> { - try { - UpdateStatus result = future.get(); - runOnUiThread( - () -> { - String message; - if (result instanceof UpdateStatus.NewRelease) { - UpdateStatus.NewRelease newRelease = (UpdateStatus.NewRelease) result; - message = - "Update available: " - + newRelease.getInfo().getBuildVersion() - + " (Build " - + newRelease.getInfo().getBuildNumber() - + ")\nDownload URL: " - + newRelease.getInfo().getDownloadUrl(); - } else if (result instanceof UpdateStatus.UpToDate) { - message = "App is up to date!"; - } else if (result instanceof UpdateStatus.NoNetwork) { - UpdateStatus.NoNetwork noNetwork = (UpdateStatus.NoNetwork) result; - message = "No network connection: " + noNetwork.getMessage(); - } else if (result instanceof UpdateStatus.UpdateError) { - UpdateStatus.UpdateError error = (UpdateStatus.UpdateError) result; - message = "Error checking for updates: " + error.getMessage(); - } else { - message = "Unknown status"; - } - Toast.makeText(this, message, Toast.LENGTH_LONG).show(); - }); - } catch (Exception e) { - runOnUiThread( - () -> - Toast.makeText( - this, - "Error checking for updates: " + e.getMessage(), - Toast.LENGTH_LONG) - .show()); - } - }) - .start(); - }); - - binding.openCameraActivity.setOnClickListener( - view -> { - startActivity(new Intent(this, CameraXActivity.class)); - }); - - binding.openHttpRequestActivity.setOnClickListener( - view -> startActivity(new Intent(this, TriggerHttpRequestActivity.class))); - - Sentry.logger().log(SentryLogLevel.INFO, "Creating content view"); - setContentView(binding.getRoot()); - - Sentry.logger().log(SentryLogLevel.INFO, "MainActivity created"); - } - - private void stackOverflow() { - stackOverflow(); - } - - @Override - protected void onResume() { - super.onResume(); - screenLoadCount++; - final ISpan span = Sentry.getSpan(); - if (span != null) { - ISpan measurementSpan = span.startChild("screen_load_measurement", "test measurement"); - measurementSpan.setMeasurement( - "screen_load_count", screenLoadCount, new MeasurementUnit.Custom("test")); - measurementSpan.finish(); - } - Sentry.reportFullyDisplayed(); - } -} diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt new file mode 100644 index 0000000000..f477e0d9a6 --- /dev/null +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.kt @@ -0,0 +1,795 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + +package io.sentry.samples.android + +import android.annotation.SuppressLint +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.res.Configuration +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Extension +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Speed +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.NavigationRailItemDefaults +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.sentry.Attachment +import io.sentry.MeasurementUnit +import io.sentry.Sentry +import io.sentry.SentryLogLevel +import io.sentry.UpdateStatus +import io.sentry.compose.SentryTraced +import io.sentry.compose.SentryUserFeedbackButton +import io.sentry.protocol.User +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.Calendar +import java.util.concurrent.CountDownLatch +import kotlinx.coroutines.launch +import timber.log.Timber + +@OptIn(ExperimentalComposeUiApi::class) +class MainActivity : AppCompatActivity() { + + private var screenLoadCount = 0 + internal lateinit var imageFile: File + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + SharedState.isOrientationChange = intent.getBooleanExtra("isOrientationChange", false) + + imageFile = createSentryImageFile() + + val image = Attachment(imageFile.absolutePath, "sentry.png", "image/png") + Sentry.configureScope { scope -> scope.addAttachment(image) } + + Sentry.logger().log(SentryLogLevel.INFO, "Creating content view") + + setContent { + val colorScheme = + if (isSystemInDarkTheme()) + darkColorScheme( + primary = Color(resources.getColor(R.color.colorPrimary, theme)), + secondary = Color(resources.getColor(R.color.colorAccent, theme)), + tertiary = Color(resources.getColor(R.color.colorPrimary, theme)), + ) + else + lightColorScheme( + primary = Color(resources.getColor(R.color.colorPrimary, theme)), + secondary = Color(resources.getColor(R.color.colorAccent, theme)), + tertiary = Color(resources.getColor(R.color.colorPrimary, theme)), + ) + MaterialTheme(colorScheme = colorScheme) { MainScreen() } + } + + Sentry.logger().log(SentryLogLevel.INFO, "MainActivity created") + } + + override fun onResume() { + super.onResume() + screenLoadCount++ + val span = Sentry.getSpan() + if (span != null) { + val measurementSpan = span.startChild("screen_load_measurement", "test measurement") + measurementSpan.setMeasurement( + "screen_load_count", + screenLoadCount, + MeasurementUnit.Custom("test"), + ) + measurementSpan.finish() + } + Sentry.reportFullyDisplayed() + } + + private fun createSentryImageFile(): File { + val file = applicationContext.getFileStreamPath("sentry.png") + try { + applicationContext.resources.openRawResource(R.raw.sentry).use { inputStream -> + FileOutputStream(file).use { outputStream -> + val bytes = ByteArray(1024) + while (inputStream.read(bytes) != -1) { + outputStream.write(bytes) + } + outputStream.flush() + } + } + } catch (e: IOException) { + Sentry.captureException(e) + } + return file + } +} + +enum class Category(val displayName: String, val icon: ImageVector) { + ERRORS("Errors", Icons.Filled.Error), + TRACING("Tracing", Icons.Filled.Speed), + SESSION_REPLAY("Session Replay", Icons.Filled.Videocam), + USER_FEEDBACK("User & Feedback", Icons.Filled.Person), + INTEGRATIONS("Integrations", Icons.Filled.Extension), + UPDATES("Updates", Icons.Filled.Settings), +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen() { + var selectedCategory by remember { mutableStateOf(Category.ERRORS) } + + Surface(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.fillMaxSize()) { + CategoryNavigationRail( + selectedCategory = selectedCategory, + onCategorySelected = { selectedCategory = it }, + ) + Surface(modifier = Modifier.fillMaxSize()) { + when (selectedCategory) { + Category.ERRORS -> ErrorsScreen() + Category.TRACING -> TracingScreen() + Category.SESSION_REPLAY -> SessionReplayScreen() + Category.USER_FEEDBACK -> UserFeedbackScreen() + Category.INTEGRATIONS -> IntegrationsScreen() + Category.UPDATES -> UpdatesScreen() + } + } + } + } +} + +@Composable +fun CategoryNavigationRail( + selectedCategory: Category, + onCategorySelected: (Category) -> Unit, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + + NavigationRail( + modifier = + modifier.fillMaxHeight().defaultMinSize(minWidth = 100.dp).verticalScroll(scrollState), + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ) { + Spacer(Modifier.height(16.dp)) + val scope = rememberCoroutineScope() + val rotation = remember { Animatable(1f) } + + Icon( + painterResource(R.drawable.sentry_glyph), + contentDescription = "Sentry Logo", + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = + Modifier.size(48.dp) + .shadow(4.dp, shape = CircleShape) + .background(color = MaterialTheme.colorScheme.surfaceBright, shape = CircleShape) + .clickable { scope.launch { rotation.animateTo(rotation.targetValue + 360.0f) } } + .padding(12.dp) + .rotate(rotation.value), + ) + Spacer(Modifier.height(16.dp)) + Category.entries.forEach { category -> + NavigationRailItem( + modifier = Modifier.defaultMinSize(minWidth = 100.dp), + selected = selectedCategory == category, + onClick = { onCategorySelected(category) }, + colors = + NavigationRailItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + ), + icon = { Icon(imageVector = category.icon, contentDescription = category.displayName) }, + alwaysShowLabel = true, + label = { + Text( + text = category.displayName, + style = MaterialTheme.typography.labelSmall, + maxLines = 2, + textAlign = TextAlign.Center, + fontWeight = if (selectedCategory == category) FontWeight.Bold else FontWeight.Normal, + ) + }, + ) + } + } +} + +@Composable +fun ErrorsScreen() { + val crashCount = remember { mutableIntStateOf(0) } + val mutex = remember { Object() } + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 180.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SentryTraced("crash_from_java") { + OutlinedButton(onClick = { throw RuntimeException("Uncaught Exception from Java.") }) { + Text("Crash from Java", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("capture_exception") { + OutlinedButton( + onClick = { Sentry.captureException(Exception(Exception(Exception("Some exception.")))) }, + modifier = Modifier, + ) { + Text("Capture Exception", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("breadcrumb") { + OutlinedButton( + onClick = { + Sentry.addBreadcrumb("Breadcrumb") + Sentry.setExtra("extra", "extra") + Sentry.setFingerprint(listOf("fingerprint")) + Sentry.setTransaction("transaction") + Sentry.captureException(Exception("Some exception with scope.")) + }, + modifier = Modifier, + ) { + Text("Breadcrumb", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("stack_overflow") { + OutlinedButton(onClick = { stackOverflow() }, modifier = Modifier) { + Text("Stack Overflow", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("native_crash") { + OutlinedButton(onClick = { NativeSample.crash() }, modifier = Modifier) { + Text("Native Crash", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("native_capture") { + OutlinedButton(onClick = { NativeSample.message() }, modifier = Modifier) { + Text("Native Capture", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("anr") { + OutlinedButton( + onClick = { + Thread { + synchronized(mutex) { + while (true) { + try { + Thread.sleep(10000) + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + } + } + .start() + + Handler(Looper.getMainLooper()) + .postDelayed({ synchronized(mutex) { throw IllegalStateException() } }, 1000) + }, + modifier = Modifier, + ) { + Text("ANR", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("native_anr") { + OutlinedButton( + onClick = { + Thread { NativeSample.freezeMysteriously(mutex) }.start() + + Handler(Looper.getMainLooper()) + .postDelayed({ synchronized(mutex) { throw IllegalStateException() } }, 1000) + }, + modifier = Modifier, + ) { + Text("ANR (native)", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("out_of_memory") { + OutlinedButton( + onClick = { + val latch = CountDownLatch(1) + for (i in 0 until 20) { + Thread { + val data = ArrayList() + try { + latch.await() + for (j in 0 until 1_000_000) { + data.add(String(ByteArray(1024 * 8))) + } + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + .start() + } + latch.countDown() + }, + modifier = Modifier, + ) { + Text("Out of Memory", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("send_message") { + OutlinedButton(onClick = { Sentry.captureMessage("Some message.") }, modifier = Modifier) { + Text("Send Message", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("test_timber") { + OutlinedButton( + onClick = { + crashCount.intValue++ + Timber.i("Some info here") + Timber.e( + RuntimeException("Uncaught Exception from Java."), + "Something wrong happened ${crashCount.intValue} times", + ) + }, + modifier = Modifier, + ) { + Text("Test Timber", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + } +} + +@Composable +fun TracingScreen() { + val activity = LocalContext.current.getActivity() + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 180.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SentryTraced("open_second_activity") { + OutlinedButton( + onClick = { + activity.finish() + activity.startActivity(Intent(activity, SecondActivity::class.java)) + }, + modifier = Modifier, + ) { + Text("Open Second Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_gestures_activity") { + OutlinedButton( + onClick = { activity.startActivity(Intent(activity, GesturesActivity::class.java)) }, + modifier = Modifier, + ) { + Text("Open Gestures Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_frame_data") { + OutlinedButton( + onClick = { + activity.startActivity(Intent(activity, FrameDataForSpansActivity::class.java)) + }, + modifier = Modifier, + ) { + Text("Open Frame Data for Spans", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_profiling") { + OutlinedButton( + onClick = { activity.startActivity(Intent(activity, ProfilingActivity::class.java)) }, + modifier = Modifier, + ) { + Text("Open Profiling Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + } +} + +@SuppressLint("SourceLockedOrientationActivity") +@Composable +fun SessionReplayScreen() { + val activity = LocalContext.current.getActivity() + var showDialog by remember { mutableStateOf(false) } + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 180.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SentryTraced("enable_replay_debug") { + OutlinedButton( + onClick = { Sentry.replay().enableDebugMaskingOverlay() }, + modifier = Modifier, + ) { + Text("Enable Replay Debug Mode", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("show_dialog") { + OutlinedButton(onClick = { showDialog = true }, modifier = Modifier) { + Text("Show Dialog", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + } + + // AlertDialog managed by local state + if (showDialog) { + AlertDialog( + onDismissRequest = { + if (SharedState.isOrientationChange) { + val currentOrientation = activity.resources.configuration.orientation + if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + } else { + showDialog = false + } + }, + title = { Text("Example Title") }, + text = { Text("Example Message") }, + confirmButton = { + TextButton( + onClick = { + if (SharedState.isOrientationChange) { + val currentOrientation = activity.resources.configuration.orientation + if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + } else if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { + activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + } else { + showDialog = false + } + } + ) { + Text("Close") + } + }, + ) + } +} + +@Composable +fun UserFeedbackScreen() { + val activity = LocalContext.current.getActivity() + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 180.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SentryTraced("set_user") { + OutlinedButton( + onClick = { + Sentry.setTag("user_set", "instance") + val user = + User().apply { + username = "username_from_java" + email = "email_from_java" + ipAddress = "{{auto}}" + } + Sentry.setUser(user) + }, + modifier = Modifier, + ) { + Text("Set User", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("unset_user") { + OutlinedButton( + onClick = { + Sentry.setTag("user_set", "null") + Sentry.setUser(null) + }, + modifier = Modifier, + ) { + Text("Unset User", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("add_attachment") { + OutlinedButton( + onClick = { + val fileName = Calendar.getInstance().timeInMillis.toString() + "_file.txt" + val file = activity.application.getFileStreamPath(fileName) + try { + io.sentry.instrumentation.file.SentryFileOutputStream.Factory.create( + FileOutputStream(file), + file, + ) + .use { fos -> fos.write("Hello, World!".toByteArray()) } + } catch (e: IOException) { + Sentry.captureException(e) + } + + Sentry.configureScope { scope -> + val json = "{ \"number\": 10 }" + val attachment = Attachment(json.toByteArray(), "log.json") + scope.addAttachment(attachment) + scope.addAttachment(Attachment(file.path)) + } + }, + modifier = Modifier, + ) { + Text("Add Attachment", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + + // SentryUserFeedbackButton as a special item + item(span = { GridItemSpan(maxLineSpan) }) { SentryUserFeedbackButton(modifier = Modifier) } + } +} + +@Composable +fun IntegrationsScreen() { + val activity = LocalContext.current.getActivity() + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 180.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SentryTraced("open_compose_activity") { + OutlinedButton( + onClick = { + activity.startActivity( + Intent(activity, io.sentry.samples.android.compose.ComposeActivity::class.java) + ) + }, + modifier = Modifier, + ) { + Text("Open Compose Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_sample_fragment") { + OutlinedButton( + onClick = { + SampleFragment.newInstance() + .show((activity as AppCompatActivity).supportFragmentManager, null) + }, + modifier = Modifier, + ) { + Text("Open Sample Fragment", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_third_fragment") { + OutlinedButton( + onClick = { activity.startActivity(Intent(activity, ThirdActivityFragment::class.java)) }, + modifier = Modifier, + ) { + Text("Open Third Fragment", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_permissions_activity") { + OutlinedButton( + onClick = { activity.startActivity(Intent(activity, PermissionsActivity::class.java)) }, + modifier = Modifier, + ) { + Text("Open Permissions Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_custom_tabs_activity") { + OutlinedButton( + onClick = { activity.startActivity(Intent(activity, CustomTabsActivity::class.java)) }, + modifier = Modifier, + ) { + Text("Open Custom Tabs Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_camera_activity") { + OutlinedButton( + onClick = { activity.startActivity(Intent(activity, CameraXActivity::class.java)) }, + modifier = Modifier, + ) { + Text("Open Camera Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("open_http_request_activity") { + OutlinedButton( + onClick = { + activity.startActivity(Intent(activity, TriggerHttpRequestActivity::class.java)) + }, + modifier = Modifier, + ) { + Text("Open HTTP Request Activity", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + item { + SentryTraced("throw_in_coroutine") { + OutlinedButton(onClick = { CoroutinesUtil.throwInCoroutine() }, modifier = Modifier) { + Text("Throw in Coroutine", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + } +} + +@Composable +fun UpdatesScreen() { + val activity = LocalContext.current.getActivity() + + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 180.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + SentryTraced("check_for_update") { + OutlinedButton( + onClick = { + Toast.makeText(activity, "Checking for updates...", Toast.LENGTH_SHORT).show() + val future = Sentry.distribution().checkForUpdate() + + Thread { + try { + val result = future.get() + activity.runOnUiThread { + val message = + when (result) { + is UpdateStatus.NewRelease -> { + "Update available: ${result.info.buildVersion} " + + "(Build ${result.info.buildNumber})\n" + + "Download URL: ${result.info.downloadUrl}" + } + + is UpdateStatus.UpToDate -> "App is up to date!" + is UpdateStatus.NoNetwork -> "No network connection: ${result.message}" + is UpdateStatus.UpdateError -> + "Error checking for updates: ${result.message}" + + else -> "Unknown status" + } + Toast.makeText(activity, message, Toast.LENGTH_LONG).show() + } + } catch (e: Exception) { + activity.runOnUiThread { + Toast.makeText( + activity, + "Error checking for updates: ${e.message}", + Toast.LENGTH_LONG, + ) + .show() + } + } + } + .start() + }, + modifier = Modifier, + ) { + Text("Check for Update", maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + } +} + +fun Context.getActivity(): ComponentActivity { + var currentContext = this + while (currentContext is ContextWrapper) { + if (currentContext is ComponentActivity) { + return currentContext + } + currentContext = currentContext.baseContext + } + if (currentContext is ComponentActivity) { + return currentContext + } + throw IllegalArgumentException("Context is not an Activity.") +} + +fun stackOverflow() { + stackOverflow() +} diff --git a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml b/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml deleted file mode 100644 index c86c904f6a..0000000000 --- a/sentry-samples/sentry-samples-android/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,201 +0,0 @@ - - - - - -