接入准备

APK包必须签名,提交Google Play商店以后,签名不可更改

APK包必须 zipalign 优化

必须保证测试手机的Google框架完整

minSdk 24

compileSdk 35 (34升级到35常见问题)

targetSdk 35

buildToolsVersion = "35.0.0"

Gradle JDK 不能低于17

SDK支持AndroidX (检查AndroidX配置项)

项目需支持AndroidX,检查配置

检查gradle.properties中是否缺少以下配置:
		
android.useAndroidX=true
android.enableJetifier=true
		
	

Gradle版本推荐

查看并配置Gradle版本

针对targetSdkVersion 35,SDK使用的Gradle plugin版本为8.8.2,Gradle版本为8.10.2。(不要低于推荐版本)
您可以在 Android Studio 的 File > Project Structure > Project 菜单中查看或指定插件版本。如图:

配置PnSDK的Maven仓库地址

注意:Android Studio在创建项目时会因为版本不同,配置的仓库的位置也不同,请根据项目的实际情况,选择对应的配置过程。

老版本

  1. 打开Android Studio项目级"build.gradle"文件。

  2. 添加PnSDK的Maven仓库地址。
    在“allprojects > repositories”中配置PnSDK的Maven仓地址。
                
    				
    buildscript {
    
      repositories {
        google()
        jcenter()
        mavenCentral()
      }
    
      dependencies {
        ...
      }
    }
    
    allprojects {
      ...
      repositories {
        // ...
        google()
        jcenter()
        mavenCentral()
    	
        maven {//PnSDK
            allowInsecureProtocol = true
            url 'http://git-maven.17995api.net:8888/api/v4/projects/112/packages/maven'
        }
         
      }
    }				
    
    			

新版本

  1. 打开Android Studio项目级"settings.gradle"文件。

  2. 添加PnSDK的Maven仓库地址。
                
                    
    dependencyResolutionManagement {
        ...
        repositories {
            google()
            mavenCentral() 
            // 配置PnSDK的Maven仓地址。
            
            maven {//PnSDK
                allowInsecureProtocol = true
                url 'http://git-maven.17995api.net:8888/api/v4/projects/112/packages/maven'
            }
    
            
        }
    
    }
         
                  
    
                

添加对PnSDK的依赖

  1. 打开应用级“build.gradle”文件。

  2. 在该文件中配置动态版本的缓存策略,并在“dependencies”中添加PnSDK的编译依赖。

                
                    
    android {
        //...
    
        //解决编译时的重复问题。
        packagingOptions {
            exclude 'META-INF/INDEX.LIST'
            exclude 'META-INF/DEPENDENCIES'
        }
    }
    
    //建议配置动态版本的缓存策略,保证能够及时更新sdk的版本
    configurations.all {
        resolutionStrategy.cacheDynamicVersionsFor 10, 'minutes'
    } 
    
    dependencies {
        // ...
        implementation 'com.pn:pnsdk:+'
        implementation 'com.pn.pnsdk:res-[APPID]:+'
        //注意:[APPID]请替换为实际参数,AppID参数可以在提供的说明文档中找到。
        //例如:implementation 'com.pn.pnsdk:res-9000001:+'
    }
       
    
                

混淆规则


# ==================== PnSDK 核心防混淆规则 ====================
# 保护PnSDK核心包
-keep class com.pn.sdk.** {*;}
-keep interface com.pn.sdk.** {*;}

# ==================== 第三方SDK防混淆规则 ====================

# TapTap SDK
-keep class com.taptap.**{*;}
-keep class com.tapsdk.**{*;}
-keep class com.tapdb.**{*;}

# TikTok SDK
-keep class com.tiktok.** { *; }

# Google相关SDK
-keep class com.android.billingclient.api.** { *; }
-keep class com.google.android.gms.** { *; }
-keep class com.google.android.play.** { *; }
-keep class com.google.firebase.** { *; }
-keep class com.google.api.** { *; }
-keep class com.google.android.libraries.identity.** { *; }

# Google Credential Manager
-if class androidx.credentials.CredentialManager
-keep class androidx.credentials.playservices.** {
  *;
}

# Facebook SDK
-keep class com.facebook.** { *; }

# AppLovin SDK
-keep class com.applovin.** { *; }
-keep class com.unity3d.** { *; }


-keep class androidx.credentials.** { *; }

# AppsFlyer SDK
-keep class com.appsflyer.** { *; }

# ==================== 通用防混淆规则 ====================

# 保护WebView
-keepclassmembers class * {
    @android.webkit.JavascriptInterface ;
}

# 保护native方法
-keepclasseswithmembernames class * {
    native ;
}

# 保护注解
-keepclassmembers class * {
    @android.support.annotation.Keep ;
    @android.support.annotation.Keep ;
    @androidx.annotation.Keep ;
    @androidx.annotation.Keep ;
}
-keep @android.support.annotation.Keep class * {*;}
-keep @androidx.annotation.Keep class * {*;}

# 保护序列化相关
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 保护枚举
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保护反射
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Deprecated
-keepattributes SourceFile,LineNumberTable


# 保护Parcelable
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 保护R文件
-keep class **.R$* {
    public static ;
}

# 保护BuildConfig
-keep class **.BuildConfig {
    public static final *;
}


                        

SDK API使用说明

初始化SDK

		
请确保在 Launcher Activity OnCreate 函数中初始化SDK

protected void onCreate(Bundle savedInstanceState) {

    String appID  = "APPID";
    String appKey = "APPKEY";
    String channel  = "渠道标识";
    String langID   = "语言字符";// 查看多语言标签

    Map extraMap = new HashMap();
    extraMap.put("termAgreed", true或者false);

    //termAgreed为true,表明游戏在调用startWithAppID之前用户已经同意过条款,该条款由游戏自行处理。
    //termAgreed为false,使用SDK条款页面。
    //extraMap为null,默认使用SDK条款页面。
    
    PnSDK.startWithAppID(activity , appId, appKey, channel, langID, extraMap);
}

所需要的参数,可以在对接说明文档中找到.
		
	

广播监听

		

<实现广播接收器>

public class MyBroadcastReceiver extends BroadcastReceiver { final String TAG = "MyBroadcastReceiver"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction();
if (TextUtils.equals(action, "PnSDKPassportNotification")) { // 通行证状态发生变化, 已登录或者已登出 Log.v(TAG, "游戏接收到广播:PnSDKPassportNotification"); Map accountInfoMap = PnSDK.getAccountInfo(); if (null == accountInfoMap) { Log.d(TAG, "游戏接收到PnSDKPassportNotification广播,账户没登录不向下执行代码"); //TODO 没有登录 return; } String uid = (String) accountInfoMap.get("uid");// 获取用户唯一ID String jwt = (String) accountInfoMap.get("jwt");// 登录凭证 int age = (int) accountInfoMap.get("age"); // 实名制用户年龄, 未登记返回为0 String idmd5 = (String) accountInfoMap.get("idmd5"); // 实名制用户身份证md5, 未登记返回为“” String aaStatus = (String) accountInfoMap.get("aa-status");// 防沉迷状态, 返回 "restricted" 禁止继续游戏 int guest = (int) accountInfoMap.get("guest"); // 是否游客,1为游客,0为其它账号或者已绑定游客 Log.d(TAG, "PnAccountInfo ,uid: " + uid + " jwt: " + jwt); //TODO 客户端需要将 jwt 登录凭证上传至游戏服务器, 游戏服务器再通过SDK服务器 验证接口 进行验证,以确保登录账号合法。 return; }
if (TextUtils.equals(action, "PnSDKPaymentNotification")) { //TODO 充值结束(返回充值信息) Log.v(TAG, "游戏接收到广播:PnSDKPaymentNotification"); String productid = intent.getStringExtra("productid"); String nonce = intent.getStringExtra("nonce"); String orderid = intent.getStringExtra("orderid"); String transactionid = intent.getStringExtra("transactionid"); if (TextUtils.isEmpty(productid)) { // 用户取消 或者 充值失败 或者 拉取了第三方充值 // 通常情况下,客户端不需要做任何处理,不要弹出充值失败等提示 } else { Log.d(TAG, "游戏广播接收,充值成功 " + productid); //TODO 充值成功 } return; }
if (TextUtils.equals(action, "PnSDKRequestPaymentNotification")) { //发起充值 (返回充值项ID),用于完成补单 Log.v(TAG, "游戏接收到广播:PnSDKRequestPaymentNotification"); //当玩家购买掉单或者应用外购买完成时,游戏内会收到此事件通知,客户端需要根据事件通知指定的产品项ID发起一次购买。 //游戏发起新充值请求,与之前充值无关,当前场景无法发起充值时,可以无视此事件通知。 String productId = intent.getStringExtra("productid"); String srvid = "游戏服务器"; //TODO 游戏传入 String nonce = 透传参数; //TODO 游戏传入 Log.d(TAG, "需要发起购买的产品项ID: " + productId); PnSDK.payWithProductID((Activity) context, productId, srvid, nonce, null); // 注意: nonce 必须是有效的透传参数,如果在补单时无法获取有效的参数,则传入空字符串。充值平台会自动匹配可用的透传参数. return; } } }

<在主页面注册广播>

public class MainActivity extends Activity { private MyBroadcastReceiver myBroadcastReceiver; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); myBroadcastReceiver = new MyBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("PnSDKPassportNotification"); intentFilter.addAction("PnSDKRequestPaymentNotification"); intentFilter.addAction("PnSDKPaymentNotification"); intentFilter.addAction("PnSDKAdRewardsNotification"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //Android14(34)及以上必须添加标记 registerReceiver(broadcastReceiver, intentFilter , Context.RECEIVER_NOT_EXPORTED); }else { registerReceiver(broadcastReceiver, intentFilter); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); //... PnSDK.onActivityResult(requestCode, resultCode, data); } @Override protected void onDestroy() { super.onDestroy(); unregisterReceiver(myBroadcastReceiver); } }

登录通行证

		
PnSDK.signIn(activity);
		
	

切换账号

		
// 首先登出当前账号
PnSDK.signOut(activity);

// 紧接着调登入接口
PnSDK.signIn(activity);
        
	

客服中心

游戏必须接入SDK的客服中心,建议在游戏内设置界面和登录界面放置客服中心按钮,详情需求请查阅对接说明文档。


若未登录游戏服务器,服务器ID 请传入 "none"
若游戏角色未登录,角色昵称 请传入 "not logged in"
extraMap 扩展参数 需增加roleid(角色ID), level(角色等级)参数,角色未登录时可为NULL

Map extraMap = new HashMap();
extraMap.put("roleid", 角色ID);
extraMap.put("level", 角色等级);
...

PnSDK.bugReport(activity, "服务器ID", "角色昵称", "游戏客户端版本号", extraMap);


						

帐号管理


//帐号管理主要提供 账号绑定, 重置游客 等功能, 建议在游戏设置界面中放置 帐号管理 按钮

extraMap 扩展参数 需增加roleid(角色ID), level(角色等级)参数,角色未登录时可为NULL

Map extraMap = new HashMap();
extraMap.put("roleid", 角色ID);
extraMap.put("level", 角色等级);
...

PnSDK.userCenter(activity, "服务器ID", "角色昵称", "游戏客户端版本号", extraMap);


						

事件埋点


// 事件标识 详细说明请查阅对接说明文档

PnSDK.evtTrack(activity,"eventName");

						

评分接口


//用户评分接口的使用时机,请参阅对接说明文档

若未登录游戏服务器,服务器ID 请传入 "none"
若游戏角色未登录,角色昵称 请传入 "not logged in"
extraMap 扩展参数 需增加roleid(角色ID), level(角色等级)参数

Map extraMap = new HashMap();
extraMap.put("roleid", 角色ID);
extraMap.put("level", 角色等级);
...

PnSDK.appReview(activity, "服务器ID", "角色昵称", "游戏客户端版本号", extraMap);

						

动态申请权限

Android 6.0及更高版本中,用户与需要用到危险权限(下图)的功能进行互动时需要动态请求权限。

Android中的危险权限分为9组24种

		
			

只需请求用到的危险权限。

/** * @param activity * @param listener 请求监听(所请求的权限全部允许回调onSuccess) * @param isNecessary 是否必要权限 * @param permissions 权限名称(可以传递多个,用逗号隔开): * Manifest.permission.WRITE_EXTERNAL_STORAGE * ... */ PnSDK.requestPermissions(this, new IRequestPermissionsListener() { @Override public void onSuccess() { Log.d(TAG, "获取授权成功。"); } @Override public void onFailed() { Log.d(TAG, "获取授权失败。"); } }, isNecessary ,Manifest.permission.WRITE_EXTERNAL_STORAGE );

调用该接口前,确认应用清单(AndroidManifest.xml)中已经声明需要请求的权限。

声明权限例子:

		

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.snazzyapp">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!-- other permissions go here -->

    <application ...>
        ...
    </application>
</manifest>

        
	

系统回调

实现系统回调,否则SDK无法正常工作

onActivityResult

		
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    //...
    PnSDK.onActivityResult(requestCode, resultCode, data);
}		
	

充值接入说明

充值界面由游戏开发方提供.

充值项的设定,请参阅对接说明文档.

支付产品的价格需要通过SDK提供的接口查询.

查询充值项价格

请在游戏启动过程中,后台异步查询价格.

避免在显示商店时同步查询价格,这会导致打开商店界面有明显的延迟.


List products = new ArrayList<>();
products.add("game_001");
products.add("game_002");
PnSDK.queryWithProducts(activity, products, new IProductDetailsResponseListener() {
    @Override
    public void onPnProductDetailsResponse(List<com.pn.sdk.billing.ProductDetails> productDetailsList) {
        //查询失败
        if (null == productDetailsList)
            return;

        for (ProductDetails product : productDetailsList) {
            product.getProductId();	//-- 产品ID
            product.getDisplayPrice();//格式化价格,包含货币符号,可直接用于价格显示
            product.getPrice();//价格
            product.getPriceAmountMicros();//微单位价格,其中1,000,000微单位等于货币的一单位。long 类型
            String priceCurrencyCode = product.getPriceCurrencyCode();//货币代码
            Currency.getInstance(priceCurrencyCode).getSymbol();//货币符号
        }

    }
});





						

价格格式说明

如未使用格式化价格, 则需要将小数部分为0的情况,化为整数去掉小数部分.

e.g. $100.00 => $100

$99.99 => 不变 $99.99

注意

某些情况下 queryWithProducts 会失败,返回空的产品列表,请显示默认的产品价格,默认价格请查阅对接说明文档

充值


product        充值产品ID
srvid          服务器ID
nonce 透传参数  充值透传参数,SDK充值平台会原样传回到游戏服务器,,详见支付服务器对接
extraMap       扩展参数 需增加roleid(角色ID), level(角色等级)参数

Map extraMap = new HashMap();
extraMap.put("roleid", 角色ID);
extraMap.put("level", 角色等级);
    ...
PnSDK.payWithProductID(activity, "产品ID", "服务器ID", "nonce 透传参数", extraMap);

// 充值完成后, SDK会发送广播PnSDKPaymentNotification, 查看 广播监听 说明

						

注意

收到 PnSDKPaymentNotification 回调不代表充值已到账,以游戏服务端收到SDK充值平台回调为准.

充值流程图

查询未完成订单

玩家完成游戏角色登录后,游戏需要主动查询是否有未完成的订单

  • 角色登录成功后
  • 打开商店后
  • 
    PnSDK.queryPendingTransaction(activity);
    
    //如果查询到存在未完成订单, SDK会发送广播"PnSDKRequestPaymentNotification", 查看广播监听说明
    
    
    						

    查询订阅

    用于查询当前生效的订阅项, 如游戏没有可续订充值项则无需处理

    查询特定订阅项目

    
    String subscriptionId = "com.subs.01";
    
    PnSDK.querySubscription(this, subscriptionId, new ISpeificPurchaseListener() {
        @Override
        public void onPurchaseResponse(com.android.billingclient.api.Purchase  purchase) {
            if(null == purchase){
                return;
            }
    
            Log.d(TAG , "purchase originalJson: " + purchase.getOriginalJson());
    
            /*
                {
                    "orderId": "GPA.3304-7399-****-21711", ---商店订单ID
                    "packageName": "com.xxx.demo",         ---包名
                    "productId": "com.subs.01",            ---订阅项ID
                    "purchaseTime": 1637899846435,         ---订阅时间
                    "purchaseState": 0,                    ---订阅状态
                    "purchaseToken": "alajnicmlhiip******Fo0LQVaJTw",  ---订阅token
                    "autoRenewing": true,                  ---是否自动续订
                    "acknowledged": true                   ---订阅是否确认
                }
                 */          
    
        }
    });
    
    
    						

    查询所有有效的订阅项目

    
    PnSDK.querySubscriptions(activity, new IPurchaseResponseListener() {
            @Override
            public void onPurchaseResponse(List<Purchase> purchaseList) {
                if (null == purchaseList) {
                    PnLog.d(TAG, "没有已订阅充值项");
                    return;
                }
    
                for (Purchase purchase : purchaseList) {
                    PnLog.d(TAG , "purchase originalJson:" + purchase.getOriginalJson());
                    
                    /*
                	{
    	                "orderId": "GPA.3304-7399-****-21711", ---订单ID
    	                "packageName": "com.xxx.demo",         ---包名
    	                "productId": "com.subs.01",            ---订阅项ID
    	                "purchaseTime": 1637899846435,         ---订阅时间
    	                "purchaseState": 0,                    ---订阅状态
    	                "purchaseToken": "alajnicmlhiip******Fo0LQVaJTw",  ---订阅token
    	                "autoRenewing": true,                  ---是否自动续订
    	                "acknowledged": true                   ---订阅是否确认
                	}
                 */
                }
    
    
            }
        });
    
    
    						

    补单功能测试

    用于测试客户端充值掉单后处理的流程,模拟系统出现掉单时SDK的广播行为

    
    测试方式一:
    android代码发送广播测试:
    
    Intent intent = new Intent("PnSDKRequestPaymentNotification");
    intent.putExtra("productid", [产品项ID]);
    activity.sendBroadcast(intent);
    
    
    测试方式二:
    超链接测试,创建htlm文件,将以下地址放入<a href="链接地址">标签中。浏览器打开创建的html文件,点击该链接。
    
    pnsdk://[Apk PackageName]?action=purchaseIntent&productIdentifier=[产品项ID]
    
    其中 [Apk PackageName], [产品项ID] 请替换为实际参数
    
    实例: <a href="pnsdk://com.pnsdk.demo?action=purchaseIntent&productIdentifier=com.pnsdk.demo.sku.01">点击该链接</a>
    
    请求地址后,浏览器会拉起App,并接收到 "PnSDKRequestPaymentNotification" 广播
    
    游戏应当在已完成登录的情况下拉起指定充值项的充值请求(全新的充值,与未完成订单无关),并正确填写服务器,nonce等参数.
    如当前是未登录状态,则忽略事件广播
    
    
    测试方式三:
    使用adb测试,在应用开启的状态下执行下面adb命令:
    adb shell am start -W -a android.intent.action.VIEW -d "替换下面的URI地址"
    
    pnsdk://[Apk PackageName]?action=purchaseIntent\&productIdentifier=[产品项ID]
    
    其中 [Apk PackageName], [产品项ID] 请替换为实际参数。
    注意:执行的adb命令中,如果有&符号,需要在&符号前面加上\
    
    实例:
    adb shell am start -W -a android.intent.action.VIEW -d "pnsdk://com.pnsdk.demo?action=purchaseIntent\&productIdentifier=com.pnsdk.demo.sku.01"
    
    
    
    						

    进阶

    本地推送

    
    本地推送说明:
    
    1. 原则上无互动性的消息,都应该使用本地推送
    2. 服务器推送仅使用在交互性的消息上
    
    						

    设置推送

    
    
    // 延迟60秒后弹出推送 
    PnSDK.setNotification("推送ID", 60, "通知标题", "通知内容", null);
    
    
    // 特定时间弹出推送
    SimpleDateFormat formatter = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);
    Date date = null;
    try {
        date = formatter.parse("2021-10-01 12:00:00");
    } catch (ParseException e) {
        e.printStackTrace();
    }
    if(null != date){
        PnSDK.setNotification("推送ID" , date , "通知标题" , "通知内容" , null);
    }
    
    
    
    						

    移除推送

    
    
    // 移除ID为 @"推送ID1", @"推送ID2" 的本地推送
    List idList = new ArrayList<>();
    idList.add("推送ID1");
    idList.add("推送ID2");
    PnSDK.deleteNotifications(idList);
                
                
    // 移除所有本地推送
    PnSDK.deleteNotifications(null);
    
    
    
    						

    Google 商店推荐

    返回和向上导航的规则

    
    
    “向上”按钮用于根据屏幕之间的层级关系在某个应用内部导航。 例如,如果屏幕 A 显示项目列表,并且选择某个项目会调出屏幕 B(该屏幕显示项目的更多详情),则屏幕 B 应提供可返回屏幕 A 的“向上”按钮。
    
    如果屏幕是应用中层级最高的屏幕(即应用的主屏幕),则无需提供向上按钮。
    
    系统的“返回”按钮用于按照用户最近操作的屏幕历史记录,按时间逆序导航。 它通常基于屏幕之间的时间关系,而非应用的层级关系。
    
    如果之前查看的屏幕也是当前屏幕的父级项,按下“返回”按钮的作用跟按下“向上”按钮一样 — 这种情况很常见。 但是,“向上”按钮可确保用户留在应用内,与此不同的是,“返回”按钮可让用户返回到主屏幕,甚至返回不同的应用。
    
    战斗支持暂停的游戏,战斗中按返回键要暂停
    
    战斗支持暂停的游戏,返回桌面时游戏要自动暂停
    
    关闭按钮需要关闭窗口不能返回上一层级
    
    						

    隐藏虚拟键盘

    
    public void hideNavigationBar() {
      int uiFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    	| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    	| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
    	| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
    	| View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
    	| View.SYSTEM_UI_FLAG_IMMERSIVE ;
    
      if( android.os.Build.VERSION.SDK_INT >= 19 ){
    	uiFlags |= 0x00001000;
      } else {
    	uiFlags |= View.SYSTEM_UI_FLAG_LOW_PROFILE;
      }
    
      getWindow().getDecorView().setSystemUiVisibility(uiFlags);
    }
    
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
      super.onWindowFocusChanged(hasFocus);
      if( hasFocus ) {
    	hideNavigationBar();
      }
    }
    
    						

    整理权限

    推荐使用PnSDK提供的接口,动态申请权限


    尽量避免在启动时候请求设备权限。仅在需要使用的位置请求权限。
    详情参考:https://developer.android.com/training/permissions/index.html
    必须去掉的权限 (文档中要求增加的除外)
    
    android.permission.WRITE_SETTINGS
    android.permission.NFC
    android.permission.MANAGE_ACCOUNTS
    android.permission.GET_ACCOUNTS
    android.permission.USE_CREDENTIALS
    android.permission.AUTHENTICATE_ACCOUNTS
    android.permission.MOUNT_UNMOUNT_FILESYSTEMS
    android.permission.CHANGE_WIFI_STATE
    android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
    android.permission.CHANGE_CONFIGURATION
    android.permission.SEND_SMS
    android.permission.CALL_PHONE
    android.permission.CHANGE_NETWORK_STATE
    android.permission.PROCESS_OUTGOING_CALLS
    android.permission.RECEIVE_BOOT_COMPLETED
    android.permission.READ_SMS
    android.permission.RECEIVE_SMS
    android.permission.DOWNLOAD_WITHOUT_NOTIFICATION
    com.android.vending.CHECK_LICENSE
    
    						
    建议去掉的权限 (文档中要求增加的除外)
                            
    
    android.permission.ACCESS_COARSE_LOCATION
    android.permission.ACCESS_FINE_LOCATION
    android.permission.BROADCAST_STICKY
    android.permission.GET_TASKS
    android.permission.KILL_BACKGROUND_PROCESSES
    android.permission.SYSTEM_ALERT_WINDOW
    android.permission.RECORD_AUDIO
    android.permission.READ_LOGS
    android.permission.CAMERA
    android.permission.READ_PHONE_STATE
    
    
    						

    推荐标准

    以下是经过提炼的需求,一般做到本文档需求即可获得Google Play 商店推荐

    <application
    	...
    	android:theme="@style/Theme.AppCompat.Light">
    
    在AndroidManifest.xml中appilication层加上主题
    targetSdkVersion 33 或者更高
    图标设计,参考官方规范文档 Google Play 图标设计规范

    可使用Android Studio创建自适应图标,请参阅使用 Image Asset Studio 创建应用图标

    切换至后台,或者设备锁屏时,游戏应处于暂停状态。
    游戏有可暂停的功能时,当在游戏界面弹出对话框时,必须把游戏暂停。

    其它接口

    服务接口

    
    service         服务名
    Map extraMap    扩展参数,角色未登录时可为NULL
    
    Map extraMap = new HashMap();
    extraMap.put("roleid", "角色ID");
    extraMap.put("nickname", "角色昵称");
    ...
    
    PnSDK.openService(this, "服务名", extraMap);
    
                            
    服务名 extraMap 参数 说明
    survey roleid 角色ID(可选)
    nickname 角色昵称(可选)
    打开调查问卷
    terms 打开服务条款
    privacy 打开隐私条款

    Facebook分享

    Facebook 应用由PnSDK平台配置,并提供可以测试分享的Facebook账号.

    测试Facebook的相关功能,需要翻墙!

    Facebook图片分享

    
    Bitmap   Bitmap类型图片对象   
    Map extraMap    扩展参数,传入null      
    
    PnSDK.facebookFeedWithImage(activity, Bitmap, extraMap ,new IFeedListener() {
        @Override
        public void onError(String error) {
            if (TextUtils.isEmpty(error)){
                //分享成功
            }else{
                //分享失敗
            }
        }
    });
    
    						
       
    Map extraMap    扩展参数,传入null   
       
    PnSDK.facebookFeedWithMessage(this, "测试分享链接", "https://www.yourlink.com", extraMap , new IFeedListener() {
        @Override
        public void onError(String error) {
            if(TextUtils.isEmpty(error)){
                PnLog.d(TAG, "Facebook分享链接成功");
            }else {
                PnLog.d(TAG, "Facebook分享链接失败: " + error);
            }
        }
    });
    
                            

    Instagram分享

    
    Bitmap   Bitmap 类型图片对象     
    Map extraMap   扩展参数, 传入null   
    
    PnSDK.instagramFeedWithImage(activity, Bitmap , extraMap);
    
    						

    聊天室屏蔽规则

    
    聊天屏蔽規則:
    
    1,对指定等级做屏蔽字限制;
    2,对等级高于我们设定值的玩家不做限制;
    3,被屏蔽发言自己客户端可正常显示,无特殊提示;
    4,被屏蔽的发言整句都不显示在聊天频道,触发屏蔽的玩家和其他玩家都无法得知是哪句话触发了屏蔽
    
    
    舉例:
    我們限制等級15級以下的玩家,聊天有限制
    
    15級以下的玩家,如果聊天中包涵:line,LINE,=,元寶,等字眼,他所發的消息,就只有他自己看得到,其他玩家是收不到了。
    
    						

    社区

    facebook,twitter,navercafe App内打开网页

    
    //Facebook  社区
    String url = "https://facebook.com/UltimateSchool";
    PackageManager pm = this.getPackageManager();
    Uri uri = Uri.parse(url);
    
    try {
        ApplicationInfo applicationInfo = pm.getApplicationInfo("com.facebook.katana", 0);
        if (applicationInfo.enabled) {
            uri = Uri.parse("fb://facewebmodal/f?href=" + Uri.encode(url));
        }
    }catch (PackageManager.NameNotFoundException ignored) {
    }
    startActivity(new Intent(Intent.ACTION_VIEW, uri));
    
    // Twitter 社区
    String url = "https://twitter.com/Ultimate";
    PackageManager pm = this.getPackageManager();
    Uri uri = Uri.parse(url);
    
    startActivity(new Intent(Intent.ACTION_VIEW, uri));
    
    // Café 社区
    String url = "https://cafe.naver.com/UltimateSchool";
    PackageManager pm = this.getPackageManager();
    Uri uri = Uri.parse(url);
    
    try {
        ApplicationInfo applicationInfo = pm.getApplicationInfo("com.nhn.android.navercafe", 0);
        if (applicationInfo.enabled) {
            uri = Uri.parse("navercafe://cafe?cafeUrl=" + Uri.encode("UltimateSchool"));
        }
    }catch (PackageManager.NameNotFoundException ignored) {
    }
    startActivity(new Intent(Intent.ACTION_VIEW, uri));
    
    						

    url参数 由平台提供

    多语言

    设置SDK 语言

    设置SDK的系统语言(多语言游戏适用)
    		
    PnSDK.setLanguage("语言字符");
    		
    	
    tw 繁体 cn 简体 en 英语 de 德语
    jp 日语 ko 韩语 th 泰国 id 印尼语
    tr 土耳其语 es 西班牙语 ru 俄语 fr 法语
    pt 葡萄牙语 vi 越南语

    越南地区

    依赖项配置

    应用级build.gradle中额外添加依赖项:

            
    dependencies {
        // ...
        implementation 'com.pn:pnsdk:+'
        implementation 'com.pn.pnsdk:res-[APPID]:+'
        //其中[APPID]请替换为实际参数,参数可以在提供的对接说明文档中找到
    
        //仅限越南地区:
        implementation 'com.pn:vn-float-window:+'
        
    }
       
    

    激励广告

    配置

    1. 添加依赖项的仓库地址配置:

                  
      				
        //...
        repositories {
          //...
      
          maven {//PnSDK
              allowInsecureProtocol = true
              url 'http://git-maven.17995api.net:8888/api/v4/projects/112/packages/maven'
          }
      
      
          maven { 
          	url "https://android-sdk.is.com" 
          } 
          maven { 
          	url "https://dl-maven-android.mintegral.com/repository/mbridge_android_sdk_oversea" 
          }
          maven {
          	url "https://artifact.bytedance.com/repository/pangle" 
          }
           
        }
      
      			
      
      			
    2. 应用级build.gradle中添加依赖项:

              
      dependencies {
          // ...
          implementation 'com.pn:pnsdk:+'
          implementation 'com.pn.pnsdk:res-[APPID]:+'
          //其中[APPID]请替换为实际参数,参数可以在提供的对接说明文档中找到
      
          implementation ('com.pn:pn-applovin:+'){
              exclude module: 'protobuf-kotlin-lite'
          }
          
      }
         
      

    显示广告

                
    				
    extraMap 扩展参数,传入null 
    
    PnSDK.showAdWithExtra(activity , null);
    				
    
    			

    显示广告的Debug页面(只用于测试)

                
    				
    Map map = new HashMap();
    map.put("debug" , true);
                
    PnSDK.showAdWithExtra(activity , map);
    				
    
    			

    广告状态监听

    		
    

    <实现广播接收器>

    public class MyBroadcastReceiver extends BroadcastReceiver { final String TAG = "MyBroadcastReceiver"; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (TextUtils.equals(action, "PnSDKAdRewardsNotification")) { Log.v(TAG, "游戏接收到广播:" + "PnSDKAdRewardsNotification"); String status = intent.getStringExtra("status"); Log.d(TAG , "广告状态:" + status); if(TextUtils.equals(status , "cancel")){ Log.d(TAG , "无广告,取消显示"); return; } if(TextUtils.equals(status , "start")){ Log.d(TAG , "广告显示开始"); return; } if(TextUtils.equals(status , "complete")){ Log.d(TAG , "广告显示结束"); return; } if(TextUtils.equals(status , "reward")){ Log.d(TAG , "玩家已观看广告,需发送广告奖励"); String label = intent.getStringExtra("label"); int amount = intent.getIntExtra("amount" , -1); Log.d(TAG , "label: " + label + " amount: " + amount ); //todo 发放奖励 } } } }

    常见问题

    compileSdkVersion从33升级到34问题

    报错:One of RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified when a receiver isn't being registered exclusively for system broadcasts
    原因:以 Android 14(API级别34)或更高版本为目标平台并使用上下文注册的接收器的应用和服务必须指定一个标志,以指明接收器是否监听其他应用的广播: 分别为 RECEIVER_EXPORTED(接收所有系统广播,包括本应用的广播) 或 RECEIVER_NOT_EXPORTED(只接收本应用的广播)。
    解决:
        
    参考代码:
    
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction("XXXXXX");
    
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        //Android14(34)及以上必须添加标记
        registerReceiver(broadcastReceiver, intentFilter , Context.RECEIVER_NOT_EXPORTED);
    }else {
        registerReceiver(broadcastReceiver, intentFilter);
    }
    
    
        
    

    废弃拷贝aar包的接入方式

    由于之前拷贝aar的接入方式不利于sdk以及依赖库的及时更新,容易对开发者造成误解,所以我们废弃了拷贝aar的接入方式,改用线上远程库的对接方式。 对于依旧使用拷贝aar方式的需要更新至使用远程库的对接方式。 修改方法:
    1. 删除libs目录下所有的sdk提供的aar包。
    2. 删除build.gradle中所有sdk相关的依赖项配置。
    3. 删除应用级build.gradle中配置的apply plugin: 'com.google.gms.google-services'
    4. 删除应用级模块中的google-services.json文件。
    5. 参考PnSDK的远程依赖的配置完成使用远程库的对接方式。

    充值项价格查询失败?

    • 首先平台需要先配置好充值项
    • 必须使用真机测试,模拟器取不到价格,也不可测试充值
    • 必须翻墙!!!
    • APK包名必须正确,而且正确签名

    登录通行证账号出现问题?

    • 显示 游戏错误, 平台没有开放登录,请与平台运营联系
    • 使用 Facebook 方式登录时,Facebook App显示应用处于开发模式。请使用平台提供的Facebook 测试账号登录,或者请与平台运营联系
    • 使用 Facebook 方式登录时,登录成功跳回游戏后,SDK界面没有继续操作。系统回调 部分编码没正确完成.

    怎样测试Facebook分享和邀请?

    • 手机网络必须翻墙
    • APK包必须正确签名
    • Facebook 账号必须是Facebook App的测试人员账号,邀请的好友也必须是测试人员。
    • 邀请信息只能在手机Facebook App中看到.

    更新说明

    SDK在2025-06-16日支持targetSdk 35,游戏在升级targetSdk时需要注意编译工具的版本问题:

    官方说明: Android 15 SDK 包含与某些较低版本的 Android Studio 不兼容的变更。 为了获得 Android 15 SDK 的最佳开发体验,请使用 Android Studio Koala 功能更新 | 2024.1.2 或更高版本。 并要求Android Gradle插件版本为8.6.1 或更高版本。
    为了更好的版本兼容,推荐使用以下版本:
                                
    1、推荐Android Studio Meerkat | 2024.3.1 (支持Gradle plugin8.8.2)  
    
    
    2、升级Gradle Plugin至8.8.2
    3、升级Gradle 至8.10.2 (gradle-8.10.2-all)
    (您可以在 Android Studio 的 File > Project Structure > Project 菜单中查看或指定插件版本)
    
    
    4、SDK Build Tools 35.0.0
    5、JDK 17
    6、miniSDK Version 24
    7、compile Version 35 
    
                                
                            

    历史更新:

    SDK在2025-03-18日发布了比较大的更新(Google登录方式),在这个日期之后重新做包会发生一些常见的编译错误:

            

    问题1:Failed to transform error_prone_annotations-2.36.0.jar....

    解决方法:

    1、升级gradle插件至8.1.1 2、升级gradle至8.0
        

    问题2:DuplicateRelativeFileException: 2 files found with path 'META-INF/INDEX.LIST'....

    解决方法:

    application级别的build.gradle上增加配置: android { ... packagingOptions { exclude 'META-INF/INDEX.LIST' exclude 'META-INF/DEPENDENCIES' } }