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/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/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/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", 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;