|
| 1 | +package io.sentry.android.core; |
| 2 | + |
| 3 | +import android.annotation.SuppressLint; |
| 4 | +import android.content.Context; |
| 5 | +import android.net.ConnectivityManager; |
| 6 | +import android.net.Network; |
| 7 | +import android.net.NetworkCapabilities; |
| 8 | +import android.os.Build; |
| 9 | +import androidx.annotation.NonNull; |
| 10 | +import androidx.annotation.RequiresApi; |
| 11 | +import io.sentry.Breadcrumb; |
| 12 | +import io.sentry.Hint; |
| 13 | +import io.sentry.IHub; |
| 14 | +import io.sentry.ILogger; |
| 15 | +import io.sentry.Integration; |
| 16 | +import io.sentry.SentryLevel; |
| 17 | +import io.sentry.SentryOptions; |
| 18 | +import io.sentry.android.core.internal.util.ConnectivityChecker; |
| 19 | +import io.sentry.util.Objects; |
| 20 | +import java.io.Closeable; |
| 21 | +import java.io.IOException; |
| 22 | +import org.jetbrains.annotations.NotNull; |
| 23 | +import org.jetbrains.annotations.Nullable; |
| 24 | +import org.jetbrains.annotations.TestOnly; |
| 25 | + |
| 26 | +public class NetworkBreadcrumbsIntegration implements Integration, Closeable { |
| 27 | + |
| 28 | + private final @NotNull Context context; |
| 29 | + |
| 30 | + private final @NotNull BuildInfoProvider buildInfoProvider; |
| 31 | + |
| 32 | + private final @NotNull ILogger logger; |
| 33 | + |
| 34 | + @TestOnly @Nullable NetworkBreadcrumbsNetworkCallback networkCallback; |
| 35 | + |
| 36 | + public NetworkBreadcrumbsIntegration( |
| 37 | + final @NotNull Context context, |
| 38 | + final @NotNull BuildInfoProvider buildInfoProvider, |
| 39 | + final @NotNull ILogger logger) { |
| 40 | + this.context = Objects.requireNonNull(context, "Context is required"); |
| 41 | + this.buildInfoProvider = |
| 42 | + Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); |
| 43 | + this.logger = Objects.requireNonNull(logger, "ILogger is required"); |
| 44 | + } |
| 45 | + |
| 46 | + @SuppressLint("NewApi") |
| 47 | + @Override |
| 48 | + public void register(final @NotNull IHub hub, final @NotNull SentryOptions options) { |
| 49 | + Objects.requireNonNull(hub, "Hub is required"); |
| 50 | + SentryAndroidOptions androidOptions = |
| 51 | + Objects.requireNonNull( |
| 52 | + (options instanceof SentryAndroidOptions) ? (SentryAndroidOptions) options : null, |
| 53 | + "SentryAndroidOptions is required"); |
| 54 | + |
| 55 | + logger.log( |
| 56 | + SentryLevel.DEBUG, |
| 57 | + "NetworkBreadcrumbsIntegration enabled: %s", |
| 58 | + androidOptions.isEnableNetworkEventBreadcrumbs()); |
| 59 | + |
| 60 | + if (androidOptions.isEnableNetworkEventBreadcrumbs()) { |
| 61 | + |
| 62 | + // The specific error is logged in the ConnectivityChecker method |
| 63 | + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) { |
| 64 | + networkCallback = null; |
| 65 | + logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration requires Android 5+"); |
| 66 | + return; |
| 67 | + } |
| 68 | + |
| 69 | + networkCallback = new NetworkBreadcrumbsNetworkCallback(hub, buildInfoProvider); |
| 70 | + final boolean registered = |
| 71 | + ConnectivityChecker.registerNetworkCallback( |
| 72 | + context, logger, buildInfoProvider, networkCallback); |
| 73 | + |
| 74 | + // The specific error is logged in the ConnectivityChecker method |
| 75 | + if (!registered) { |
| 76 | + networkCallback = null; |
| 77 | + logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration not installed."); |
| 78 | + return; |
| 79 | + } |
| 80 | + logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration installed."); |
| 81 | + addIntegrationToSdkVersion(); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + @Override |
| 86 | + public void close() throws IOException { |
| 87 | + if (networkCallback != null) { |
| 88 | + ConnectivityChecker.unregisterNetworkCallback( |
| 89 | + context, logger, buildInfoProvider, networkCallback); |
| 90 | + logger.log(SentryLevel.DEBUG, "NetworkBreadcrumbsIntegration remove."); |
| 91 | + } |
| 92 | + networkCallback = null; |
| 93 | + } |
| 94 | + |
| 95 | + @SuppressLint("ObsoleteSdkInt") |
| 96 | + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) |
| 97 | + static final class NetworkBreadcrumbsNetworkCallback extends ConnectivityManager.NetworkCallback { |
| 98 | + final @NotNull IHub hub; |
| 99 | + final @NotNull BuildInfoProvider buildInfoProvider; |
| 100 | + |
| 101 | + @Nullable Network currentNetwork = null; |
| 102 | + |
| 103 | + @Nullable NetworkCapabilities lastCapabilities = null; |
| 104 | + |
| 105 | + NetworkBreadcrumbsNetworkCallback( |
| 106 | + final @NotNull IHub hub, final @NotNull BuildInfoProvider buildInfoProvider) { |
| 107 | + this.hub = Objects.requireNonNull(hub, "Hub is required"); |
| 108 | + this.buildInfoProvider = |
| 109 | + Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); |
| 110 | + } |
| 111 | + |
| 112 | + @Override |
| 113 | + public void onAvailable(final @NonNull Network network) { |
| 114 | + if (network.equals(currentNetwork)) { |
| 115 | + return; |
| 116 | + } |
| 117 | + final Breadcrumb breadcrumb = createBreadcrumb("networkAvailable"); |
| 118 | + hub.addBreadcrumb(breadcrumb); |
| 119 | + currentNetwork = network; |
| 120 | + lastCapabilities = null; |
| 121 | + } |
| 122 | + |
| 123 | + @Override |
| 124 | + public void onCapabilitiesChanged( |
| 125 | + final @NonNull Network network, final @NonNull NetworkCapabilities networkCapabilities) { |
| 126 | + if (!network.equals(currentNetwork)) { |
| 127 | + return; |
| 128 | + } |
| 129 | + final @Nullable NetworkBreadcrumbConnectionDetail connectionDetail = |
| 130 | + getNewConnectionDetails(lastCapabilities, networkCapabilities); |
| 131 | + if (connectionDetail == null) { |
| 132 | + return; |
| 133 | + } |
| 134 | + lastCapabilities = networkCapabilities; |
| 135 | + final Breadcrumb breadcrumb = createBreadcrumb("networkCapabilitiesChanged"); |
| 136 | + Hint hint = new Hint(); |
| 137 | + hint.set("data", connectionDetail); |
| 138 | + hub.addBreadcrumb(breadcrumb, hint); |
| 139 | + } |
| 140 | + |
| 141 | + @Override |
| 142 | + public void onLost(final @NonNull Network network) { |
| 143 | + if (!network.equals(currentNetwork)) { |
| 144 | + return; |
| 145 | + } |
| 146 | + final Breadcrumb breadcrumb = createBreadcrumb("networkLost"); |
| 147 | + hub.addBreadcrumb(breadcrumb); |
| 148 | + currentNetwork = null; |
| 149 | + lastCapabilities = null; |
| 150 | + } |
| 151 | + |
| 152 | + private Breadcrumb createBreadcrumb(String action) { |
| 153 | + final Breadcrumb breadcrumb = new Breadcrumb(); |
| 154 | + breadcrumb.setType("system"); |
| 155 | + breadcrumb.setCategory("network.event"); |
| 156 | + breadcrumb.setData("action", action); |
| 157 | + breadcrumb.setLevel(SentryLevel.INFO); |
| 158 | + return breadcrumb; |
| 159 | + } |
| 160 | + |
| 161 | + private @Nullable NetworkBreadcrumbConnectionDetail getNewConnectionDetails( |
| 162 | + final @Nullable NetworkCapabilities oldCapabilities, |
| 163 | + final @NotNull NetworkCapabilities newCapabilities) { |
| 164 | + if (oldCapabilities == null) { |
| 165 | + return new NetworkBreadcrumbConnectionDetail(newCapabilities, buildInfoProvider); |
| 166 | + } |
| 167 | + NetworkBreadcrumbConnectionDetail oldConnectionDetails = |
| 168 | + new NetworkBreadcrumbConnectionDetail(oldCapabilities, buildInfoProvider); |
| 169 | + NetworkBreadcrumbConnectionDetail newConnectionDetails = |
| 170 | + new NetworkBreadcrumbConnectionDetail(newCapabilities, buildInfoProvider); |
| 171 | + |
| 172 | + // We compare the details and if they are similar we return null, so that we don't spam the |
| 173 | + // user with lots of breadcrumbs for e.g. an increase of signal strength of 1 point |
| 174 | + if (newConnectionDetails.isSimilar(oldConnectionDetails)) { |
| 175 | + return null; |
| 176 | + } |
| 177 | + return newConnectionDetails; |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | + static class NetworkBreadcrumbConnectionDetail { |
| 182 | + final int downBandwidth, upBandwidth, signalStrength; |
| 183 | + final boolean isVpn; |
| 184 | + final @NotNull String type; |
| 185 | + |
| 186 | + @SuppressLint({"NewApi", "ObsoleteSdkInt"}) |
| 187 | + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) |
| 188 | + NetworkBreadcrumbConnectionDetail( |
| 189 | + final @NotNull NetworkCapabilities networkCapabilities, |
| 190 | + final @NotNull BuildInfoProvider buildInfoProvider) { |
| 191 | + Objects.requireNonNull(networkCapabilities, "NetworkCapabilities is required"); |
| 192 | + Objects.requireNonNull(buildInfoProvider, "BuildInfoProvider is required"); |
| 193 | + this.downBandwidth = networkCapabilities.getLinkDownstreamBandwidthKbps(); |
| 194 | + this.upBandwidth = networkCapabilities.getLinkUpstreamBandwidthKbps(); |
| 195 | + int strength = |
| 196 | + buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.Q |
| 197 | + ? networkCapabilities.getSignalStrength() |
| 198 | + : 0; |
| 199 | + // If the system reports a signalStrength of Integer.MIN_VALUE, we adjust it to be 0 |
| 200 | + this.signalStrength = strength > -100 ? strength : 0; |
| 201 | + this.isVpn = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN); |
| 202 | + String connectionType = |
| 203 | + ConnectivityChecker.getConnectionType(networkCapabilities, buildInfoProvider); |
| 204 | + this.type = connectionType != null ? connectionType : ""; |
| 205 | + } |
| 206 | + |
| 207 | + /** |
| 208 | + * Compares this connection detail to another one. |
| 209 | + * |
| 210 | + * @param other The other NetworkBreadcrumbConnectionDetail to compare |
| 211 | + * @return true if the details are similar enough, false otherwise |
| 212 | + */ |
| 213 | + boolean isSimilar(final @NotNull NetworkBreadcrumbConnectionDetail other) { |
| 214 | + return isVpn == other.isVpn |
| 215 | + && type.equals(other.type) |
| 216 | + && (-5 <= signalStrength - other.signalStrength |
| 217 | + && signalStrength - other.signalStrength <= 5) |
| 218 | + && (-1000 <= downBandwidth - other.downBandwidth |
| 219 | + && downBandwidth - other.downBandwidth <= 1000) |
| 220 | + && (-1000 <= upBandwidth - other.upBandwidth && upBandwidth - other.upBandwidth <= 1000); |
| 221 | + } |
| 222 | + } |
| 223 | +} |
0 commit comments