diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e857b194..388f2438b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,10 +41,21 @@ jobs: working-directory: OneSignalSDK run: | ./gradlew testDebugUnitTest --console=plain --continue - - name: "[Build] Demo app (minified GMS + Huawei release)" + - name: "[Build] Demo app (minified GMS release)" working-directory: OneSignalSDK run: | - ./gradlew :app:assembleGmsRelease :app:assembleHuaweiRelease --console=plain + ./gradlew :app:assembleGmsRelease --console=plain + # Runs against the GMS release seeds emitted by the build above (before the Huawei build + # overwrites them). Fails if a consumer keep protecting a reflectively-referenced member + # silently disappeared -- the class of regression that assemble alone cannot catch. + - name: "[R8] Consumer keep-rules diff gate" + working-directory: OneSignalSDK + run: | + ../scripts/r8-keep-check.sh + - name: "[Build] Demo app (minified Huawei release)" + working-directory: OneSignalSDK + run: | + ./gradlew :app:assembleHuaweiRelease --console=plain - name: "[Diff Coverage] Check for bypass" id: coverage_bypass if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' diff --git a/OneSignalSDK/onesignal/core/consumer-rules.pro b/OneSignalSDK/onesignal/core/consumer-rules.pro index ccca56308..8fa3f799d 100644 --- a/OneSignalSDK/onesignal/core/consumer-rules.pro +++ b/OneSignalSDK/onesignal/core/consumer-rules.pro @@ -1,19 +1,35 @@ --dontwarn com.onesignal.** - +# Amazon ADM is a compileOnly dependency; suppress missing-class warnings when the app omits it. -dontwarn com.amazon.** --keepclassmembers class com.onesignal.core.** { *; } - --keepclassmembers class com.onesignal.session.** { *; } - --keepclassmembers class com.onesignal.user.** { *; } - --keepclassmembers class com.onesignal.internal.** { *; } - --keepclassmembers class com.onesignal.debug.** { *; } - --keepclassmembers class com.onesignal.common.** { *; } - +# Service implementations are instantiated by reflectively selecting a constructor +# (ServiceRegistrationReflection.resolve -> constructor.newInstance). The classes are retained via +# ::class.java literals in the modules, so only their constructors need protecting here. +-keepclassmembers class com.onesignal.** { + (...); +} + +# Models are populated from JSON by matching getter method *names* via reflection +# (Model.initializeFromJson looks up "get"/"is"). Getters must survive and keep +# their names, so this must run without allowobfuscation. +-keepclassmembers class * extends com.onesignal.common.modeling.Model { + *** get*(); + *** is*(); +} + +# Enum constants are looked up by name via valueOf/enumValueOf against server-supplied strings +# (e.g. Model.getOptEnumProperty -> ConfigModel.logLevel from remote-params JSON). Their names must +# survive obfuscation, so keep enum fields and the synthetic values()/valueOf(String) methods. +-keepclassmembers enum com.onesignal.** { + ; + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +# Optional feature modules are loaded by fully-qualified name (OneSignalImp loads them via +# Class.forName(...).newInstance()), so the name must be preserved and a no-arg constructor kept. +-keep class ** implements com.onesignal.common.modules.IModule { + (); +} + +# Fallback "misconfigured" managers are reflectively constructed when an optional module is absent. -keepclassmembers @com.onesignal.core.internal.minification.KeepStub class * { (...); } - --keep class ** implements com.onesignal.common.modules.IModule { *; } diff --git a/OneSignalSDK/onesignal/in-app-messages/consumer-rules.pro b/OneSignalSDK/onesignal/in-app-messages/consumer-rules.pro index 644bfe12f..165ed4507 100644 --- a/OneSignalSDK/onesignal/in-app-messages/consumer-rules.pro +++ b/OneSignalSDK/onesignal/in-app-messages/consumer-rules.pro @@ -1,3 +1,13 @@ --dontwarn com.onesignal.iam.** +# In-app-messages service implementations are instantiated via reflective constructor selection +# (ServiceRegistrationReflection). Keep only their constructors. +-keepclassmembers class com.onesignal.inAppMessages.** { + (...); +} --keepclassmembers class com.onesignal.inAppMessages.** { *; } \ No newline at end of file +# IAM display Position is resolved via Position.valueOf(serverString) (WebViewManager) and the +# IllegalArgumentException on a renamed constant is uncaught. Keep enum constant names. +-keepclassmembers enum com.onesignal.inAppMessages.** { + ; + public static **[] values(); + public static ** valueOf(java.lang.String); +} diff --git a/OneSignalSDK/onesignal/location/consumer-rules.pro b/OneSignalSDK/onesignal/location/consumer-rules.pro index 40ec90dc9..7fd650ea1 100644 --- a/OneSignalSDK/onesignal/location/consumer-rules.pro +++ b/OneSignalSDK/onesignal/location/consumer-rules.pro @@ -1,3 +1,13 @@ --dontwarn com.onesignal.location.** +# Location service implementations are instantiated via reflective constructor selection +# (ServiceRegistrationReflection). Keep only their constructors. +-keepclassmembers class com.onesignal.location.** { + (...); +} --keepclassmembers class com.onesignal.location.** { *; } \ No newline at end of file +# GoogleApiClientCompatProxy invokes these GMS methods reflectively by string name (for +# compatibility across Google Play services versions), so their names must be preserved. +-keepclassmembers class com.google.android.gms.common.api.GoogleApiClient { + com.google.android.gms.common.ConnectionResult blockingConnect(); + void connect(); + void disconnect(); +} diff --git a/OneSignalSDK/onesignal/notifications/consumer-rules.pro b/OneSignalSDK/onesignal/notifications/consumer-rules.pro index c348aaaa1..6393ef139 100644 --- a/OneSignalSDK/onesignal/notifications/consumer-rules.pro +++ b/OneSignalSDK/onesignal/notifications/consumer-rules.pro @@ -1,62 +1,42 @@ --dontwarn com.onesignal.notification.** - -# These 2 methods are called with reflection. --keep class com.google.android.gms.common.api.GoogleApiClient { - void connect(); - void disconnect(); -} - -# Need to keep as these 2 methods are called with reflection from com.onesignal.PushRegistratorFCM --keep class com.google.firebase.iid.FirebaseInstanceId { - static com.google.firebase.iid.FirebaseInstanceId getInstance(com.google.firebase.FirebaseApp); - java.lang.String getToken(java.lang.String, java.lang.String); -} - --keep class ** implements com.onesignal.notifications.IPermissionObserver{ - void onNotificationPermissionChange(java.lang.Boolean); -} - --keep class ** implements com.onesignal.user.subscriptions.IPushSubscriptionObserver { - void onPushSubscriptionChange(com.onesignal.user.subscriptions.PushSubscriptionChangedState); -} - --keep class ** implements com.onesignal.user.state.IUserStateObserver { - void onUserStateChange(com.onesignal.user.state.UserChangedState); -} - --keep class ** implements com.onesignal.notifications.INotificationServiceExtension{ +# The notification service extension class is named in the app's AndroidManifest and is +# reflectively instantiated (NotificationLifecycleService -> Class.forName(name).newInstance()), so the +# class name and its no-arg constructor must be preserved. +-keep class ** implements com.onesignal.notifications.INotificationServiceExtension { + (); void onNotificationReceived(com.onesignal.notifications.INotificationReceivedEvent); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.AdwHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.ApexHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.AsusHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.DefaultBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.EverythingMeHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.HuaweiHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.LGHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.NewHtcHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.NovaHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.OPPOHomeBader { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.SamsungHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.SonyHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.VivoHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.XiaomiHomeBadger { (...); } --keep class com.onesignal.notifications.internal.badges.impl.shortcutbadger.impl.ZukHomeBadger { (...); } +# Home-screen badgers are instantiated via newInstance() from a list of class literals +# (ShortcutBadger.initBadger), so each implementation needs its constructor kept. +-keep class * implements com.onesignal.notifications.internal.badges.impl.shortcutbadger.Badger { + (...); +} +# Optional push providers are compileOnly in the SDK and referenced directly by the +# provider-specific registrators. A consuming app only bundles the provider(s) it targets, so the +# others are absent at R8 time: Huawei-only apps exclude com.google.firebase:firebase-messaging +# (removing com.google.firebase.**, referenced from PushRegistratorFCM), just as GMS apps omit HMS +# (com.huawei.**) and ADM (com.amazon.**). Suppress the missing-class errors for all three. -dontwarn com.huawei.** -dontwarn com.amazon.** +-dontwarn com.google.firebase.** -# Proguard ends up removing this class even if it is used in AndroidManifest.xml so force keeping it. --keep public class com.onesignal.notifications.services.ADMMessageHandler {*;} +# ADM handlers are instantiated by name from the app manifest AND their on* lifecycle callbacks +# (onMessage/onRegistered/onRegistrationError/onUnregistered) are invoked by the ADM framework, not +# the SDK, so keep both constructors and those methods. (Amazon-device-only path, untestable in CI.) +-keep public class com.onesignal.notifications.services.ADMMessageHandler { (...); void on*(...); } +-keep public class com.onesignal.notifications.services.ADMMessageHandlerJob { (...); void on*(...); } --keep public class com.onesignal.notifications.services.ADMMessageHandlerJob {*;} +# Legacy v4 job shim is referenced by name (older scheduled jobs); keep its constructors. +-keep class com.onesignal.JobIntentService$* { (...); } --keep class com.onesignal.JobIntentService$* {*;} - --keepclassmembers class com.onesignal.notifications.** { *; } +# Notification service implementations are instantiated via reflective constructor selection +# (ServiceRegistrationReflection). Keep only their constructors. +-keepclassmembers class com.onesignal.notifications.** { + (...); +} -# Keep OneSignal WorkManager workers and constructors used for runtime instantiation. +# WorkManager instantiates workers via reflection using the (Context, WorkerParameters) constructor. -keep class com.onesignal.notifications.internal.** extends androidx.work.ListenableWorker { public (android.content.Context, androidx.work.WorkerParameters); } @@ -64,9 +44,7 @@ # WorkManager instantiates InputMerger classes via reflection (InputMerger.fromClassName). # R8 full mode (AGP 8+) strips no-arg constructors, causing: # java.lang.NoSuchMethodException: androidx.work.OverwritingInputMerger.() -# WM-WorkerWrapper E Could not create Input Merger androidx.work.OverwritingInputMerger # Keep all InputMerger subclasses (OverwritingInputMerger, ArrayCreatingInputMerger, etc.) -keep class * extends androidx.work.InputMerger { public (); } - diff --git a/examples/demo/app/build.gradle.kts b/examples/demo/app/build.gradle.kts index f28cae61a..802ee40c1 100644 --- a/examples/demo/app/build.gradle.kts +++ b/examples/demo/app/build.gradle.kts @@ -98,7 +98,10 @@ android { signingConfig = signingConfigs.getByName("debug") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", + // Emits R8 seeds/config so CI can diff kept consumer members against a baseline + // and catch a reflectively-referenced keep being silently dropped. + "proguard-r8-report.pro" ) } debug { diff --git a/examples/demo/app/proguard-r8-report.pro b/examples/demo/app/proguard-r8-report.pro new file mode 100644 index 000000000..ad65e8cf4 --- /dev/null +++ b/examples/demo/app/proguard-r8-report.pro @@ -0,0 +1,15 @@ +# R8 keep-rules reporting for the consumer keep-rules diff gate. +# +# Why: the OneSignal library modules ship consumer keep rules (consumer-rules.pro) that protect +# members reached only via string-based reflection (e.g. GoogleApiClient.connect/disconnect, +# Model getters, service constructors). Those breakages are invisible at assemble time because R8 +# only errors on missing classes, not on renamed/removed methods reached by reflection string. +# Emitting the seeds (members R8 kept) lets CI diff them against a baseline and fail when a +# previously-protected member silently stops being kept. +# +# -printseeds lists exactly which classes/members survived shrinking (the primary diff signal). +# -printconfiguration writes the fully-merged R8 configuration (human-readable secondary signal). +# Paths are relative to the module dir; both land under build/ (gitignored) and are consumed by +# scripts/r8-keep-check.sh after the minified release build. +-printseeds build/outputs/r8-report/seeds.txt +-printconfiguration build/outputs/r8-report/configuration.txt diff --git a/examples/demo/app/r8-keep-baseline.txt b/examples/demo/app/r8-keep-baseline.txt new file mode 100644 index 000000000..0dc5340f7 --- /dev/null +++ b/examples/demo/app/r8-keep-baseline.txt @@ -0,0 +1,297 @@ +com.google.android.gms.common.api.GoogleApiClient: com.google.android.gms.common.ConnectionResult blockingConnect() +com.google.android.gms.common.api.GoogleApiClient: void connect() +com.google.android.gms.common.api.GoogleApiClient: void disconnect() +com.onesignal.common.AndroidUtils$SchemaType: com.onesignal.common.AndroidUtils$SchemaType valueOf(java.lang.String) +com.onesignal.common.AndroidUtils$SchemaType: com.onesignal.common.AndroidUtils$SchemaType[] values() +com.onesignal.common.AndroidUtils: boolean opaqueHasClass(java.lang.Class) +com.onesignal.common.NetworkUtils$ResponseStatusType: com.onesignal.common.NetworkUtils$ResponseStatusType valueOf(java.lang.String) +com.onesignal.common.NetworkUtils$ResponseStatusType: com.onesignal.common.NetworkUtils$ResponseStatusType[] values() +com.onesignal.common.consistency.enums.IamFetchRywTokenKey: com.onesignal.common.consistency.enums.IamFetchRywTokenKey valueOf(java.lang.String) +com.onesignal.common.consistency.enums.IamFetchRywTokenKey: com.onesignal.common.consistency.enums.IamFetchRywTokenKey[] values() +com.onesignal.common.modeling.MapModel: boolean isEmpty() +com.onesignal.common.modeling.MapModel: int getSize() +com.onesignal.common.modeling.MapModel: java.util.Collection getValues() +com.onesignal.common.modeling.MapModel: java.util.Set getEntries() +com.onesignal.common.modeling.MapModel: java.util.Set getKeys() +com.onesignal.common.modeling.Model: boolean getHasSubscribers() +com.onesignal.common.modeling.Model: java.lang.String getId() +com.onesignal.common.modeling.Model: java.util.Map getData() +com.onesignal.core.internal.application.AppEntryAction: com.onesignal.core.internal.application.AppEntryAction valueOf(java.lang.String) +com.onesignal.core.internal.application.AppEntryAction: com.onesignal.core.internal.application.AppEntryAction[] values() +com.onesignal.core.internal.config.ConfigModel: boolean getClearGroupOnSummaryClick() +com.onesignal.core.internal.config.ConfigModel: boolean getDisableGMSMissingPrompt() +com.onesignal.core.internal.config.ConfigModel: boolean getEnterprise() +com.onesignal.core.internal.config.ConfigModel: boolean getFirebaseAnalytics() +com.onesignal.core.internal.config.ConfigModel: boolean getLocationShared() +com.onesignal.core.internal.config.ConfigModel: boolean getReceiveReceiptEnabled() +com.onesignal.core.internal.config.ConfigModel: boolean getRestoreTTLFilter() +com.onesignal.core.internal.config.ConfigModel: boolean getUnsubscribeWhenNotificationsDisabled() +com.onesignal.core.internal.config.ConfigModel: boolean getUserRejectedGMSUpdate() +com.onesignal.core.internal.config.ConfigModel: boolean isInitializedWithRemote() +com.onesignal.core.internal.config.ConfigModel: com.onesignal.core.internal.config.FCMConfigModel getFcmParams() +com.onesignal.core.internal.config.ConfigModel: com.onesignal.core.internal.config.InfluenceConfigModel getInfluenceParams() +com.onesignal.core.internal.config.ConfigModel: com.onesignal.core.internal.config.RemoteLoggingConfigModel getRemoteLoggingParams() +com.onesignal.core.internal.config.ConfigModel: com.onesignal.user.internal.jwt.JwtRequirement getUseIdentityVerification$com_onesignal_core() +com.onesignal.core.internal.config.ConfigModel: int getHttpGetTimeout() +com.onesignal.core.internal.config.ConfigModel: int getHttpRetryAfterParseFailFallback() +com.onesignal.core.internal.config.ConfigModel: int getHttpTimeout() +com.onesignal.core.internal.config.ConfigModel: java.lang.Boolean getConsentGiven() +com.onesignal.core.internal.config.ConfigModel: java.lang.Boolean getConsentRequired() +com.onesignal.core.internal.config.ConfigModel: java.lang.String getApiUrl() +com.onesignal.core.internal.config.ConfigModel: java.lang.String getAppId() +com.onesignal.core.internal.config.ConfigModel: java.lang.String getGoogleProjectNumber() +com.onesignal.core.internal.config.ConfigModel: java.lang.String getPushSubscriptionId() +com.onesignal.core.internal.config.ConfigModel: java.lang.String getSdkRemoteFeatureFlagMetadata() +com.onesignal.core.internal.config.ConfigModel: java.util.List getSdkRemoteFeatureFlags() +com.onesignal.core.internal.config.ConfigModel: long getBackgroundFetchNotificationPermissionInterval() +com.onesignal.core.internal.config.ConfigModel: long getFetchIAMMinInterval() +com.onesignal.core.internal.config.ConfigModel: long getForegroundFetchNotificationPermissionInterval() +com.onesignal.core.internal.config.ConfigModel: long getOpRepoDefaultFailRetryBackoff() +com.onesignal.core.internal.config.ConfigModel: long getOpRepoExecutionInterval() +com.onesignal.core.internal.config.ConfigModel: long getOpRepoPostCreateDelay() +com.onesignal.core.internal.config.ConfigModel: long getOpRepoPostCreateRetryUpTo() +com.onesignal.core.internal.config.ConfigModel: long getOpRepoPostWakeDelay() +com.onesignal.core.internal.config.ConfigModel: long getSessionFocusTimeout() +com.onesignal.core.internal.config.ConfigModel: org.json.JSONArray getNotificationChannels() +com.onesignal.core.internal.config.FCMConfigModel: java.lang.String getApiKey() +com.onesignal.core.internal.config.FCMConfigModel: java.lang.String getAppId() +com.onesignal.core.internal.config.FCMConfigModel: java.lang.String getProjectId() +com.onesignal.core.internal.config.InfluenceConfigModel: boolean isDirectEnabled() +com.onesignal.core.internal.config.InfluenceConfigModel: boolean isIndirectEnabled() +com.onesignal.core.internal.config.InfluenceConfigModel: boolean isUnattributedEnabled() +com.onesignal.core.internal.config.InfluenceConfigModel: int getIamLimit() +com.onesignal.core.internal.config.InfluenceConfigModel: int getIndirectIAMAttributionWindow() +com.onesignal.core.internal.config.InfluenceConfigModel: int getIndirectNotificationAttributionWindow() +com.onesignal.core.internal.config.InfluenceConfigModel: int getNotificationLimit() +com.onesignal.core.internal.config.RemoteLoggingConfigModel: boolean isEnabled() +com.onesignal.core.internal.config.RemoteLoggingConfigModel: com.onesignal.debug.LogLevel getLogLevel() +com.onesignal.core.internal.device.IDeviceService$DeviceType: com.onesignal.core.internal.device.IDeviceService$DeviceType valueOf(java.lang.String) +com.onesignal.core.internal.device.IDeviceService$DeviceType: com.onesignal.core.internal.device.IDeviceService$DeviceType[] values() +com.onesignal.core.internal.device.IDeviceService$JetpackLibraryStatus: com.onesignal.core.internal.device.IDeviceService$JetpackLibraryStatus valueOf(java.lang.String) +com.onesignal.core.internal.device.IDeviceService$JetpackLibraryStatus: com.onesignal.core.internal.device.IDeviceService$JetpackLibraryStatus[] values() +com.onesignal.core.internal.features.FeatureActivationMode: com.onesignal.core.internal.features.FeatureActivationMode valueOf(java.lang.String) +com.onesignal.core.internal.features.FeatureActivationMode: com.onesignal.core.internal.features.FeatureActivationMode[] values() +com.onesignal.core.internal.features.FeatureFlag: com.onesignal.core.internal.features.FeatureFlag valueOf(java.lang.String) +com.onesignal.core.internal.features.FeatureFlag: com.onesignal.core.internal.features.FeatureFlag[] values() +com.onesignal.core.internal.operations.ExecutionResult: com.onesignal.core.internal.operations.ExecutionResult valueOf(java.lang.String) +com.onesignal.core.internal.operations.ExecutionResult: com.onesignal.core.internal.operations.ExecutionResult[] values() +com.onesignal.core.internal.operations.GroupComparisonType: com.onesignal.core.internal.operations.GroupComparisonType valueOf(java.lang.String) +com.onesignal.core.internal.operations.GroupComparisonType: com.onesignal.core.internal.operations.GroupComparisonType[] values() +com.onesignal.core.internal.operations.Operation: boolean getCanStartExecute() +com.onesignal.core.internal.operations.Operation: boolean getRequiresJwt() +com.onesignal.core.internal.operations.Operation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.core.internal.operations.Operation: java.lang.String getApplyToRecordId() +com.onesignal.core.internal.operations.Operation: java.lang.String getCreateComparisonKey() +com.onesignal.core.internal.operations.Operation: java.lang.String getExternalId() +com.onesignal.core.internal.operations.Operation: java.lang.String getModifyComparisonKey() +com.onesignal.core.internal.operations.Operation: java.lang.String getName() +com.onesignal.debug.LogLevel: com.onesignal.debug.LogLevel valueOf(java.lang.String) +com.onesignal.debug.LogLevel: com.onesignal.debug.LogLevel[] values() +com.onesignal.inAppMessages.InAppMessageActionUrlType: com.onesignal.inAppMessages.InAppMessageActionUrlType valueOf(java.lang.String) +com.onesignal.inAppMessages.InAppMessageActionUrlType: com.onesignal.inAppMessages.InAppMessageActionUrlType[] values() +com.onesignal.inAppMessages.internal.Trigger$OSTriggerKind: com.onesignal.inAppMessages.internal.Trigger$OSTriggerKind valueOf(java.lang.String) +com.onesignal.inAppMessages.internal.Trigger$OSTriggerKind: com.onesignal.inAppMessages.internal.Trigger$OSTriggerKind[] values() +com.onesignal.inAppMessages.internal.Trigger$OSTriggerOperator: com.onesignal.inAppMessages.internal.Trigger$OSTriggerOperator valueOf(java.lang.String) +com.onesignal.inAppMessages.internal.Trigger$OSTriggerOperator: com.onesignal.inAppMessages.internal.Trigger$OSTriggerOperator[] values() +com.onesignal.inAppMessages.internal.display.impl.DraggableRelativeLayout: void setListener(com.onesignal.inAppMessages.internal.display.impl.DraggableRelativeLayout$DraggableListener) +com.onesignal.inAppMessages.internal.display.impl.DraggableRelativeLayout: void setParams(com.onesignal.inAppMessages.internal.display.impl.DraggableRelativeLayout$Params) +com.onesignal.inAppMessages.internal.display.impl.WebViewManager$OSJavaScriptInterface: void postMessage(java.lang.String) +com.onesignal.inAppMessages.internal.display.impl.WebViewManager$Position: com.onesignal.inAppMessages.internal.display.impl.WebViewManager$Position valueOf(java.lang.String) +com.onesignal.inAppMessages.internal.display.impl.WebViewManager$Position: com.onesignal.inAppMessages.internal.display.impl.WebViewManager$Position[] values() +com.onesignal.inAppMessages.internal.prompt.impl.InAppMessagePrompt$PromptActionResult: com.onesignal.inAppMessages.internal.prompt.impl.InAppMessagePrompt$PromptActionResult valueOf(java.lang.String) +com.onesignal.inAppMessages.internal.prompt.impl.InAppMessagePrompt$PromptActionResult: com.onesignal.inAppMessages.internal.prompt.impl.InAppMessagePrompt$PromptActionResult[] values() +com.onesignal.inAppMessages.internal.triggers.TriggerModel: java.lang.Object getValue() +com.onesignal.inAppMessages.internal.triggers.TriggerModel: java.lang.String getKey() +com.onesignal.internal.InitState: com.onesignal.internal.InitState valueOf(java.lang.String) +com.onesignal.internal.InitState: com.onesignal.internal.InitState[] values() +com.onesignal.notifications.services.ADMMessageHandler: void onMessage(android.content.Intent) +com.onesignal.notifications.services.ADMMessageHandler: void onRegistered(java.lang.String) +com.onesignal.notifications.services.ADMMessageHandler: void onRegistrationError(java.lang.String) +com.onesignal.notifications.services.ADMMessageHandler: void onUnregistered(java.lang.String) +com.onesignal.notifications.services.ADMMessageHandlerJob: void onMessage(android.content.Context,android.content.Intent) +com.onesignal.notifications.services.ADMMessageHandlerJob: void onRegistered(android.content.Context,java.lang.String) +com.onesignal.notifications.services.ADMMessageHandlerJob: void onRegistrationError(android.content.Context,java.lang.String) +com.onesignal.notifications.services.ADMMessageHandlerJob: void onUnregistered(android.content.Context,java.lang.String) +com.onesignal.session.internal.influence.InfluenceChannel: com.onesignal.session.internal.influence.InfluenceChannel valueOf(java.lang.String) +com.onesignal.session.internal.influence.InfluenceChannel: com.onesignal.session.internal.influence.InfluenceChannel[] values() +com.onesignal.session.internal.influence.InfluenceType: com.onesignal.session.internal.influence.InfluenceType valueOf(java.lang.String) +com.onesignal.session.internal.influence.InfluenceType: com.onesignal.session.internal.influence.InfluenceType[] values() +com.onesignal.session.internal.session.SessionModel: boolean isValid() +com.onesignal.session.internal.session.SessionModel: java.lang.String getSessionId() +com.onesignal.session.internal.session.SessionModel: long getActiveDuration() +com.onesignal.session.internal.session.SessionModel: long getFocusTime() +com.onesignal.session.internal.session.SessionModel: long getStartTime() +com.onesignal.user.internal.backend.SubscriptionObjectType: com.onesignal.user.internal.backend.SubscriptionObjectType valueOf(java.lang.String) +com.onesignal.user.internal.backend.SubscriptionObjectType: com.onesignal.user.internal.backend.SubscriptionObjectType[] values() +com.onesignal.user.internal.identity.IdentityModel: java.lang.String getExternalId() +com.onesignal.user.internal.identity.IdentityModel: java.lang.String getOnesignalId() +com.onesignal.user.internal.jwt.JwtRequirement: com.onesignal.user.internal.jwt.JwtRequirement valueOf(java.lang.String) +com.onesignal.user.internal.jwt.JwtRequirement: com.onesignal.user.internal.jwt.JwtRequirement[] values() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: boolean getEnabled() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: com.onesignal.user.internal.subscriptions.SubscriptionStatus getStatus() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: com.onesignal.user.internal.subscriptions.SubscriptionType getType() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getAddress() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.CreateSubscriptionOperation: java.lang.String getSubscriptionId() +com.onesignal.user.internal.operations.DeleteAliasOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.DeleteAliasOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.DeleteAliasOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.DeleteAliasOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.DeleteAliasOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.DeleteAliasOperation: java.lang.String getLabel() +com.onesignal.user.internal.operations.DeleteAliasOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.DeleteAliasOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.DeleteSubscriptionOperation: java.lang.String getSubscriptionId() +com.onesignal.user.internal.operations.DeleteTagOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.DeleteTagOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.DeleteTagOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.DeleteTagOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.DeleteTagOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.DeleteTagOperation: java.lang.String getKey() +com.onesignal.user.internal.operations.DeleteTagOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.DeleteTagOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation: java.lang.String getSubscriptionId() +com.onesignal.user.internal.operations.LoginUserOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.LoginUserOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.LoginUserOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.LoginUserOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.LoginUserOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.LoginUserOperation: java.lang.String getExistingOnesignalId() +com.onesignal.user.internal.operations.LoginUserOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.LoginUserOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.PurchaseInfo: java.lang.String getIso() +com.onesignal.user.internal.operations.PurchaseInfo: java.lang.String getSku() +com.onesignal.user.internal.operations.PurchaseInfo: java.math.BigDecimal getAmount() +com.onesignal.user.internal.operations.RefreshUserOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.RefreshUserOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.RefreshUserOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.RefreshUserOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.RefreshUserOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.RefreshUserOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.RefreshUserOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.SetAliasOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.SetAliasOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getLabel() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.SetAliasOperation: java.lang.String getValue() +com.onesignal.user.internal.operations.SetPropertyOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.SetPropertyOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.Object getValue() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.SetPropertyOperation: java.lang.String getProperty() +com.onesignal.user.internal.operations.SetTagOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.SetTagOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getKey() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.SetTagOperation: java.lang.String getValue() +com.onesignal.user.internal.operations.TrackCustomEventOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.TrackCustomEventOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getEventName() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getEventProperties() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.TrackCustomEventOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.TrackCustomEventOperation: long getTimeStamp() +com.onesignal.user.internal.operations.TrackPurchaseOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.TrackPurchaseOperation: boolean getTreatNewAsExisting() +com.onesignal.user.internal.operations.TrackPurchaseOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.math.BigDecimal getAmountSpent() +com.onesignal.user.internal.operations.TrackPurchaseOperation: java.util.List getPurchases() +com.onesignal.user.internal.operations.TrackSessionEndOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.TrackSessionEndOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.TrackSessionEndOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.TrackSessionEndOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.TrackSessionEndOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.TrackSessionEndOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.TrackSessionEndOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.TrackSessionEndOperation: long getSessionTime() +com.onesignal.user.internal.operations.TrackSessionStartOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.TrackSessionStartOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.TrackSessionStartOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.TrackSessionStartOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.TrackSessionStartOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.TrackSessionStartOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.TrackSessionStartOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.TransferSubscriptionOperation: java.lang.String getSubscriptionId() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: boolean getCanStartExecute() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: boolean getEnabled() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: com.onesignal.core.internal.operations.GroupComparisonType getGroupComparisonType() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: com.onesignal.user.internal.subscriptions.SubscriptionStatus getStatus() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: com.onesignal.user.internal.subscriptions.SubscriptionType getType() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getAddress() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getAppId() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getApplyToRecordId() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getCreateComparisonKey() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getModifyComparisonKey() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getOnesignalId() +com.onesignal.user.internal.operations.UpdateSubscriptionOperation: java.lang.String getSubscriptionId() +com.onesignal.user.internal.properties.PropertiesModel: com.onesignal.common.modeling.MapModel getTags() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.Boolean getLocationBackground() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.Double getLocationLatitude() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.Double getLocationLongitude() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.Float getLocationAccuracy() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.Integer getLocationType() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.Long getLocationTimestamp() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.String getCountry() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.String getLanguage() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.String getOnesignalId() +com.onesignal.user.internal.properties.PropertiesModel: java.lang.String getTimezone() +com.onesignal.user.internal.subscriptions.SubscriptionModel: boolean getOptedIn() +com.onesignal.user.internal.subscriptions.SubscriptionModel: boolean isDisabledInternally() +com.onesignal.user.internal.subscriptions.SubscriptionModel: com.onesignal.user.internal.subscriptions.SubscriptionStatus getStatus() +com.onesignal.user.internal.subscriptions.SubscriptionModel: com.onesignal.user.internal.subscriptions.SubscriptionType getType() +com.onesignal.user.internal.subscriptions.SubscriptionModel: java.lang.String getAddress() +com.onesignal.user.internal.subscriptions.SubscriptionModel: java.lang.String getAppVersion() +com.onesignal.user.internal.subscriptions.SubscriptionModel: java.lang.String getCarrier() +com.onesignal.user.internal.subscriptions.SubscriptionModel: java.lang.String getDeviceOS() +com.onesignal.user.internal.subscriptions.SubscriptionModel: java.lang.String getSdk() +com.onesignal.user.internal.subscriptions.SubscriptionStatus: com.onesignal.user.internal.subscriptions.SubscriptionStatus valueOf(java.lang.String) +com.onesignal.user.internal.subscriptions.SubscriptionStatus: com.onesignal.user.internal.subscriptions.SubscriptionStatus[] values() +com.onesignal.user.internal.subscriptions.SubscriptionType: com.onesignal.user.internal.subscriptions.SubscriptionType valueOf(java.lang.String) +com.onesignal.user.internal.subscriptions.SubscriptionType: com.onesignal.user.internal.subscriptions.SubscriptionType[] values() diff --git a/scripts/r8-keep-check.sh b/scripts/r8-keep-check.sh new file mode 100755 index 000000000..42f3acb0c --- /dev/null +++ b/scripts/r8-keep-check.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# +# R8 consumer keep-rules diff gate. +# +# Why this exists: the OneSignal library modules ship consumer keep rules +# (OneSignalSDK/onesignal/*/consumer-rules.pro) that protect members reached ONLY via +# string-based reflection -- e.g. GoogleApiClient.connect/disconnect/blockingConnect (invoked +# by name from GoogleApiClientCompatProxy), Model getters (matched by name in +# Model.initializeFromJson), and service constructors (ServiceRegistrationReflection). +# Dropping or mis-scoping such a keep does NOT fail the build: R8 only errors on missing +# classes, not on a method it renamed/removed that is later looked up by a reflection string. +# The breakage surfaces only at runtime. This gate makes the demo's minified release R8 run +# emit its seeds (the members R8 actually kept), normalizes them to a stable set, and diffs +# that against a checked-in baseline so a silently-dropped keep fails the PR instead. +# +# Usage: +# scripts/r8-keep-check.sh # compare against baseline, fail on drift +# UPDATE_R8_BASELINE=1 scripts/r8-keep-check.sh # accept current seeds as the new baseline +# +# Run it AFTER a minified GMS release build (:app:assembleGmsRelease) so the seeds file exists. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# GMS release is the variant we diff: it includes Google Play services (so GoogleApiClient is on +# the classpath) plus every OneSignal module. The Huawei variant excludes play-services, so its +# seeds legitimately lack the GMS keeps and are not a stable baseline for them. +SEEDS_FILE="$REPO_ROOT/examples/demo/app/build/outputs/r8-report/seeds.txt" +BASELINE_FILE="$REPO_ROOT/examples/demo/app/r8-keep-baseline.txt" + +RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; NC=$'\033[0m' + +if [[ ! -f "$SEEDS_FILE" ]]; then + echo "${RED}ERROR:${NC} seeds file not found at:" + echo " $SEEDS_FILE" + echo "Run the minified GMS release build first, e.g.:" + echo " (cd OneSignalSDK && ./gradlew :app:assembleGmsRelease)" + exit 2 +fi + +# Third-party classes we keep reflectively are derived from the consumer rules rather than +# hardcoded, so a new explicit third-party -keep target is picked up automatically. We take the +# concrete class token from '-keep[classmembers] class {' lines, excluding wildcards and our +# own com.onesignal.* packages (those are covered by the com.onesignal prefix below). +# '|| true' so an empty result (e.g. every explicit third-party keep was removed) doesn't abort +# under 'set -e' -- that removal is exactly the drift the gate must still report on. +THIRD_PARTY_PREFIXES="$( + { grep -rhE '^[[:space:]]*-keep(classmembers|classeswithmembers)?\b' \ + "$REPO_ROOT"/OneSignalSDK/onesignal/*/consumer-rules.pro 2>/dev/null \ + | sed -nE 's/.*class[[:space:]]+([A-Za-z_][A-Za-z0-9_.]*)([[:space:]{].*)?$/\1/p' \ + | grep '\.' \ + | grep -v '^com\.onesignal' \ + | sort -u ; } || true +)" + +# Normalize the raw seeds into the stable, high-signal set we diff: +# - METHOD seeds only. A seed line is "owner: member"; a method member has a return type before +# its name (so it contains a space) and an argument list (so it contains "("). This deliberately +# excludes constructors ("Owner(...)": name only, no return type) and fields/enum-constants +# ("Type NAME": no "("). Those categories are subject to host-dependent R8 behavior -- Kotlin +# default-arg / DefaultConstructorMarker constructor synthesis and enum unboxing differ between +# build hosts -- and were the sole cause of macOS-vs-Linux baseline drift. Name-matched method +# seeds are deterministic and are what the reflective keeps we care about actually protect +# (GoogleApiClient.connect/disconnect/blockingConnect, Model getters, the NSE callback). +# - owned by com.onesignal.* (the SDK) or one of the reflectively-kept third-party classes. +# - excluding com.onesignal.example.* (the demo app's own code, not the SDK under test). +# - excluding anonymous/lambda owners (a '$' followed by a digit) whose R8 numbering churns. +normalize() { + awk -v tp="$THIRD_PARTY_PREFIXES" ' + BEGIN { n = split(tp, arr, "\n") } + index($0, ": ") == 0 { next } + { + owner = $0; sub(/: .*/, "", owner) + member = $0; sub(/^[^:]*: /, "", member) + if (index(member, "(") == 0) next + if (index(member, " ") == 0) next + if (owner ~ /^com\.onesignal\.example/) next + if (owner ~ /\$[0-9]/) next + keep = 0 + if (owner ~ /^com\.onesignal\./) keep = 1 + else { for (i = 1; i <= n; i++) if (arr[i] != "" && index(owner, arr[i]) == 1) { keep = 1; break } } + if (keep) print + } + ' "$SEEDS_FILE" | LC_ALL=C sort -u +} + +CURRENT="$(normalize)" + +if [[ "${UPDATE_R8_BASELINE:-0}" == "1" ]]; then + printf '%s\n' "$CURRENT" > "$BASELINE_FILE" + echo "${GREEN}Updated baseline${NC} ($(printf '%s\n' "$CURRENT" | wc -l | tr -d ' ') kept members):" + echo " $BASELINE_FILE" + exit 0 +fi + +if [[ ! -f "$BASELINE_FILE" ]]; then + echo "${RED}ERROR:${NC} baseline not found at:" + echo " $BASELINE_FILE" + echo "Generate it once from the current (intended) state:" + echo " UPDATE_R8_BASELINE=1 scripts/r8-keep-check.sh" + exit 2 +fi + +DIFF="$(diff -u "$BASELINE_FILE" <(printf '%s\n' "$CURRENT") || true)" + +if [[ -z "$DIFF" ]]; then + echo "${GREEN}R8 keep-rules gate passed${NC} — kept consumer members match the baseline ($(wc -l < "$BASELINE_FILE" | tr -d ' ') entries)." + exit 0 +fi + +REMOVED="$(printf '%s\n' "$DIFF" | grep -E '^-[^-]' | sed 's/^-/ - /' || true)" +ADDED="$(printf '%s\n' "$DIFF" | grep -E '^\+[^+]' | sed 's/^+/ + /' || true)" + +echo "${RED}R8 keep-rules gate FAILED — the set of kept consumer members drifted from the baseline.${NC}" +echo +if [[ -n "$REMOVED" ]]; then + echo "${RED}Members NO LONGER kept (a keep rule may have been dropped or mis-scoped —" + echo "verify none of these are reached by reflection before accepting):${NC}" + echo "$REMOVED" + echo +fi +if [[ -n "$ADDED" ]]; then + echo "${YELLOW}Newly kept members (expected when adding rules or SDK code):${NC}" + echo "$ADDED" + echo +fi +echo "If this change is intentional (you added/changed a keep rule or SDK surface), refresh the" +echo "baseline and include it in your commit:" +echo " ${YELLOW}UPDATE_R8_BASELINE=1 scripts/r8-keep-check.sh${NC}" +exit 1