Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Report the cellular network technology generation in `device.connection_effective_type`, for example `4g` or `5g` ([#6146](https://github.com/getsentry/sentry-java/pull/6146))

## 8.59.0

### Breaking Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
import android.os.SystemClock;
import android.util.DisplayMetrics;
import io.sentry.DateUtils;
import io.sentry.IConnectionStatusProvider;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.android.core.internal.util.AndroidConnectionStatusProvider;
import io.sentry.android.core.internal.util.CpuInfoUtils;
import io.sentry.android.core.internal.util.DeviceOrientations;
import io.sentry.android.core.internal.util.NetworkConnection;
import io.sentry.android.core.internal.util.RootChecker;
import io.sentry.protocol.Device;
import io.sentry.protocol.OperatingSystem;
Expand Down Expand Up @@ -228,8 +231,19 @@ private void setDeviceIO(
}

if (device.getConnectionType() == null) {
// wifi, ethernet or cellular, null if none
device.setConnectionType(options.getConnectionStatusProvider().getConnectionType());
final @NotNull IConnectionStatusProvider connectionStatusProvider =
options.getConnectionStatusProvider();
if (connectionStatusProvider instanceof AndroidConnectionStatusProvider) {
// Reading both at once keeps the type and the technology describing the same network.
final NetworkConnection connection =
((AndroidConnectionStatusProvider) connectionStatusProvider).getConnection();
// wifi, ethernet or cellular, null if none
device.setConnectionType(connection.type);
// 2g, 3g, 4g or 5g, null unless the device is on a known cellular technology
device.setConnectionEffectiveType(connection.effectiveType);
} else {
device.setConnectionType(connectionStatusProvider.getConnectionType());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public final class AndroidConnectionStatusProvider
private final @NotNull BuildInfoProvider buildInfoProvider;
private final @NotNull MonotonicTicker ticker;
private final @NotNull List<IConnectionStatusObserver> connectionStatusObservers;
private final @NotNull CellularNetworkTechnologyProvider cellularNetworkTechnologyProvider;
private final @Nullable Handler handler;
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
private volatile @Nullable NetworkCallback networkCallback;
Expand Down Expand Up @@ -94,6 +95,8 @@ public AndroidConnectionStatusProvider(
this.cacheFreshUntil = Deadline.passed(ticker);
this.handler = handler;
this.connectionStatusObservers = new ArrayList<>();
this.cellularNetworkTechnologyProvider =
new CellularNetworkTechnologyProvider(this.context, options.getLogger(), buildInfoProvider);

capabilities[0] = NetworkCapabilities.NET_CAPABILITY_INTERNET;
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.M) {
Expand Down Expand Up @@ -170,6 +173,24 @@ private boolean isNetworkEffectivelyConnected(
return getConnectionType(context, options.getLogger(), buildInfoProvider);
}

/**
* The connection type and, for cellular connections, the generation of the network technology.
*
* <p>Both are derived from one cache read, so they always describe the same network instead of
* straddling a connectivity change.
*/
public @NotNull NetworkConnection getConnection() {
if (!isCacheValid()) {
updateCache(null);
}
final @Nullable String connectionType = getConnectionTypeFromCache();
if (!"cellular".equals(connectionType)) {
return new NetworkConnection(connectionType, null);
}
return new NetworkConnection(
connectionType, cellularNetworkTechnologyProvider.getCellularNetworkTechnology());
}

private void ensureNetworkCallbackRegistered() {
if (!ContextUtils.isForegroundImportance()) {
return;
Expand Down Expand Up @@ -344,6 +365,9 @@ private boolean hasSignificantTransportChanges(
if (registerNetworkCallback(
context, options.getLogger(), buildInfoProvider, handler, callback)) {
networkCallback = callback;
// Only start listening once the network callback is registered, because unregistering is
// skipped while there is no network callback, which would leave the listener running.
cellularNetworkTechnologyProvider.register();
options.getLogger().log(SentryLevel.DEBUG, "Network callback registered successfully");
} else {
options.getLogger().log(SentryLevel.WARNING, "Failed to register network callback");
Expand Down Expand Up @@ -459,6 +483,7 @@ private void unregisterNetworkCallback(final boolean clearObservers) {
if (callbackRef != null) {
unregisterNetworkCallback(context, options.getLogger(), callbackRef);
}
cellularNetworkTechnologyProvider.unregister();
// Clear cached state
cachedNetworkCapabilities = null;
currentNetwork = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package io.sentry.android.core.internal.util;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import io.sentry.ILogger;
import io.sentry.SentryLevel;
import io.sentry.android.core.BuildInfoProvider;
import io.sentry.android.core.ContextUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Reports the generation of the cellular network technology the device currently uses for data, for
* example {@code 4g} or {@code 5g}.
*
* <p>From Android 12 (API 31) the value comes from {@link TelephonyCallback.DisplayInfoListener},
* which requires no permission and reports what the device shows to the user. On older versions it
* is read from {@link TelephonyManager#getDataNetworkType()}, which the SDK only calls when the
* hosting app already holds one of the required permissions. The SDK never declares those
* permissions itself, so apps without them simply get no technology reported.
*/
@ApiStatus.Internal
public final class CellularNetworkTechnologyProvider {

public static final @NotNull String GENERATION_2G = "2g";
public static final @NotNull String GENERATION_3G = "3g";
public static final @NotNull String GENERATION_4G = "4g";
public static final @NotNull String GENERATION_5G = "5g";

private final @NotNull Context context;
private final @NotNull ILogger logger;
private final @NotNull BuildInfoProvider buildInfoProvider;

/** Guards the registration of the display info listener. */
private final @NotNull Object registrationLock = new Object();

/**
* Set from {@link TelephonyCallback.DisplayInfoListener} on API 31 and above. Declared as {@link
* Object} so that loading this class on older devices never has to resolve a type that does not
* exist there.
*/
private volatile @Nullable Object displayInfoCallback;

private volatile @Nullable String displayInfoTechnology;

public CellularNetworkTechnologyProvider(
final @NotNull Context context,
final @NotNull ILogger logger,
final @NotNull BuildInfoProvider buildInfoProvider) {
this.context = ContextUtils.getApplicationContext(context);
this.logger = logger;
this.buildInfoProvider = buildInfoProvider;
}

/**
* The generation of the cellular network technology currently used for data, or {@code null} when
* it is unknown.
*/
public @Nullable String getCellularNetworkTechnology() {
if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
return displayInfoTechnology;
}
return getDataNetworkTechnology();
}
Comment thread
cursor[bot] marked this conversation as resolved.

/**
* Starts listening for display info changes on API 31 and above. Does nothing on older versions,
* where the technology is read on demand instead.
*/
@SuppressLint("NewApi")
public void register() {
if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.S) {
return;
}
// Checking and storing the callback has to be atomic, otherwise concurrent callers can each
// register a listener while only the last one is kept and can ever be unregistered.
synchronized (registrationLock) {
if (displayInfoCallback != null) {
return;
}
final @Nullable TelephonyManager telephonyManager = getTelephonyManager();
if (telephonyManager == null) {
return;
}
try {
final @NotNull DisplayInfoCallback callback = new DisplayInfoCallback(this);
// The callback maps the display info to a generation and stores it, so it runs on the
// binder thread the telephony framework calls it on. An executor of the SDK would add a
// reference that goes stale: the options hold a no-op executor until
// SentryOptions.activate replaces it, and a second SentryAndroid.init shuts the previous
// one down.
telephonyManager.registerTelephonyCallback(Runnable::run, callback);
displayInfoCallback = callback;
logger.log(SentryLevel.DEBUG, "Started listening for cellular network technology changes.");
Comment thread
sentry[bot] marked this conversation as resolved.
} catch (SecurityException | IllegalStateException | UnsupportedOperationException e) {
// Devices without telephony support and processes that are not allowed to listen throw
// here, and the technology is optional data.
logger.log(
SentryLevel.INFO, "Could not listen for cellular network technology changes.", e);
}
}
}

/**
* Stops listening for display info changes.
*
* <p>The last reported technology is kept. Monitoring stops when the app goes to the background,
* and from API 31 on the listener is the only source, so discarding it would leave every
* background event without a technology even though the connection type still resolves. It is
* only ever paired with a connection that is cellular at the time the event is captured.
*/
@SuppressLint("NewApi")
public void unregister() {
synchronized (registrationLock) {
final @Nullable Object callback = displayInfoCallback;
if (callback == null) {
return;
}
final @Nullable TelephonyManager telephonyManager = getTelephonyManager();
if (telephonyManager == null) {
// Keep the reference so a later unregister can still reach the registered callback.
return;
}
Comment thread
sentry[bot] marked this conversation as resolved.
try {
telephonyManager.unregisterTelephonyCallback((TelephonyCallback) callback);
displayInfoCallback = null;
logger.log(SentryLevel.DEBUG, "Stopped listening for cellular network technology changes.");
} catch (SecurityException | IllegalStateException | UnsupportedOperationException e) {
logger.log(
SentryLevel.INFO,
"Could not stop listening for cellular network technology changes.",
e);
}
}
}

@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.
if (!Permissions.hasPermission(context, Manifest.permission.READ_PHONE_STATE)
&& !Permissions.hasPermission(context, Manifest.permission.READ_BASIC_PHONE_STATE)) {
return null;
}
final @Nullable TelephonyManager telephonyManager = getTelephonyManager();
if (telephonyManager == null) {
return null;
}
try {
return networkTypeToGeneration(telephonyManager.getDataNetworkType());
} catch (SecurityException | UnsupportedOperationException e) {
Comment thread
alwx marked this conversation as resolved.
// The permission can be revoked between the check above and the call, and devices without
// telephony hardware throw UnsupportedOperationException from API 36 on.
logger.log(SentryLevel.INFO, "Could not retrieve the cellular network technology.", e);
return null;
}
}

private @Nullable TelephonyManager getTelephonyManager() {
final @Nullable Object service = context.getSystemService(Context.TELEPHONY_SERVICE);
if (!(service instanceof TelephonyManager)) {
logger.log(SentryLevel.INFO, "TelephonyManager is not available.");
return null;
}
return (TelephonyManager) service;
}

/**
* Maps a {@link TelephonyDisplayInfo} to a generation, preferring the override type so that a
* device showing 5G to the user is also reported as 5G.
*/
@RequiresApi(api = Build.VERSION_CODES.S)
@SuppressWarnings("deprecation")
static @Nullable String toGeneration(final @NotNull TelephonyDisplayInfo displayInfo) {
switch (displayInfo.getOverrideNetworkType()) {
case TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA:
case TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED:
// Deprecated in favour of NR_ADVANCED, but still reported on Android 11. Its underlying
// network type is LTE, so without this case a 5G mmWave connection reports 4g.
case TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE:
return GENERATION_5G;
case TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA:
case TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO:
return GENERATION_4G;
default:
return networkTypeToGeneration(displayInfo.getNetworkType());
}
}
Comment thread
sentry[bot] marked this conversation as resolved.

/** Maps a {@code TelephonyManager.NETWORK_TYPE_*} constant to a generation. */
@SuppressWarnings("deprecation")
static @Nullable String networkTypeToGeneration(final int networkType) {
switch (networkType) {
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_IDEN:
case TelephonyManager.NETWORK_TYPE_GSM:
return GENERATION_2G;
case TelephonyManager.NETWORK_TYPE_UMTS:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
case TelephonyManager.NETWORK_TYPE_EHRPD:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_HSPAP:
case TelephonyManager.NETWORK_TYPE_TD_SCDMA:
return GENERATION_3G;
case TelephonyManager.NETWORK_TYPE_LTE:
case TelephonyManager.NETWORK_TYPE_IWLAN:
return GENERATION_4G;
case TelephonyManager.NETWORK_TYPE_NR:
return GENERATION_5G;
default:
return null;
}
}

/** Caches the display info so that reading the technology never blocks on telephony. */
@RequiresApi(api = Build.VERSION_CODES.S)
private static final class DisplayInfoCallback extends TelephonyCallback
implements TelephonyCallback.DisplayInfoListener {

private final @NotNull CellularNetworkTechnologyProvider provider;

DisplayInfoCallback(final @NotNull CellularNetworkTechnologyProvider provider) {
this.provider = provider;
}

@Override
public void onDisplayInfoChanged(final @NonNull TelephonyDisplayInfo telephonyDisplayInfo) {
provider.displayInfoTechnology = toGeneration(telephonyDisplayInfo);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.sentry.android.core.internal.util;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

/** The connection type and the generation of the cellular network technology, if any. */
@ApiStatus.Internal
public final class NetworkConnection {
/** Maps to the {@code network.connection.type} attribute, {@code null} when unknown. */
public final @Nullable String type;

/**
* Maps to the {@code network.connection.effective_type} attribute, for example {@code 5g}. {@code
* null} when the connection is not cellular or the technology is unknown.
*/
public final @Nullable String effectiveType;

NetworkConnection(final @Nullable String type, final @Nullable String effectiveType) {
this.type = type;
this.effectiveType = effectiveType;
}
}
Loading
Loading