教你快速高效接入SDK——渠道SDK的接入(就是實現抽象層的介面而已),sdk渠道
題記:很多做遊戲開發的人,估計都或多或少地接過渠道SDK,什麼UC,當樂,91,小米,360......據統計國內市場當前不下於100家渠道,還包括一些沒有SDK的小渠道。每個渠道SDK接入的方法呢,多是大同小異。但是,正是這些小異,又讓SDK的接入,產生了無窮無盡的變數。所以,接入SDK之前,如果你沒有經驗,或者沒有被SDK坑過,那麼當你看到這系列文章的時候,你很幸運,你可以避免這一切了。如果你之前被坑過,而且還在繼續被坑著,那麼現在,就是你解脫的時刻。
先將之前的每一篇做個索引,方便親們查閱:
記小黑——統一SDK接入架構(類似稜鏡和AnySDK)
教你快速高效接入SDK——總體思路和架構
教你快速高效接入SDK——SDK接入抽象層的設計
教你快速高效接入SDK——遊戲接入SDK(只接入抽象架構)
上一篇我們展示了遊戲在需要接入SDK時的調用。它僅僅調用抽象層提供的各個外掛程式的單例封裝類就可以了。而每個單例封裝類裡面,就是引用對應的外掛程式介面。那麼這個介面的執行個體化是怎麼做了,上上一篇,我們說到,他是從asssets下面讀取的配置。然後根據配置裡填寫的完整類名來執行個體化的。這個實作類別就是在接入各個渠道的時候實現的。那麼,本篇我們就來以UC渠道為例,將其接入到我們的U8 SDK中來。
首先,根據UC提供的文檔,我們知道,需要接入登陸和支付兩大功能。那麼,對應的,在我們這裡,我們就需要兩個類,一個類實現抽象SDK的IUser介面,一個實現抽象SDK的IPay介面。那麼,我們這裡再回顧下,實作類別需要注意的地方。在抽象層講解的文章中,我們看各個外掛程式的執行個體化過程:
public Object initComponent(int type){Class localClass = null;try {if(!isSupportComponent(type)){Log.e("U8SDK", "The config of the U8SDK is not support plugin type:"+type);return null;}String name = getComponentName(type);localClass = Class.forName(name);} catch (ClassNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();return null;}try {return localClass.getDeclaredConstructor(new Class[]{Activity.class}).newInstance(new Object[]{U8SDK.getInstance().getContext()});} catch (Exception e) {e.printStackTrace();}return null;}
我們可以看到,我們是調用實作類別的帶有一個Activity參數的建構函式進行執行個體化的。這就要求我們在實現外掛程式介面的時候,需要定義一個帶有Activity參數的建構函式。我們看看UC的登陸實作類別和支付實作類別。
package com.u8.sdk;import com.u8.sdk.IUser;import com.u8.sdk.U8SDK;import android.app.Activity;public class UCUser implements IUser {private Activity context;public UCUser(Activity context){this.context = context;this.initSDK();}public void initSDK(){UCSDK.getInstance().initSDK(this.context, U8SDK.getInstance().getSDKParams());}@Overridepublic void login() {UCSDK.getInstance().login(this.context, U8SDK.getInstance().getSDKParams());}}
支付:
package com.u8.sdk;import com.u8.sdk.IPay;import com.u8.sdk.PayParams;import android.app.Activity;public class UCPay implements IPay {private Activity context;public UCPay(Activity context){this.context = context;}@Overridepublic void pay(PayParams data) {UCSDK.getInstance().pay(this.context, data);}}
我們先看上面的登陸外掛程式實作類別,可以看到它實現了login介面,同時,定義了一個以Activity為參數的建構函式。在login方法裡面,通過調用UCSDK這個單例類的login來完成登陸介面的調用。同時,注意到,登陸實作類別裡面還有一個initSDK方法,在執行個體化的時候調用。這個是因為UC SDK要求必須在遊戲剛開始啟動並執行時候,就初始化SDK。
同樣的,支付實作類別裡面,實現了pay方法,也是通過調用UCSDK單例來完成支付介面的調用。
那麼,大家看到,現在所有的問題都簡單化了。就是需要在UCSDK這個單例類裡面來實現所有UC SDK需要實現的功能。我們先看下UCSDK裡面的代碼:
package com.u8.sdk;import org.json.JSONException;import org.json.JSONObject;import com.u8.sdk.ActivityCallbackAdapter;import com.u8.sdk.LoginResult;import com.u8.sdk.PayParams;import com.u8.sdk.SDKConfigData;import com.u8.sdk.U8Code;import com.u8.sdk.U8SDK;import com.u8.sdk.utils.SDKTools;import android.app.Activity;import android.app.ProgressDialog;import android.content.DialogInterface;import android.util.Log;import cn.uc.gamesdk.UCCallbackListener;import cn.uc.gamesdk.UCCallbackListenerNullException;import cn.uc.gamesdk.UCFloatButtonCreateException;import cn.uc.gamesdk.UCGameSDK;import cn.uc.gamesdk.UCGameSDKStatusCode;import cn.uc.gamesdk.UCLogLevel;import cn.uc.gamesdk.UCLoginFaceType;import cn.uc.gamesdk.UCOrientation;import cn.uc.gamesdk.info.FeatureSwitch;import cn.uc.gamesdk.info.GameParamInfo;import cn.uc.gamesdk.info.OrderInfo;import cn.uc.gamesdk.info.PaymentInfo;public class UCSDK {enum SDKState{StateDefault,StateIniting,StateInited,StateLogin,StateLogined}private SDKState state = SDKState.StateDefault;private boolean loginAfterInit = false;private ProgressDialog loadingActivity = null;private static UCSDK instance;private UCLogLevel logLevel = UCLogLevel.DEBUG;private int cpId;private int gameId;private int channel;private boolean debugMode = true;private UCSDK(){}public static UCSDK getInstance(){if(instance == null){instance = new UCSDK();}return instance;}private void parseSDKParams(SDKConfigData params) {this.gameId = params.getInt("UCGameId");this.cpId = params.getInt("UCCpId");this.channel = U8SDK.getInstance().getCurrChannel();if(this.channel <= 0){this.channel = params.getInt("UCChannel");}this.debugMode = params.getBoolean("UCDebugMode");}public void initSDK(Activity context, SDKConfigData params){this.parseSDKParams(params);this.initSDK(context);}public void login(Activity context, SDKConfigData params){this.parseSDKParams(params);this.login(context);}/** * 必接功能<br> * sdk初始化功能<br> */public void initSDK(final Activity context) {this.state = SDKState.StateIniting;try {showWaitDialog(context);U8SDK.getInstance().setActivityCallback(new ActivityCallbackAdapter(){@Overridepublic void onBackPressed() {ucSdkExit(context);}@Overridepublic void onDestroy() {ucSdkDestoryFloatButton(context);}});if(loginAfterInit){//showProgressDialog(context);}GameParamInfo gpi = new GameParamInfo();// 下面的值僅供參考gpi.setCpId(this.cpId);gpi.setGameId(this.gameId);gpi.setServerId(0); // 伺服器ID可根據遊戲自身定義設定,或傳入0// 在九遊社區設定顯示查詢儲值曆史和顯示切換帳號按鈕,// 在不設定的情況下,預設情況情況下,生產環境顯示查詢儲值記錄按鈕,不顯示切換賬戶按鈕// 測試環境設定無效gpi.setFeatureSwitch(new FeatureSwitch(true, false));// 設定SDK登入介面為橫屏,個人中心及儲值頁面預設為強制豎屏,無法修改// UCGameSDK.defaultSDK().setOrientation(UCOrientation.LANDSCAPE);// 設定SDK登入介面為豎屏UCGameSDK.defaultSDK().setOrientation(UCOrientation.LANDSCAPE);// 設定登入介面:// USE_WIDGET - 簡版登入介面// USE_STANDARD - 標準版登入介面UCGameSDK.defaultSDK().setLoginUISwitch(UCLoginFaceType.USE_WIDGET);// setUIStyle已淘汰,不需調用。// UCGameSDK.defaultSDK().setUIStyle(UCUIStyle.STANDARD);UCGameSDK.defaultSDK().initSDK(context, this.logLevel,this.debugMode, gpi,new UCCallbackListener<String>() {@Overridepublic void callback(int code, String msg) {Log.e("UCGameSDK", "UCGameSDK初始化介面返回資料 msg:" + msg+ ",code:" + code + ",debug:"+ debugMode + "\n");hideWaitDialog(context);if(code == UCGameSDKStatusCode.SUCCESS){if(state != SDKState.StateIniting){U8SDK.getInstance().onResult(U8Code.CODE_INIT_FAIL, "uc sdk init fail. not initing. curr state is:"+state);return;}state = SDKState.StateInited;U8SDK.getInstance().onResult(U8Code.CODE_INIT_SUCCESS, "uc sdk init success");if(loginAfterInit){login(context);}}else{hideWaitDialog(context);state = SDKState.StateDefault;U8SDK.getInstance().onResult(U8Code.CODE_INIT_FAIL, "uc sdk init fail.err:"+msg);}}});} catch (UCCallbackListenerNullException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}private LoginResult encodeLoginResult(String sid){String channel = "" + getChannel();String expansion = "";JSONObject ext = new JSONObject();try {ext.put("ext", "u8");expansion = ext.toString();} catch (JSONException e) {e.printStackTrace();}return new LoginResult(sid, channel, expansion);}public void login(final Activity context){if(state.ordinal() < SDKState.StateInited.ordinal()){loginAfterInit = true;initSDK(context);return;}if(!SDKTools.isNetworkAvailable(context)){U8SDK.getInstance().onResult(U8Code.CODE_NO_NETWORK, "The network now is unavailable");return;}try {state = SDKState.StateLogin;UCGameSDK.defaultSDK().login(context, new UCCallbackListener<String>() {@Overridepublic void callback(int code, String msg) {Log.e("UCGameSDK", "UCGameSdk登入介面返回資料:code=" + code+ ",msg=" + msg);// 登入成功。此時可以擷取sid。並使用sid進行遊戲的登入邏輯。// 用戶端無法直接擷取UCIDif (code == UCGameSDKStatusCode.SUCCESS) {state = SDKState.StateLogined;String sid = UCGameSDK.defaultSDK().getSid();U8SDK.getInstance().onResult(U8Code.CODE_LOGIN_SUCCESS, sid);LoginResult result = encodeLoginResult(sid);U8SDK.getInstance().onLoginResult(result);// 執行懸浮按鈕建立方法ucSdkCreateFloatButton(context);// 執行懸浮按鈕顯示方法ucSdkShowFloatButton(context);}else{state = SDKState.StateInited;U8SDK.getInstance().onResult(U8Code.CODE_LOGIN_FAIL, msg);}// 登入失敗。應該先執行初始化成功後再進行登入調用。if (code == UCGameSDKStatusCode.NO_INIT) {// 沒有初始化就進行登入調用,需要遊戲調用SDK初始化方法initSDK(context);}// 登入退出。該回調會在登入介面退出時執行。if (code == UCGameSDKStatusCode.LOGIN_EXIT) {// 登入介面關閉,遊戲需判斷此時是否已登入成功進行相應操作}}});} catch (UCCallbackListenerNullException e) {e.printStackTrace();}}private PaymentInfo decodePayParams(PayParams payParams){Log.i("UCSDK", "The payParams is "+payParams.toString());PaymentInfo pInfo = new PaymentInfo(); // 建立Payment對象,用於傳遞儲值資訊// 設定儲值自訂參數,此參數不作任何處理,// 在儲值完成後,sdk伺服器通知遊戲伺服器儲值結果時原封不動傳給遊戲伺服器傳值,欄位為服務端回調的callbackInfo欄位if(!SDKTools.isNullOrEmpty(payParams.getExtension())){pInfo.setCustomInfo(payParams.getExtension());}// 非必選參數,可不設定,此參數已廢棄,預設傳入0即可。// 如無法支付,請在開放平台檢查是否已經配置了對應環境的支付回調地址,如無請配置,如有但仍無法支付請聯絡UC技術介面人。pInfo.setServerId(0);pInfo.setRoleId(payParams.getRoleId()); // 設定使用者的遊戲角色的ID,此為必選參數,請根據實際業務資料傳入真實資料pInfo.setRoleName(payParams.getRoleName()); // 設定使用者的遊戲角色名稱字,此為必選參數,請根據實際業務資料傳入真實資料pInfo.setGrade(""+payParams.getRoleLevel()); // 設定使用者的遊戲角色等級,此為選擇性參數// 非必填參數,設定遊戲在支付完成後的遊戲接收訂單結果回調地址,必須為帶有http頭的URL形式。//pInfo.setNotifyUrl("http://192.168.1.1/notifypage.do");// 當傳入一個amount作為金額值進行調用支付功能時,SDK會根據此amount可用的支付方式顯示儲值渠道// 如你傳入6元,則不顯示儲值卡選項,因為市面上暫時沒有6元的儲值卡,建議使用可以顯示儲值卡方式的金額pInfo.setAmount(payParams.getPrice());// 設定儲值金額,此為選擇性參數return pInfo;}public void pay(Activity context, PayParams data){try {if(!isInited()){U8SDK.getInstance().onResult(U8Code.CODE_INIT_FAIL, "The sdk is not inited.");return;}if(!SDKTools.isNetworkAvailable(context)){U8SDK.getInstance().onResult(U8Code.CODE_NO_NETWORK, "The network now is unavailable");return;}PaymentInfo pInfo = decodePayParams(data);UCGameSDK.defaultSDK().pay(context, pInfo,new UCCallbackListener<OrderInfo>() {@Overridepublic void callback(int code, OrderInfo orderInfo) {if (code == UCGameSDKStatusCode.NO_INIT) {// 沒有初始化就進行登入調用,需要遊戲調用SDK初始化方法U8SDK.getInstance().onResult(U8Code.CODE_INIT_FAIL, "The SDK is not inited");}if (code == UCGameSDKStatusCode.SUCCESS) {// 成功儲值if (orderInfo != null) {String ordereId = orderInfo.getOrderId();// 擷取訂單號float orderAmount = orderInfo.getOrderAmount();// 擷取訂單金額int payWay = orderInfo.getPayWay();String payWayName = orderInfo.getPayWayName();System.out.print(ordereId + "," + orderAmount + ","+ payWay + "," + payWayName);U8SDK.getInstance().onResult(U8Code.CODE_PAY_SUCCESS, "uc pay success.");}}else{U8SDK.getInstance().onResult(U8Code.CODE_PAY_FAIL, "uc pay failed.");}if (code == UCGameSDKStatusCode.PAY_USER_EXIT) {// 使用者退出儲值介面。U8SDK.getInstance().onResult(U8Code.CODE_PAY_FAIL, "The user is exit.");}}});} catch (Exception e) {e.printStackTrace();}}/** * 必接功能<br> * 懸浮按鈕建立及顯示<br> * 懸浮按鈕必須保證在SDK進行初始化成功之後再進行建立需要在UI線程中調用<br> */private void ucSdkCreateFloatButton(final Activity context) {U8SDK.getInstance().runOnMainThread(new Runnable() {public void run() {try {// 建立懸浮按鈕。該懸浮按鈕將懸浮顯示在GameActivity頁面上,點擊時將會展開懸浮菜單,菜單中含有// SDK 一些功能的操作入口。// 建立完成後,並不自動顯示,需要調用showFloatButton(Activity,// double, double, boolean)方法進行顯示或隱藏。UCGameSDK.defaultSDK().createFloatButton(context,new UCCallbackListener<String>() {@Overridepublic void callback(int statuscode, String data) {Log.d("SelectServerActivity`floatButton Callback","statusCode == " + statuscode+ " data == " + data);}});} catch (UCCallbackListenerNullException e) {e.printStackTrace();} catch (UCFloatButtonCreateException e) {// TODO Auto-generated catch blocke.printStackTrace();}}});}/** * 必接功能<br> * 懸浮按鈕顯示<br> * 懸浮按鈕顯示需要在UI線程中調用<br> */private void ucSdkShowFloatButton(final Activity context) {U8SDK.getInstance().runOnMainThread(new Runnable() {public void run() {// 顯示懸浮表徵圖,遊戲可在某些情境選擇隱藏此表徵圖,避免影響遊戲體驗try {UCGameSDK.defaultSDK().showFloatButton(context, 100, 50, true);} catch (UCCallbackListenerNullException e) {// TODO Auto-generated catch blocke.printStackTrace();}}});}/** * 必接功能<br> * 懸浮按鈕銷毀<br> * 懸浮按鈕銷毀需要在UI線程中調用<br> */private void ucSdkDestoryFloatButton(final Activity context) {U8SDK.getInstance().runOnMainThread(new Runnable() {public void run() {// 懸浮按鈕銷毀功能UCGameSDK.defaultSDK().destoryFloatButton(context);}});}/** * 必接功能<br> * 當遊戲退出前必須調用該方法,進行清理工作。建議在遊戲退出事件中進行調用,必須在遊戲退出前執行<br> * 如果遊戲直接退出,而不調用該方法,可能會出現未知錯誤,導致程式崩潰<br> */private void ucSdkExit(final Activity context) {UCGameSDK.defaultSDK().exitSDK(context, new UCCallbackListener<String>() {@Overridepublic void callback(int code, String msg) {if (UCGameSDKStatusCode.SDK_EXIT_CONTINUE == code) {// 此加入繼續遊戲的代碼} else if (UCGameSDKStatusCode.SDK_EXIT == code) {// 在此加入離開遊戲的代碼ucSdkDestoryFloatButton(context);System.exit(0);}}});}public int getChannel(){return this.channel;}public boolean isInited(){return this.state.ordinal() >= SDKState.StateInited.ordinal();}public boolean isLogined(){return this.state.ordinal() >= SDKState.StateLogined.ordinal();}private void showWaitDialog(Activity context){if(loadingActivity != null){return;} loadingActivity = new ProgressDialog(context); loadingActivity.setIndeterminate(true); loadingActivity.setCancelable(true); loadingActivity.setMessage("正在初始化,請稍後..."); loadingActivity.setOnCancelListener(new DialogInterface.OnCancelListener() {@Overridepublic void onCancel(DialogInterface arg0) {state = SDKState.StateDefault;}}); loadingActivity.show();}private void hideWaitDialog(Activity context){if(loadingActivity == null){return;}loadingActivity.dismiss();loadingActivity = null;}}
這個類似乎有點龐大。但是,沒關係。我們慢慢地剝皮抽絲。首先,從我們實作類別的調用入口進入。首先是initSDK。我們看看initSDK做了些什麼:
initSDK首先進行了參數的解析:看UCUser的initSDK方法裡面,我們通過U8SDK.getInstance().getSDKParams()方法擷取到了當前SDK需要的參數。那麼這些參數是什麼呢。這些參數就是每個SDK在運行時,需要傳入的參數。也就是你在接入渠道SDK之前,向渠道方申請的appID,appKey等資訊。大家這裡可能會有疑問,這些資訊按說各個渠道都是不同的。這裡怎麼能通過抽象層獲得到呢?說的沒錯,抽象層如何得到呢?這個要歸功於我們後面要說的打包工具,我們會通過一個巧妙的設計來完成這一工作。這裡你Crowdsourced Security Testing道,所有渠道的接入需要的appID等資訊,這裡都直接通過U8SDK.getInstance().getSDKParams()方法來擷取就可以了。
緊接著,我們看到initSDK裡面設定了U8SDK的IActivityListener介面。這是因為UC的SDK需要在activity的某些系統事件中完成相應的工作。
接下來,大家可以看到就是按照UC的Demo裡面往下走就可以了。關鍵是初始化好之後的回調裡,不管初始化成功還是失敗,最好調用下U8SDK.getInstance().onResult()方法來向抽象層拋出一個狀態資訊。這樣你在debug調試的時候,在遊戲層實現的介面裡加上輸出或者Toast就可以及時看到這些狀態資訊,方便調試和查錯。
然後,我們看login方法,login方法也一樣,直接調用UC提供的登陸方法,關鍵是在登陸回調中,我們除了調用onResult方法之外,如果登陸成功,我們還需要調用U8SDK.getInstance().onLoginResult(result)方法。這個是因為遊戲層會在onLoginResult中來處理登陸成功的回調,同時需要將SDK返回的資料封裝到LoginResult類中。
最後,我們來看pay方法,pay方法也一樣的簡單。只是多了支付參數的解析。之前在說抽象層的實現時,我們說到了支付參數PayParams。這個類裡所有的資訊是遊戲裡面可以提供的。但是,每個渠道需要的可能各不相同。所以,這裡各個渠道需要根據渠道自身的需要,按需所取。比如這裡,我們通過decodePayParams方法從PayParams裡面取到UC需要的參數。然後,同樣的,我們在回調中調用onResult方法來提示狀態資訊。
對於其他的方法,向什麼隱藏懸浮表徵圖,顯示懸浮表徵圖都在UCSDK這裡接入就可以了。需要在對應的地方加以調用。
那麼,到這裡我們UC SDK就算接入完畢了。接好之後他的目錄結構大致如下:
這裡大家也許看到了工程裡面,有sdk_confgi.xml和sdk_manifest.xml。這裡,我們先留個懸念。後面我們說到打包工具的時候,在回頭來說。因為這裡大家可以看到,我們沒有提SDK需要在AndroidManifest.xml中設定的許可權資訊和一些Activity或者Service等資料。也不知道這樣接好了之後,然後怎麼用呢,怎麼測試,怎麼維護呢?所有這些我們後面來說。
本文作者:小黑