# SuperSDK Android 接入文档 > **版本:dev** | **稳定性:开发中** — 内容随时变更,仅供内部参考 ## 概述 SuperSDK 是一个 Android WebView 容器 SDK,提供 WebView 容器、JSBridge、离线包、云控配置等能力,帮助宿主 App 快速集成 H5 容器。 ## 环境要求 | 项目 | 要求 | |------|------| | minSdkVersion | 23 (Android 6.0) | | compileSdkVersion | 35 或更高(本仓使用 36) | | Java 版本 | Java 11+ | | 依赖库 | AndroidX | ## 集成方式 ### Maven 依赖(推荐) ```groovy dependencies { // 必选:SDK 核心(容器 / JSBridge / 离线包 / 云控) implementation 'com.supersdk:supersdk:版本号' // 可选:Google 登录(详见「可选能力模块接入 · Google 登录」) implementation 'com.supersdk:supersdk-auth-google:版本号' // 可选:Google Pay 应用内购买(详见「可选能力模块接入 · Google Pay」) implementation 'com.supersdk:supersdk-pay-google:版本号' // 可选:AdMob 广告 Provider(详见「AdService · AdMob 接入」) implementation 'com.supersdk:supersdk-ad-admob:版本号' } ``` > **可选模块按需引入**:只接 H5 容器不需要登录、支付或广告时,仅引 `com.supersdk:supersdk` 即可;核心 artifact 不传递引入 Google Mobile Ads。 > > **SNAPSHOT 版本**需额外添加快照仓库(最新版本号见 [CHANGELOG](https://www.superplatform.ai/changelog.md)): > ```groovy > repositories { > maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } > } > ``` ### AAR 手动集成 1. 将 `supersdk-版本号.aar` 放入项目 `libs/` 目录 2. 在 `build.gradle` 中添加: ```groovy dependencies { implementation fileTree(dir: 'libs', include: ['*.aar']) } ``` ### 必要的 AndroidManifest 配置 SDK 核心模块**不会**替宿主声明 `INTERNET` 权限。`android-sdk-impl` **会 merge `CAMERA`**(用于 `` 拍照),**不会 merge `RECORD_AUDIO`**。宿主**必须**自行在 `AndroidManifest.xml` 添加: ```xml android:usesCleartextTraffic="true"> ... ``` > `CAMERA` 仍由 SDK merge,宿主不必再抄一遍。`RECORD_AUDIO` 必须由宿主自己声明(即使只用 `SuperPageActivity`、Java 零改动)。 > > 可选模块(登录/支付/广告)各自的 Manifest 与 Google 后台配置,见下文「可选能力模块接入」与「AdService」。 > **HTML5 视频全屏横屏**:SDK 内置的 `SuperPageActivity`(即 `IPageController.open()` 打开的全屏页)已声明 `configChanges`,视频进入全屏会自动横屏。若你把 `AbsSuperPageView` **嵌入自己的 Activity**(`createPageView()` 方式),需给该 Activity 声明 `android:configChanges="orientation|screenSize|keyboardHidden"`,否则 SDK 为避免 Activity 重建中断播放,**不会**触发横屏旋转(仅竖屏全屏)。 ### WebView 麦克风与文件选择(宿主接入) H5 `navigator.mediaDevices.getUserMedia({ audio: true })` 与 `` 由 SDK 容器内的 `WebChromeClient` 处理。**禁止替换 `WebChromeClient` / `WebViewClient`**(含 `getWebView().setWebChromeClient(...)`),否则会丢掉 JSBridge、离线包、文件选择和麦克风授权。 **路径 1 · 默认全屏页(零 Java)** `IPageController.open()` / `SuperPageActivity` 已实现公开 `com.supersdk.api.page.IFileChooserHost`(含独立的 `launchMicrophonePermission`),**不用写 Java**。宿主只需在自己的 Manifest 声明 `RECORD_AUDIO`(见上一节)。`CAMERA` 仍由 SDK merge。 **路径 2 · 自包装 Activity** 把 `createPageView()` 嵌进自己的 Activity 时:实现 `com.supersdk.api.page.IFileChooserHost`,在 `onCreate`(`STARTED` 之前)注册 launcher,再 `setFileChooserHost(this)`。不要反射、不要强转 impl 类型、不要换 ChromeClient。相机与麦克风必须使用**独立** pending / launcher,不可共用一个 callback 槽。 ```java import android.Manifest; import android.content.Context; import android.content.Intent; import android.os.Bundle; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.supersdk.SuperSDK; import com.supersdk.api.page.AbsSuperPageView; import com.supersdk.api.page.FileChooserResultCallback; import com.supersdk.api.page.IFileChooserHost; import com.supersdk.api.page.PageLoadConfig; import com.supersdk.api.page.PermissionResultCallback; public class MyWebActivity extends AppCompatActivity implements IFileChooserHost { private AbsSuperPageView pageView; private ActivityResultLauncher fileChooserLauncher; private ActivityResultLauncher cameraPermissionLauncher; private ActivityResultLauncher microphonePermissionLauncher; @Nullable private FileChooserResultCallback pendingFileChooserCallback; @Nullable private PermissionResultCallback pendingCameraCallback; @Nullable private PermissionResultCallback pendingMicrophoneCallback; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); fileChooserLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { FileChooserResultCallback cb = pendingFileChooserCallback; pendingFileChooserCallback = null; if (cb != null) cb.onResult(result.getResultCode(), result.getData()); }); cameraPermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), granted -> { PermissionResultCallback cb = pendingCameraCallback; pendingCameraCallback = null; if (cb != null) cb.onResult(Boolean.TRUE.equals(granted)); }); microphonePermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), granted -> { PermissionResultCallback cb = pendingMicrophoneCallback; pendingMicrophoneCallback = null; if (cb != null) cb.onResult(Boolean.TRUE.equals(granted)); }); pageView = SuperSDK.getPageController().createPageView(); setContentView(pageView); pageView.setFileChooserHost(this); pageView.load("https://example.com", new PageLoadConfig("https://example.com")); } @Override public void launchFileChooser(@NonNull Intent intent, @NonNull FileChooserResultCallback callback) { pendingFileChooserCallback = callback; fileChooserLauncher.launch(intent); } @Override public void launchCameraPermission(@NonNull PermissionResultCallback callback) { pendingCameraCallback = callback; cameraPermissionLauncher.launch(Manifest.permission.CAMERA); } @Override public void launchMicrophonePermission(@NonNull PermissionResultCallback callback) { pendingMicrophoneCallback = callback; microphonePermissionLauncher.launch(Manifest.permission.RECORD_AUDIO); } @NonNull @Override public Context getHostContext() { return getApplicationContext(); } @Override protected void onDestroy() { super.onDestroy(); if (pageView != null) pageView.destroyView(); } } ``` > 本轮 Android 只授权纯音频。H5 若同时要摄像头(`VIDEO_CAPTURE`),整次 deny。getUserMedia 需 **https** 安全源。 --- ## 初始化 在 `Application.onCreate()` 中初始化 SDK: ```java import com.supersdk.SuperSDK; public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // Replace ab12cd34 with the real spid issued by the SuperSDK console. SuperSDK.init(this, "ab12cd34", new MySDKProvider()); } } ``` **参数说明:** | 参数 | 类型 | 说明 | |------|------|------| | context | Context | Application Context | | spid | String | 控制台签发的 8 位小写字母/数字项目标识 | | provider | SuperSDKProvider | 宿主实现的服务提供者 | --- ## 项目环境(API Environment) 项目环境对应控制台顶栏的 env(如 `prod` / `staging`),决定 SDK 拉取哪套云控配置(settings)以及埋点上报归属的环境。它不再是 init 参数,而是独立的运行时状态。 方法签名:`public static void setAPIEnvironment(String environment)` / `public static String getAPIEnvironment()` ```java // 设置项目环境(init 前后均可调用;非法值抛 IllegalArgumentException 且不落地) SuperSDK.setAPIEnvironment("staging"); // 读取当前项目环境(未设置时返回 "prod",不要求已 init) String env = SuperSDK.getAPIEnvironment(); ``` **规则与行为:** - 取值限 1–10 位小写字母/数字(`^[a-z0-9]{1,10}$`),不 trim、不做大小写转换;`null` 视为 `prod` - **SDK 不持久化该值**:进程启动默认 `prod`,宿主如需记忆用户选择,需自行持久化并在启动后调用 `setAPIEnvironment` - 运行时切换的副作用顺序:冲刷旧环境的存量上报事件 → 换值 → settings 清本地缓存并按新环境重拉 → 刷新存活 WebView 的 UA(仅影响后续导航) - 切换后 settings 拉取自动带 `env` 参数、上报 `basic_data.env` 与请求头 `X-Super-API-ENV` 均使用新值;H5 侧经 `getEnv()` 读到新值 - 该环境与 `HostConfigService.getSdkApiBaseUrls()`(API 域名)是不同维度:切环境不需要也不应该改 API 域名 --- ## 实现 SuperSDKProvider `SuperSDKProvider` 是宿主 App 向 SDK 提供能力的桥梁。所有 Service 方法均为可选实现(默认返回 `null`),按需实现即可。 ```java import com.supersdk.api.SuperSDKProvider; public class MySDKProvider extends SuperSDKProvider { private final MyHostConfigService hostConfigService = new MyHostConfigService(); @Override public HostConfigService getHostConfigService() { return hostConfigService; } @Override public EventService getEventService() { return null; // 按需实现 } @Override public LoginService getLoginService() { return null; // 按需实现 } @Override public HostMethodService getHostMethodService() { return null; // 按需实现 } @Override public NavigationService getNavigationService() { return null; // 按需实现 } @Override public AdService getAdService() { return null; // 按需实现 } @Override public PayService getPayService() { return null; // 按需实现(见「可选能力模块接入 · Google Pay」) } @Override public INativeBlockProvider getNativeBlockService() { return null; // 按需实现 } } ``` --- ## 核心 API ### IPageController - 页面管理 通过 `SuperSDK.getPageController()` 获取。 ```java IPageController pageController = SuperSDK.getPageController(); // 打开全屏页面 pageController.open("https://example.com"); // 创建 PageView 嵌入到自己的布局中 AbsSuperPageView pageView = pageController.createPageView(); ``` | 方法 | 说明 | |------|------| | `open(String url)` | 在新的全屏页面中打开 URL | | `createPageView()` | 创建一个可嵌入的 PageView 实例 | ### AbsSuperPageView - 页面视图 `AbsSuperPageView` 继承自 `FrameLayout`,是 SDK 的核心容器视图。 ```java AbsSuperPageView pageView = SuperSDK.getPageController().createPageView(); // 添加到布局 container.addView(pageView); // 加载 URL pageView.load("https://example.com", new PageLoadConfig("https://example.com")); // 适配沉浸式安全区域(自动获取状态栏和导航栏高度) pageView.fitSystemWindows(this); // 处理返回键(优先级:前端拦截 > 页面历史回退 > exitAction) @Override public void onBackPressed() { pageView.handleBackPressed(() -> super.onBackPressed()); } // 销毁(必须调用) @Override protected void onDestroy() { super.onDestroy(); pageView.destroyView(); } ``` | 方法 | 说明 | |------|------| | `load(String url, PageLoadConfig config)` | 加载 URL | | `reload()` | 重新加载当前页面 | | `stopLoading()` | 停止加载 | | `canGoBack()` | 是否可以后退 | | `goBack()` | 后退一页 | | `handleBackPressed(Runnable exitAction)` | 处理返回键,优先级:前端拦截 > 页面历史回退 > exitAction | | `setSafeArea(int statusBarHeight, int navigationBarHeight)` | 设置安全区域(物理像素) | | `fitSystemWindows(Activity activity)` | 自动适配沉浸式安全区域 | | `setImmersiveBottom(boolean immersive)` | 底部是否沉浸(默认 `true`=内容延伸到系统导航栏下,现有行为)。置 `false`(浏览器场景):WebView 内容缩短到导航栏之上 + 页面 `env(safe-area-inset-bottom)=0` + UA `SafeAreaBottom=0`,第三方站点底栏紧贴系统导航栏、无多余白边。仅作用底部(顶部恒沉浸);应在 `fitSystemWindows`/首次 `load` 前调用。需宿主为 edge-to-edge 且不在上游消费底部 inset | | `setOnClosePageListener(Runnable listener)` | 设置关闭页面监听(前端调用 closePage 时触发) | | `sendBroadcast(String eventKey, String jsonData)` | 向当前 WebView 发送广播 | | `destroyView()` | 销毁视图,释放资源 | | `getWebView()` | 获取底层 WebView 实例(用于直调 evaluateJavascript/settings 等原生能力);⚠️ 勿替换 WebViewClient/WebChromeClient 或重置 settings,否则会破坏内置 JSBridge 与离线包拦截 | ### IOfflineController - 离线包管理 通过 `SuperSDK.getOfflineController()` 获取。 ```java IOfflineController offlineController = SuperSDK.getOfflineController(); // 查询所有离线包状态 List packages = offlineController.getAllPackages(); // 查询指定离线包 OfflinePackageState state = offlineController.getPackage("example.com"); // 更新指定离线包(拉取站点远程清单 → 下载安装,详见「离线包远程更新」小节) offlineController.preloadPackage("example.com", new OfflineUpdateListener() { @Override public void onProgress(@NonNull String host, int progress) { // 下载进度 0-100 } @Override public void onComplete(@NonNull OfflineUpdateResult result) { if (result.isSuccess()) { // 更新成功 } } }); // 更新全部离线包 offlineController.preloadAllPackages(listener); ``` #### OfflinePackageState 仅包含当前状态正常(可用)的离线包信息。 | 方法 | 返回类型 | 说明 | |------|----------|------| | `getHost()` | String | 离线包域名标识 | | `getLocalVersion()` | String | 本地版本号 | | `getSource()` | Source | 来源:`ASSETS`(内置)/ `DOWNLOAD`(云端下载)/ `MANUAL`(外部手动安装) | #### OfflineUpdateResult | 方法 | 返回类型 | 说明 | |------|----------|------| | `getHost()` | String | 离线包域名标识 | | `getCode()` | Code | 结果码(见下表) | | `getMessage()` | String | 结果描述信息 | | `getNewVersion()` | String | 更新后的版本号 | | `isSuccess()` | boolean | 是否成功(SUCCESS 或 ALREADY_UP_TO_DATE) | **Code 枚举值:** | Code | 说明 | |------|------| | `SUCCESS` | 更新成功 | | `ALREADY_UP_TO_DATE` | 已是最新版本 | | `DOWNLOAD_FAILED` | 下载失败 | | `VERIFY_FAILED` | 校验失败 | | `EXTRACT_FAILED` | 解压失败 | | `PACKAGE_NOT_CONFIGURED` | 离线包未配置 | | `ALREADY_UPDATING` | 正在更新中 | | `UNKNOWN_ERROR` | 未知错误 | #### 离线包远程更新 SDK 支持从远程站点自动获取并安装最新离线包,无需宿主额外接入代码即可生效。 **自动更新行为:** - **探测时机**:打开任意 http(s) 页面时,SDK 在页面加载过程中后台静默探测该 host 站点根路径下的 `super_meta.json`(站点侧发布规范见 [`OFFLINE_PKG_SPEC.md`](https://www.superplatform.ai/offline-pkg-spec.md)),不阻塞页面渲染,站点未接入(404)视为正常状态 - **生效时机**:探测到新版本并下载安装成功后,**下次加载**该 host 页面时生效,不会刷新当前已打开的页面 - **大小上限**:自动模式下单包 **50MB** 以内才会自动下载,超过则跳过(仅显式调用可下载) - **探测频控**:同一 host 最小探测间隔 30 分钟,避免频繁开关页对站点造成请求压力 **显式触发更新**:宿主可主动调用 `preloadPackage` / `preloadAllPackages` 立即检查更新,绕过频控与大小上限: ```java IOfflineController offlineController = SuperSDK.getOfflineController(); // 立即检查并更新指定 host 的离线包(绕过频控与 50MB 上限) offlineController.preloadPackage("example.com", new OfflineUpdateListener() { @Override public void onProgress(@NonNull String host, int progress) { // 下载进度 0-100 } @Override public void onComplete(@NonNull OfflineUpdateResult result) { if (result.isSuccess()) { // 更新成功,下次加载该 host 页面生效 } } }); // 立即检查并更新全部已装离线包 offlineController.preloadAllPackages(listener); ``` > 首次调用时若本地尚未安装该 host 的离线包,只要远程清单探测成功,同样会走首装流程(首装同样在下次加载时生效,不影响本次页面渲染)。 **站点侧要求**:站点需在根路径发布符合 `super_meta.json` v1 规范的清单文件,并保证声明的 `pkg_path` 与站点同源。完整字段定义、zip 打包工具链约束(`zip -r` deflate,不支持流式 data-descriptor 产物)见 [`OFFLINE_PKG_SPEC.md`](https://www.superplatform.ai/offline-pkg-spec.md)。 ### SFeedback - 用户反馈 SDK 内置的用户反馈提交(`com.supersdk.base.feedback`),与 H5 的 `SuperSDK.submitFeedback` 同一条链路:`POST /superapi/v2/feedback` → 控制台「业务运营 → 用户反馈」按项目 × 环境查看、搜索并标记处理状态。SDK 自动补齐 `spid / env / device_id / platform / app_version / sdk_version / os_version / device_model`,服务端记录提交时间与用户 IP;宿主只需提供业务字段。 ```java import com.supersdk.base.feedback.FeedbackRequest; import com.supersdk.base.feedback.SFeedback; FeedbackRequest request = new FeedbackRequest("settings", "打开设置页闪退", "进入设置页后约 1 秒必现闪退") .setSubScene("about") // 可选 .setUserId(currentUserId) // 可选:业务用户 ID,SDK 不维护用户身份 .setExtra(new JSONObject().put("page", "settings")); // 可选:附加信息(≤4KB) SFeedback.submit(request, new SFeedback.Callback() { @Override public void onSuccess(long feedbackId) { /* 已入库,feedbackId 为服务端记录 ID */ } @Override public void onFailure(@NonNull String code, @NonNull String message) { // code: INVALID_ARGUMENT(本地校验失败,未发请求)/ NETWORK_ERROR / RATE_LIMITED / // UNAVAILABLE / PROTOCOL_MISMATCH,或服务端业务 code(INVALID_REQUEST / NOT_FOUND …) } }); ``` | 字段 | 必填 | 限制 | |------|:----:|------| | scene | ✓ | ≤64 字符 | | title | ✓ | ≤200 字符 | | content | ✓ | ≤5000 字符 | | subScene | | ≤64 字符 | | userId | | ≤128 字符 | | extra | | JSON object,序列化后 ≤4KB | > 回调恒在 UI 线程;所有字符串会被 trim,长度按字符数计(服务端为权威)。限流:每 IP 10 次/分钟、每项目 600 次/分钟。提交**不是幂等**的,SDK 不自动重试。 ### INativeBlockProvider - Native Block Native Block 允许 H5 页面嵌入宿主 App 提供的原生视图(如广告、视频播放器等)。 ```java // 1. 实现 INativeBlockProvider public class MyNativeBlockProvider implements INativeBlockProvider { @Override public boolean supports(String blockType) { return "ad-banner".equals(blockType) || "video-player".equals(blockType); } @Override public View createBlockView(Context context, String blockId, String blockType, String config) { switch (blockType) { case "ad-banner": return createAdView(context, config); case "video-player": return createVideoView(context, config); default: return null; } } @Override public void onBlockShow(String blockId, View view) { // Block 进入可视区域 } @Override public void onBlockHide(String blockId, View view) { // Block 离开可视区域 } @Override public void onBlockDestroy(String blockId, View view) { // Block 被销毁,释放资源 } } // 2. 注册 Provider(二选一) // 方式 A:通过 SuperSDKProvider @Override public INativeBlockProvider getNativeBlockService() { return new MyNativeBlockProvider(); } // 方式 B:动态设置 SuperSDK.setNativeBlockProvider(new MyNativeBlockProvider()); ``` | 方法 | 说明 | |------|------| | `supports(String blockType)` | 是否支持指定类型 | | `createBlockView(Context, String, String, String)` | 创建原生视图 | | `onBlockShow(String, View)` | Block 进入可视区域回调 | | `onBlockHide(String, View)` | Block 离开可视区域回调 | | `onBlockDestroy(String, View)` | Block 被销毁回调 | --- ## Service 协议说明 ### HostConfigService - 宿主配置 提供宿主 App 的基本配置信息。 ```java public class MyHostConfigService implements HostConfigService { @Override public String getAppChannel() { return "GooglePlay"; // 渠道标识 } // getNetEnv() / ApiEnv 已废弃(SDK 从未消费):项目环境请使用 // SuperSDK.setAPIEnvironment(...);API 域名请使用 getSdkApiBaseUrls()。 @Override public int getAppVersionCode() { return BuildConfig.VERSION_CODE; } @Override public String getAppVersionName() { return BuildConfig.VERSION_NAME; } @Override public String getSdkApiBaseUrl() { return null; // 自定义 API 地址,null 使用默认 } @Override public String generateDeviceId() { return null; // 宿主接管 deviceId 生成,null 使用 SDK 默认逻辑 } } ``` **generateDeviceId - 宿主接管 deviceId 生成**(可选): - 仅在 SDK 需要**生成新 deviceId** 时调用(首次安装、或本地旧值失效),本地已持久化的 deviceId 继续沿用,不受影响(缓存优先)。 - 返回非空字符串:SDK 采用该值作为 deviceId 并持久化;返回 `null`/空串:走 SDK 默认生成逻辑(自生成随机 UUID 并持久化到 SP)。 - 实现要求:**同步快速返回**(可能在网络请求/事件上报路径早期调用),且多次调用应返回稳定值。宿主实现抛异常时 SDK 会捕获并回退默认逻辑。 > 🔒 **隐私合规**:SDK **不读取** `Settings.Secure.ANDROID_ID`,也不读取 IMEI / MAC / OAID 等系统设备标识。 > deviceId 的完整降级链为 **内存缓存 → SP 持久化值 → 宿主 `generateDeviceId()` → SDK 自生成 UUID**。 > 旧版本 SDK 曾把 ANDROID_ID 写入 SP,升级后这些**存量值继续沿用**(不迁移,避免老用户设备标识断层); > 如宿主的合规要求需要彻底清除存量值,可在自身 `generateDeviceId()` 中接管并返回自有标识。 ### EventService - 事件上报 接收 SDK 产生的事件,宿主可对接自己的埋点平台。 ```java public class MyEventService implements EventService { @Override public void onEvent(@NonNull Event evt) { String eventName = evt.getEventName(); Map params = evt.getParams(); // 上报到自己的埋点平台 Analytics.track(eventName, params); } } ``` ### LoginService - 登录服务 当 H5 页面需要登录时,SDK 会调用此服务。 ```java public class MyLoginService implements LoginService { @Override public void requestLogin(@NonNull Context context, @NonNull LoginResultCallback callback) { // 拉起宿主的登录流程 LoginManager.login(context, result -> { if (result.isSuccess()) { HostLoginInfo loginInfo = new HostLoginInfo(result.getAuthCode()); callback.onResult("SUCCESS", "登录成功", loginInfo); } else { callback.onResult("FAIL", "登录失败", null); } }); } } ``` **HostLoginInfo:** | 方法 | 类型 | 说明 | |------|------|------| | `getAuthCode()` | String | 登录授权码 | | `getExtraAuthInfo()` | HostLoginExtraAuthInfo | 扩展登录信息(可选) | **HostLoginExtraAuthInfo:** | 方法 | 类型 | 说明 | |------|------|------| | `getLoginType()` | String | 登录方式 | | `getToken()` | String | Token | | `getRefreshToken()` | String | Refresh Token | | `getRefreshTokenExpiresAt()` | long | Refresh Token 过期时间 | | `isFirstLogin()` | boolean | 是否首次登录 | | `getExtra()` | JSONObject | 额外扩展数据 | ### HostMethodService - 宿主自定义方法 允许 H5 调用宿主自定义的方法,实现业务扩展。 ```java public class MyHostMethodService implements HostMethodService { @Override public void invokeAsync(String methodName, @Nullable JSONObject param, @NonNull HostMethodCallback callback) { switch (methodName) { case "getUserInfo": JSONObject result = new JSONObject(); result.put("name", "张三"); callback.onResult("SUCCESS", "", result); break; default: callback.onResult("NOT_FOUND", "方法不存在", null); break; } } } ``` ### NavigationService - 接管 openPage(URL 路由) 宿主实现此接口即可**接管 openPage**:拿到 **URL + 配置**,自行决定如何打开(自有原生页 / 重定向 / 自有 WebView 等)。返回 `true` 表示已自行处理;返回 `false` 交回 SDK 默认容器打开(选择性拦截 + SDK 兜底)。 ```java public class MyNavigationService implements NavigationService { @Override public boolean openPage(Context context, String url, PageConfig config) { // 命中宿主路由 → 打开自己的原生页,返回 true 表示已接管 if (MyRouter.matchNative(url)) { MyRouter.openNative(context, url); return true; } // 其余交回 SDK 默认容器打开 return false; } } ``` > 若你想**用 SDK 的 WebView 容器但自己控制展示**(放进自己的 Activity/Fragment),在 `openPage` 里自取容器即可,无需走 SDK 默认页: > ```java > AbsSuperPageView v = SuperSDK.getPageController().createPageView(); > v.load(url, null); > // v.setOnClosePageListener(...) 可响应 H5 的 closePage > // 把 v 放进你自己的容器展示,然后 return true > ``` > **容器行为 · `target="_blank"` / `window.open`**:H5 中 `` 链接点击与 `window.open(url)` 会被容器自动识别,统一通过 `openPage` 在端上以**新页**打开(等价于 `appbridge.openPage`),并走上面的 `NavigationService`。H5 无需额外适配;与 iOS 行为一致。 ### AdService - 广告服务 提供广告加载和展示能力。 ```java public class MyAdService implements AdService { @Override public void loadNativeAd(@NonNull Context context, @NonNull AdSource adSource, @NonNull String adUnitId, @NonNull NativeAdLoadCallback callback) { // 加载原生广告 } @Override public void loadRewardedAd(@NonNull Context context, @NonNull AdSource adSource, @NonNull String adUnitId, @NonNull SuperCallback callback) { // 加载激励视频广告 } @Override public void loadInterstitialAd(@NonNull Context context, @NonNull AdSource adSource, @NonNull String adUnitId, @NonNull SuperCallback callback) { // 加载插屏广告 } @Override public void loadRewardedInterstitialAd(@NonNull Context context, @NonNull AdSource adSource, @NonNull String adUnitId, @NonNull SuperCallback callback) { // 加载激励插屏广告 } } ``` **广告类型:** | AdType | 说明 | |--------|------| | `NATIVE` | 原生广告 | | `REWARD` | 激励视频广告 | | `REWARD_INTERSTITIAL` | 激励插屏广告 | | `INTERSTITIAL` | 插屏广告 | **广告来源:** | AdSource | 说明 | |----------|------| | `ADMOB` | Google AdMob | | `APPLOVIN` | AppLovin | #### AdMob 接入示例 AdMob 由可选 artifact `supersdk-ad-admob` 提供完整的 Android 原生 `AdService` 实现。模块传递解析 Google Mobile Ads `25.4.0`,核心 `supersdk` 不包含该依赖。 **宿主接入清单(全部必读):** | 宿主必须做 | 要求 | |---|---| | 引入 Provider | 添加 `supersdk-ad-admob`;不需要直接添加 `play-services-ads` | | 配置 App ID | 每个白牌产品在 `` 声明自己的 `com.google.android.gms.ads.APPLICATION_ID` | | 配置五类广告位 | 在 AdMob 后台创建 Rewarded、Interstitial、Banner、Native、Rewarded Interstitial Ad Unit ID,并在对应 `load*` 调用中传入 | | 完成隐私流程 | 宿主自行处理 GDPR/CCPA、儿童与青少年广告策略以及需要时的 UMP,同意完成后才初始化 | | 初始化并注册 | `AdMobAdService.initialize` 成功后向 `SuperSDKProvider` 注册 `AdService` | | 管理 UI/生命周期 | Banner/Native 页面退出、替换或 stale callback 时 `destroy()`;Rewarded Interstitial 展示前提供奖励说明和可跳过 intro | | 真机验收 | Debug 使用 Google 官方测试 ID;正式发版前验证五类广告加载、展示、失败、奖励和资源释放 | 模块已处理 Google Ads 依赖、网络权限、主线程调度、初始化幂等、错误映射、重复终态保护和 Native 默认模板;不会替宿主创建 AdMob 后台资源、保存业务 ID、执行隐私同意或决定页面生命周期。模块级完整说明见仓库 `packages/android-sdk-ad-admob/README.md`。 1. 引入 Provider(minSdk ≥ 23、compileSdk ≥ 35): ```groovy implementation 'com.supersdk:supersdk-ad-admob:版本号' ``` 2. 在 `AndroidManifest.xml` 的 `` 内声明 AdMob 应用 ID(**缺失会导致 App 启动崩溃**): ```xml ``` > 测试期可用 Google 官方测试 App ID:`ca-app-pub-3940256099942544~3347511713` 多白牌项目建议用 manifest placeholder 按 flavor/build type 注入,避免不同产品或测试/生产环境串用: ```groovy android { defaultConfig { manifestPlaceholders.ADMOB_APP_ID = 'ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy' } } ``` Manifest 中对应使用 `android:value="${ADMOB_APP_ID}"`。App ID 必须进入 Manifest;五类 Ad Unit ID 可由 BuildConfig 或远程配置管理,但每次 `load*` 必须显式传入。 3. 先完成宿主自己的 GDPR/CCPA/儿童与青少年广告等隐私同意流程,再初始化 Provider。只在回调成功后创建并注册服务;回调只使用 SuperSDK 的 `SuperResult`,不会向宿主泄漏 Google SDK 类型: ```java AdMobAdService.initialize(getApplicationContext(), result -> { if (!result.isSuccess()) { Log.e("MyApp", "AdMob init failed: " + result.getCode() + " " + result.getMessage()); return; } AdService adService = new AdMobAdService(); // AdMob App ID 独立配置;这里使用控制台签发的 SuperSDK spid。 SuperSDK.init(getApplicationContext(), "ab12cd34", new SuperSDKProvider() { @Override public AdService getAdService() { return adService; } }); }); ``` `AdMobAdService.initialize` 对并发、重复调用幂等;未初始化就调用任一 `load*` 会返回 `NOT_INITIALIZED`。Provider 不内置 App ID、广告位 ID,也不会自动处理隐私同意。 **能力边界:** | 调用入口 | 支持类型 | |----------|----------| | Android 原生 `AdService` | Rewarded、Interstitial、Banner、Native、Rewarded Interstitial(五类) | | Android H5 / JSBridge | Rewarded、Rewarded Interstitial、Interstitial、Banner(四类) | - Native 使用 Provider 内置的默认 `NativeAdView` 最低资产集,通过 `INativeAd.showAtContainer(container)` 挂载;宿主退出页面时必须调用 `destroy()`。 - Rewarded Interstitial 在调用 `show(...)` 前,**宿主必须先展示明确说明奖励内容且允许用户跳过的 intro screen**。Provider 只响应宿主的显式 `show`,不会生成或替代这段业务 UI。 **Google 后台**([AdMob 控制台](https://apps.admob.com)): | 步骤 | 说明 | |------|------| | 注册应用 | 添加 App,获得 **App ID**(填入上面的 `APPLICATION_ID`) | | 创建广告单元 | 为每种广告位创建 **Ad Unit ID**;Android H5 四类由 `adUnitId` 指定,Native 由宿主原生调用传入 | | 测试广告 | 开发期使用[官方测试广告单元](https://developers.google.com/admob/android/test-ads),避免无效流量导致封号 | > 参考:[AdMob Android 快速开始](https://developers.google.com/admob/android/quick-start) --- ### HostNetworkExtensionService - 网络请求扩展(可选) 允许宿主为 **SDK 内部网络请求**(settings / 事件上报 / 归因 / 短链等)追加 query 参数与 request header。接口全部为 `default` 方法,不实现即不生效,既有接入方无需改动。 ```java public class MyProvider implements SuperSDKProvider { @Override public HostNetworkExtensionService getHostNetworkExtensionService() { return new HostNetworkExtensionService() { @Override public Map getExtraQueryParams(SuperRequestContext context) { // 开启响应混淆:SDK 自身不会默认追加该参数 return Collections.singletonMap("suenc", "1"); } @Override public Map getExtraHeaders(SuperRequestContext context) { return Collections.singletonMap("X-Host-Trace", traceId()); } }; } } ``` `SuperRequestContext` 提供 `getUrl()` / `getPath()` / `getHost()`,宿主可据此按请求条件化返回。 > **合并规则(SDK 侧强制,宿主无法绕过):** > 1. **SDK 自有 query / header 恒胜**——query 名精确匹配,header 名**大小写不敏感**匹配。例如 SDK 已设 `X-Super-API-ENV`,宿主返回 `x-super-api-env` 也不会覆盖它 > 2. `Authorization`、`Cookie` 等认证凭据 header **不接受本接口注入** > 3. 非法项(key/value 为空、key 含非法字符、header 名不合 token 规范)**单项跳过**,其余照常生效 > 4. 实现抛出的异常一律被捕获,**不阻断 SDK 主流程** > 5. **每次请求实时读取**,SDK 不缓存返回值——宿主可随时改变返回内容 > 6. 两个方法在发起请求的线程上**同步调用**,实现必须快速返回、不得阻塞 > 7. 下载类请求(离线包、`downloadFile`)与支付回执直投**不注入 query**,但仍适用 header 扩展 ### 响应混淆(suenc) 服务端在请求带 `?suenc=1` 时会用自定义 Base64 表编码 JSON 响应体并回 `X-Su-Enc: b64t1`。**SDK 内部请求会自动解码**(含非 2xx 的错误响应),宿主无需介入——只要按上文注入 `suenc=1` 即可。 > ⚠️ **只用于降低通用关键字扫描的有效性,不是加密**:不提供机密性、防篡改、防重放。 若宿主自己发起了 suenc 请求,可调用 SDK 的解码能力: ```java // 查询本端支持的 codec,决定是否发送 suenc=1 JSONObject capabilities = SuperSDK.getSuEncCapabilities(); // {"supported":true,"codecs":["b64t1"]} // codec 必须直接使用响应 X-Su-Enc header 的原始值,不接受自定义编码表 try { String json = SuperSDK.decodeSuEnc(encodedBody, codec); } catch (SuEncException e) { // UNSUPPORTED_SUENC_CODEC / SUENC_DECODE_FAILED / SUENC_INVALID_UTF8 String errorCode = e.getErrorCode(); } ``` > **注意事项:** > 1. 解码失败一律视为请求失败,**绝不回落读取明文**——那正是该协议要防的降级 > 2. 若请求带了 `suenc=1` 却收到无 `X-Su-Enc` 的响应,SDK 判定该线路失败并自动切换下一个 baseUrl > 3. 编码表在 SDK 发布后永不变更;新表一定使用新的 codec key,因此 `codec` 必须取自响应 header 而非硬编码 --- ## 可选能力模块接入 以下模块为**可选**,按宿主业务需要引入。每个模块的接入都包含三件事:**① 引依赖 · ② 注册 Provider · ③ Google 后台配置**。(广告 AdMob 接入见上文「AdService」章节。) ### Google 登录(supersdk-auth-google) 基于 Android **Credential Manager + Sign in with Google**,返回 Google 签发的 **ID Token**,交由业务后端校验后换取业务登录态。 **① 依赖**(minSdk ≥ 23): ```groovy implementation 'com.supersdk:supersdk-auth-google:版本号' ``` **② 注册 Provider** —— 在 `LoginService` 中挂载 `GoogleAuthProvider`: ```java private final LoginService loginService = new SDKLoginService.Builder() .addProvider(new GoogleAuthProvider(SERVER_CLIENT_ID)) // provider 名 = "google" .build(); @Override public LoginService getLoginService() { return loginService; } ``` - `SERVER_CLIENT_ID` 是 **Web application 类型** 的 OAuth Client ID(**不是** Android 类型),同时也是返回 ID Token 的 `aud`。 - 模块 Manifest 为空,无需额外权限(Credential Manager 内部处理),仅依赖公共 `INTERNET` 权限。 **③ Google 后台**([Google Cloud Console → APIs & Services → 凭据](https://console.cloud.google.com/apis/credentials)): | 步骤 | 说明 | |------|------| | 配置 OAuth 同意屏幕 | 填应用信息与 Scope(至少 `email` / `profile`) | | 创建 **Web** OAuth Client ID | 即代码中的 `SERVER_CLIENT_ID`;业务后端校验 ID Token 时 `aud` 须等于它 | | 创建 **Android** OAuth Client ID | 填 **包名 + SHA-1 签名指纹**(debug / release 各一份)。Credential Manager 取 Google 凭据要求该指纹已注册,否则回调 `ERROR` / 取不到账号 | 获取 SHA-1:`./gradlew signingReport`(或 `keytool -list -v -keystore `)。 > 参考:[Sign in with Google (Credential Manager)](https://developer.android.com/identity/sign-in/credential-manager-siwg) · [后端校验 ID Token](https://developers.google.com/identity/sign-in/android/backend-auth) ### Google Pay 应用内购买(supersdk-pay-google) 基于 **Google Play Billing Library**。H5 通过 `SuperSDK.pay.*` 查询当前账号可购买的订阅 offer 并发起购买,原生侧由 `GooglePayProvider`(provider 名 `"google_play"`)对接 Google Play。 **① 依赖**(minSdk ≥ 23): ```groovy implementation 'com.supersdk:supersdk-pay-google:1.0.0.63-SNAPSHOT' ``` **② 注册 Provider** —— 在 `PayService` 中挂载 `GooglePayProvider`: ```java private final PayService payService = new SDKPayService.Builder() .addProvider(new GooglePayProvider(context)) // provider 名 = "google_play" .build(); @Override public PayService getPayService() { return payService; } ``` - `com.android.vending.BILLING` 权限**已由模块 Manifest 声明并自动合并**,宿主无需手动添加。 - Android 公共查询 DTO 为 `SubscriptionOfferInfo` / `SubscriptionPricingPhase`;`PayProvider` 与 `PayService` 的新查询方法都有默认 `NOT_SUPPORTED` 实现,旧宿主/provider 兼容。 **③ 查询当前账号 eligible offers 并购买**: ```javascript const offers = await SuperSDK.pay.querySubscriptionOffers({ provider: 'google_play', productIds: ['matrixvplan01'] }); const selected = offers[0]; const result = await SuperSDK.pay.purchase({ provider: 'google_play', payParams: { productId: selected.productId, productType: 'subs', basePlanId: selected.basePlanId, offerId: selected.offerId, // 普通 base plan 为 null accountId: accountHash, profileId: orderTokenUuid } }); ``` - 查询结果只含 Play Billing 为**当前账号**返回的 `SubscriptionOfferDetails`,不拼接 Play Console 完整目录,也不经 Settings 过滤。 - 查询响应不会包含 `offerToken`。购买时 Provider 会重新查询 `ProductDetails`,精确匹配 `productId + basePlanId + nullable offerId`,只在 Native 内把最新 token 交给 `setOfferToken()`。 - 资格变化返回 `OFFER_UNAVAILABLE`,不会静默切换到其他价格。其他订阅错误:`PRODUCT_NOT_FOUND`、`BILLING_UNAVAILABLE`、`QUERY_FAILED`、`USER_CANCELLED`、`PURCHASE_FAILED`。 - 旧 INAPP 与 `queryProducts()` 的公开行为不变;业务创单、权益映射、服务端验单/acknowledge/RTDN 不在本模块范围。 **④ Google 后台**([Google Play Console](https://play.google.com/console)): | 步骤 | 说明 | |------|------| | 上传应用 | 须以**相同包名 + 签名**上传到至少 **internal testing** 轨道(Billing 仅对 Play 安装的签名包生效,本地 debug 直装无法测) | | 创建商品 | Monetize → Products → 应用内商品 / 订阅,**product ID 必须与 H5 传入的一致** | | 配置测试账号 | 添加 **License testers**,用沙盒账号测试购买,不产生真实扣费 | | 服务端发货(推荐) | 配置 **Real-time developer notifications (RTDN) + Cloud Pub/Sub + Play Developer API**,以服务端为真相源做幂等发货 —— 完整架构见 JSSDK 文档《IAP 推荐架构》 | > 参考:[Play Billing 概览](https://developer.android.com/google/play/billing) · [创建商品](https://support.google.com/googleplay/android-developer/answer/1153481) · [配置 RTDN](https://developer.android.com/google/play/billing/getting-ready#configure-rtdn) · [测试购买](https://developer.android.com/google/play/billing/test) --- ## 通用回调模式 SDK 使用 `SuperCallback` + `SuperResult` 模式进行异步回调。 ```java SuperSDK.someAsyncMethod(new SuperCallback() { @Override public void onResult(@NonNull SuperResult result) { if (result.isSuccess()) { SomeData data = result.getData(); // 处理成功 } else { String code = result.getCode(); String message = result.getMessage(); // 处理失败 } } }); ``` **SuperResult 错误码:** | Code | 说明 | |------|------| | `SUCCESS` | 成功 | | `FAIL_UNKNOWN` | 未知错误 | | `FAIL_TIMEOUT` | 超时 | --- ## 完整接入示例 ```java // 1. Application 初始化 public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); SuperSDK.init(this, "ab12cd34", new MySDKProvider()); } } // 2. 在 Activity 中使用 public class WebActivity extends AppCompatActivity { private AbsSuperPageView pageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web); FrameLayout container = findViewById(R.id.web_container); // 创建 PageView pageView = SuperSDK.getPageController().createPageView(); container.addView(pageView); // 适配沉浸式安全区域 pageView.fitSystemWindows(this); // 设置关闭页面监听(前端调用 closePage 时触发) pageView.setOnClosePageListener(this::finish); // 加载 URL String url = "https://example.com"; pageView.load(url, new PageLoadConfig(url)); } @Override protected void onDestroy() { super.onDestroy(); if (pageView != null) { pageView.destroyView(); } } @Override public void onBackPressed() { // 优先级:前端拦截 > 页面历史回退 > 关闭 Activity if (pageView != null) { pageView.handleBackPressed(() -> super.onBackPressed()); } else { super.onBackPressed(); } } } ``` --- ## 混淆配置 如果项目开启了 ProGuard/R8 混淆,请添加以下规则: ```proguard -keep class com.supersdk.** { *; } -keep class * implements com.supersdk.api.SuperSDKProvider { *; } ``` --- ## 注意事项 1. **初始化时机**:必须在使用任何 SDK 功能前调用 `SuperSDK.init()`,建议在 `Application.onCreate()` 中调用 2. **生命周期**:SDK 内部通过 `onWindowVisibilityChanged` 自动感知 Activity 生命周期,无需手动分发 3. **返回键处理**:使用 `handleBackPressed(exitAction)` 处理返回键,SDK 内部自动处理前端拦截和页面历史回退 4. **销毁**:Activity 销毁时必须调用 `pageView.destroyView()` 释放资源 5. **线程安全**:SDK 的 UI 操作已自动切换到主线程,宿主无需额外处理 6. **离线包**:SDK 内置离线包支持,加载 URL 时会自动匹配本地资源