Skip to content
Open
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
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;
}

70 changes: 70 additions & 0 deletions instrumentation/pans/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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(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.
*/
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 suppression annotation @android.annotation.SuppressLint("WrongConstant") on line 46 suggests that constants CAP_OEM_PAID (19) and CAP_OEM_PRIVATE (20) might not be recognized as valid network capability constants by the linter.

While this may be intentional for forward compatibility, it should be documented why these constants are being used and on which API levels they're actually valid. The comment on line 117-120 provides some context but doesn't explain the SuppressLint annotation or potential compatibility issues.

Suggested change
*/
*/
// SuppressLint("WrongConstant") is used here because CAP_OEM_PAID (19) and CAP_OEM_PRIVATE (20)
// are not recognized as valid network capability constants by the linter. These constants are
// defined for forward compatibility with Android API 31+ (Android 12), where they represent
// OEM-managed network capabilities. On earlier API levels, these values may not be supported,
// but the code is protected by try/catch blocks and @RequiresApi(23). Developers should be
// aware that use of these constants may not be valid on all Android versions.

Copilot uses AI. Check for mistakes.
@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