11/08/2024 16:35:55
MSDK Policy 接入和升级指引
一、 Policy 组件说明
MSDK Policy 组件是 MSDK 团队为业务提供的启动时,弹窗通用解决方案,主要用于解决合规要求中的启动时,用户协议及权限处理相关内容。 参考 UI 界面如下
二、 Android 接入指引
2.1 Policy 接入升级指引
2.1.1 基础环境配置
2.1.1.1 引入 SDK 插件包
MSDK V5 默认携带了 MSDK Policy 组件,业务无需处理
2.1.1.2 请求 MSDK 配置服务器地址
在 MSDK 的配置文件中,添加或确认 MSDK_POP_URL 配置。该配置为 MSDK 弹窗服务器地址,用于下发弹窗规则。
配置文件一般在 assets 目录下,文件名:
- MSDKConfig.ini
当前 MSDK 有四个环境公有集群环节:
- 业务联调域名: https://pop-hktest.itop.qq.com
- 国内正式域名:https://pop-release.itop.qq.com
- 新加坡正式域名:https://pop-sg.itopsdk.com
业务需要跟进安装包类型进行配置,如国内正式环境,配置信息如下:
MSDK_POP_URL = https://pop-release.itop.qq.com
如果有其他独立部署需求,请与我们联系 !
2.1.2 配置 Policy 主 Activity
2.1.2.1 版本升级
如果之前有接入过旧版本 MSDKPolicy,升级到新版本 MSDK 时,仅需修改 AndroidManifest.xml 中的 Policy 的 Activity 配置
V1 版本 | V2 版本 |
---|---|
com.tencent.gcloud.msdk.core.policy.MSDKPolicyActivity | com.tencent.gcloud.msdk.core.policy.MSDKPolicyV2Activity |
其他配置保存不变即可
2.1.2.2 新接入
注意:该方案需要修改游戏的主 Activity,项目需要跟进自身情况进行对应的主 Activity 逻辑调整。对于新接入业务,则需在 AndroidManifest.xml 添加一个 Activity 配置:
<activity android:name="com.tencent.gcloud.msdk.core.policy.MSDKPolicyV2Activity"
android:configChanges = "keyboard|keyboardHidden|screenLayout|screenSize"
android:launchMode="singleTask"
android:theme="@style/MSDKPolicyTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- MSDK_POLICY_TARGET_ACTIVITY 用于指定要跳转到的【应用或者游戏的首个 Activity】-->
<meta-data
android:name="MSDK_POLICY_TARGET_ACTIVITY"
android:value="com.tencent.msdk.example.StartupActivity" />
<meta-data android:name="MSDK_POLICY_DEBUG" android:value="true"/>
<!-- 下面这个 MSDK_RESULT_FILE_NAME 配置从 MSDKCore V5.17 版本, V3-3.3.19 版本开始默认关闭 -->
<!-- 去掉此配置后,如果用户之前未同意过协议,覆盖安装的情况下都必须弹协议-->
<!-- <meta-data android:name="MSDK_RESULT_FILE_NAME" android:value="itop_login.txt"/> -->
<!-- 如需关闭 TDM 上报,需要将下面的 MSDK_POLICY_TDM_REPORT_DISABLE 开关设置为 true-->
<!-- <meta-data android:name="MSDK_POLICY_TDM_REPORT_DISABLE" android:value="false"/> -->
</activity>
- name 固定为 com.tencent.gcloud.msdk.core.policy.MSDKPolicyV2Activity
- configChanges 建议保持,如果有其他需求也可以根据业务需求调整
- launchMode 一般为 singleTask。更多信息请参考:https://developer.android.com/guide/topics/manifest/activity-element
- theme 可以调整为业务自己的主题
- intent-filter 示例中的为配置为启动 Activity,业务可根据自身情况进行确认后调整。注意:一个 App 只能有一个主 Activity
- MSDK_POLICY_TARGET_ACTIVITY 游戏的原主 Activity,Policy 组件会在完成逻辑后进行跳转
- MSDK_RESULT_FILE_NAME 建议保持默认值,如有变更请与我们进行确认
- MSDK_POLICY_TDM_REPORT_DISABLE 上报用户开启、关闭日志数据,一般用于监控和排查问题,视情况自行关闭
2.1.3 自定义 Policy 内容
2.1.3.1 调整协议内容
如需替换 Policy 展示的文字内容,则可按下面步骤处理:
1、自行开发一个 离线 HTML 用于展示 Policy 内容,然后将其放在 assets 目录下
2、调整 MSDK Core 包下的 assets/popup 目录下 MSDKPopupLocal.config 配置文件
文件格式:JSON
调整文件:将 scene = MSDKPolicy 节点为中的 uri 指向需要替换的业务自定义的 HTML 文件路径
调整文件签名:计算自定义的 HTML 文件的 MD5 值,将其填入到 sig 字段中
注意: 这里的路径是从 assets 开始,如 HTML 文件放在 assets 目录下,则 uri 为 xxx.html;文件放在assets/popup目录下,则 uri 为 popup/MSDKPopup{scene_type}{poptype}{contenttype}{sig}.html,其中{scene_type}、{pop_type}、{content_type}、{sig}分别替换为config 配置文件中对应弹窗配置的值scene_type、pop_type、content_type、sig。
至此,就完成了policy内容的替换,完成后的配置参考如下,字段解释可参考附录:
另外,MSDK 支持在管理端上配置并下发(请联系我们开发和确认)2.1.3.2 【可选】调整权限内容
同上,如果需要使用 MSDK 的权限弹窗,也可以参考上述配置自行进行调整。
三、 iOS 接入指引
3.1 Policy 接入指引
3.1.1 基础环境配置
3.1.1.1 引入 SDK 插件包
MSDK V5 版本已经默认携带了 MSDK Policy 组件,业务无需单独处理
3.1.1.2 请求 MSDK 配置服务器地址
在 MSDK 的配置文件中,添加或确认 MSDK_POP_URL 配置。该配置为 MSDK 弹窗服务器地址,用于下发弹窗规则。
配置文件名如下:
- MSDK V5 版本:MSDKConfig.ini (在 MSDKAppSetting.bundle 中)
当前 MSDK 有四个环境公有集群环节:
- 业务联调域名: https://pop-hktest.itop.qq.com
- 国内正式域名:https://pop-release.itop.qq.com
- 新加坡正式域名:https://pop-sg.itopsdk.com
业务需要跟进安装包类型进行配置,如国内正式环境,配置信息如下:
MSDK_POP_URL = https://pop-release.itop.qq.com
如果有其他独立部署需求,请与我们联系 !
3.1.2 MSDKV5 接入 policy
3.1.2.1 unity 接入 policy
MSDKPolicy对相关联应用启动的各个生命周期方法进行了拦截,优先执行MSDKPolicy的弹窗逻辑,等到点击弹窗“同意”按钮后,恢复执行原有的生命周期函数,保障用户同意隐私协议前应用不进行初始化 。
接入步骤:
- 将MSDKPolicyDelegate.h 及 MSDKPolicyDelegate.mm放入Assets/Plugins/iOS目录下(注:MSDKPolicyDelegate文件见附录示例);
- 在GCloudSDK/Editor/XUPorter/XCodePostProcess.cs文件中,将代码“EditorCode4StopLaunching(pathToBuiltProject);”注释放开。
- 在Info.plist中配置游戏对应的MSDK_POP_URL、MSDK_GAME_ID 以及 MSDK_SDK_KEY。
3.1.2.2 ue 接入 Policy:
接入步骤:
- 在Info.plist中配置游戏对应的MSDK_POP_URL、MSDK_GAME_ID 以及 MSDK_SDK_KEY。
在IOSAppDelegate的分类中,仿照IOSAppDelegate+MSDK.mm的逻辑进行相关生命周期的拦截与处理:
hook以下关键生命周期方法:
application:didFinishLaunchingWithOptions:
application:openURL:options:
application:openURL:sourceApplication:annotation:
application:continueUserActivity:restorationHandler:
调用showPolicyPopup方法,并在用户完成授权的回调中,通过反射回调主类 IOSAppDelegate 的对应生命周期方法,例如:
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
showPolicyPopup(^(id param){
dispatch_async(dispatch_get_main_queue(), ^() {
//这里反射调用主类的对应方法,注意传参中的函数名要与当前所在函数名一致
invokeOriginalMethod([IOSAppDelegate class], @"application:didFinishLaunchingWithOptions:", ^(IMP imp, SEL sel){
((BOOL(*)(id, SEL, id, id))imp)(self, sel, application, launchOptions);
});
if (!isPolicyProcessed) {
invokeOriginalMethod([IOSAppDelegate class], @"applicationDidBecomeActive:", ^(IMP imp, SEL sel){
((void(*)(id, SEL, id))imp)(self, sel, application);
});
isPolicyProcessed = true;
}
});
});
return YES;
}
2.反射的方式可仿照以下函数实现
static void invokeOriginalMethod(Class clazz, NSString* methodName, void(^callback)(IMP imp, SEL sel)) {
if (clazz) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(clazz, &methodCount);
IMP currentImp = NULL;
SEL currentSel = NULL;
int index = 0;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *currentMethodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
if ([methodName isEqualToString:currentMethodName]) {
currentImp = method_getImplementation(method);
if (currentImp != NULL && index != 0) {
currentSel = method_getName(method);
callback(currentImp, currentSel);
break;
}
index++;
}
}
free(methodList);
}
}
3.在下列生命周期方法中拦截业务的处理逻辑:
applicationDidBecomeActive
applicationWillResignActive
applicationDidEnterBackground
applicationWillEnterForeground
applicationWillTerminate
例如:
- (void)applicationWillResignActive:(UIApplication*)application
{
if (isPolicyProcessed) {
invokeOriginalMethod([IOSAppDelegate class], @"applicationWillResignActive:", ^(IMP imp, SEL sel){
((void(*)(id, SEL, id))imp)(self, sel, application);
});
}
}
注意:需要配置编译规则,保证处理后的分类在IOSAppDelegate所有分类中最后一个参与编译。如果只有当前这一个分类,则无需保证其编译的先后顺序。
如下图中,在 xcode 中将 IOSAppDelegate+MSDK.mm 放在列表最后
3.1.3 自定义 Policy 内容
3.1.3.1 调整协议内容
如需替换 Policy 展示的文字内容,则可按下面步骤处理:
1、自行开发一个 离线 HTML 用于展示 Policy 内容,然后将其放在 assets 目录下
2、调整 MSDK Core 包下的 ThirdSDK/MSDKPopupResource.bundle 目录下 MSDKPopupLocal.config 配置文件
文件格式:JSON
调整文件:将 scene = MSDKPolicy 节点为中的 uri 指向需要替换的业务自定义的 HTML 文件路径
调整文件签名:计算自定义的 HTML 文件的 MD5 值,将其填入到 sig 字段中
注意: 这里的路径是从 MSDKPopupResource.bundle 的根目录开始,如 HTML 文件放在根目录下,则 uri 为 MSDKPopup{scene_type}{poptype}{contenttype}{sig}.html,其中{scene_type}、{pop_type}、{content_type}、{sig}分别替换为config 配置文件中对应弹窗配置的值scene_type、pop_type、content_type、sig。
至此,就完成了policy内容的替换,完成后的配置参考如下:
3.1.4 其他注意事项
以上修改针对绝大部分游戏的使用,请注意若游戏对ViewController进行了自定义修改,并且在ViewController加载时进行了某些功能的初始化,需要考虑在调用showPolicy接口的同时拦截ViewController的加载,以达到用户点击同意前不进行相应功能的初始化,可考虑参考以下代码:
UIViewController* rootViewController = self.window.rootViewController;
UIViewController* tempViewController = [[UIViewController alloc] init];
self.window.rootViewController = tempViewController;
[MSDKPolicy showPolicy:launchOptions complete:^(id param) {
self.window.rootViewController = rootViewController;
NSLog(@"Show Policy complete, continue didFinishLaunchingWithOptions:%@", param);
}];
附录
MSDKPolicyDelegate示例代码:
MSDKPolicyDelegate.h:
#import <Foundation/Foundation.h>
#include "UnityAppController.h"
NS_ASSUME_NONNULL_BEGIN
@interface MSDKPolicyDelegate : UnityAppController
@end
NS_ASSUME_NONNULL_END
MSDKPolicyDelegate.mm:
#import "MSDKPolicyDelegate.h"
typedef void(^MSDKPolicyLifecycleEventCallback)(void);
static NSMutableArray<MSDKPolicyLifecycleEventCallback>* events;
static NSCondition *eventsLock;
static bool isPolicyProcessed = false;
static void addLifecycleEvent(void(^callback)()) {
[eventsLock lock];
[events addObject:callback];
[eventsLock unlock];
}
static void invokeLifecycleEvent() {
if (events) {
NSUInteger count = [events count];
if (count > 0 && eventsLock) {
[eventsLock lock];
for (MSDKPolicyLifecycleEventCallback callback in events) {
callback();
}
[events removeAllObjects];
[eventsLock unlock];
}
}
}
static void showPolicyPopup(void(^callback)(id param))
{
Class policyClass = NSClassFromString(@"MSDKPolicy");
if (policyClass) {
SEL selector = NSSelectorFromString(@"showPolicy:complete:");
if (selector) {
NSMethodSignature *signature = [policyClass methodSignatureForSelector:selector];
if (signature) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = policyClass;
invocation.selector = selector;
void(^policyCallback)(id) = ^(id param) {
if (callback) {
callback(param);
}
};
id firstParams = nil;
[invocation setArgument:&firstParams atIndex:2];
[invocation setArgument:&policyCallback atIndex:3];
[invocation invoke];
return;
}
}
}
if (callback) {
callback(nil);
}
}
@implementation MSDKPolicyDelegate
+(void)load {
eventsLock = [[NSCondition alloc] init];
events = [NSMutableArray new];
}
- (BOOL)application:(UIApplication*)application willFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
return YES;
}
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
showPolicyPopup(^(id param){
dispatch_async(dispatch_get_main_queue(), ^() {
[super application:application willFinishLaunchingWithOptions:launchOptions];
[super application:application didFinishLaunchingWithOptions:launchOptions];
invokeLifecycleEvent();
isPolicyProcessed = true;
});
});
return YES;
}
- (BOOL)application:(UIApplication*)application openURL:(NSURL*)url sourceApplication:(NSString*)sourceApplication annotation:(id)annotation
{
if (isPolicyProcessed) {
return [super application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
} else {
addLifecycleEvent(^() {
[super application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
});
}
return YES;
}
// 这里加宏的原因:由于unity5.6.4版本的UnityAppController没有application:openURL:options:方法,因此需要隔离。
// 注意:unity5.x系列版本,业务需要自行确认,并添加宏来隔离。
#ifndef UNITY_5_6_4
- (BOOL)application:(UIApplication*)application openURL:(NSURL*)url options:(NSDictionary<NSString*, id>*)options
{
if (isPolicyProcessed) {
return [super application:application openURL:url options:options];
} else {
addLifecycleEvent(^() {
[super application:application openURL:url options:options];
});
}
return YES;
}
#endif
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler
{
if (isPolicyProcessed) {
return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
} else {
addLifecycleEvent(^() {
[super application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
});
}
return YES;
}
- (void)applicationDidBecomeActive:(UIApplication*)application
{
if (isPolicyProcessed) {
[super applicationDidBecomeActive:application];
} else {
addLifecycleEvent(^() {
[super applicationDidBecomeActive:application];
});
}
}
- (void)applicationWillResignActive:(UIApplication*)application
{
if (isPolicyProcessed) {
[super applicationWillResignActive:application];
}
}
- (void)applicationDidEnterBackground:(UIApplication*)application
{
if (isPolicyProcessed) {
[super applicationDidEnterBackground:application];
}
}
- (void)applicationWillEnterForeground:(UIApplication*)application
{
if (isPolicyProcessed) {
[super applicationWillEnterForeground:application];
}
}
- (void)applicationWillTerminate:(UIApplication*)application
{
if (isPolicyProcessed) {
[super applicationWillTerminate:application];
}
}
@end
All rights reserved.