diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 511fd41d4c0..0f791a46019 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -55,8 +55,6 @@ android { lint { warningsAsErrors = true checkDependencies = true - // TODO(JAVA-547): Re-enable once the SDK_INT checks made obsolete by minSdk 26 are removed. - disable += "ObsoleteSdkInt" // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. checkReleaseBuilds = false diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 20fea39a28d..0d3296dbe93 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -4,7 +4,6 @@ import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; import static java.util.concurrent.TimeUnit.SECONDS; -import android.os.Build; import io.sentry.CompositePerformanceCollector; import io.sentry.DataCategory; import io.sentry.IContinuousProfiler; @@ -174,10 +173,6 @@ private void initScopes() { private void start() { initScopes(); - // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler - // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; - // Let's initialize trace folder and profiling interval init(); // init() didn't create profiler, should never happen @@ -279,12 +274,6 @@ private void stop(final boolean restartProfiler) { return; } - // onTransactionStart() is only available since Lollipop_MR1 - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) { - return; - } - List performanceCollectionData = null; if (performanceCollector != null) { performanceCollectionData = performanceCollector.stop(chunkId.toString()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 83f9d9b3492..4e0b7440763 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -202,15 +202,6 @@ static void initializeIntegrationsAndProcessors( if (options.getTransportGate() instanceof NoOpTransportGate) { options.setTransportGate(new AndroidTransportGate(options)); } - // API 25 and lower may lack ISRG Root X1 (Let's Encrypt), so the SDK's own envelope uploads to - // Sentry SaaS trust the bundled root in addition to the system ones. Self-hosted DSNs keep the - // platform default, so Network Security Config domain-config trust anchors still apply there. - if (buildInfoProvider.getSdkInfoVersion() <= Build.VERSION_CODES.N_MR1 - && options.getSslSocketFactory() == null - && SentryRootCaSslSocketFactory.isSentrySaasDsn(options.getDsn())) { - options.setSslSocketFactory(new SentryRootCaSslSocketFactory(options.getLogger())); - } - final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); options.setAppStartExtender(appStartMetrics.getAppStartExtension()); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index e44ed746a08..f6e828de83d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -151,10 +151,6 @@ private void init() { @Override public void start() { - // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler - // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; - // When the first transaction is starting, we can start profiling if (!isRunning.getAndSet(true)) { // Let's initialize trace folder and profiling interval @@ -234,11 +230,6 @@ public void bindTransaction(final @NotNull ITransaction transaction) { final @Nullable List performanceCollectionData, final @NotNull SentryOptions options) { - // onTransactionStart() is only available since Lollipop_MR1 - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - // and SUPPORTED_ABIS since KITKAT - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; - // check if profiler was created if (profiler == null) { return null; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java index c7f3182b97d..0151c96de67 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NetworkBreadcrumbsIntegration.java @@ -64,12 +64,6 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions if (androidOptions.isEnableNetworkEventBreadcrumbs()) { - // The specific error is logged in the ConnectivityChecker method - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) { - options.getLogger().log(SentryLevel.DEBUG, "NetworkCallbacks need Android N+."); - return; - } - try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { networkCallback = new NetworkBreadcrumbsNetworkCallback( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryRootCaSslSocketFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryRootCaSslSocketFactory.java deleted file mode 100644 index 00e4ec17696..00000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryRootCaSslSocketFactory.java +++ /dev/null @@ -1,211 +0,0 @@ -package io.sentry.android.core; - -import io.sentry.DsnUtil; -import io.sentry.ILogger; -import io.sentry.SentryLevel; -import io.sentry.util.LazyEvaluator; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Locale; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; -import org.jetbrains.annotations.VisibleForTesting; - -/** - * An {@link SSLSocketFactory} that trusts the system root CAs plus the root CAs bundled in {@link - * SentryRootCertificates}. Both are put into a single trust store which is handed to the platform's - * default {@link TrustManagerFactory}, so certificate validation itself is still done by the - * platform. - * - *

Android API 25 and lower may not ship ISRG Root X1, so TLS handshakes with Let's Encrypt - * certificates fail on those devices. This factory is only set as {@link - * io.sentry.SentryOptions#setSslSocketFactory(SSLSocketFactory)} on those API levels and only when - * the DSN points to Sentry SaaS ({@code sentry.io}), see {@link #isSentrySaasDsn(String)}. So it - * only applies to the SDK's own envelope uploads to Sentry and not to any other connection of the - * app. - * - *

On API 24 and 25, the system CAs are read from the Network Security Config's base config. Any - * {@code domain-config} trust anchors or pins do not apply to connections using this factory. This - * is why it's limited to Sentry SaaS: self-hosted Sentry with a private CA keeps using the platform - * default. - * - *

The underlying {@link SSLContext} is created lazily on first use, so the cost of parsing the - * certificates is paid on the transport thread instead of during {@code SentryAndroid.init}. If - * creating it fails for any reason, the platform default {@link SSLSocketFactory} is used and the - * failure is not retried. - */ -@ApiStatus.Internal -final class SentryRootCaSslSocketFactory extends SSLSocketFactory { - - static final @NotNull String SENTRY_ROOT_CA_ALIAS_PREFIX = "sentry-root-ca-"; - - private final @NotNull LazyEvaluator delegate; - - /** Provides the CAs trusted by the platform. */ - interface SystemTrustedIssuersProvider { - @NotNull - X509Certificate[] getSystemTrustedIssuers() throws GeneralSecurityException; - } - - SentryRootCaSslSocketFactory(final @NotNull ILogger logger) { - this(logger, SentryRootCaSslSocketFactory::getSystemTrustedIssuers); - } - - @VisibleForTesting - SentryRootCaSslSocketFactory( - final @NotNull ILogger logger, - final @NotNull SystemTrustedIssuersProvider systemTrustedIssuersProvider) { - this.delegate = new LazyEvaluator<>(() -> createDelegate(logger, systemTrustedIssuersProvider)); - } - - /** - * Returns {@code true} if the DSN's host is {@code sentry.io} or a subdomain of it, for example - * {@code o123.ingest.us.sentry.io}. Those are the hosts whose certificates chain to the bundled - * roots. The host is read with {@link DsnUtil#getHost(String)}, which parses it the same way as - * the DSN the SDK sends envelopes to. - */ - static boolean isSentrySaasDsn(final @Nullable String dsn) { - @Nullable String host = DsnUtil.getHost(dsn); - if (host == null) { - return false; - } - if (host.endsWith(".")) { - host = host.substring(0, host.length() - 1); - } - host = host.toLowerCase(Locale.ROOT); - return host.equals("sentry.io") || host.endsWith(".sentry.io"); - } - - private static @NotNull SSLSocketFactory createDelegate( - final @NotNull ILogger logger, - final @NotNull SystemTrustedIssuersProvider systemTrustedIssuersProvider) { - try { - final @NotNull TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init( - createTrustStore(systemTrustedIssuersProvider.getSystemTrustedIssuers())); - - final @NotNull SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); - return sslContext.getSocketFactory(); - } catch (Exception e) { - // Catches Exception, not only GeneralSecurityException and IOException: some OEM builds - // throw RuntimeExceptions from their trust store or certificate code. An exception escaping - // here would make every envelope upload fail without being retried, and the LazyEvaluator - // would try to build the delegate again on every send. Errors such as OutOfMemoryError are - // not caught on purpose, the process can't recover from them. - logger.log( - SentryLevel.ERROR, - "Failed to create SSLSocketFactory with bundled Sentry root CAs, using the default one.", - e); - return HttpsURLConnection.getDefaultSSLSocketFactory(); - } - } - - /** Returns the CAs trusted by the platform's default trust manager. */ - private static @NotNull X509Certificate[] getSystemTrustedIssuers() - throws GeneralSecurityException { - final @NotNull TrustManagerFactory trustManagerFactory = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - for (final TrustManager trustManager : trustManagerFactory.getTrustManagers()) { - if (trustManager instanceof X509TrustManager) { - return ((X509TrustManager) trustManager).getAcceptedIssuers(); - } - } - throw new GeneralSecurityException("No default X509TrustManager available"); - } - - /** - * Creates a trust store containing the given system trusted CAs plus the bundled Sentry root CAs. - */ - @VisibleForTesting - static @NotNull KeyStore createTrustStore(final @NotNull X509Certificate[] systemTrustedIssuers) - throws GeneralSecurityException, IOException { - final @NotNull KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null, null); - for (int i = 0; i < systemTrustedIssuers.length; i++) { - keyStore.setCertificateEntry("system-ca-" + i, systemTrustedIssuers[i]); - } - final @NotNull CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - for (int i = 0; i < SentryRootCertificates.ALL.length; i++) { - final @NotNull Certificate certificate = - certificateFactory.generateCertificate( - new ByteArrayInputStream( - SentryRootCertificates.ALL[i].getBytes(StandardCharsets.UTF_8))); - keyStore.setCertificateEntry(SENTRY_ROOT_CA_ALIAS_PREFIX + i, certificate); - } - return keyStore; - } - - @TestOnly - @NotNull - SSLSocketFactory getDelegate() { - return delegate.getValue(); - } - - @Override - public String[] getDefaultCipherSuites() { - return delegate.getValue().getDefaultCipherSuites(); - } - - @Override - public String[] getSupportedCipherSuites() { - return delegate.getValue().getSupportedCipherSuites(); - } - - @Override - public Socket createSocket() throws IOException { - return delegate.getValue().createSocket(); - } - - @Override - public Socket createSocket( - final Socket socket, final String host, final int port, final boolean autoClose) - throws IOException { - return delegate.getValue().createSocket(socket, host, port, autoClose); - } - - @Override - public Socket createSocket(final String host, final int port) throws IOException { - return delegate.getValue().createSocket(host, port); - } - - @Override - public Socket createSocket( - final String host, final int port, final InetAddress localHost, final int localPort) - throws IOException { - return delegate.getValue().createSocket(host, port, localHost, localPort); - } - - @Override - public Socket createSocket(final InetAddress host, final int port) throws IOException { - return delegate.getValue().createSocket(host, port); - } - - @Override - public Socket createSocket( - final InetAddress address, - final int port, - final InetAddress localAddress, - final int localPort) - throws IOException { - return delegate.getValue().createSocket(address, port, localAddress, localPort); - } -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryRootCertificates.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryRootCertificates.java deleted file mode 100644 index f6a53189adb..00000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryRootCertificates.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.sentry.android.core; - -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -/** - * Root CA certificates the SDK bundles for its own envelope uploads, see {@link - * SentryRootCaSslSocketFactory}. - * - *

Sentry SaaS uses Let's Encrypt, whose default certificate chains all end at ISRG Root X1 (see - * https://docs.sentry.io/security-legal-pii/security/ssl/ and - * https://letsencrypt.org/certificates/). ISRG Root X1 was only added to the Android CA store in - * Android 7.1 (API 25), so older devices can't validate those chains on their own. It's also - * applied on API 25 to cover vendor builds that lack it. The other Sentry roots (DigiCert, and - * Google Trust Services via its GlobalSign cross-sign) are already trusted on all supported API - * levels. - */ -@ApiStatus.Internal -final class SentryRootCertificates { - - private SentryRootCertificates() {} - - /** - * ISRG Root X1, SHA-256 - * 96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6 - */ - static final @NotNull String ISRG_ROOT_X1 = - "-----BEGIN CERTIFICATE-----\n" - + "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" - + "TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" - + "cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" - + "WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" - + "ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" - + "MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" - + "h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" - + "0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" - + "A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" - + "T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" - + "B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" - + "B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" - + "KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" - + "OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" - + "jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" - + "qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" - + "rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" - + "HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" - + "hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" - + "ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" - + "3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" - + "NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" - + "ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" - + "TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" - + "jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" - + "oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" - + "4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" - + "mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" - + "emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" - + "-----END CERTIFICATE-----\n"; - - // TODO(2028-01-28): Google Trust Services certs are only trusted on API <= 28 via the GTS - // Root R1 cross-sign by GlobalSign Root CA, which expires on 2028-01-28. Before that date, - // bundle GTS Root R1-R4 here and raise the API gate in AndroidOptionsInitializer to <= 28. - // See https://github.com/getsentry/sentry-java/pull/6227 - static final @NotNull String[] ALL = new String[] {ISRG_ROOT_X1}; -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java index 62a212e6dcc..2ad28a563e1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProvider.java @@ -7,10 +7,8 @@ import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; -import android.os.Build; import android.os.Handler; import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import io.sentry.IConnectionStatusProvider; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; @@ -99,9 +97,7 @@ public AndroidConnectionStatusProvider( new CellularNetworkTechnologyProvider(this.context, options.getLogger(), buildInfoProvider); capabilities[0] = NetworkCapabilities.NET_CAPABILITY_INTERNET; - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.M) { - capabilities[1] = NetworkCapabilities.NET_CAPABILITY_VALIDATED; - } + capabilities[1] = NetworkCapabilities.NET_CAPABILITY_VALIDATED; // Register network callback immediately for caching //noinspection Convert2MethodRef @@ -123,13 +119,9 @@ private boolean isNetworkEffectivelyConnected( } // Check for general internet capability AND validated status - boolean hasInternetAndValidated = - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.M) { - hasInternetAndValidated = - hasInternetAndValidated - && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); - } + final boolean hasInternetAndValidated = + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); if (!hasInternetAndValidated) { return false; @@ -223,7 +215,6 @@ public void onAvailable(final @NotNull Network network) { } } - @RequiresApi(Build.VERSION_CODES.O) @Override public void onUnavailable() { clearCacheAndNotifyObservers(); @@ -393,12 +384,6 @@ private void updateCache(@Nullable NetworkCapabilities networkCapabilities) { return; } - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.M) { - cachedNetworkCapabilities = null; - cacheFreshUntil = Deadline.after(ticker, CACHE_TTL_MINUTES, TimeUnit.MINUTES); - return; - } - // Fallback: query current active network final ConnectivityManager connectivityManager = getConnectivityManager(context, options.getLogger()); @@ -630,65 +615,25 @@ public NetworkCapabilities getCachedNetworkCapabilities() { boolean wifi = false; boolean cellular = false; - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.M) { - - final Network activeNetwork = connectivityManager.getActiveNetwork(); - if (activeNetwork == null) { - logger.log(SentryLevel.INFO, "Network is null and cannot check network status"); - return null; - } - final NetworkCapabilities networkCapabilities = - connectivityManager.getNetworkCapabilities(activeNetwork); - if (networkCapabilities == null) { - logger.log(SentryLevel.INFO, "NetworkCapabilities is null and cannot check network type"); - return null; - } - if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { - ethernet = true; - } - if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { - wifi = true; - } - if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - cellular = true; - } - } else { - // ideally using connectivityManager.getAllNetworks(), but its >= Android L only - - // for some reason linting didn't allow a single @SuppressWarnings("deprecation") on method - // signature - @SuppressWarnings("deprecation") - final android.net.NetworkInfo activeNetworkInfo = - connectivityManager.getActiveNetworkInfo(); - - if (activeNetworkInfo == null) { - logger.log(SentryLevel.INFO, "NetworkInfo is null, there's no active network."); - return null; - } - - @SuppressWarnings("deprecation") - final int type = activeNetworkInfo.getType(); - - @SuppressWarnings("deprecation") - final int TYPE_ETHERNET = ConnectivityManager.TYPE_ETHERNET; - - @SuppressWarnings("deprecation") - final int TYPE_WIFI = ConnectivityManager.TYPE_WIFI; - - @SuppressWarnings("deprecation") - final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE; - - switch (type) { - case TYPE_ETHERNET: - ethernet = true; - break; - case TYPE_WIFI: - wifi = true; - break; - case TYPE_MOBILE: - cellular = true; - break; - } + final Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + logger.log(SentryLevel.INFO, "Network is null and cannot check network status"); + return null; + } + final NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + if (networkCapabilities == null) { + logger.log(SentryLevel.INFO, "NetworkCapabilities is null and cannot check network type"); + return null; + } + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + ethernet = true; + } + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + wifi = true; + } + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + cellular = true; } // TODO: change the protocol to be a list of transports as a device may have the capability of @@ -758,10 +703,6 @@ public static boolean addNetworkCallback( final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull NetworkCallback networkCallback) { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) { - logger.log(SentryLevel.DEBUG, "NetworkCallbacks need Android N+."); - return false; - } if (!Permissions.hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE)) { logger.log(SentryLevel.INFO, "No permission (ACCESS_NETWORK_STATE) to check network status."); @@ -787,10 +728,6 @@ static boolean registerNetworkCallback( final @NotNull BuildInfoProvider buildInfoProvider, final @Nullable Handler handler, final @NotNull NetworkCallback networkCallback) { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) { - logger.log(SentryLevel.DEBUG, "NetworkCallbacks need Android N+."); - return false; - } final ConnectivityManager connectivityManager = getConnectivityManager(context, logger); if (connectivityManager == null) { return false; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProvider.java index a5468ae913f..5cff72f68fd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProvider.java @@ -144,10 +144,6 @@ public void unregister() { @SuppressLint({"MissingPermission", "InlinedApi", "NewApi"}) private @Nullable String getDataNetworkTechnology() { - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) { - // getDataNetworkType was added in API 24. - return null; - } // The SDK doesn't declare these permissions, so the technology is only available to apps that // already request one of them. READ_BASIC_PHONE_STATE only exists from API 31 on, where the // display info listener is used instead, but checking it is harmless. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java index 533739d51d8..bed670b3d2c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/FirstDrawDoneListener.java @@ -19,7 +19,6 @@ package io.sentry.android.core.internal.util; import android.app.Activity; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.view.View; @@ -69,32 +68,13 @@ public static void registerForNextDraw( } /** Registers a post-draw callback for the next draw of a view. */ + // The BuildInfoProvider param is kept for compatibility, as hybrid SDKs call this method. public static void registerForNextDraw( final @NotNull View view, final @NotNull Runnable drawDoneCallback, final @NotNull BuildInfoProvider buildInfoProvider) { final FirstDrawDoneListener listener = new FirstDrawDoneListener(view, drawDoneCallback); - // Handle bug prior to API 26 where OnDrawListener from the floating ViewTreeObserver is not - // merged into the real ViewTreeObserver. - // https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3 - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.O - && !isAliveAndAttached(view)) { - view.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View view) { - view.getViewTreeObserver().addOnDrawListener(listener); - view.removeOnAttachStateChangeListener(this); - } - - @Override - public void onViewDetachedFromWindow(View view) { - view.removeOnAttachStateChangeListener(this); - } - }); - } else { - view.getViewTreeObserver().addOnDrawListener(listener); - } + view.getViewTreeObserver().addOnDrawListener(listener); } private FirstDrawDoneListener(final @NotNull View view, final @NotNull Runnable callback) { @@ -120,17 +100,4 @@ public void onDraw() { }); mainThreadHandler.postAtFrontOfQueue(callback); } - - /** - * Helper to avoid bug - * prior to API 26, where the floating ViewTreeObserver's OnDrawListeners are not merged into - * the real ViewTreeObserver during attach. - * - * @return true if the View is already attached and the ViewTreeObserver is not a floating - * placeholder. - */ - private static boolean isAliveAndAttached(final @NotNull View view) { - return view.getViewTreeObserver().isAlive() && view.isAttachedToWindow(); - } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java index 657ba54cbd2..2d4ebeb07ff 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/ScreenshotUtils.java @@ -3,8 +3,6 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.view.PixelCopy; @@ -122,8 +120,8 @@ public class ScreenshotUtils { final @NotNull IThreadChecker threadChecker, final @NotNull ILogger logger, final @NotNull BuildInfoProvider buildInfoProvider) { - // We are keeping BuildInfoProvider param for compatibility, as it's being used by - // cross-platform SDKs + // We are keeping the IThreadChecker and BuildInfoProvider params for compatibility, as they are + // being used by cross-platform SDKs if (!isActivityValid(activity)) { logger.log(SentryLevel.DEBUG, "Activity isn't valid, not taking screenshot."); @@ -159,63 +157,38 @@ public class ScreenshotUtils { final @NotNull CountDownLatch latch = new CountDownLatch(1); - // Use Pixel Copy API on new devices, fallback to canvas rendering on older ones - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.O) { - - final HandlerThread thread = new HandlerThread("SentryScreenshot"); - thread.start(); - - boolean success = false; - final AtomicInteger copyResultCode = new AtomicInteger(-1); - try { - final Handler handler = new Handler(thread.getLooper()); - - PixelCopy.request( - window, - bitmap, - copyResult -> { - copyResultCode.set(copyResult); - latch.countDown(); - }, - handler); - - final boolean completed = latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS); - success = completed && copyResultCode.get() == PixelCopy.SUCCESS; - } catch (Throwable e) { - // ignored - logger.log(SentryLevel.ERROR, "Taking screenshot using PixelCopy failed.", e); - } finally { - thread.quit(); - } - - if (!success) { - logger.log( - SentryLevel.WARNING, - "PixelCopy failed for screenshot capture (result=%d).", - copyResultCode.get()); - return null; - } - } else { - final Canvas canvas = new Canvas(bitmap); - if (threadChecker.isMainThread()) { - view.draw(canvas); - latch.countDown(); - } else { - activity.runOnUiThread( - () -> { - try { - view.draw(canvas); - } catch (Throwable e) { - logger.log(SentryLevel.ERROR, "Taking screenshot failed (view.draw).", e); - } finally { - latch.countDown(); - } - }); - } + final HandlerThread thread = new HandlerThread("SentryScreenshot"); + thread.start(); + + boolean success = false; + final AtomicInteger copyResultCode = new AtomicInteger(-1); + try { + final Handler handler = new Handler(thread.getLooper()); + + PixelCopy.request( + window, + bitmap, + copyResult -> { + copyResultCode.set(copyResult); + latch.countDown(); + }, + handler); + + final boolean completed = latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS); + success = completed && copyResultCode.get() == PixelCopy.SUCCESS; + } catch (Throwable e) { + // ignored + logger.log(SentryLevel.ERROR, "Taking screenshot using PixelCopy failed.", e); + } finally { + thread.quit(); + } - if (!latch.await(CAPTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { - return null; - } + if (!success) { + logger.log( + SentryLevel.WARNING, + "PixelCopy failed for screenshot capture (result=%d).", + copyResultCode.get()); + return null; } return bitmap; } catch (Throwable e) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java index 2c0ae246558..1c643f36bf2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollector.java @@ -12,7 +12,6 @@ import android.view.Choreographer; import android.view.FrameMetrics; import android.view.Window; -import androidx.annotation.RequiresApi; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.SentryLevel; @@ -110,10 +109,6 @@ public SentryFrameMetricsCollector( if (!(appContext instanceof Application)) { return; } - // FrameMetrics api is only available since sdk version N - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.N) { - return; - } isAvailable = true; // The frame metrics HandlerThread is started lazily on the first startCollection() call. @@ -222,25 +217,14 @@ public static boolean isSlow(long frameDuration, final long expectedFrameDuratio return frameDuration > expectedFrameDuration; } - /** - * Return the frame start timestamp. On API 26+, this value is read directly from {@link - * FrameMetrics}; older APIs use the reflected Choreographer timestamp. - */ - @SuppressLint("NewApi") private long getFrameStartTimestamp(final @NotNull FrameMetrics frameMetrics) { - - if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.O) { - return frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP); - } - - return getLastKnownFrameStartTimeNanos(); + return frameMetrics.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP); } /** * Return time spent on the main thread (cpu) for frame creation. It doesn't consider time spent * on the render thread (gpu). */ - @RequiresApi(api = Build.VERSION_CODES.N) private long getFrameCpuDuration(final @NotNull FrameMetrics frameMetrics) { // Inspired by JankStats // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt;l=74-79;drc=1de6215c6bd9e887e3d94556e9ac55cfb7b8c797 @@ -512,7 +496,6 @@ void onFrameMetricCollected( @ApiStatus.Internal public interface WindowFrameMetricsManager { - @RequiresApi(api = Build.VERSION_CODES.N) default void addOnFrameMetricsAvailableListener( final @NotNull Window window, final @Nullable Window.OnFrameMetricsAvailableListener frameMetricsAvailableListener, @@ -523,7 +506,6 @@ default void addOnFrameMetricsAvailableListener( window.addOnFrameMetricsAvailableListener(frameMetricsAvailableListener, handler); } - @RequiresApi(api = Build.VERSION_CODES.N) default void removeOnFrameMetricsAvailableListener( final @NotNull Window window, final @Nullable Window.OnFrameMetricsAvailableListener frameMetricsAvailableListener) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index dacab11e4f4..e42ecaf1c21 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -554,28 +554,15 @@ private void scheduleHeadlessAppStartCheckOnMain() { if (!headlessAppStartCheckPending.compareAndSet(false, true)) { return; } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Looper.getMainLooper() - .getQueue() - .addIdleHandler( - () -> { - firstIdle = SystemClock.uptimeMillis(); - headlessAppStartCheckPending.set(false); - handleHeadlessAppStartIfNeededOnMain(); - return false; - }); - } else { - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post( - () -> { - firstIdle = SystemClock.uptimeMillis(); - handler.post( - () -> { - headlessAppStartCheckPending.set(false); - handleHeadlessAppStartIfNeededOnMain(); - }); - }); - } + Looper.getMainLooper() + .getQueue() + .addIdleHandler( + () -> { + firstIdle = SystemClock.uptimeMillis(); + headlessAppStartCheckPending.set(false); + handleHeadlessAppStartIfNeededOnMain(); + return false; + }); } /** diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt index caaa30152a4..806f00ee8b1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -293,17 +293,6 @@ class AndroidContinuousProfilerTest { assertEquals(0, profiler.rootSpanCounter) } - @Test - fun `profiler works only on api 22+`() { - val buildInfo = - mock { - whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) - } - val profiler = fixture.getSut(buildInfo) - profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - assertFalse(profiler.isRunning) - } - @Test fun `profiler evaluates profilingTracesDirPath options only on first start`() { val profiler = fixture.getSut { it.cacheDirPath = null } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index b37a6bbdee5..fe3f2cffdf3 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -243,17 +243,6 @@ class AndroidTransactionProfilerTest { assertEquals(profilingTraceData.transactionId, fixture.transaction1.eventId.toString()) } - @Test - fun `profiler works only on api 22+`() { - val buildInfo = - mock { - whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) - } - val profiler = fixture.getSut(context, buildInfo) - profiler.start() - assertFalse(profiler.isRunning) - } - @Test fun `profiler on profilesSampleRate=0 false`() { fixture.options.apply { profilesSampleRate = 0.0 } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt index 711f5f7fe0b..8085189b029 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/NetworkBreadcrumbsIntegrationTest.kt @@ -94,18 +94,6 @@ class NetworkBreadcrumbsIntegrationTest { assertNull(sut.networkCallback) } - @Test - fun `It doesn't register callback if not on Android N+`() { - val buildInfo = mock() - whenever(buildInfo.sdkInfoVersion).thenReturn(Build.VERSION_CODES.M) - val sut = fixture.getSut(buildInfo = buildInfo) - - sut.register(fixture.scopes, fixture.options) - - assertTrue(AndroidConnectionStatusProvider.getChildCallbacks().isEmpty()) - assertNull(sut.networkCallback) - } - @Test fun `When NetworkBreadcrumbsIntegration is closed, it should unregister the callback`() { val sut = fixture.getSut() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryRootCaSslSocketFactoryTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryRootCaSslSocketFactoryTest.kt deleted file mode 100644 index e55eb5508fc..00000000000 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryRootCaSslSocketFactoryTest.kt +++ /dev/null @@ -1,191 +0,0 @@ -package io.sentry.android.core - -import com.google.common.truth.Truth.assertThat -import io.sentry.ILogger -import io.sentry.SentryLevel -import java.security.KeyStore -import java.security.MessageDigest -import java.security.cert.CertPathValidator -import java.security.cert.CertPathValidatorException -import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.security.cert.PKIXParameters -import java.security.cert.X509Certificate -import java.util.Date -import java.util.concurrent.atomic.AtomicInteger -import javax.net.ssl.HttpsURLConnection -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager -import kotlin.test.Test -import kotlin.test.assertFailsWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify - -class SentryRootCaSslSocketFactoryTest { - - @Test - fun `bundles ISRG Root X1 matching the fingerprint published on docs_sentry_io`() { - val keyStore = SentryRootCaSslSocketFactory.createTrustStore(emptyArray()) - - val fingerprints = - keyStore.aliases().toList().map { alias -> keyStore.getCertificate(alias).sha256() } - - assertThat(fingerprints) - .containsExactly( - // ISRG Root X1 - ISRG_ROOT_X1_SHA256 - ) - } - - @Test - fun `trust store keeps the system trusted CAs and adds the bundled root`() { - // The JVM's cacerts already has the ISRG roots, so remove them to act like an old Android - // device. - val systemIssuers = systemIssuersWithoutIsrgRoots() - assertThat(systemIssuers).isNotEmpty() - - val trustManager = - defaultTrustManager(SentryRootCaSslSocketFactory.createTrustStore(systemIssuers)) - - val accepted = trustManager.acceptedIssuers.map { it.sha256() } - assertThat(accepted).containsAtLeastElementsIn(systemIssuers.map { it.sha256() }) - assertThat(accepted).contains(ISRG_ROOT_X1_SHA256) - } - - @Test - fun `Let's Encrypt chain only validates with the bundled root`() { - val chain = letsEncryptChain() - - // Without any ISRG root, like Android 7.1.0 and older. - val ex = - assertFailsWith { - validate(chain, plainKeyStore(systemIssuersWithoutIsrgRoots())) - } - assertThat(ex.message).contains("trust anchor") - - // The same system CAs plus the bundled root. - validate(chain, SentryRootCaSslSocketFactory.createTrustStore(systemIssuersWithoutIsrgRoots())) - // Only the bundled root. - validate(chain, SentryRootCaSslSocketFactory.createTrustStore(emptyArray())) - } - - @Test - fun `falls back to the default SSLSocketFactory once when building the delegate throws a RuntimeException`() { - val logger = mock() - val calls = AtomicInteger() - val sut = - SentryRootCaSslSocketFactory(logger) { - calls.incrementAndGet() - throw IllegalStateException("broken OEM trust store") - } - - assertThat(sut.delegate).isSameInstanceAs(HttpsURLConnection.getDefaultSSLSocketFactory()) - assertThat(sut.supportedCipherSuites).isNotEmpty() - assertThat(sut.delegate).isSameInstanceAs(HttpsURLConnection.getDefaultSSLSocketFactory()) - - assertThat(calls.get()).isEqualTo(1) - verify(logger).log(eq(SentryLevel.ERROR), any(), any()) - } - - @Test - fun `creates a custom delegate SSLSocketFactory lazily and only once`() { - val sut = SentryRootCaSslSocketFactory(mock()) - - val delegate = sut.delegate - - assertThat(delegate).isNotSameInstanceAs(HttpsURLConnection.getDefaultSSLSocketFactory()) - assertThat(sut.supportedCipherSuites).isNotEmpty() - assertThat(sut.delegate).isSameInstanceAs(delegate) - } - - @Test - fun `isSentrySaasDsn matches sentry_io and its subdomains only`() { - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@o1.ingest.us.sentry.io/1")) - .isTrue() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@o1.ingest.sentry.io/1")) - .isTrue() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key:secret@sentry.io:443/1")) - .isTrue() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@O1.Ingest.SENTRY.IO./1")) - .isTrue() - - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn(null)).isFalse() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("")).isFalse() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@sentry.example.com/1")) - .isFalse() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@notsentry.io/1")).isFalse() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@sentry.io.evil.com/1")) - .isFalse() - assertThat(SentryRootCaSslSocketFactory.isSentrySaasDsn("https://key@evil.com/sentry.io")) - .isFalse() - } - - /** The JVM's system CAs without ISRG Root X1 and any other ISRG root, such as Root YR. */ - private fun systemIssuersWithoutIsrgRoots(): Array = - defaultTrustManager(null) - .acceptedIssuers - .filter { - val subject = it.subjectX500Principal.name - it.sha256() != ISRG_ROOT_X1_SHA256 && - !subject.contains("O=ISRG") && - !subject.contains("O=Internet Security Research Group") - } - .toTypedArray() - - private fun plainKeyStore(certificates: Array): KeyStore = - KeyStore.getInstance(KeyStore.getDefaultType()).apply { - load(null, null) - certificates.forEachIndexed { i, certificate -> setCertificateEntry("ca-$i", certificate) } - } - - /** - * A real chain served by valid-isrgrootx1.letsencrypt.org: leaf, YR1 intermediate and Root YR - * cross-signed by ISRG Root X1. - */ - private fun letsEncryptChain(): List { - val factory = CertificateFactory.getInstance("X.509") - return listOf( - "letsencrypt-leaf.pem", - "letsencrypt-yr1.pem", - "letsencrypt-root-yr-cross-signed-by-isrg-root-x1.pem", - ) - .map { name -> - javaClass.classLoader!!.getResourceAsStream("root-ca/$name").use { - factory.generateCertificate(it) as X509Certificate - } - } - } - - /** - * Validates the chain with PKIX. Validates at a fixed date within the leaf's validity, so the - * test doesn't break when the leaf certificate expires. - */ - private fun validate(chain: List, trustStore: KeyStore) { - val params = - PKIXParameters(trustStore).apply { - isRevocationEnabled = false - date = Date(VALIDATION_DATE_MILLIS) - } - CertPathValidator.getInstance("PKIX") - .validate(CertificateFactory.getInstance("X.509").generateCertPath(chain), params) - } - - private fun defaultTrustManager(keyStore: KeyStore?): X509TrustManager { - val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - factory.init(keyStore) - return factory.trustManagers.filterIsInstance().first() - } - - private fun Certificate.sha256(): String = - MessageDigest.getInstance("SHA-256").digest(encoded).joinToString(":") { "%02X".format(it) } - - companion object { - private const val ISRG_ROOT_X1_SHA256 = - "96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6" - - // 2026-10-14T00:00:00Z, within the validity of all certificates in the Let's Encrypt chain. - private const val VALIDATION_DATE_MILLIS = 1791936000000L - } -} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt index 4bf1d7de427..29047ee304e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt @@ -335,14 +335,6 @@ class CellularNetworkTechnologyProviderTest { assertThat(provider.cellularNetworkTechnology).isNull() } - @Test - fun `below Android 7 the technology is not read because getDataNetworkType is unavailable`() { - val provider = fixture.getSut(sdkVersion = Build.VERSION_CODES.M, hasPermission = true) - - assertThat(provider.cellularNetworkTechnology).isNull() - verify(fixture.telephonyManager, never()).dataNetworkType - } - @Test fun `below Android 12 registering does nothing`() { val provider = fixture.getSut(sdkVersion = Build.VERSION_CODES.R) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt index 57e72ac4d72..edae2fefe3d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/FirstDrawDoneListenerTest.kt @@ -12,7 +12,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.android.core.BuildInfoProvider import io.sentry.test.getProperty -import java.util.concurrent.CopyOnWriteArrayList import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertIs @@ -55,34 +54,7 @@ class FirstDrawDoneListenerTest { private val fixture = Fixture() @Test - fun `registerForNextDraw adds listener on attach state changed on sdk 25-`() { - val view = fixture.getSut(Build.VERSION_CODES.N_MR1) - - // OnDrawListener is not registered, it is delayed for later - FirstDrawDoneListener.registerForNextDraw(view, {}, fixture.buildInfo) - assertTrue(fixture.onDrawListeners.isEmpty()) - - // Register listener after the view is attached to a window - val listenerInfo = Class.forName("android.view.View\$ListenerInfo") - val mListenerInfo: Any = view.getProperty("mListenerInfo") - val mOnAttachStateChangeListeners: CopyOnWriteArrayList = - mListenerInfo.getProperty(listenerInfo, "mOnAttachStateChangeListeners") - assertFalse(mOnAttachStateChangeListeners.isEmpty()) - - // Dispatch onViewAttachedToWindow() - for (listener in mOnAttachStateChangeListeners) { - listener.onViewAttachedToWindow(view) - } - - assertFalse(fixture.onDrawListeners.isEmpty()) - assertIs(fixture.onDrawListeners[0]) - - // mOnAttachStateChangeListeners is automatically removed - assertTrue(mOnAttachStateChangeListeners.isEmpty()) - } - - @Test - fun `registerForNextDraw adds listener on sdk 26+`() { + fun `registerForNextDraw adds listener immediately`() { val view = fixture.getSut() // Immediately register an OnDrawListener to ViewTreeObserver diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt index 334ce229066..80184c2d643 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/SentryFrameMetricsCollectorTest.kt @@ -117,15 +117,6 @@ class SentryFrameMetricsCollectorTest { } } - @Test - fun `collector works only on api 24+`() { - val buildInfo = - mock { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.M) } - val collector = fixture.getSut(context, buildInfo) - val id = collector.startCollection(mock()) - assertNull(id) - } - @Test fun `collector works only if context is instance of Application`() { val collector = fixture.getSut(mock()) @@ -338,35 +329,6 @@ class SentryFrameMetricsCollectorTest { assertEquals(100, collector.getLastKnownFrameStartTimeNanos()) } - @Test - fun `collector reads frame start from choreographer field under version O`() { - val collector = fixture.getSut(context) - // Execute all posted tasks - Shadows.shadowOf(Looper.getMainLooper()).idle() - val listener = - collector.getProperty("frameMetricsAvailableListener") - val choreographer = collector.getProperty("choreographer") - choreographer.injectForField("mLastFrameTimeNanos", 100) - val frameMetrics = createMockFrameMetrics() - - var timesCalled = 0 - collector.startCollection { - frameStartNanos, - frameEndNanos, - durationNanos, - delayNanos, - isSlow, - isFrozen, - refreshRate -> - // The frame end is 100 (Choreographer.mLastFrameTimeNanos) plus frame duration - assertEquals(100 + durationNanos, frameEndNanos) - timesCalled++ - } - listener.onFrameMetricsAvailable(createMockWindow(), frameMetrics, 0) - // Assert the callback was called - assertEquals(1, timesCalled) - } - @Test fun `collector reads frame start from frameMetrics object on version O+`() { val buildInfo = @@ -611,9 +573,7 @@ class SentryFrameMetricsCollectorTest { @Test fun `getFramesDelay returns -1 when not available`() { - val buildInfo = - mock { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.M) } - val collector = fixture.getSut(context, buildInfo) + val collector = fixture.getSut(mock()) val result = collector.getFramesDelay(0, TimeUnit.SECONDS.toNanos(1)) assertEquals(-1.0, result.delaySeconds) diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 8b176ccd3be..dff85906ca1 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -44,8 +44,6 @@ android { lint { warningsAsErrors = true checkDependencies = true - // TODO(JAVA-547): Re-enable once the SDK_INT checks made obsolete by minSdk 26 are removed. - disable += "ObsoleteSdkInt" // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. checkReleaseBuilds = false diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 1132a966d74..1a92f9a0b36 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -55,8 +55,6 @@ android { lint { warningsAsErrors = true checkDependencies = true - // TODO(JAVA-547): Re-enable once the SDK_INT checks made obsolete by minSdk 26 are removed. - disable += "ObsoleteSdkInt" // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks. checkReleaseBuilds = false diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index 4d6614a7832..bca4f5796a3 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -3,7 +3,6 @@ package io.sentry.android.replay import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.os.Build import android.view.MotionEvent import io.sentry.Breadcrumb import io.sentry.DataCategory.All @@ -148,11 +147,6 @@ public class ReplayIntegration( override fun register(scopes: IScopes, options: SentryOptions) { this.options = options - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - options.logger.log(INFO, "Session replay is only supported on API 26 and above") - return - } - this.scopes = scopes recorder = recorderProvider?.invoke() diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index ce987c24ce8..1b354a27f0e 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay import android.annotation.SuppressLint -import android.annotation.TargetApi import android.content.Context import android.graphics.Bitmap import android.view.View @@ -23,7 +22,6 @@ import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.roundToInt @SuppressLint("UseKtx", "UseRequiresApi") -@TargetApi(26) internal class ScreenshotRecorder( val config: ScreenshotRecorderConfig, val options: SentryOptions, diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt index 19c61900889..c786f1e2a0f 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/WindowRecorder.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay import android.annotation.SuppressLint -import android.annotation.TargetApi import android.graphics.Point import android.os.Handler import android.os.HandlerThread @@ -22,7 +21,6 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.atomic.AtomicBoolean @SuppressLint("UseRequiresApi") -@TargetApi(26) internal class WindowRecorder( private val options: SentryOptions, private val screenshotRecorderCallback: ScreenshotRecorderCallback? = null, diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt index b6b8981f8ce..05d773aeb6a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay.capture import android.annotation.SuppressLint -import android.annotation.TargetApi import android.view.MotionEvent import io.sentry.Breadcrumb import io.sentry.DateUtils @@ -50,7 +49,6 @@ import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @SuppressLint("UseRequiresApi") -@TargetApi(26) internal abstract class BaseCaptureStrategy @Suppress("LongParameterList") constructor( diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt index 071acc19cae..81c30ca2540 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay.capture import android.annotation.SuppressLint -import android.annotation.TargetApi import android.graphics.Bitmap import android.view.MotionEvent import io.sentry.DataCategory.All @@ -39,7 +38,6 @@ import java.util.concurrent.ScheduledExecutorService * rate-limited, so the buffer stays warm for when the limit expires. */ @SuppressLint("UseRequiresApi") -@TargetApi(26) internal class BufferCaptureStrategy @Suppress("LongParameterList") constructor( diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt index 6956782cc27..b3d46108ecb 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt @@ -440,22 +440,18 @@ private class TextIgnoringDelegateCanvas : Canvas() { return delegate.clipRect(left, top, right, bottom) } - @RequiresApi(Build.VERSION_CODES.O) override fun clipOutRect(rect: RectF): Boolean { return delegate.clipOutRect(rect) } - @RequiresApi(Build.VERSION_CODES.O) override fun clipOutRect(rect: Rect): Boolean { return delegate.clipOutRect(rect) } - @RequiresApi(Build.VERSION_CODES.O) override fun clipOutRect(left: Float, top: Float, right: Float, bottom: Float): Boolean { return delegate.clipOutRect(left, top, right, bottom) } - @RequiresApi(Build.VERSION_CODES.O) override fun clipOutRect(left: Int, top: Int, right: Int, bottom: Int): Boolean { return delegate.clipOutRect(left, top, right, bottom) } @@ -469,7 +465,6 @@ private class TextIgnoringDelegateCanvas : Canvas() { return delegate.clipPath(path) } - @RequiresApi(Build.VERSION_CODES.O) override fun clipOutPath(path: Path): Boolean { return delegate.clipOutPath(path) } @@ -571,7 +566,6 @@ private class TextIgnoringDelegateCanvas : Canvas() { delegate.drawARGB(a, r, g, b) } - @RequiresApi(Build.VERSION_CODES.O) override fun drawBitmap(bitmap: Bitmap, left: Float, top: Float, paint: Paint?) { val sampledColor = sampleBitmapColor(bitmap, paint, null) solidPaint.setColor(sampledColor) @@ -579,7 +573,6 @@ private class TextIgnoringDelegateCanvas : Canvas() { delegate.drawRect(left, top, left + bitmap.width, top + bitmap.height, solidPaint) } - @RequiresApi(Build.VERSION_CODES.O) override fun drawBitmap(bitmap: Bitmap, src: Rect?, dst: RectF, paint: Paint?) { val sampledColor = sampleBitmapColor(bitmap, paint, src) solidPaint.setColor(sampledColor) @@ -587,7 +580,6 @@ private class TextIgnoringDelegateCanvas : Canvas() { delegate.drawRect(dst, solidPaint) } - @RequiresApi(Build.VERSION_CODES.O) override fun drawBitmap(bitmap: Bitmap, src: Rect?, dst: Rect, paint: Paint?) { val sampledColor = sampleBitmapColor(bitmap, paint, src) solidPaint.setColor(sampledColor) @@ -625,7 +617,6 @@ private class TextIgnoringDelegateCanvas : Canvas() { // not supported } - @RequiresApi(Build.VERSION_CODES.O) override fun drawBitmap(bitmap: Bitmap, matrix: Matrix, paint: Paint?) { val sampledColor = sampleBitmapColor(bitmap, paint, null) solidPaint.setColor(sampledColor) @@ -964,7 +955,6 @@ private class TextIgnoringDelegateCanvas : Canvas() { drawMaskedText(paint, x, y) } - @RequiresApi(Build.VERSION_CODES.O) private fun sampleBitmapColor(bitmap: Bitmap, paint: Paint?, src: Rect?): Int { if (bitmap.isRecycled) { return Color.BLACK diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt index 9de0bd3aab5..3de61735c94 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/util/Persistable.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay.util import android.annotation.SuppressLint -import android.annotation.TargetApi import io.sentry.ReplayRecording import io.sentry.SentryOptions import io.sentry.android.replay.ReplayCache @@ -15,7 +14,6 @@ import java.util.concurrent.ScheduledExecutorService // cpu @SuppressLint("UseRequiresApi") @Suppress("unused") -@TargetApi(26) internal class PersistableLinkedList( private val propertyName: String, private val options: SentryOptions, diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt index b1e3cd20513..072147cafcd 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt @@ -28,7 +28,6 @@ package io.sentry.android.replay.video import android.annotation.SuppressLint -import android.annotation.TargetApi import android.graphics.Bitmap import android.media.MediaCodec import android.media.MediaCodecInfo @@ -57,7 +56,6 @@ private const val TIMEOUT_USEC = 100_000L private const val MAX_EOS_STALL_ITERATIONS = 10 @SuppressLint("UseRequiresApi") -@TargetApi(26) internal class SimpleVideoEncoder( val options: SentryOptions, val muxerConfig: MuxerConfig, @@ -350,7 +348,6 @@ internal fun shouldUseSoftwareCanvas(): Boolean = Build.HARDWARE.contains("ranchu", ignoreCase = true) @SuppressLint("UseRequiresApi") -@TargetApi(24) internal data class MuxerConfig( val file: File, var recordingWidth: Int, diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt index 2e40144e2de..ec1ab680f8a 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt @@ -3,7 +3,6 @@ package io.sentry.android.replay.viewhierarchy import android.annotation.SuppressLint -import android.annotation.TargetApi import android.view.View import androidx.compose.ui.graphics.isUnspecified import androidx.compose.ui.graphics.toArgb @@ -36,7 +35,6 @@ import java.lang.ref.WeakReference import java.lang.reflect.Method @SuppressLint("UseRequiresApi") -@TargetApi(26) internal object ComposeViewHierarchyNode { private val getCollapsedSemanticsMethod: Method? by lazy(LazyThreadSafetyMode.NONE) { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt index e55ba659a8e..8e7ca2d8fe0 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ViewHierarchyNode.kt @@ -1,7 +1,6 @@ package io.sentry.android.replay.viewhierarchy import android.annotation.SuppressLint -import android.annotation.TargetApi import android.graphics.Rect import android.view.SurfaceView import android.view.View @@ -19,7 +18,6 @@ import io.sentry.android.replay.util.totalPaddingTopSafe import java.lang.ref.WeakReference @SuppressLint("UseRequiresApi") -@TargetApi(26) internal sealed class ViewHierarchyNode( val x: Float, val y: Float,