From 04da2335a9c5a2e09ceba54353478a11a6fbbef2 Mon Sep 17 00:00:00 2001 From: swatijha Date: Thu, 5 Feb 2026 11:47:11 +0530 Subject: [PATCH 01/18] NR-517762: feat: add toggle feature for QOE --- .../videoagent/core/config/NRVideoConfig.java | 68 +++++++++++++++++++ .../core/tracker/NRVideoTracker.java | 4 +- app/build.gradle | 2 + .../newrelic/nrvideoproject/MainActivity.java | 3 + local.properties | 9 ++- 5 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java new file mode 100644 index 00000000..23b85095 --- /dev/null +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java @@ -0,0 +1,68 @@ +package com.newrelic.videoagent.core.config; + +/** + * Configuration manager for New Relic Video Agent functionality. + * Default values are sourced from local.properties, can be overridden by client configuration. + */ +public class NRVideoConfig { + + private static final NRVideoConfig INSTANCE = new NRVideoConfig(); + + // Configuration value - set from local.properties via BuildConfig + private boolean qoeAggregate; + private boolean initialized = false; + + private NRVideoConfig() { + } + + /** + * Get the singleton instance of NRVideoConfig + * @return NRVideoConfig instance + */ + public static NRVideoConfig getInstance() { + return INSTANCE; + } + + /** + * Check if QOE_AGGREGATE events should be sent during harvest cycles + * @return true if QOE_AGGREGATE should be sent, false otherwise + */ + public boolean isQoeAggregateEnabled() { + if (!initialized) { + throw new IllegalStateException("NRVideoConfig not initialized! Call initializeDefaults() first."); + } + return qoeAggregate; + } + + /** + * Set whether QOE_AGGREGATE events should be sent during harvest cycles + * This will be called with client configuration in the future + * @param enabled true to enable QOE_AGGREGATE, false to disable + */ + public void setQoeAggregateEnabled(boolean enabled) { + this.qoeAggregate = enabled; + } + + /** + * Initialize configuration with default values from local.properties + * This method should be called during app initialization + * @param defaultQoeAggregateEnabled Default value for QOE aggregate from BuildConfig + */ + public void initializeDefaults(boolean defaultQoeAggregateEnabled) { + if (!initialized) { + this.qoeAggregate = defaultQoeAggregateEnabled; + this.initialized = true; + } + } + + /** + * Initialize configuration with client settings + * @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided) + */ + public void initializeFromClient(Boolean clientQoeAggregateEnabled) { + // If client provides a value, use it; otherwise keep current default + if (clientQoeAggregateEnabled != null) { + this.qoeAggregate = clientQoeAggregateEnabled; + } + } +} \ No newline at end of file diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index 12107320..97a49bef 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -3,6 +3,7 @@ import android.os.Handler; import com.newrelic.videoagent.core.NRVideo; +import com.newrelic.videoagent.core.config.NRVideoConfig; import com.newrelic.videoagent.core.model.NRTimeSince; import com.newrelic.videoagent.core.model.NRTrackerState; import com.newrelic.videoagent.core.utils.NRLog; @@ -587,7 +588,8 @@ private void resetHarvestCycleFlags() { * Send QOE_AGGREGATE event (internal method - called once per cycle) */ private void sendQoeAggregate() { - if (!state.isAd) { // Only send for content, not ads + if (!state.isAd && NRVideoConfig.getInstance().isQoeAggregateEnabled()) { + // Only send for content, not ads, and only if QOE aggregate is enabled Map kpiAttributes = calculateQOEKpiAttributes(); sendVideoEvent(QOE_AGGREGATE, kpiAttributes); qoeAggregateAlreadySent = true; diff --git a/app/build.gradle b/app/build.gradle index 2ae73439..d761b31a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,12 +10,14 @@ if (localPropertiesFile.exists()) { } } def applicationToken = localProperties.getProperty('nr.applicationToken', "") +def qoeAggregateEnabled = localProperties.getProperty('nr.qoeAggregate', "true").toBoolean() android { compileSdkVersion 34 buildToolsVersion "34.0.0" defaultConfig { buildConfigField "String", "NR_APPLICATION_TOKEN", "\"${applicationToken}\"" + buildConfigField "boolean", "QOE_AGGREGATE_DEFAULT", "${qoeAggregateEnabled}" applicationId "com.newrelic.nrvideoproject" minSdkVersion 16 targetSdkVersion 34 diff --git a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java index 008e88d3..3d4a8e93 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java @@ -8,6 +8,7 @@ import com.newrelic.videoagent.core.NRVideo; import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.config.NRVideoConfig; import java.util.HashMap; import java.util.Map; @@ -26,6 +27,8 @@ protected void onCreate(Bundle savedInstanceState) { .enableLogging() .build(); NRVideo.newBuilder(getApplicationContext()).withConfiguration(config).build(); + // Initialize QOE configuration with value from local.properties + NRVideoConfig.getInstance().initializeDefaults(BuildConfig.QOE_AGGREGATE_DEFAULT); setContentView(R.layout.activity_main); adsSwitch = findViewById(R.id.ads_switch); diff --git a/local.properties b/local.properties index 589d9679..d7ba55a2 100644 --- a/local.properties +++ b/local.properties @@ -1,10 +1,9 @@ -## This file is automatically generated by Android Studio. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file should *NOT* be checked into Version Control Systems, +## This file must *NOT* be checked into Version Control Systems, # as it contains information specific to your local configuration. # # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the # header note. -sdk.dir=/Users/asantaren/Library/Android/sdk \ No newline at end of file +#Thu Feb 05 11:43:29 IST 2026 +nr.qoeAggregate=true +sdk.dir=/Users/swatijha/Library/Android/sdk From ec11bf165660de2cab7d330f111f30b8675d1ada Mon Sep 17 00:00:00 2001 From: swatijha Date: Thu, 5 Feb 2026 13:15:10 +0530 Subject: [PATCH 02/18] NR-517762: feat: added a toggle feature in sample add --- .../newrelic/nrvideoproject/MainActivity.java | 18 +++++++++++++++++- app/src/main/res/layout/activity_main.xml | 12 ++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java index 3d4a8e93..0579d1d7 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java @@ -16,12 +16,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListener { Switch adsSwitch; + Switch qoeSwitch; int counter = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - NRVideoConfiguration config = new NRVideoConfiguration.Builder(BuildConfig.NR_APPLICATION_TOKEN) + NRVideoConfiguration config = new NRVideoConfiguration.Builder("AAdc5e75073d8720260b9b0139b9a6fc686b954936-NRMA") .autoDetectPlatform(getApplicationContext()) .withHarvestCycle(60) .enableLogging() @@ -32,6 +33,21 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); adsSwitch = findViewById(R.id.ads_switch); + qoeSwitch = findViewById(R.id.qoe_switch); + + // Initialize QOE switch with current configuration state + qoeSwitch.setChecked(NRVideoConfig.getInstance().isQoeAggregateEnabled()); + + // Set up QOE switch listener + qoeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + // Toggle QOE aggregate functionality at runtime + NRVideoConfig.getInstance().setQoeAggregateEnabled(isChecked); + android.util.Log.d("QOE_TOGGLE", "QOE Aggregate " + (isChecked ? "ENABLED" : "DISABLED")); + + // Show user feedback + String message = "QOE Aggregate " + (isChecked ? "Enabled" : "Disabled"); + android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show(); + }); findViewById(R.id.video0).setOnClickListener(this); findViewById(R.id.video1).setOnClickListener(this); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 65a5f605..439b5de0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -72,6 +72,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/video2" /> + + Date: Thu, 5 Feb 2026 17:42:37 +0530 Subject: [PATCH 03/18] NR-517762: feat: refactored the code --- .../videoagent/core/NRVideoConfiguration.java | 87 ++++++++++++++++++- .../videoagent/core/config/NRVideoConfig.java | 68 --------------- .../core/tracker/NRVideoTracker.java | 4 +- .../newrelic/nrvideoproject/MainActivity.java | 23 +++-- 4 files changed, 99 insertions(+), 83 deletions(-) delete mode 100644 NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java index 193cc799..8506b947 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.pm.PackageManager; import com.newrelic.videoagent.core.utils.NRLog; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.HashMap; import java.util.Map; @@ -43,6 +44,14 @@ public final class NRVideoConfiguration { private final boolean debugLoggingEnabled; private final boolean isTV; + // Runtime configuration fields (mutable, thread-safe) - Using AtomicBoolean for better performance + private final AtomicBoolean qoeAggregateEnabled = new AtomicBoolean(true); + private final AtomicBoolean runtimeConfigInitialized = new AtomicBoolean(false); + + // Static holder for current configuration instance (for tracker access) + // WeakReference to prevent memory leaks if static cleanup is missed + private static volatile java.lang.ref.WeakReference currentInstanceRef; + // Performance optimization constants private static final int DEFAULT_HARVEST_CYCLE_SECONDS = 5 * 60; // 5 minutes private static final int DEFAULT_LIVE_HARVEST_CYCLE_SECONDS = 30; // 30 seconds @@ -78,6 +87,10 @@ private NRVideoConfiguration(Builder builder) { this.memoryOptimized = builder.memoryOptimized; this.debugLoggingEnabled = builder.debugLoggingEnabled; this.isTV = builder.isTV; + + // Initialize runtime configuration + this.qoeAggregateEnabled.set(builder.qoeAggregateEnabled); + this.runtimeConfigInitialized.set(true); } // Immutable getters @@ -92,6 +105,55 @@ private NRVideoConfiguration(Builder builder) { public boolean isDebugLoggingEnabled() { return debugLoggingEnabled; } public boolean isTV() { return isTV; } + // Runtime configuration getters and setters + /** + * Check if QOE_AGGREGATE events should be sent during harvest cycles + * @return true if QOE_AGGREGATE should be sent, false otherwise + */ + public boolean isQoeAggregateEnabled() { + if (!runtimeConfigInitialized.get()) { + throw new IllegalStateException("NRVideoConfiguration not initialized! Call build() first."); + } + return qoeAggregateEnabled.get(); + } + + /** + * Set whether QOE_AGGREGATE events should be sent during harvest cycles + * Lock-free, thread-safe runtime configuration using AtomicBoolean + * @param enabled true to enable QOE_AGGREGATE, false to disable + */ + public void setQoeAggregateEnabled(boolean enabled) { + this.qoeAggregateEnabled.set(enabled); + } + + /** + * Initialize configuration with client settings + * @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided) + */ + public void initializeFromClient(Boolean clientQoeAggregateEnabled) { + // If client provides a value, use it; otherwise keep current default + if (clientQoeAggregateEnabled != null) { + this.qoeAggregateEnabled.set(clientQoeAggregateEnabled); + } + } + + /** + * Get the current configuration instance (for tracker access) + * @return Current NRVideoConfiguration instance + * @throws IllegalStateException if no configuration has been built yet + */ + public static NRVideoConfiguration getCurrentInstance() { + if (currentInstanceRef == null) { + throw new IllegalStateException("No NRVideoConfiguration has been built yet!"); + } + + NRVideoConfiguration instance = currentInstanceRef.get(); + if (instance == null) { + throw new IllegalStateException("NRVideoConfiguration has been garbage collected! Create a new instance."); + } + + return instance; + } /** * Get dead letter retry interval in milliseconds * Optimized for different device types and network conditions @@ -158,6 +220,7 @@ public static final class Builder { private boolean memoryOptimized = true; private boolean debugLoggingEnabled = false; private boolean isTV = false; + private boolean qoeAggregateEnabled = true; // Default enabled public Builder(String applicationToken) { this.applicationToken = applicationToken; @@ -235,6 +298,25 @@ public Builder enableLogging() { return this; } + /** + * Enable QOE aggregate events (default: enabled) + * @return Builder instance for method chaining + */ + public Builder enableQoeAggregate() { + this.qoeAggregateEnabled = true; + return this; + } + + /** + * Configure QOE aggregate events + * @param enabled true to enable QOE_AGGREGATE events, false to disable + * @return Builder instance for method chaining + */ + public Builder enableQoeAggregate(boolean enabled) { + this.qoeAggregateEnabled = enabled; + return this; + } + private void applyTVOptimizations() { this.harvestCycleSeconds = TV_HARVEST_CYCLE_SECONDS; this.liveHarvestCycleSeconds = TV_LIVE_HARVEST_CYCLE_SECONDS; @@ -251,7 +333,10 @@ private void applyMemoryOptimizations() { } public NRVideoConfiguration build() { - return new NRVideoConfiguration(this); + NRVideoConfiguration config = new NRVideoConfiguration(this); + // Set current instance for tracker access using WeakReference + NRVideoConfiguration.currentInstanceRef = new java.lang.ref.WeakReference<>(config); + return config; } } diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java deleted file mode 100644 index 23b85095..00000000 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.newrelic.videoagent.core.config; - -/** - * Configuration manager for New Relic Video Agent functionality. - * Default values are sourced from local.properties, can be overridden by client configuration. - */ -public class NRVideoConfig { - - private static final NRVideoConfig INSTANCE = new NRVideoConfig(); - - // Configuration value - set from local.properties via BuildConfig - private boolean qoeAggregate; - private boolean initialized = false; - - private NRVideoConfig() { - } - - /** - * Get the singleton instance of NRVideoConfig - * @return NRVideoConfig instance - */ - public static NRVideoConfig getInstance() { - return INSTANCE; - } - - /** - * Check if QOE_AGGREGATE events should be sent during harvest cycles - * @return true if QOE_AGGREGATE should be sent, false otherwise - */ - public boolean isQoeAggregateEnabled() { - if (!initialized) { - throw new IllegalStateException("NRVideoConfig not initialized! Call initializeDefaults() first."); - } - return qoeAggregate; - } - - /** - * Set whether QOE_AGGREGATE events should be sent during harvest cycles - * This will be called with client configuration in the future - * @param enabled true to enable QOE_AGGREGATE, false to disable - */ - public void setQoeAggregateEnabled(boolean enabled) { - this.qoeAggregate = enabled; - } - - /** - * Initialize configuration with default values from local.properties - * This method should be called during app initialization - * @param defaultQoeAggregateEnabled Default value for QOE aggregate from BuildConfig - */ - public void initializeDefaults(boolean defaultQoeAggregateEnabled) { - if (!initialized) { - this.qoeAggregate = defaultQoeAggregateEnabled; - this.initialized = true; - } - } - - /** - * Initialize configuration with client settings - * @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided) - */ - public void initializeFromClient(Boolean clientQoeAggregateEnabled) { - // If client provides a value, use it; otherwise keep current default - if (clientQoeAggregateEnabled != null) { - this.qoeAggregate = clientQoeAggregateEnabled; - } - } -} \ No newline at end of file diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index 97a49bef..faecdbd3 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -3,7 +3,7 @@ import android.os.Handler; import com.newrelic.videoagent.core.NRVideo; -import com.newrelic.videoagent.core.config.NRVideoConfig; +import com.newrelic.videoagent.core.NRVideoConfiguration; import com.newrelic.videoagent.core.model.NRTimeSince; import com.newrelic.videoagent.core.model.NRTrackerState; import com.newrelic.videoagent.core.utils.NRLog; @@ -588,7 +588,7 @@ private void resetHarvestCycleFlags() { * Send QOE_AGGREGATE event (internal method - called once per cycle) */ private void sendQoeAggregate() { - if (!state.isAd && NRVideoConfig.getInstance().isQoeAggregateEnabled()) { + if (!state.isAd && NRVideoConfiguration.getCurrentInstance().isQoeAggregateEnabled()) { // Only send for content, not ads, and only if QOE aggregate is enabled Map kpiAttributes = calculateQOEKpiAttributes(); sendVideoEvent(QOE_AGGREGATE, kpiAttributes); diff --git a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java index 0579d1d7..46a50462 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java @@ -8,7 +8,6 @@ import com.newrelic.videoagent.core.NRVideo; import com.newrelic.videoagent.core.NRVideoConfiguration; -import com.newrelic.videoagent.core.config.NRVideoConfig; import java.util.HashMap; import java.util.Map; @@ -18,35 +17,35 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe Switch adsSwitch; Switch qoeSwitch; int counter = 0; + NRVideoConfiguration config; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - NRVideoConfiguration config = new NRVideoConfiguration.Builder("AAdc5e75073d8720260b9b0139b9a6fc686b954936-NRMA") + NRVideoConfiguration config = new NRVideoConfiguration.Builder(BuildConfig.NR_APPLICATION_TOKEN) .autoDetectPlatform(getApplicationContext()) .withHarvestCycle(60) .enableLogging() + .enableQoeAggregate(BuildConfig.QOE_AGGREGATE_DEFAULT) .build(); NRVideo.newBuilder(getApplicationContext()).withConfiguration(config).build(); - // Initialize QOE configuration with value from local.properties - NRVideoConfig.getInstance().initializeDefaults(BuildConfig.QOE_AGGREGATE_DEFAULT); setContentView(R.layout.activity_main); adsSwitch = findViewById(R.id.ads_switch); qoeSwitch = findViewById(R.id.qoe_switch); // Initialize QOE switch with current configuration state - qoeSwitch.setChecked(NRVideoConfig.getInstance().isQoeAggregateEnabled()); + qoeSwitch.setChecked(config.isQoeAggregateEnabled()); - // Set up QOE switch listener + // Set up QOE switch listener with optimized UI operations qoeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - // Toggle QOE aggregate functionality at runtime - NRVideoConfig.getInstance().setQoeAggregateEnabled(isChecked); - android.util.Log.d("QOE_TOGGLE", "QOE Aggregate " + (isChecked ? "ENABLED" : "DISABLED")); + // Perform config update on background thread to avoid UI blocking + // Toggle QOE aggregate functionality at runtime + config.setQoeAggregateEnabled(isChecked); - // Show user feedback - String message = "QOE Aggregate " + (isChecked ? "Enabled" : "Disabled"); - android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show(); + // Show user feedback on UI thread + String message = "QOE Aggregate " + (isChecked ? "Enabled" : "Disabled"); + android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show(); }); findViewById(R.id.video0).setOnClickListener(this); From 12b5e0f488380ea8516c62e5ed7623cd01be805c Mon Sep 17 00:00:00 2001 From: Divya Latha Date: Fri, 30 Jan 2026 11:25:12 +0530 Subject: [PATCH 04/18] chore: remove pricing information from README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index ba2cc5cd..e87220e4 100644 --- a/README.md +++ b/README.md @@ -209,10 +209,6 @@ As noted in our [security policy](../../security/policy), New Relic is committed If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [HackerOne](https://hackerone.com/newrelic). -## Pricing - -Important: Ingesting video telemetry data via this video agent requires a subscription to an Advanced Compute. Contact your New Relic account representative for more details on pricing and entitlement. - ## License New Relic Video Agent is licensed under the [Apache 2.0](http://apache.org/licenses/LICENSE-2.0.txt) License. From 77a33a7781b67df8b6dedbf1edb7bf4cd5090dfb Mon Sep 17 00:00:00 2001 From: mavinash Date: Tue, 3 Feb 2026 14:30:32 +0530 Subject: [PATCH 05/18] fix: lint error in sample app --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ae9a0c9a..5767e420 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,4 +26,6 @@ + + \ No newline at end of file From 6939e28278701c9d2092cc2140acdcf28c16c338 Mon Sep 17 00:00:00 2001 From: mavinash Date: Wed, 4 Feb 2026 12:06:08 +0530 Subject: [PATCH 06/18] fix: Lint issues --- .../exoplayer/tracker/NRTrackerExoPlayer.java | 7 ++++++ .../nrvideoproject/VideoPlayerAds.java | 22 ++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java b/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java index 81602546..b4700330 100644 --- a/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java +++ b/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java @@ -5,6 +5,8 @@ import android.os.Looper; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; import androidx.media3.common.C; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaMetadata; @@ -37,7 +39,12 @@ /** * New Relic Video tracker for ExoPlayer. + *

+ * @OptIn is required for Media3 APIs which are marked as @UnstableApi. + * This is by design as per Google's Media3 documentation. + * @see Media3 Unstable API Documentation */ +@OptIn(markerClass = UnstableApi.class) public class NRTrackerExoPlayer extends NRVideoTracker implements Player.Listener, AnalyticsListener { protected ExoPlayer player; diff --git a/app/src/main/java/com/newrelic/nrvideoproject/VideoPlayerAds.java b/app/src/main/java/com/newrelic/nrvideoproject/VideoPlayerAds.java index 11717447..31f2e157 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/VideoPlayerAds.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/VideoPlayerAds.java @@ -8,21 +8,28 @@ import com.newrelic.videoagent.core.NRVideoPlayerConfiguration; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; -import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; -import androidx.media3.datasource.DefaultDataSourceFactory; -import androidx.media3.exoplayer.SimpleExoPlayer; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ima.ImaAdsLoader; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.ui.PlayerView; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; import android.util.Log; import com.newrelic.videoagent.core.NewRelicVideoAgent; import com.newrelic.videoagent.ima.tracker.NRTrackerIMA; +/** + * @OptIn is required for Media3 IMA ads integration APIs which are marked as @UnstableApi. + * This is by design as per Google's Media3 documentation. + * @see Media3 Unstable API Documentation + */ +@OptIn(markerClass = UnstableApi.class) public class VideoPlayerAds extends AppCompatActivity implements AdErrorEvent.AdErrorListener, AdEvent.AdEventListener { - private SimpleExoPlayer player; + private ExoPlayer player; private Integer trackerId; private ImaAdsLoader adsLoader; private PlayerView playerView; @@ -67,13 +74,12 @@ protected void onDestroy() { private void playVideo(String videoUrl) { // Set up the factory for media sources, passing the ads loader and ad view providers. - DataSource.Factory dataSourceFactory = - new DefaultDataSourceFactory(this, Util.getUserAgent(this, getString(R.string.app_name))); + DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this); DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory); mediaSourceFactory.setAdsLoaderProvider(unusedAdTagUri -> adsLoader); mediaSourceFactory.setAdViewProvider(playerView); - player = new SimpleExoPlayer.Builder(this).setMediaSourceFactory(mediaSourceFactory).build(); + player = new ExoPlayer.Builder(this).setMediaSourceFactory(mediaSourceFactory).build(); NRVideoPlayerConfiguration playerConfiguration = new NRVideoPlayerConfiguration("test-player-something-else", player, true, null); trackerId = NRVideo.addPlayer(playerConfiguration); adTracker = (NRTrackerIMA) NewRelicVideoAgent.getInstance().getAdTracker(trackerId); @@ -93,7 +99,7 @@ private void playVideo(String videoUrl) { Uri contentUri = Uri.parse(videoUrl); Uri adTagUri = Uri.parse(getString(R.string.ad_tag_url)); MediaItem mediaItem = new MediaItem.Builder().setUri(contentUri).setAdTagUri(adTagUri).build(); - // Prepare the content and ad to be played with the SimpleExoPlayer. + // Prepare the content and ad to be played with the ExoPlayer. player.setMediaItem(mediaItem); // Set PlayWhenReady. If true, content and ads will autoplay. player.setPlayWhenReady(true); From 992569395371bde9b3bcff407878f35dfc283d85 Mon Sep 17 00:00:00 2001 From: Bharat Saraswat Date: Thu, 5 Feb 2026 11:11:55 +0530 Subject: [PATCH 07/18] Merge pull request #104 from newrelic/fix/drop-bitrate-attributes-before-start fix: Drop bitrate attributes before content/ad start --- .../videoagent/core/tracker/NRVideoTracker.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index 12107320..6ce5b943 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -211,8 +211,11 @@ public Map getAttributes(String action, Map attr if (state.isAd) { attr.put("adTitle", getTitle()); - attr.put("adBitrate", getBitrate()); - attr.put("adRenditionBitrate", getRenditionBitrate()); + // Only add bitrate attributes after ad has started (first frame shown) + if (state.isStarted) { + attr.put("adBitrate", getBitrate()); + attr.put("adRenditionBitrate", getRenditionBitrate()); + } attr.put("adRenditionWidth", getRenditionWidth()); attr.put("adRenditionHeight", getRenditionHeight()); attr.put("adDuration", getDuration()); @@ -247,9 +250,11 @@ public Map getAttributes(String action, Map attr } attr.put("contentTitle", getTitle()); -// attr.put("contentBitrate", getBitrate()); - attr.put("contentBitrate", getActualBitrate()); - attr.put("contentRenditionBitrate", getRenditionBitrate()); + // Only add bitrate attributes after content has started (first frame shown) + if (state.isStarted) { + attr.put("contentBitrate", getActualBitrate()); + attr.put("contentRenditionBitrate", getRenditionBitrate()); + } attr.put("contentRenditionWidth", getRenditionWidth()); attr.put("contentRenditionHeight", getRenditionHeight()); attr.put("contentDuration", getDuration()); From fb40db388cddfc78da3a6c8a6ceb7ed0cd9c407b Mon Sep 17 00:00:00 2001 From: mavinash Date: Sun, 1 Feb 2026 12:18:02 +0530 Subject: [PATCH 08/18] fix: Trigger HEARTBEAT at elapsed time 0 --- .../com/newrelic/videoagent/core/tracker/NRVideoTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index 6ce5b943..789f4d25 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -513,7 +513,7 @@ public void sendHeartbeat() { state.accumulatedVideoWatchTime = (Math.abs(state.accumulatedVideoWatchTime - heartbeatInterval) <= 5 ? heartbeatInterval : state.accumulatedVideoWatchTime); Map eventData = new HashMap<>(); eventData.put("elapsedTime", state.accumulatedVideoWatchTime); - if (state.accumulatedVideoWatchTime != null && state.accumulatedVideoWatchTime > 0L) { + if (state.accumulatedVideoWatchTime != null) { if (state.isAd) { sendVideoAdEvent(AD_HEARTBEAT,eventData); } else { From 7187413c7313c6e02486e881bcfce53d02a73852 Mon Sep 17 00:00:00 2001 From: mavinash Date: Tue, 6 Jan 2026 11:11:18 +0530 Subject: [PATCH 09/18] fix: Staging credentials --- .../videoagent/core/NRVideoConfiguration.java | 77 +++++++++++++------ .../videoagent/core/auth/TokenManager.java | 18 +++-- .../core/harvest/OptimizedHttpClient.java | 22 +++--- 3 files changed, 78 insertions(+), 39 deletions(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java index 193cc799..86d9dc17 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java @@ -42,6 +42,7 @@ public final class NRVideoConfiguration { private final boolean memoryOptimized; private final boolean debugLoggingEnabled; private final boolean isTV; + private final String collectorAddress; // Performance optimization constants private static final int DEFAULT_HARVEST_CYCLE_SECONDS = 5 * 60; // 5 minutes @@ -78,6 +79,7 @@ private NRVideoConfiguration(Builder builder) { this.memoryOptimized = builder.memoryOptimized; this.debugLoggingEnabled = builder.debugLoggingEnabled; this.isTV = builder.isTV; + this.collectorAddress = builder.collectorAddress; } // Immutable getters @@ -91,6 +93,7 @@ private NRVideoConfiguration(Builder builder) { public boolean isMemoryOptimized() { return memoryOptimized; } public boolean isDebugLoggingEnabled() { return debugLoggingEnabled; } public boolean isTV() { return isTV; } + public String getCollectorAddress() { return collectorAddress; } /** * Get dead letter retry interval in milliseconds @@ -107,41 +110,56 @@ public long getDeadLetterRetryInterval() { } /** - * Enterprise-grade region identification with multiple fallback strategies - * Thread-safe and optimized for performance + * Parse region code from application token prefix + * Matches NewRelic iOS Agent pattern: extracts region prefix before 'x' + * Examples: "EUxABCD..." -> "EU", "APxABCD..." -> "AP", "AA..." -> "" + */ + private static String parseRegionFromToken(String applicationToken) { + if (applicationToken == null || applicationToken.length() < 3) { + return ""; + } + + // Find the first 'x' in the token + int xIndex = applicationToken.indexOf('x'); + if (xIndex == -1) { + return ""; // No region prefix found + } + + // Extract everything before the first 'x' + String regionCode = applicationToken.substring(0, xIndex); + + // Remove any trailing 'x' characters + while (regionCode.length() > 0 && regionCode.charAt(regionCode.length() - 1) == 'x') { + regionCode = regionCode.substring(0, regionCode.length() - 1); + } + + return regionCode; + } + + /** + * Identify region with proper token parsing and fallback logic + * Behavior similar to NewRelic iOS Agent's NRMAAgentConfiguration */ private static String identifyRegion(String applicationToken) { if (applicationToken == null || applicationToken.length() < 10) { return "US"; // Safe default } - String cleanToken = applicationToken.trim().toLowerCase(); + // First, try to parse region from token prefix (e.g., "EUx", "APx") + String regionCode = parseRegionFromToken(applicationToken); - // Strategy 1: Direct prefix matching (most reliable) - for (Map.Entry entry : REGION_MAPPINGS.entrySet()) { - String regionKey = entry.getKey().toLowerCase(); - if (cleanToken.startsWith(regionKey) || cleanToken.contains("-" + regionKey + "-")) { - return entry.getValue(); - } - } + if (regionCode != null && regionCode.length() > 0) { + // Convert region code to uppercase and validate + String upperRegion = regionCode.toUpperCase(); - // Strategy 2: Token structure analysis - if (cleanToken.length() >= 40) { // Standard NR token length - // EU tokens often have specific patterns - if (cleanToken.contains("eu") || cleanToken.contains("europe")) { - return "EU"; - } - // AP tokens often have specific patterns - if (cleanToken.contains("ap") || cleanToken.contains("asia") || cleanToken.contains("pacific")) { - return "AP"; - } - // Gov tokens have specific patterns - if (cleanToken.contains("gov") || cleanToken.contains("fed")) { - return "GOV"; + // Map region codes to standard regions + String mappedRegion = REGION_MAPPINGS.get(upperRegion); + if (mappedRegion != null) { + return mappedRegion; } } - // Strategy 3: Default to US for production stability + // Default to US for standard tokens without region prefix return "US"; } @@ -158,6 +176,7 @@ public static final class Builder { private boolean memoryOptimized = true; private boolean debugLoggingEnabled = false; private boolean isTV = false; + private String collectorAddress = null; public Builder(String applicationToken) { this.applicationToken = applicationToken; @@ -235,6 +254,16 @@ public Builder enableLogging() { return this; } + /** + * Set custom collector domain address for /connect and /data endpoints (optional) + * Example: "staging-mobile-collector.newrelic.com" or "mobile-collector.newrelic.com" + * If not set, will be auto-detected from application token region + */ + public Builder withCollectorAddress(String collectorAddress) { + this.collectorAddress = collectorAddress; + return this; + } + private void applyTVOptimizations() { this.harvestCycleSeconds = TV_HARVEST_CYCLE_SECONDS; this.liveHarvestCycleSeconds = TV_LIVE_HARVEST_CYCLE_SECONDS; diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java index 6159d856..e87d407f 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java @@ -154,20 +154,26 @@ private boolean isTokenValid() { /** * Build token endpoint URL based on region + * If collectorAddress is explicitly set, use it for /connect endpoint + * Otherwise, auto-detect from region */ private String buildTokenEndpoint() { + // If collectorAddress is explicitly set, use it for /connect endpoint + if (configuration.getCollectorAddress() != null && !configuration.getCollectorAddress().isEmpty()) { + return "https://" + configuration.getCollectorAddress() + "/mobile/v5/connect"; + } + + // Otherwise, auto-detect from region String region = configuration.getRegion().toUpperCase(); switch (region) { case "EU": - return "https://mobile-collector.eu.newrelic.com/mobile/v5/connect"; + return "https://mobile-collector.eu.nr-data.net/mobile/v5/connect"; case "AP": - return "https://mobile-collector.ap.newrelic.com/mobile/v5/connect"; + return "https://mobile-collector.ap.nr-data.net/mobile/v5/connect"; case "GOV": - return "https://mobile-collector.gov.newrelic.com/mobile/v5/connect"; - case "STAGING": - return "https://mobile-collector.staging.newrelic.com/mobile/v5/connect"; + return "https://gov-mobile-collector.newrelic.com/mobile/v5/connect"; default: - return "https://mobile-collector.newrelic.com/mobile/v5/connect"; + return "https://mobile-collector.newrelic.com/mobile/v5/connect"; // US/DEFAULT } } diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java index c92dd987..116c4cd0 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java @@ -40,10 +40,9 @@ public class OptimizedHttpClient implements HttpClientInterface { static { REGIONAL_ENDPOINTS.put("US", "https://mobile-collector.newrelic.com/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("EU", "https://mobile-collector.eu.newrelic.com/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("AP", "https://mobile-collector.ap.newrelic.com/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("GOV", "https://mobile-collector.gov.newrelic.com/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("STAGING", "https://mobile-collector.staging.newrelic.com/mobile/v3/data"); + REGIONAL_ENDPOINTS.put("EU", "https://mobile-collector.eu.nr-data.net/mobile/v3/data"); + REGIONAL_ENDPOINTS.put("AP", "https://mobile-collector.ap.nr-data.net/mobile/v3/data"); + REGIONAL_ENDPOINTS.put("GOV", "https://gov-mobile-collector.newrelic.com/mobile/v3/data"); REGIONAL_ENDPOINTS.put("DEFAULT", REGIONAL_ENDPOINTS.get("US")); } @@ -59,10 +58,15 @@ public OptimizedHttpClient(NRVideoConfiguration configuration, android.content.C this.tokenManager = new TokenManager(context, configuration); this.deviceInfo = DeviceInformation.getInstance(context); - // Set endpoint URL based on region, defaulting to US if not found - String region = configuration.getRegion().toUpperCase(); - String regionEndpoint = REGIONAL_ENDPOINTS.get(region); - this.endpointUrl = regionEndpoint != null ? regionEndpoint : REGIONAL_ENDPOINTS.get("DEFAULT"); + // If collectorAddress is explicitly set, use it + if (configuration.getCollectorAddress() != null && !configuration.getCollectorAddress().isEmpty()) { + this.endpointUrl = "https://" + configuration.getCollectorAddress() + "/mobile/v3/data"; + } else { + // Otherwise, auto-detect from region + String region = configuration.getRegion().toUpperCase(); + String regionEndpoint = REGIONAL_ENDPOINTS.get(region); + this.endpointUrl = regionEndpoint != null ? regionEndpoint : REGIONAL_ENDPOINTS.get("DEFAULT"); + } if (configuration.isMemoryOptimized()) { connectionTimeoutMs = 6000; @@ -74,7 +78,7 @@ public OptimizedHttpClient(NRVideoConfiguration configuration, android.content.C System.setProperty("http.keepAliveDuration", "300000"); // 5 minutes System.setProperty("http.maxConnections", "5"); - NRLog.d("Initialized with region: " + region + + NRLog.d("Initialized with region: " + configuration.getRegion() + ", endpoint URL: " + endpointUrl); } From ac2811ba8da931a357ab78a0f86fa199e94f21b8 Mon Sep 17 00:00:00 2001 From: mavinash Date: Tue, 20 Jan 2026 10:22:55 +0530 Subject: [PATCH 10/18] fix: Regional Collector URLs --- .../com/newrelic/videoagent/core/auth/TokenManager.java | 6 +++--- .../videoagent/core/harvest/OptimizedHttpClient.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java index e87d407f..eddf4b8a 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java @@ -167,11 +167,11 @@ private String buildTokenEndpoint() { String region = configuration.getRegion().toUpperCase(); switch (region) { case "EU": - return "https://mobile-collector.eu.nr-data.net/mobile/v5/connect"; + return "https://mobile-collector.eu.newrelic.com/mobile/v5/connect"; case "AP": - return "https://mobile-collector.ap.nr-data.net/mobile/v5/connect"; + return "https://mobile-collector.ap.newrelic.com/mobile/v5/connect"; case "GOV": - return "https://gov-mobile-collector.newrelic.com/mobile/v5/connect"; + return "https://mobile-collector.gov.newrelic.com/mobile/v5/connect"; default: return "https://mobile-collector.newrelic.com/mobile/v5/connect"; // US/DEFAULT } diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java index 116c4cd0..2f6a52a3 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java @@ -40,9 +40,9 @@ public class OptimizedHttpClient implements HttpClientInterface { static { REGIONAL_ENDPOINTS.put("US", "https://mobile-collector.newrelic.com/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("EU", "https://mobile-collector.eu.nr-data.net/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("AP", "https://mobile-collector.ap.nr-data.net/mobile/v3/data"); - REGIONAL_ENDPOINTS.put("GOV", "https://gov-mobile-collector.newrelic.com/mobile/v3/data"); + REGIONAL_ENDPOINTS.put("EU", "https://mobile-collector.eu.newrelic.com/mobile/v3/data"); + REGIONAL_ENDPOINTS.put("AP", "https://mobile-collector.ap.newrelic.com/mobile/v3/data"); + REGIONAL_ENDPOINTS.put("GOV", "https://mobile-collector.gov.newrelic.com/mobile/v3/data"); REGIONAL_ENDPOINTS.put("DEFAULT", REGIONAL_ENDPOINTS.get("US")); } From 7aa12265e6f6c6970a0a7ace8e15886a5cdac08f Mon Sep 17 00:00:00 2001 From: mavinash Date: Tue, 20 Jan 2026 10:25:17 +0530 Subject: [PATCH 11/18] fix: Remove extra comment --- .../java/com/newrelic/videoagent/core/auth/TokenManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java index eddf4b8a..bc1f2e82 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java @@ -173,7 +173,7 @@ private String buildTokenEndpoint() { case "GOV": return "https://mobile-collector.gov.newrelic.com/mobile/v5/connect"; default: - return "https://mobile-collector.newrelic.com/mobile/v5/connect"; // US/DEFAULT + return "https://mobile-collector.newrelic.com/mobile/v5/connect"; } } From 0885a1a620cd2d1d21393319325bbf629fbed219 Mon Sep 17 00:00:00 2001 From: mavinash Date: Thu, 29 Jan 2026 01:46:17 +0530 Subject: [PATCH 12/18] fix: pr review comments --- .../com/newrelic/videoagent/core/auth/TokenManager.java | 3 ++- .../videoagent/core/harvest/OptimizedHttpClient.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java index bc1f2e82..b1ecda1d 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/auth/TokenManager.java @@ -164,7 +164,8 @@ private String buildTokenEndpoint() { } // Otherwise, auto-detect from region - String region = configuration.getRegion().toUpperCase(); + String region = configuration.getRegion(); + region = (region != null) ? region.toUpperCase() : "US"; switch (region) { case "EU": return "https://mobile-collector.eu.newrelic.com/mobile/v5/connect"; diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java index 2f6a52a3..6f1c2d34 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java @@ -63,9 +63,9 @@ public OptimizedHttpClient(NRVideoConfiguration configuration, android.content.C this.endpointUrl = "https://" + configuration.getCollectorAddress() + "/mobile/v3/data"; } else { // Otherwise, auto-detect from region - String region = configuration.getRegion().toUpperCase(); - String regionEndpoint = REGIONAL_ENDPOINTS.get(region); - this.endpointUrl = regionEndpoint != null ? regionEndpoint : REGIONAL_ENDPOINTS.get("DEFAULT"); + String region = configuration.getRegion(); + region = (region != null) ? region.toUpperCase() : "US"; + this.endpointUrl = REGIONAL_ENDPOINTS.getOrDefault(region, REGIONAL_ENDPOINTS.get("DEFAULT")); } if (configuration.isMemoryOptimized()) { From 5cb62a6f1be8f933f5e87a2f43706f4bea9d639a Mon Sep 17 00:00:00 2001 From: mavinash Date: Thu, 29 Jan 2026 15:41:16 +0530 Subject: [PATCH 13/18] fix: revert use of getOrDefault --- .../newrelic/videoagent/core/harvest/OptimizedHttpClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java index 6f1c2d34..ac391141 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/harvest/OptimizedHttpClient.java @@ -65,7 +65,8 @@ public OptimizedHttpClient(NRVideoConfiguration configuration, android.content.C // Otherwise, auto-detect from region String region = configuration.getRegion(); region = (region != null) ? region.toUpperCase() : "US"; - this.endpointUrl = REGIONAL_ENDPOINTS.getOrDefault(region, REGIONAL_ENDPOINTS.get("DEFAULT")); + String endpoint = REGIONAL_ENDPOINTS.get(region); + this.endpointUrl = (endpoint != null) ? endpoint : REGIONAL_ENDPOINTS.get("DEFAULT"); } if (configuration.isMemoryOptimized()) { From 8b7a7bc657b65a9fee9b70570fa8375686ed238f Mon Sep 17 00:00:00 2001 From: swatijha Date: Thu, 5 Feb 2026 11:47:11 +0530 Subject: [PATCH 14/18] NR-517762: feat: add toggle feature for QOE --- .../videoagent/core/config/NRVideoConfig.java | 68 +++++++++++++++++++ .../core/tracker/NRVideoTracker.java | 4 +- app/build.gradle | 2 + .../newrelic/nrvideoproject/MainActivity.java | 3 + local.properties | 9 ++- 5 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java new file mode 100644 index 00000000..23b85095 --- /dev/null +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java @@ -0,0 +1,68 @@ +package com.newrelic.videoagent.core.config; + +/** + * Configuration manager for New Relic Video Agent functionality. + * Default values are sourced from local.properties, can be overridden by client configuration. + */ +public class NRVideoConfig { + + private static final NRVideoConfig INSTANCE = new NRVideoConfig(); + + // Configuration value - set from local.properties via BuildConfig + private boolean qoeAggregate; + private boolean initialized = false; + + private NRVideoConfig() { + } + + /** + * Get the singleton instance of NRVideoConfig + * @return NRVideoConfig instance + */ + public static NRVideoConfig getInstance() { + return INSTANCE; + } + + /** + * Check if QOE_AGGREGATE events should be sent during harvest cycles + * @return true if QOE_AGGREGATE should be sent, false otherwise + */ + public boolean isQoeAggregateEnabled() { + if (!initialized) { + throw new IllegalStateException("NRVideoConfig not initialized! Call initializeDefaults() first."); + } + return qoeAggregate; + } + + /** + * Set whether QOE_AGGREGATE events should be sent during harvest cycles + * This will be called with client configuration in the future + * @param enabled true to enable QOE_AGGREGATE, false to disable + */ + public void setQoeAggregateEnabled(boolean enabled) { + this.qoeAggregate = enabled; + } + + /** + * Initialize configuration with default values from local.properties + * This method should be called during app initialization + * @param defaultQoeAggregateEnabled Default value for QOE aggregate from BuildConfig + */ + public void initializeDefaults(boolean defaultQoeAggregateEnabled) { + if (!initialized) { + this.qoeAggregate = defaultQoeAggregateEnabled; + this.initialized = true; + } + } + + /** + * Initialize configuration with client settings + * @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided) + */ + public void initializeFromClient(Boolean clientQoeAggregateEnabled) { + // If client provides a value, use it; otherwise keep current default + if (clientQoeAggregateEnabled != null) { + this.qoeAggregate = clientQoeAggregateEnabled; + } + } +} \ No newline at end of file diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index 789f4d25..e1835b80 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -3,6 +3,7 @@ import android.os.Handler; import com.newrelic.videoagent.core.NRVideo; +import com.newrelic.videoagent.core.config.NRVideoConfig; import com.newrelic.videoagent.core.model.NRTimeSince; import com.newrelic.videoagent.core.model.NRTrackerState; import com.newrelic.videoagent.core.utils.NRLog; @@ -592,7 +593,8 @@ private void resetHarvestCycleFlags() { * Send QOE_AGGREGATE event (internal method - called once per cycle) */ private void sendQoeAggregate() { - if (!state.isAd) { // Only send for content, not ads + if (!state.isAd && NRVideoConfig.getInstance().isQoeAggregateEnabled()) { + // Only send for content, not ads, and only if QOE aggregate is enabled Map kpiAttributes = calculateQOEKpiAttributes(); sendVideoEvent(QOE_AGGREGATE, kpiAttributes); qoeAggregateAlreadySent = true; diff --git a/app/build.gradle b/app/build.gradle index 2ae73439..d761b31a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,12 +10,14 @@ if (localPropertiesFile.exists()) { } } def applicationToken = localProperties.getProperty('nr.applicationToken', "") +def qoeAggregateEnabled = localProperties.getProperty('nr.qoeAggregate', "true").toBoolean() android { compileSdkVersion 34 buildToolsVersion "34.0.0" defaultConfig { buildConfigField "String", "NR_APPLICATION_TOKEN", "\"${applicationToken}\"" + buildConfigField "boolean", "QOE_AGGREGATE_DEFAULT", "${qoeAggregateEnabled}" applicationId "com.newrelic.nrvideoproject" minSdkVersion 16 targetSdkVersion 34 diff --git a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java index 008e88d3..3d4a8e93 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java @@ -8,6 +8,7 @@ import com.newrelic.videoagent.core.NRVideo; import com.newrelic.videoagent.core.NRVideoConfiguration; +import com.newrelic.videoagent.core.config.NRVideoConfig; import java.util.HashMap; import java.util.Map; @@ -26,6 +27,8 @@ protected void onCreate(Bundle savedInstanceState) { .enableLogging() .build(); NRVideo.newBuilder(getApplicationContext()).withConfiguration(config).build(); + // Initialize QOE configuration with value from local.properties + NRVideoConfig.getInstance().initializeDefaults(BuildConfig.QOE_AGGREGATE_DEFAULT); setContentView(R.layout.activity_main); adsSwitch = findViewById(R.id.ads_switch); diff --git a/local.properties b/local.properties index 589d9679..d7ba55a2 100644 --- a/local.properties +++ b/local.properties @@ -1,10 +1,9 @@ -## This file is automatically generated by Android Studio. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file should *NOT* be checked into Version Control Systems, +## This file must *NOT* be checked into Version Control Systems, # as it contains information specific to your local configuration. # # Location of the SDK. This is only used by Gradle. # For customization when using a Version Control System, please read the # header note. -sdk.dir=/Users/asantaren/Library/Android/sdk \ No newline at end of file +#Thu Feb 05 11:43:29 IST 2026 +nr.qoeAggregate=true +sdk.dir=/Users/swatijha/Library/Android/sdk From 0a4502078564564715973e2a800d68c60f6a3233 Mon Sep 17 00:00:00 2001 From: swatijha Date: Thu, 5 Feb 2026 13:15:10 +0530 Subject: [PATCH 15/18] NR-517762: feat: added a toggle feature in sample add --- .../newrelic/nrvideoproject/MainActivity.java | 18 +++++++++++++++++- app/src/main/res/layout/activity_main.xml | 12 ++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java index 3d4a8e93..0579d1d7 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java @@ -16,12 +16,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListener { Switch adsSwitch; + Switch qoeSwitch; int counter = 0; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - NRVideoConfiguration config = new NRVideoConfiguration.Builder(BuildConfig.NR_APPLICATION_TOKEN) + NRVideoConfiguration config = new NRVideoConfiguration.Builder("AAdc5e75073d8720260b9b0139b9a6fc686b954936-NRMA") .autoDetectPlatform(getApplicationContext()) .withHarvestCycle(60) .enableLogging() @@ -32,6 +33,21 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); adsSwitch = findViewById(R.id.ads_switch); + qoeSwitch = findViewById(R.id.qoe_switch); + + // Initialize QOE switch with current configuration state + qoeSwitch.setChecked(NRVideoConfig.getInstance().isQoeAggregateEnabled()); + + // Set up QOE switch listener + qoeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + // Toggle QOE aggregate functionality at runtime + NRVideoConfig.getInstance().setQoeAggregateEnabled(isChecked); + android.util.Log.d("QOE_TOGGLE", "QOE Aggregate " + (isChecked ? "ENABLED" : "DISABLED")); + + // Show user feedback + String message = "QOE Aggregate " + (isChecked ? "Enabled" : "Disabled"); + android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show(); + }); findViewById(R.id.video0).setOnClickListener(this); findViewById(R.id.video1).setOnClickListener(this); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 65a5f605..439b5de0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -72,6 +72,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/video2" /> + + Date: Thu, 5 Feb 2026 17:42:37 +0530 Subject: [PATCH 16/18] NR-517762: feat: refactored the code # Conflicts: # NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java --- .../videoagent/core/NRVideoConfiguration.java | 84 ++++++++++++++++++- .../videoagent/core/config/NRVideoConfig.java | 68 --------------- .../core/tracker/NRVideoTracker.java | 4 +- .../newrelic/nrvideoproject/MainActivity.java | 23 +++-- 4 files changed, 95 insertions(+), 84 deletions(-) delete mode 100644 NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java index 86d9dc17..128bf179 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.pm.PackageManager; import com.newrelic.videoagent.core.utils.NRLog; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.HashMap; import java.util.Map; @@ -44,6 +45,14 @@ public final class NRVideoConfiguration { private final boolean isTV; private final String collectorAddress; + // Runtime configuration fields (mutable, thread-safe) - Using AtomicBoolean for better performance + private final AtomicBoolean qoeAggregateEnabled = new AtomicBoolean(true); + private final AtomicBoolean runtimeConfigInitialized = new AtomicBoolean(false); + + // Static holder for current configuration instance (for tracker access) + // WeakReference to prevent memory leaks if static cleanup is missed + private static volatile java.lang.ref.WeakReference currentInstanceRef; + // Performance optimization constants private static final int DEFAULT_HARVEST_CYCLE_SECONDS = 5 * 60; // 5 minutes private static final int DEFAULT_LIVE_HARVEST_CYCLE_SECONDS = 30; // 30 seconds @@ -79,7 +88,6 @@ private NRVideoConfiguration(Builder builder) { this.memoryOptimized = builder.memoryOptimized; this.debugLoggingEnabled = builder.debugLoggingEnabled; this.isTV = builder.isTV; - this.collectorAddress = builder.collectorAddress; } // Immutable getters @@ -95,6 +103,55 @@ private NRVideoConfiguration(Builder builder) { public boolean isTV() { return isTV; } public String getCollectorAddress() { return collectorAddress; } + // Runtime configuration getters and setters + /** + * Check if QOE_AGGREGATE events should be sent during harvest cycles + * @return true if QOE_AGGREGATE should be sent, false otherwise + */ + public boolean isQoeAggregateEnabled() { + if (!runtimeConfigInitialized.get()) { + throw new IllegalStateException("NRVideoConfiguration not initialized! Call build() first."); + } + return qoeAggregateEnabled.get(); + } + + /** + * Set whether QOE_AGGREGATE events should be sent during harvest cycles + * Lock-free, thread-safe runtime configuration using AtomicBoolean + * @param enabled true to enable QOE_AGGREGATE, false to disable + */ + public void setQoeAggregateEnabled(boolean enabled) { + this.qoeAggregateEnabled.set(enabled); + } + + /** + * Initialize configuration with client settings + * @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided) + */ + public void initializeFromClient(Boolean clientQoeAggregateEnabled) { + // If client provides a value, use it; otherwise keep current default + if (clientQoeAggregateEnabled != null) { + this.qoeAggregateEnabled.set(clientQoeAggregateEnabled); + } + } + + /** + * Get the current configuration instance (for tracker access) + * @return Current NRVideoConfiguration instance + * @throws IllegalStateException if no configuration has been built yet + */ + public static NRVideoConfiguration getCurrentInstance() { + if (currentInstanceRef == null) { + throw new IllegalStateException("No NRVideoConfiguration has been built yet!"); + } + + NRVideoConfiguration instance = currentInstanceRef.get(); + if (instance == null) { + throw new IllegalStateException("NRVideoConfiguration has been garbage collected! Create a new instance."); + } + + return instance; + } /** * Get dead letter retry interval in milliseconds * Optimized for different device types and network conditions @@ -177,6 +234,7 @@ public static final class Builder { private boolean debugLoggingEnabled = false; private boolean isTV = false; private String collectorAddress = null; + private boolean qoeAggregateEnabled = true; // Default enabled public Builder(String applicationToken) { this.applicationToken = applicationToken; @@ -264,6 +322,25 @@ public Builder withCollectorAddress(String collectorAddress) { return this; } + /** + * Enable QOE aggregate events (default: enabled) + * @return Builder instance for method chaining + */ + public Builder enableQoeAggregate() { + this.qoeAggregateEnabled = true; + return this; + } + + /** + * Configure QOE aggregate events + * @param enabled true to enable QOE_AGGREGATE events, false to disable + * @return Builder instance for method chaining + */ + public Builder enableQoeAggregate(boolean enabled) { + this.qoeAggregateEnabled = enabled; + return this; + } + private void applyTVOptimizations() { this.harvestCycleSeconds = TV_HARVEST_CYCLE_SECONDS; this.liveHarvestCycleSeconds = TV_LIVE_HARVEST_CYCLE_SECONDS; @@ -280,7 +357,10 @@ private void applyMemoryOptimizations() { } public NRVideoConfiguration build() { - return new NRVideoConfiguration(this); + NRVideoConfiguration config = new NRVideoConfiguration(this); + // Set current instance for tracker access using WeakReference + NRVideoConfiguration.currentInstanceRef = new java.lang.ref.WeakReference<>(config); + return config; } } diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java deleted file mode 100644 index 23b85095..00000000 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/config/NRVideoConfig.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.newrelic.videoagent.core.config; - -/** - * Configuration manager for New Relic Video Agent functionality. - * Default values are sourced from local.properties, can be overridden by client configuration. - */ -public class NRVideoConfig { - - private static final NRVideoConfig INSTANCE = new NRVideoConfig(); - - // Configuration value - set from local.properties via BuildConfig - private boolean qoeAggregate; - private boolean initialized = false; - - private NRVideoConfig() { - } - - /** - * Get the singleton instance of NRVideoConfig - * @return NRVideoConfig instance - */ - public static NRVideoConfig getInstance() { - return INSTANCE; - } - - /** - * Check if QOE_AGGREGATE events should be sent during harvest cycles - * @return true if QOE_AGGREGATE should be sent, false otherwise - */ - public boolean isQoeAggregateEnabled() { - if (!initialized) { - throw new IllegalStateException("NRVideoConfig not initialized! Call initializeDefaults() first."); - } - return qoeAggregate; - } - - /** - * Set whether QOE_AGGREGATE events should be sent during harvest cycles - * This will be called with client configuration in the future - * @param enabled true to enable QOE_AGGREGATE, false to disable - */ - public void setQoeAggregateEnabled(boolean enabled) { - this.qoeAggregate = enabled; - } - - /** - * Initialize configuration with default values from local.properties - * This method should be called during app initialization - * @param defaultQoeAggregateEnabled Default value for QOE aggregate from BuildConfig - */ - public void initializeDefaults(boolean defaultQoeAggregateEnabled) { - if (!initialized) { - this.qoeAggregate = defaultQoeAggregateEnabled; - this.initialized = true; - } - } - - /** - * Initialize configuration with client settings - * @param clientQoeAggregateEnabled QOE aggregate setting from client (null if not provided) - */ - public void initializeFromClient(Boolean clientQoeAggregateEnabled) { - // If client provides a value, use it; otherwise keep current default - if (clientQoeAggregateEnabled != null) { - this.qoeAggregate = clientQoeAggregateEnabled; - } - } -} \ No newline at end of file diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index e1835b80..60c8bcad 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -3,7 +3,7 @@ import android.os.Handler; import com.newrelic.videoagent.core.NRVideo; -import com.newrelic.videoagent.core.config.NRVideoConfig; +import com.newrelic.videoagent.core.NRVideoConfiguration; import com.newrelic.videoagent.core.model.NRTimeSince; import com.newrelic.videoagent.core.model.NRTrackerState; import com.newrelic.videoagent.core.utils.NRLog; @@ -593,7 +593,7 @@ private void resetHarvestCycleFlags() { * Send QOE_AGGREGATE event (internal method - called once per cycle) */ private void sendQoeAggregate() { - if (!state.isAd && NRVideoConfig.getInstance().isQoeAggregateEnabled()) { + if (!state.isAd && NRVideoConfiguration.getCurrentInstance().isQoeAggregateEnabled()) { // Only send for content, not ads, and only if QOE aggregate is enabled Map kpiAttributes = calculateQOEKpiAttributes(); sendVideoEvent(QOE_AGGREGATE, kpiAttributes); diff --git a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java index 0579d1d7..46a50462 100644 --- a/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java +++ b/app/src/main/java/com/newrelic/nrvideoproject/MainActivity.java @@ -8,7 +8,6 @@ import com.newrelic.videoagent.core.NRVideo; import com.newrelic.videoagent.core.NRVideoConfiguration; -import com.newrelic.videoagent.core.config.NRVideoConfig; import java.util.HashMap; import java.util.Map; @@ -18,35 +17,35 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe Switch adsSwitch; Switch qoeSwitch; int counter = 0; + NRVideoConfiguration config; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - NRVideoConfiguration config = new NRVideoConfiguration.Builder("AAdc5e75073d8720260b9b0139b9a6fc686b954936-NRMA") + NRVideoConfiguration config = new NRVideoConfiguration.Builder(BuildConfig.NR_APPLICATION_TOKEN) .autoDetectPlatform(getApplicationContext()) .withHarvestCycle(60) .enableLogging() + .enableQoeAggregate(BuildConfig.QOE_AGGREGATE_DEFAULT) .build(); NRVideo.newBuilder(getApplicationContext()).withConfiguration(config).build(); - // Initialize QOE configuration with value from local.properties - NRVideoConfig.getInstance().initializeDefaults(BuildConfig.QOE_AGGREGATE_DEFAULT); setContentView(R.layout.activity_main); adsSwitch = findViewById(R.id.ads_switch); qoeSwitch = findViewById(R.id.qoe_switch); // Initialize QOE switch with current configuration state - qoeSwitch.setChecked(NRVideoConfig.getInstance().isQoeAggregateEnabled()); + qoeSwitch.setChecked(config.isQoeAggregateEnabled()); - // Set up QOE switch listener + // Set up QOE switch listener with optimized UI operations qoeSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - // Toggle QOE aggregate functionality at runtime - NRVideoConfig.getInstance().setQoeAggregateEnabled(isChecked); - android.util.Log.d("QOE_TOGGLE", "QOE Aggregate " + (isChecked ? "ENABLED" : "DISABLED")); + // Perform config update on background thread to avoid UI blocking + // Toggle QOE aggregate functionality at runtime + config.setQoeAggregateEnabled(isChecked); - // Show user feedback - String message = "QOE Aggregate " + (isChecked ? "Enabled" : "Disabled"); - android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show(); + // Show user feedback on UI thread + String message = "QOE Aggregate " + (isChecked ? "Enabled" : "Disabled"); + android.widget.Toast.makeText(MainActivity.this, message, android.widget.Toast.LENGTH_SHORT).show(); }); findViewById(R.id.video0).setOnClickListener(this); From 91e5898012e0c5cc108b81b2019f143a4244ab34 Mon Sep 17 00:00:00 2001 From: swatijha Date: Fri, 6 Feb 2026 07:42:26 +0530 Subject: [PATCH 17/18] NR-517762: rebased with stable-beta --- .../com/newrelic/videoagent/core/NRVideoConfiguration.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java index 128bf179..ea4e12b4 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java @@ -88,6 +88,7 @@ private NRVideoConfiguration(Builder builder) { this.memoryOptimized = builder.memoryOptimized; this.debugLoggingEnabled = builder.debugLoggingEnabled; this.isTV = builder.isTV; + this.collectorAddress = builder.collectorAddress; } // Immutable getters @@ -360,6 +361,8 @@ public NRVideoConfiguration build() { NRVideoConfiguration config = new NRVideoConfiguration(this); // Set current instance for tracker access using WeakReference NRVideoConfiguration.currentInstanceRef = new java.lang.ref.WeakReference<>(config); + // Mark runtime configuration as initialized + config.runtimeConfigInitialized.set(true); return config; } } @@ -438,4 +441,4 @@ public String toString() { ", debugLogging=" + debugLoggingEnabled + '}'; } -} +} \ No newline at end of file From c343c4ac0e78b27950b2c346e7291c6feab9beed Mon Sep 17 00:00:00 2001 From: swatijha Date: Fri, 6 Feb 2026 11:19:28 +0530 Subject: [PATCH 18/18] NR-517762: rebased with stable-beta --- .../exoplayer/tracker/NRTrackerExoPlayer.java | 25 +++++++ .../videoagent/ima/tracker/NRTrackerIMA.java | 17 +++++ .../com/newrelic/videoagent/core/NRVideo.java | 38 +++++++---- .../videoagent/core/NRVideoConfiguration.java | 23 +------ .../videoagent/core/tracker/NRTracker.java | 23 ++++++- .../core/tracker/NRVideoTracker.java | 66 +++++++++++++++---- 6 files changed, 146 insertions(+), 46 deletions(-) diff --git a/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java b/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java index b4700330..3c2e6ca4 100644 --- a/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java +++ b/NRExoPlayerTracker/src/main/java/com/newrelic/videoagent/exoplayer/tracker/NRTrackerExoPlayer.java @@ -19,6 +19,7 @@ import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.MediaLoadData; +import com.newrelic.videoagent.core.NRVideoConfiguration; import com.newrelic.videoagent.core.tracker.NRVideoTracker; import com.newrelic.videoagent.core.utils.NRLog; import com.newrelic.videoagent.exoplayer.BuildConfig; @@ -73,15 +74,39 @@ public class NRTrackerExoPlayer extends NRVideoTracker implements Player.Listene /** * Init a new ExoPlayer tracker. */ + public NRTrackerExoPlayer(NRVideoConfiguration configuration) { + super(configuration); + } + + /** + * Create a new NRTrackerExoPlayer (deprecated - use constructor with configuration). + * @deprecated Use NRTrackerExoPlayer(NRVideoConfiguration) constructor instead + */ + @Deprecated public NRTrackerExoPlayer() { + super(); } /** * Init a new ExoPlayer tracker. * + * @param configuration Video configuration + * @param player ExoPlayer instance. + */ + public NRTrackerExoPlayer(NRVideoConfiguration configuration, ExoPlayer player) { + super(configuration); + setPlayer(player); + } + + /** + * Init a new ExoPlayer tracker (deprecated). + * * @param player ExoPlayer instance. + * @deprecated Use NRTrackerExoPlayer(NRVideoConfiguration, ExoPlayer) constructor instead */ + @Deprecated public NRTrackerExoPlayer(ExoPlayer player) { + super(); setPlayer(player); } diff --git a/NRIMATracker/src/main/java/com/newrelic/videoagent/ima/tracker/NRTrackerIMA.java b/NRIMATracker/src/main/java/com/newrelic/videoagent/ima/tracker/NRTrackerIMA.java index 8d5e8197..690c0708 100644 --- a/NRIMATracker/src/main/java/com/newrelic/videoagent/ima/tracker/NRTrackerIMA.java +++ b/NRIMATracker/src/main/java/com/newrelic/videoagent/ima/tracker/NRTrackerIMA.java @@ -3,6 +3,7 @@ import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdEvent; +import com.newrelic.videoagent.core.NRVideoConfiguration; import com.newrelic.videoagent.core.tracker.NRVideoTracker; import com.newrelic.videoagent.core.utils.NRLog; import com.newrelic.videoagent.ima.BuildConfig; @@ -13,6 +14,22 @@ public class NRTrackerIMA extends NRVideoTracker implements AdErrorEvent.AdErrorListener, AdEvent.AdEventListener { + /** + * Create a new IMA tracker with configuration. + */ + public NRTrackerIMA(NRVideoConfiguration configuration) { + super(configuration); + } + + /** + * Create a new IMA tracker (deprecated - use constructor with configuration). + * @deprecated Use NRTrackerIMA(NRVideoConfiguration) constructor instead + */ + @Deprecated + public NRTrackerIMA() { + super(); + } + private String adPosition = null; private String creativeId = null; private Long quartile = null; diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideo.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideo.java index 91bd4b00..2bbeb2f2 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideo.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideo.java @@ -22,6 +22,7 @@ public final class NRVideo { private static final Object lock = new Object(); private volatile HarvestManager harvestManager; + private volatile NRVideoConfiguration configuration; private final Map trackerIds = new HashMap<>(); // Private constructor for singleton @@ -60,10 +61,10 @@ public static Integer addPlayer(NRVideoPlayerConfiguration config) { } // Create content tracker with ExoPlayer instance - NRTracker contentTracker = createContentTracker(); + NRTracker contentTracker = createContentTracker(instance.configuration); NRTracker adsTracker = null; if (config.isAdEnabled()) { - adsTracker = createAdTracker(); + adsTracker = createAdTracker(instance.configuration); NRLog.d("add tracker is added"); } @@ -228,6 +229,9 @@ private NRVideo initialize(Context context, NRVideoConfiguration config) { try { Context applicationContext = context.getApplicationContext(); + // Store configuration for tracker creation + this.configuration = config; + // Always use crash-safe storage - it's now the default behavior harvestManager = new HarvestManager(config, applicationContext); @@ -256,25 +260,35 @@ private NRVideo initialize(Context context, NRVideoConfiguration config) { } } - private static NRTracker createContentTracker() { + private static NRTracker createContentTracker(NRVideoConfiguration config) { try { - // Create ExoPlayer tracker with player instance + // Create ExoPlayer tracker with configuration Class exoTrackerClass = Class.forName("com.newrelic.videoagent.exoplayer.tracker.NRTrackerExoPlayer"); - return (NRTracker) exoTrackerClass.newInstance(); + return (NRTracker) exoTrackerClass.getConstructor(NRVideoConfiguration.class).newInstance(config); } catch (Exception e) { - // Fallback to basic video tracker - throw new RuntimeException("Failed to create NRTrackerExoPlayer", e); + // Fallback to deprecated constructor for backward compatibility + try { + Class exoTrackerClass = Class.forName("com.newrelic.videoagent.exoplayer.tracker.NRTrackerExoPlayer"); + return (NRTracker) exoTrackerClass.newInstance(); + } catch (Exception fallbackException) { + throw new RuntimeException("Failed to create NRTrackerExoPlayer", fallbackException); + } } } - private static NRTracker createAdTracker() { - + private static NRTracker createAdTracker(NRVideoConfiguration config) { try { - // Always use IMA tracker for ads + // Always use IMA tracker for ads with configuration Class imaTrackerClass = Class.forName("com.newrelic.videoagent.ima.tracker.NRTrackerIMA"); - return (NRTracker) imaTrackerClass.newInstance(); + return (NRTracker) imaTrackerClass.getConstructor(NRVideoConfiguration.class).newInstance(config); } catch (Exception e) { - return null; + // Fallback to deprecated constructor for backward compatibility + try { + Class imaTrackerClass = Class.forName("com.newrelic.videoagent.ima.tracker.NRTrackerIMA"); + return (NRTracker) imaTrackerClass.newInstance(); + } catch (Exception fallbackException) { + return null; + } } } diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java index d1c3afc0..48608c99 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/NRVideoConfiguration.java @@ -43,14 +43,12 @@ public final class NRVideoConfiguration { private final boolean memoryOptimized; private final boolean debugLoggingEnabled; private final boolean isTV; + private final String collectorAddress; // Runtime configuration fields (mutable, thread-safe) - Using AtomicBoolean for better performance private final AtomicBoolean qoeAggregateEnabled = new AtomicBoolean(true); private final AtomicBoolean runtimeConfigInitialized = new AtomicBoolean(false); - // Static holder for current configuration instance (for tracker access) - // WeakReference to prevent memory leaks if static cleanup is missed - private static volatile java.lang.ref.WeakReference currentInstanceRef; // Performance optimization constants private static final int DEFAULT_HARVEST_CYCLE_SECONDS = 5 * 60; // 5 minutes @@ -139,23 +137,6 @@ public void initializeFromClient(Boolean clientQoeAggregateEnabled) { } } - /** - * Get the current configuration instance (for tracker access) - * @return Current NRVideoConfiguration instance - * @throws IllegalStateException if no configuration has been built yet - */ - public static NRVideoConfiguration getCurrentInstance() { - if (currentInstanceRef == null) { - throw new IllegalStateException("No NRVideoConfiguration has been built yet!"); - } - - NRVideoConfiguration instance = currentInstanceRef.get(); - if (instance == null) { - throw new IllegalStateException("NRVideoConfiguration has been garbage collected! Create a new instance."); - } - - return instance; - } /** * Get dead letter retry interval in milliseconds * Optimized for different device types and network conditions @@ -347,8 +328,6 @@ private void applyMemoryOptimizations() { public NRVideoConfiguration build() { NRVideoConfiguration config = new NRVideoConfiguration(this); - // Set current instance for tracker access using WeakReference - NRVideoConfiguration.currentInstanceRef = new java.lang.ref.WeakReference<>(config); // Mark runtime configuration as initialized config.runtimeConfigInitialized.set(true); return config; diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRTracker.java index 521e0868..14230c1e 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRTracker.java @@ -1,6 +1,7 @@ package com.newrelic.videoagent.core.tracker; import com.newrelic.videoagent.core.NRVideo; +import com.newrelic.videoagent.core.NRVideoConfiguration; import com.newrelic.videoagent.core.NewRelicVideoAgent; import com.newrelic.videoagent.core.model.NREventAttributes; import com.newrelic.videoagent.core.model.NRTimeSince; @@ -21,17 +22,37 @@ public class NRTracker { */ public NRTracker linkedTracker; + protected final NRVideoConfiguration configuration; private final NREventAttributes eventAttributes; private NRTimeSinceTable timeSinceTable; /** - * Create a new NRTracker. + * Create a new NRTracker with configuration. */ + public NRTracker(NRVideoConfiguration configuration) { + this.configuration = configuration; + eventAttributes = new NREventAttributes(); + generateTimeSinceTable(); + } + + /** + * Create a new NRTracker (deprecated - use constructor with configuration). + * @deprecated Use NRTracker(NRVideoConfiguration) constructor instead + */ + @Deprecated public NRTracker() { + this.configuration = null; eventAttributes = new NREventAttributes(); generateTimeSinceTable(); } + /** + * Get the tracker configuration. + */ + public NRVideoConfiguration getConfiguration() { + return configuration; + } + /** * Tracker is ready. */ diff --git a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java index 60c8bcad..a928e654 100644 --- a/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java +++ b/NewRelicVideoCore/src/main/java/com/newrelic/videoagent/core/tracker/NRVideoTracker.java @@ -70,7 +70,44 @@ public class NRVideoTracker extends NRTracker { /** * Create a new NRVideoTracker. */ + public NRVideoTracker(NRVideoConfiguration configuration) { + super(configuration); + state = new NRTrackerState(); + numberOfAds = 0; + numberOfErrors = 0; + numberOfVideos = 0; + viewIdIndex = 0; + adBreakIdIndex = 0; + viewSessionId = getAgentSession() + "-" + (System.currentTimeMillis() / 1000) + new Random().nextInt(10); + playtimeSinceLastEventTimestamp = 0L; + totalPlaytime = 0L; + totalAdPlaytime = 0L; + playtimeSinceLastEvent = 0L; + bufferType = null; + isHeartbeatRunning = false; + + // Initialize heartbeat components + heartbeatHandler = new Handler(); + heartbeatRunnable = new Runnable() { + @Override + public void run() { + if (isHeartbeatRunning) { + sendHeartbeat(); + heartbeatHandler.postDelayed(heartbeatRunnable, getHeartbeatIntervalMillis()); + } + } + }; + + initializeTracker(); + } + + /** + * Create a new NRVideoTracker (deprecated - use constructor with configuration). + * @deprecated Use NRVideoTracker(NRVideoConfiguration) constructor instead + */ + @Deprecated public NRVideoTracker() { + super(); state = new NRTrackerState(); numberOfAds = 0; numberOfErrors = 0; @@ -85,6 +122,23 @@ public NRVideoTracker() { bufferType = null; isHeartbeatRunning = false; + // Initialize heartbeat components + heartbeatHandler = new Handler(); + heartbeatRunnable = new Runnable() { + @Override + public void run() { + if (isHeartbeatRunning) { + sendHeartbeat(); + heartbeatHandler.postDelayed(heartbeatRunnable, getHeartbeatIntervalMillis()); + } + } + }; + + initializeTracker(); + } + + private void initializeTracker() { + // Initialize QoE tracking fields qoePeakBitrate = 0L; qoeHadPlaybackFailure = false; @@ -106,16 +160,6 @@ public NRVideoTracker() { qoeLastRenditionChangeTime = null; qoeTotalBitrateWeightedTime = 0L; qoeTotalActiveTime = 0L; - heartbeatHandler = new Handler(); - heartbeatRunnable = new Runnable() { - @Override - public void run() { - if (isHeartbeatRunning) { - sendHeartbeat(); - heartbeatHandler.postDelayed(heartbeatRunnable, getHeartbeatIntervalMillis()); - } - } - }; } /** @@ -593,7 +637,7 @@ private void resetHarvestCycleFlags() { * Send QOE_AGGREGATE event (internal method - called once per cycle) */ private void sendQoeAggregate() { - if (!state.isAd && NRVideoConfiguration.getCurrentInstance().isQoeAggregateEnabled()) { + if (!state.isAd && configuration != null && configuration.isQoeAggregateEnabled()) { // Only send for content, not ads, and only if QOE aggregate is enabled Map kpiAttributes = calculateQOEKpiAttributes(); sendVideoEvent(QOE_AGGREGATE, kpiAttributes);