Skip to content
88 changes: 88 additions & 0 deletions instrumentation/pans/api/pans.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
public final class io/opentelemetry/android/instrumentation/pans/AppNetworkUsage {
public fun <init> (Ljava/lang/String;ILjava/lang/String;JJLio/opentelemetry/api/common/Attributes;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()I
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()J
public final fun component5 ()J
public final fun component6 ()Lio/opentelemetry/api/common/Attributes;
public final fun copy (Ljava/lang/String;ILjava/lang/String;JJLio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/android/instrumentation/pans/AppNetworkUsage;
public static synthetic fun copy$default (Lio/opentelemetry/android/instrumentation/pans/AppNetworkUsage;Ljava/lang/String;ILjava/lang/String;JJLio/opentelemetry/api/common/Attributes;ILjava/lang/Object;)Lio/opentelemetry/android/instrumentation/pans/AppNetworkUsage;
public fun equals (Ljava/lang/Object;)Z
public final fun getAttributes ()Lio/opentelemetry/api/common/Attributes;
public final fun getBytesReceived ()J
public final fun getBytesTransmitted ()J
public final fun getNetworkType ()Ljava/lang/String;
public final fun getPackageName ()Ljava/lang/String;
public final fun getUid ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/opentelemetry/android/instrumentation/pans/NetworkAvailability {
public fun <init> (Ljava/lang/String;ZILio/opentelemetry/api/common/Attributes;)V
public synthetic fun <init> (Ljava/lang/String;ZILio/opentelemetry/api/common/Attributes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Z
public final fun component3 ()I
public final fun component4 ()Lio/opentelemetry/api/common/Attributes;
public final fun copy (Ljava/lang/String;ZILio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/android/instrumentation/pans/NetworkAvailability;
public static synthetic fun copy$default (Lio/opentelemetry/android/instrumentation/pans/NetworkAvailability;Ljava/lang/String;ZILio/opentelemetry/api/common/Attributes;ILjava/lang/Object;)Lio/opentelemetry/android/instrumentation/pans/NetworkAvailability;
public fun equals (Ljava/lang/Object;)Z
public final fun getAttributes ()Lio/opentelemetry/api/common/Attributes;
public final fun getNetworkType ()Ljava/lang/String;
public final fun getSignalStrength ()I
public fun hashCode ()I
public final fun isAvailable ()Z
public fun toString ()Ljava/lang/String;
}

public final class io/opentelemetry/android/instrumentation/pans/PANSMetrics {
public fun <init> ()V
public fun <init> (Ljava/util/List;Ljava/util/List;Ljava/util/List;)V
public synthetic fun <init> (Ljava/util/List;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/util/List;
public final fun component3 ()Ljava/util/List;
public final fun copy (Ljava/util/List;Ljava/util/List;Ljava/util/List;)Lio/opentelemetry/android/instrumentation/pans/PANSMetrics;
public static synthetic fun copy$default (Lio/opentelemetry/android/instrumentation/pans/PANSMetrics;Ljava/util/List;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lio/opentelemetry/android/instrumentation/pans/PANSMetrics;
public fun equals (Ljava/lang/Object;)Z
public final fun getAppNetworkUsage ()Ljava/util/List;
public final fun getNetworkAvailability ()Ljava/util/List;
public final fun getPreferenceChanges ()Ljava/util/List;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/opentelemetry/android/instrumentation/pans/PansInstrumentation : io/opentelemetry/android/instrumentation/AndroidInstrumentation {
public static final field Companion Lio/opentelemetry/android/instrumentation/pans/PansInstrumentation$Companion;
public fun <init> ()V
public fun getName ()Ljava/lang/String;
public fun install (Lio/opentelemetry/android/instrumentation/InstallationContext;)V
}

public final class io/opentelemetry/android/instrumentation/pans/PansInstrumentation$Companion {
}

public final class io/opentelemetry/android/instrumentation/pans/PreferenceChange {
public fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JLio/opentelemetry/api/common/Attributes;)V
public synthetic fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JLio/opentelemetry/api/common/Attributes;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()I
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()J
public final fun component6 ()Lio/opentelemetry/api/common/Attributes;
public final fun copy (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JLio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/android/instrumentation/pans/PreferenceChange;
public static synthetic fun copy$default (Lio/opentelemetry/android/instrumentation/pans/PreferenceChange;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;JLio/opentelemetry/api/common/Attributes;ILjava/lang/Object;)Lio/opentelemetry/android/instrumentation/pans/PreferenceChange;
public fun equals (Ljava/lang/Object;)Z
public final fun getAttributes ()Lio/opentelemetry/api/common/Attributes;
public final fun getNewPreference ()Ljava/lang/String;
public final fun getOldPreference ()Ljava/lang/String;
public final fun getPackageName ()Ljava/lang/String;
public final fun getTimestamp ()J
public final fun getUid ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

71 changes: 71 additions & 0 deletions instrumentation/pans/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
plugins {
id("otel.android-library-conventions")
id("otel.publish-conventions")
id("jacoco")
}

description = "OpenTelemetry Android PANS (Per-Application Network Selection) instrumentation"

android {
namespace = "io.opentelemetry.android.instrumentation.pans"

defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}

testOptions {
unitTests.isReturnDefaultValues = true
unitTests.isIncludeAndroidResources = true
}
}

dependencies {
api(platform(libs.opentelemetry.platform.alpha)) // Required for sonatype publishing
implementation(project(":instrumentation:android-instrumentation"))
implementation(project(":services"))
implementation(project(":common"))
implementation(project(":agent-api"))
implementation(libs.androidx.core)
implementation(libs.opentelemetry.semconv.incubating)
implementation(libs.opentelemetry.sdk)
implementation(libs.opentelemetry.instrumentation.api)
implementation(libs.auto.service.annotations)

ksp(libs.auto.service.processor)

testImplementation(project(":test-common"))
testImplementation(project(":session"))
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
testImplementation(libs.mockk)
}

// Jacoco coverage configuration
jacoco {
toolVersion = "0.8.8"
}

tasks.register("jacocoTestReport") {
dependsOn("testDebugUnitTest")

doLast {
println("✅ Jacoco Test Report Generated")
println("📊 Coverage Report Location: build/reports/coverage/")
}
}

// Task to check coverage
tasks.register("checkCoverage") {
dependsOn("jacocoTestReport")

doLast {
println(
"""
╔════════════════════════════════════════════════════════════════╗
║ PANS INSTRUMENTATION TEST COVERAGE ║
║ Target: 80% Coverage ║
╚════════════════════════════════════════════════════════════════╝
""".trimIndent(),
)
}
}
4 changes: 4 additions & 0 deletions instrumentation/pans/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Keep the PANS instrumentation classes
-keep class io.opentelemetry.android.instrumentation.pans.** { *; }
-keepnames class io.opentelemetry.android.instrumentation.pans.** { *; }

17 changes: 17 additions & 0 deletions instrumentation/pans/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- Required to access network statistics data -->
<!-- This permission is intended for system/automotive apps -->
Comment on lines +5 to +6
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states that PACKAGE_USAGE_STATS permission is "intended for system/automotive apps" (line 6), but the PR description doesn't specify that this instrumentation is only for system/automotive apps.

This is a protected permission that normal apps cannot obtain through runtime permission requests. This should be clearly documented in the module's README or main documentation that:

  1. This permission requires special privileges
  2. The instrumentation will have limited functionality on regular consumer apps
  3. It's primarily intended for system-level or automotive apps

This is important for setting correct expectations for users of this library.

Suggested change
<!-- Required to access network statistics data -->
<!-- This permission is intended for system/automotive apps -->
<!--
Required to access network statistics data.
NOTE: The PACKAGE_USAGE_STATS permission is a protected permission.
- Normal consumer apps cannot obtain this permission via runtime requests.
- This instrumentation will have limited functionality on regular consumer apps.
- It is primarily intended for system-level or automotive apps.
Please see the module's README for more details and requirements.
-->

Copilot uses AI. Check for mistakes.
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />

<!-- Required to check current network state and changes -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Required for internet connectivity -->
<uses-permission android:name="android.permission.INTERNET" />

</manifest>

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation.pans

import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat

/**
* Wrapper around Android's ConnectivityManager for monitoring network state and preferences.
* This class provides utilities to detect available networks and their capabilities.
* Note: Most methods require API level 23+ for proper functionality.
*/
@RequiresApi(23)
internal class ConnectivityManagerWrapper(
private val context: Context,
) {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager

/**
* Checks if a specific network capability is available.
* OEM_PAID and OEM_PRIVATE are network capabilities that indicate OEM-managed networks.
*/
fun hasNetworkCapability(capabilityType: Int): Boolean {
return try {
val network = connectivityManager?.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
capabilities.hasCapability(capabilityType)
} catch (e: Exception) {
Log.w(TAG, "Error checking network capability: $capabilityType", e)
false
}
}

/**
* Gets all available networks with their capabilities.
*/
Comment thread
namanONcode marked this conversation as resolved.
@android.annotation.SuppressLint("WrongConstant")
fun getAvailableNetworks(): List<NetworkInfo> {
val networks = mutableListOf<NetworkInfo>()
return try {
val allNetworks = connectivityManager?.allNetworks ?: return networks
allNetworks.forEach { network ->
try {
val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities != null) {
networks.add(
NetworkInfo(
isOemPaid = capabilities.hasCapability(CAP_OEM_PAID),
isOemPrivate = capabilities.hasCapability(CAP_OEM_PRIVATE),
isMetered = !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED),
isConnected = isNetworkConnected(network),
),
)
}
} catch (e: Exception) {
Log.w(TAG, "Error getting network capabilities", e)
}
}
networks
} catch (e: Exception) {
Log.e(TAG, "Error getting available networks", e)
networks
}
}

/**
* Checks if a specific network is currently connected.
*/
fun isNetworkConnected(network: android.net.Network): Boolean =
try {
val capabilities = connectivityManager?.getNetworkCapabilities(network)
capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
} catch (e: Exception) {
Log.w(TAG, "Error checking network connection", e)
false
}

/**
* Gets the active network or null if none is active.
*/
fun getActiveNetwork(): android.net.Network? =
try {
connectivityManager?.activeNetwork
} catch (e: Exception) {
Log.w(TAG, "Error getting active network", e)
null
}

/**
* Checks if ACCESS_NETWORK_STATE permission is granted.
*/
fun hasAccessNetworkStatePermission(): Boolean =
ContextCompat.checkSelfPermission(
context,
"android.permission.ACCESS_NETWORK_STATE",
) == PackageManager.PERMISSION_GRANTED

data class NetworkInfo(
val isOemPaid: Boolean = false,
val isOemPrivate: Boolean = false,
val isMetered: Boolean = false,
val isConnected: Boolean = false,
)

companion object {
private const val TAG = "ConnMgrWrapper"

// Network capability constants for OEM networks
// These are defined as constants to support various Android versions
private const val CAP_OEM_PAID = 19 // NET_CAPABILITY_OEM_PAID
private const val CAP_OEM_PRIVATE = 20 // NET_CAPABILITY_OEM_PRIVATE
}
}
Loading