From 840177075429a2447e1b4d3101504eb3e82dcefc Mon Sep 17 00:00:00 2001 From: Walter Holohan Date: Sat, 21 Feb 2026 20:23:26 +0000 Subject: [PATCH 1/2] fix(android): add optional gradle dependency for appSetId to work (#1750) it is a play services library, and is required to make the appSetId functionality work, but the APIs are accessed reflexively so this shouldn't affect anyone that distributes to devices that don't have Play Services on them, or for various reasons cannot accept the licensing of the Play Services dependencies --- .github/workflows/windows-app-test.yml | 3 + README.md | 7 +- android/build.gradle | 5 ++ .../learnium/RNDeviceInfo/RNDeviceModule.java | 87 +++++++++++-------- src/internal/types.ts | 1 + 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/.github/workflows/windows-app-test.yml b/.github/workflows/windows-app-test.yml index edf82771f..d19252d0c 100644 --- a/.github/workflows/windows-app-test.yml +++ b/.github/workflows/windows-app-test.yml @@ -9,6 +9,9 @@ jobs: run-windows-tests: name: Build & run tests runs-on: windows-2019 + env: + # Node 17+ uses OpenSSL 3.0; Metro/RN bundler needs legacy provider for hashing + NODE_OPTIONS: --openssl-legacy-provider steps: - uses: actions/checkout@v5 diff --git a/README.md b/README.md index 520538120..6d91fb023 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,8 @@ This module defaults to AndroidX you should configure your library versions simi googlePlayServicesIidVersion = "17.0.0" // default: "17.0.0" - AndroidX //Option 3 (legacy GooglePlay dependency before AndroidX): googlePlayServicesIidVersion = "16.0.1" - + // getAppSetId() - optional: set to include play-services-appset (e.g. "16.1.0") + // playServicesAppSetVersion = "16.1.0" //include as needed: compileSdkVersion = "28" // default: 28 (28 is required for AndroidX) @@ -245,7 +246,7 @@ DeviceInfo.getAndroidId().then((androidId) => { ### getAppSetId() -Gets the AppSetId for Android devices. AppSetId is part of Android's Privacy Sandbox and provides a privacy-preserving identifier for advertising and analytics purposes. This API is only available on Android 14 (API level 34) and above. +Gets the App Set ID for Android devices via Google Play services. App Set ID provides a privacy-preserving identifier for correlating usage across apps from the same developer (e.g. analytics, fraud prevention). **Optional**: set `playServicesAppSetVersion` in your app's `android/build.gradle` ext to include the dependency; otherwise returns `{ id: 'unknown', scope: -1 }`. The returned object contains: - `id`: The AppSetId string value (returns "unknown" if not available) @@ -273,7 +274,7 @@ if (appSetIdInfo.id === 'unknown') { } ``` -**Note**: AppSetId requires Android 14 (API level 34) or higher. On older Android versions or when the service is unavailable, the function will return `{ id: 'unknown', scope: -1 }`. +**Note**: To use `getAppSetId()` on Android you must add the optional dependency by setting `playServicesAppSetVersion` in your app's `android/build.gradle` ext block (e.g. `playServicesAppSetVersion = "16.1.0"`). If the dependency is not included or the service is unavailable, the function returns `{ id: 'unknown', scope: -1 }`. --- diff --git a/android/build.gradle b/android/build.gradle index 81444a4ef..e352534fd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -60,6 +60,7 @@ dependencies { def firebaseBomVersion = safeExtGet("firebaseBomVersion", null) def firebaseIidVersion = safeExtGet('firebaseIidVersion', null) def googlePlayServicesIidVersion = safeExtGet('googlePlayServicesIidVersion', null) + def playServicesAppSetVersion = safeExtGet('playServicesAppSetVersion', null) if (firebaseBomVersion) { implementation platform("com.google.firebase:firebase-bom:${firebaseBomVersion}") @@ -69,6 +70,10 @@ dependencies { } else if(googlePlayServicesIidVersion){ implementation "com.google.android.gms:play-services-iid:$googlePlayServicesIidVersion" } + // Needed for getAppSetId() to work - optional: set playServicesAppSetVersion in app's ext to include + if (playServicesAppSetVersion) { + implementation "com.google.android.gms:play-services-appset:$playServicesAppSetVersion" + } testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testImplementation "org.mockito:mockito-core:3.6.28" diff --git a/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java b/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java index 64253a17c..38214c017 100644 --- a/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java +++ b/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java @@ -2,8 +2,6 @@ import android.Manifest; import android.annotation.SuppressLint; -import android.adservices.appsetid.AppSetId; -import android.adservices.appsetid.AppSetIdManager; import android.app.KeyguardManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -21,7 +19,6 @@ import android.net.wifi.WifiInfo; import android.os.Build; import android.os.Environment; -import android.os.OutcomeReceiver; import android.os.PowerManager; import android.os.StatFs; import android.os.BatteryManager; @@ -66,6 +63,9 @@ import java.math.BigInteger; import java.util.Locale; import java.util.Map; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import javax.annotation.Nonnull; @@ -1128,43 +1128,62 @@ private boolean hasKeyboard(String name) { @ReactMethod public void getAppSetId(Promise promise) { - System.err.println("RNDI: getAppSetId starting"); - if (Build.VERSION.SDK_INT >= 34) { // Android 14 (API level 34) - try { - AppSetIdManager appSetIdManager = AppSetIdManager.get(getReactApplicationContext()); - appSetIdManager.getAppSetId( - getReactApplicationContext().getMainExecutor(), - new OutcomeReceiver() { - public void onResult(AppSetId appSetId) { - System.err.println("RNDI: AppSetId success."); + try { + // Optionally load App Set classes via reflection (only when play-services-appset is included) + Class appSetClass = Class.forName("com.google.android.gms.appset.AppSet"); + ClassLoader loader = appSetClass.getClassLoader(); + Method getClientMethod = appSetClass.getMethod("getClient", Context.class); + Object client = getClientMethod.invoke(null, getReactApplicationContext()); + Method getAppSetIdInfoMethod = client.getClass().getMethod("getAppSetIdInfo"); + Object task = getAppSetIdInfoMethod.invoke(client); + + Class onSuccessListenerClass = + Class.forName("com.google.android.gms.tasks.OnSuccessListener", true, loader); + InvocationHandler successHandler = + (proxy, method, args) -> { + if ("onSuccess".equals(method.getName()) && args != null && args.length == 1) { + Object appSetIdInfo = args[0]; + String id = (String) appSetIdInfo.getClass().getMethod("getId").invoke(appSetIdInfo); + Object scopeObj = appSetIdInfo.getClass().getMethod("getScope").invoke(appSetIdInfo); + int scope = scopeObj instanceof Number ? ((Number) scopeObj).intValue() : -1; WritableMap result = Arguments.createMap(); - result.putString("id", appSetId.getId()); - result.putInt("scope", appSetId.getScope()); + result.putString("id", id != null ? id : "unknown"); + result.putInt("scope", scope); promise.resolve(result); - }; - public void onError(Exception exception) { - System.err.println("RNDI: AppSetId was a failure: " + exception); - exception.printStackTrace(System.err); - // Return default values instead of rejecting the promise + } + return null; + }; + Object successListener = + Proxy.newProxyInstance(loader, new Class[] {onSuccessListenerClass}, successHandler); + + Class onFailureListenerClass = + Class.forName("com.google.android.gms.tasks.OnFailureListener", true, loader); + InvocationHandler failureHandler = + (proxy, method, args) -> { + if ("onFailure".equals(method.getName()) && args != null && args.length == 1) { + Exception e = (Exception) args[0]; + System.err.println("RNDI: AppSetId was a failure: " + e); + e.printStackTrace(System.err); WritableMap result = Arguments.createMap(); result.putString("id", "unknown"); result.putInt("scope", -1); promise.resolve(result); - }; - } - ); - } catch (Exception e) { - System.err.println("RNDI Exception: " + e); - e.printStackTrace(System.err); - // Return default values instead of rejecting the promise - WritableMap result = Arguments.createMap(); - result.putString("id", "unknown"); - result.putInt("scope", -1); - promise.resolve(result); - } - } else { - // Return default values for unsupported Android versions - System.err.println("RNDI: simply didn't try)"); + } + return null; + }; + Object failureListener = + Proxy.newProxyInstance(loader, new Class[] {onFailureListenerClass}, failureHandler); + + task.getClass() + .getMethod("addOnSuccessListener", onSuccessListenerClass) + .invoke(task, successListener); + task.getClass() + .getMethod("addOnFailureListener", onFailureListenerClass) + .invoke(task, failureListener); + } catch (Throwable t) { + // ClassNotFoundException when play-services-appset not included, or other errors + System.err.println("RNDI Exception: " + t); + t.printStackTrace(System.err); WritableMap result = Arguments.createMap(); result.putString("id", "unknown"); result.putInt("scope", -1); diff --git a/src/internal/types.ts b/src/internal/types.ts index 1bb305e6b..ea0587406 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -40,6 +40,7 @@ export type AvailableCapacityType = 'total' | 'important' | 'opportunistic'; /** * Google Play Services App Set ID payload describing identifier and scope. + * When the API is unavailable, id is "unknown" and scope is -1. */ export interface AppSetIdInfo { id: string; From 06a9ed6d700cab9c55b363e6fa944ee8e5605745 Mon Sep 17 00:00:00 2001 From: Mike Hardy Date: Sat, 21 Feb 2026 16:03:26 -0500 Subject: [PATCH 2/2] chore: release v15.0.2 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f225593d..bf60f2f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [15.0.2](https://github.com/react-native-device-info/react-native-device-info/compare/v15.0.1...v15.0.2) (2026-02-21) + +### Bug Fixes + +* **android:** add optional gradle dependency for appSetId to work ([#1750](https://github.com/react-native-device-info/react-native-device-info/issues/1750)) ([8401770](https://github.com/react-native-device-info/react-native-device-info/commit/840177075429a2447e1b4d3101504eb3e82dcefc)) + ## [15.0.1](https://github.com/react-native-device-info/react-native-device-info/compare/v15.0.0...v15.0.1) (2025-11-13) ### Bug Fixes diff --git a/package.json b/package.json index 43dc8a22d..e85ca45c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-device-info", - "version": "15.0.1", + "version": "15.0.2", "description": "Get device information using react-native", "react-native": "src/index.ts", "types": "lib/typescript/index.d.ts",