There are many app/game market operators, such as UC, XiaoMi, Huawei, etc. These operators are also called channels. All of them have different specifications, and those specifications are always evoluting. As a result, it's very difficult to integrate your application with so many channels and keep them updated. To liberate you from this unimaginable task, SG Studios provides a solution by abstracting the channels' client SDKs and server interfaces into thin layers. With this solution you do not need care about details in various SDKs any more. Instead what you need to to is only integrating with SGUtil software package and send your APKs to SG Studios. SG Studios will then re-package them with channel SDKs you need to generate new APKs. Thus your workload get dramatically decreased. For the server side, what you need to do is to implement a simple game server (you may use SG Studios's server if you do not need complex features) that manages app/game properties and orders.
You need to register your application at channel sites and SG Studios site in order to utlize SG Studios's service. You should share the information generated by channel sites with SG Studios.
This page illustrates how to integrate with SGUtil. Please refer to SGUtil Document for details.
SGUtil requires Android version not lower than 4.4 (KitKat, API Level 19) to run. We also recommend you use Android Studios as your main development environment.
package com.sg.sgutiltest; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.TimeZone; import android.graphics.Color; import android.os.Bundle; import android.os.Handler; import android.text.Layout; import android.text.TextUtils; import android.text.method.ScrollingMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ListView; import android.widget.TextView; import com.sg.util.SGActivity; import com.sg.util.SGAgent; import com.sg.util.SGGameServerDemo; import com.sg.util.UOrder; import com.sg.util.UProduct; import static android.view.View.TEXT_ALIGNMENT_CENTER; /** * SGTutorialActivity is a main activity that extends SGActivity to utilize its * lifecycle callback forwarding. SGActivity has a protected field 'agent', which * is an instance of SGAgent. * * The activity has following content view layout. * * +-----------------------------------+ * | | * | product list view | * | | * +-----------------------------------+ * | btnUsr | btnJob | btnPay | btnExt | * +-----------------------------------+ * | | * | log view | * | | * +-----------------------------------+ */ public class SGTutorialActivity extends SGActivity { private class XProduct { UProduct product; String orderID; String state; } private class XOrder { String orderID; String productID; String productName; int buyNum; String state; String time; } private Handler mHandler = new Handler(); // an list adapter to manage products private ProductListAdapter mProductListAdapter; // an list adapter to manage orders private OrderListAdapter mOrderListAdapter; // button for login/logout private Button mBtnUsr; // button for get product list private Button mBtnJob; // button for purchasing product private Button mBtnPay; // button for exit program private Button mBtnExt; // enable/disable buttons, change button captions according to latest states private void updateButtons() { int usrState = agent.getUsrState(); int payState = agent.getPayState(); int jobState = agent.getJobState(); /* mBtnUsr */ if (usrState < SGAgent.USRSTATE_LOGINEDGAME) { setButtonText(mBtnUsr, R.string.btn_main_login); } else { setButtonText(mBtnUsr, R.string.btn_main_logout); } if ((usrState == SGAgent.USRSTATE_INITIALIZED || usrState == SGAgent.USRSTATE_LOGINEDGAME) && jobState == SGAgent.JOBSTATE_IDLE && payState == SGAgent.PAYSTATE_IDLE && (usrState < SGAgent.USRSTATE_LOGINEDGAME || agent.isLogoutSupported())) mBtnUsr.setEnabled(true); else mBtnUsr.setEnabled(false); boolean idle = usrState == SGAgent.USRSTATE_LOGINEDGAME && jobState == SGAgent.JOBSTATE_IDLE && payState == SGAgent.PAYSTATE_IDLE; /* mBtnJob */ if (mProductListAdapter.getCount() == 0) { setButtonText(mBtnJob, R.string.btn_main_products); mBtnJob.setEnabled(idle); } else { setButtonText(mBtnJob, R.string.btn_main_orders); mBtnJob.setEnabled(idle); } /* mBtnPay */ XProduct p = mProductListAdapter.getHighlightProduct(); String state = p == null ? "" : p.state; if (state.equals("")) { setButtonText(mBtnPay, R.string.btn_main_oops); } else if (state.equals("delivered") || state.equals("succeeded") || state.equals("cheated")) { setButtonText(mBtnPay, R.string.btn_main_info); } else if (state.equals("accepted")) { setButtonText(mBtnPay, R.string.btn_main_check); } else { setButtonText(mBtnPay, R.string.btn_main_buy); } mBtnPay.setEnabled(idle && !state.equals("")); /* mBtnExt */ setButtonText(mBtnExt, R.string.btn_main_exit); mBtnExt.setEnabled(idle || usrState <= SGAgent.USRSTATE_INITIALIZED); } // internal class to hold product information and views private static class ProductViewHolder { TextView name; // TextView to show product name TextView price; // TextView to product price TextView value; // TextView to product value TextView state; // TextView to product state XProduct productRef; // product information } // list adapter to manage products private class ProductListAdapter extends BaseAdapter { private LayoutInflater mInflator; private List<XProduct> mList; private XProduct mHighlightProduct; // highlighted product (current, active) public ProductListAdapter() { super(); // initialize list to empty mInflator = SGTutorialActivity.this.getLayoutInflater(); mList = new ArrayList<XProduct>(); mHighlightProduct = null; } public boolean addProduct(XProduct product) { if (!mList.contains(product)) { mList.add(product); if (mHighlightProduct == null) mHighlightProduct = product; return true; } return false; } public void changeProductState(String productID, String orderID, String state) { for (int i=0; i<mList.size(); i++) { XProduct xp = (XProduct)mList.get(i); if (!xp.product.getProductID().equals(productID)) continue; if (xp.orderID != null && !xp.orderID.equals(orderID)) continue; xp.orderID = orderID; xp.state = state; notifyDataSetChanged(); break; } } public void clear() { mList.clear(); mHighlightProduct = null; } public void setHighlightProduct(XProduct product) { if (mHighlightProduct != product) { mHighlightProduct = product; } } public XProduct getHighlightProduct() { return mHighlightProduct; } @Override public int getCount() { return mList.size(); } @Override public long getItemId(int i) { return i; } @Override public Object getItem(int i) { return mList.get(i); } private void setTextViewHighlight(TextView ... views) { int n = views.length; for (int i=0; i<n; i++) { views[i].setBackgroundColor(Color.parseColor("#c0c080")); views[i].setTextColor(Color.parseColor("#ffffff")); } } private void setTextViewNormal(TextView ... views) { int n = views.length; for (int i=0; i<n; i++) { views[i].setBackgroundColor(Color.parseColor("#70b040")); views[i].setTextColor(Color.parseColor("#000000")); } } @Override public View getView(int idx, View view, ViewGroup viewGroup) { ProductViewHolder vh; if (view == null) { view = mInflator.inflate(R.layout.product_list_item, null); vh = new ProductViewHolder(); vh.name = (TextView)view.findViewById(R.id.product_list_item_name); vh.price = (TextView)view.findViewById(R.id.product_list_item_price); vh.value = (TextView)view.findViewById(R.id.product_list_item_value); vh.state = (TextView)view.findViewById(R.id.product_list_item_state); view.setTag(vh); } else { vh = (ProductViewHolder)view.getTag(); } vh.productRef = mList.get(idx); vh.name.setText(vh.productRef.product.getProductName()); vh.price.setText("" + vh.productRef.product.getPrice()); vh.value.setText("" + vh.productRef.product.getCoins()); vh.state.setText("" + vh.productRef.state); if (vh.productRef == mHighlightProduct) { setTextViewHighlight(vh.name, vh.price, vh.value, vh.state); } else { setTextViewNormal(vh.name, vh.price, vh.value, vh.state); } return view; } } // list adapter to manage orders private class OrderListAdapter extends BaseAdapter { private List<XOrder> mList; public OrderListAdapter() { super(); mList = new ArrayList<XOrder>(); } public boolean addOrder(XOrder order) { mList.add(order); return true; } public void clear() { mList.clear(); } @Override public int getCount() { return mList.size(); } @Override public long getItemId(int i) { return i; } @Override public Object getItem(int i) { return mList.get(i); } @Override public View getView(int idx, View view, ViewGroup viewGroup) { return null; } } private void enableButtons(Button ... btns) { for (Button btn : btns) { btn.setEnabled(true); } } private void disableButtons(Button ... btns) { for (Button btn : btns) { btn.setEnabled(false); } } private void setTextViewText(TextView tv, int resid) { tv.setText(resid); tv.setTextAlignment(TEXT_ALIGNMENT_CENTER); } private void setButtonText(Button btn, int resid) { btn.setTag(resid); btn.setText(resid); } /** * Initialize UI of parent class SGActivity. This method should be called before agent.onCreate is called. */ private void initializeUI() { setContentView(R.layout.main_activity); ((TextView)findViewById(R.id.main_logs)).setMovementMethod(new ScrollingMovementMethod()); // find product list view ListView v = (ListView)findViewById(R.id.main_product_list); // create a header bar LayoutInflater inflater = getLayoutInflater(); ViewGroup header = (ViewGroup)inflater.inflate(R.layout.product_list_item, v, false); header.setBackgroundColor(Color.parseColor("#f0c080")); // set names of columns shown in header bar setTextViewText((TextView)header.findViewById(R.id.product_list_item_name), R.string.product_list_item_name_cap); setTextViewText((TextView)header.findViewById(R.id.product_list_item_price), R.string.product_list_item_price_cap); setTextViewText((TextView)header.findViewById(R.id.product_list_item_value), R.string.product_list_item_value_cap); setTextViewText((TextView)header.findViewById(R.id.product_list_item_state), R.string.product_list_item_state_cap); // add the header bar to the top of product list v.addHeaderView(header, null, false); // create list adapter mProductListAdapter = new ProductListAdapter(); v.setAdapter(mProductListAdapter); // set up click handler v.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { // if agent is idle, highlight clicked product and update buttons according to its state int usrState = agent.getUsrState(); int payState = agent.getPayState(); int jobState = agent.getJobState(); boolean idle = usrState == SGAgent.USRSTATE_LOGINEDGAME && jobState == SGAgent.JOBSTATE_IDLE && payState == SGAgent.PAYSTATE_IDLE; if (idle) { ProductViewHolder vh = (ProductViewHolder) view.getTag(); if (vh.productRef != mProductListAdapter.getHighlightProduct()) { // highlight product if not highlighted mProductListAdapter.setHighlightProduct(vh.productRef); // and show in highlighted color mProductListAdapter.notifyDataSetChanged(); // and update buttons updateButtons(); } } } }); mBtnUsr = (Button)findViewById(R.id.main_btn_usr); mBtnJob = (Button)findViewById(R.id.main_btn_job); mBtnPay = (Button)findViewById(R.id.main_btn_pay); mBtnExt = (Button)findViewById(R.id.main_btn_exit); // disable job button and purchase button as user is not logined yet disableButtons(mBtnJob, mBtnPay); // initialize button captions and set up click handler setButtonText(mBtnUsr, R.string.btn_main_login); mBtnUsr.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int tag = (int)v.getTag(); if (tag == R.string.btn_main_login) { agent.login(); } else if (tag == R.string.btn_main_logout) { agent.logout(); } } }); setButtonText(mBtnJob, R.string.btn_main_products); mBtnJob.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int tag = (int)v.getTag(); if (tag == R.string.btn_main_products) agent.getProducts(null, true); else if (tag == R.string.btn_main_orders) agent.getOrderInfoList(null); } }); setButtonText(mBtnPay, R.string.btn_main_buy); mBtnPay.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int tag = (int)v.getTag(); if (tag == R.string.btn_main_buy) { agent.buyProduct(mProductListAdapter.getHighlightProduct().product.getProductID(), 1, "coin"); } else if (tag == R.string.btn_main_check) { agent.getOrderState(mProductListAdapter.getHighlightProduct().orderID); } else if (tag == R.string.btn_main_info) { dumpProductInfo(); } else { ; } } }); mBtnExt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { agent.exit(); } }); } private void dumpProductInfo() { XProduct xp = mProductListAdapter.getHighlightProduct(); onLog("productInfo = {\n"); onLog(" productID: " + xp.product.getProductID(), ",\n"); onLog(" productName: " + xp.product.getProductName(), ",\n"); onLog(" productDesc: " + xp.product.getProductDesc(), ",\n"); onLog(" roleID: " + xp.product.getRoleID(), ",\n"); onLog(" roleName: " + xp.product.getRoleName(), ",\n"); onLog(" roleLevel: " + xp.product.getRoleLevel(), ",\n"); onLog(" serverID: " + xp.product.getServerID(), ",\n"); onLog(" price: " + xp.product.getPrice(), ",\n"); onLog(" coins: " + xp.product.getCoins(), ",\n"); onLog(" appID: " + xp.product.getAppID(), ",\n"); onLog(" appName: " + xp.product.getAppName(), ",\n"); onLog(" orderID: " + xp.orderID, ",\n"); onLog(" state: " + xp.state, "\n"); onLog("}\n"); } /** * Override onPreInitialization to do something before initialization (SGAgent.onCreate). */ @Override public void onPreInitialization(Bundle savedInstanceState) { initializeUI(); } /** * Override onPostInitialization to do more jobs than SGActivity. */ @Override public void onPostInitialization(Bundle savedInstanceState) { // Set order state checking intervals to null to disable auto state query after payment UI // operation is completed. That is, you need to click purchase button (caption changed to // "Check" in this case) to query the order state manually. agent.setOrderStateCheckingIntervals(null); // You need to set instance of your own game server - which implements SGGameServerInterface. // class YourGameServer implements SGGameServerInterface { // } // agent.setGameServer(new YourGameServer(), params); // A game server instance must be set otherwise the login process does not start. agent.setGameServer(new SGGameServerDemo(), null); } // SGUtil may print visual logs via this method. @Override public void onLog(String text) { TextView logView = (TextView)findViewById(R.id.main_logs); if (logView != null) { if (!text.endsWith("\n")) { text += "\n"; } logView.append(text); Layout layout = logView.getLayout(); if (layout != null) { int n = logView.getLineCount(); if (n > 0) { int bottom = layout.getLineBottom(n - 1); int top = logView.getScrollY(); int height = logView.getHeight(); int delta = (bottom - top) - height; if (delta > 0) logView.scrollBy(0, delta); } } } Log.d("SGUtil", text); } // SGUtil may print visual logs via this method. @Override public void onLog(String text, String postfix) { onLog(text + (postfix != null ? postfix : "")); } @Override public boolean onProductBegin(int num) { // clear product list view mProductListAdapter.clear(); // return true to let SGUtil notify us product information right now return true; } @Override public void onProductFound(UProduct product) { // add new product XProduct xp = new XProduct(); xp.product = product; xp.orderID = null; xp.state = "buyable"; mProductListAdapter.addProduct(xp); } @Override public void onProductEnd() { // all products notified, redraw product list view mProductListAdapter.notifyDataSetChanged(); } @Override public boolean onOrderInfoBegin(int num) { // clear order list view mOrderListAdapter.clear(); onLog("orderInfos = {\n"); return true; } @Override public void onOrderInfoFound(UOrder order) { // add new order XOrder xo = new XOrder(); xo.orderID = order.getOrderTicket().getOrderID(); xo.productID = order.getProduct().getProductID(); xo.productName = order.getProduct().getProductName(); xo.buyNum = order.getPayParams().getBuyNum(); xo.state = order.getState(); long t = xo.state.equals("delivered") ? order.getDeliveredTime() : (xo.state.equals("succeeded") || xo.state.equals("failed") || xo.state.equals("cheated") ? order.getCompletedTime() : order.getGeneratedTime()); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); xo.time = sdf.format(new Date(t)); onLog("(" + xo.orderID + ", " + (TextUtils.isEmpty(xo.productName) ? xo.productID : xo.productName) + ", " + xo.buyNum + ", " + xo.state + ", " + xo.time + ")\n"); mOrderListAdapter.addOrder(xo); } @Override public void onOrderInfoEnd() { // all orders notified onLog("}\n"); } @Override public void onTreasureListFound(String[] treasures) { // got list of treasures for (int i=0; i<treasures.length; i++) onLog(treasures[i]); } @Override public void onTreasureChange(String name, int count) { // count of treasure 'name' updated onLog("treasure: " + name + " " + count); } @Override public void onLogValue(String key, int value, String msg) { // got the value of log item 'key' onLog("log: " + key + " " + value + ", " + msg); } @Override public void onExtMethodNotification(String cookie, int code, String msg) { // execution of certain extended command finished, result being notified onLog("cookie=" + cookie + ", code=" + code + ", msg=" + msg); } @Override public void onOrderStateChange(String productID, int num, String orderID, String state, String message) { // update product view when its state changes mProductListAdapter.changeProductState(productID, orderID, state); } @Override public void onStateChange(int type, int orgState, int newState, int reason, int op, String arg) { // A state changed. Update buttons updateButtons(); // You may use passed parameters to judge what to do at present. onLog("State change: " + agent.getStateName(type, orgState) + " -> " + agent.getStateName(type, newState) + " # " + agent.getStateChangeReasonName(reason) + "\n"); } @Override public void onExitCancelled() { // recover screen and go on } @Override public void onFuncRequest(final String func, final String id, String arg) { if (func.equals("screenshot")) { mHandler.post(new Runnable() { @Override public void run() { // do nothing but failure notification agent.notifyFuncRequestResult(func, id, "failed"); } }); } else if (func.equals("logout")) { mHandler.post(new Runnable() { @Override public void run() { // raise logout request (asynchronous) agent.logout(); // success notification agent.notifyFuncRequestResult(func, id, "succeeded"); } }); } } }
The example has its application name being "HappyLearning", directory being "hl" and organization name being "cp.com ".
Please pick up appropriate features you need.
When this step finishes, Android Studios will show project screen. You will see 2 build.gradle files, with one for project "hl" and another one for module "app".
First, copy the library file "sgutil.aar" you downloaded to directoy "libs" of module "app".
cp sgutil.aar ~/hl/app/libs
Second, modify "build.gradle" of module "app". As shown below, add dependency on sgutil.aar, and specify "libs" as local repository so that gradle knows where to find it.
dependencies { ... compile(name:"sgutil", ext:"aar") }repositories { flatDir { dirs 'libs' } }
As shown below, change the application name to "com.sg.util.SGApplication".
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.cp.happylearning">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:name="com.sg.util.SGApplication"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
You need to create an instance of SGAgent in order to use SGUtil. We suggest you do this in onCreate method of MainActivity. You also need to set instance of your game server implementation after that, otherwise the whole login process does not start.
public class MainActivity extends AppCompatActivity implements SGAgent.SGClient { private SGAgent agent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); agent = new SGAgent(); ... agent.setGameServer(new YourGameServerImplemenation()); ... }
You should forward Android lifecycle callbacks in MainActivity to SGAgent. Especially, you initialize UI before calling onCreate because SGAgent may print logs to UI. You may implement appendLog functions as empty ones if you do not need this feature.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); agent = new SGAgent(); agent.onCreate(this, this); agent.setGameServer(new YourGameServerImplementation()); } protected void onStart() { super.onStart(); agent.onStart(); } protected void onRestart() { super.onRestart(); agent.onRestart(); } protected void onResume() { super.onResume(); agent.onResume(); } protected void onPause() { agent.onPause(); super.onPause(); } protected void onStop() { agent.onStop(); super.onStop(); } protected void onDestroy() { agent.onDestroy(); super.onDestroy(); System.exit(0); } public void onNewIntent(Intent newIntent) { agent.onNewIntent(newIntent); super.onNewIntent(newIntent); } public boolean onKeyDown(int keyCode, KeyEvent event) { return agent.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); } public void onBackPressed() { agent.onBackPressed(); } protected void onActivityResult(int requestCode, int resultCode, Intent data) { agent.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data); } @TargetApi(23) public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); agent.onRequestPermissionsResult(requestCode, permissions, grantResults); }
Many features of SGAgent are provided as asynchronous operations. SGAgent will notify you the operation results via an interface you provided. That is to say, you must provide an instance of SGAgent.SGClient as the 2nd parameter of SGAgent.onCreate method. You should handle with the notified information within methods of the interface instance. The interface is defined as follows (see SGAgent.SGClient ).
public interface SGClient { void appendLog(String text); void appendLog(String text, String postfix); boolean onProductBegin(int num); void onProductFound(UProduct product); void onProductEnd(); void onOrderStateChange(String productID, String orderID, String state); boolean onOrderInfoBegin(int num); void onOrderInfoFound(UOrder order); void onOrderInfoEnd(); void onTreasureListFound(String[] treasures); void onTreasureChange(String name, int count); void onLogValue(String key, int value); void onStateChange(int type, int orgState, int newState, int reason, int op, String arg); void onExtMethodNotification(String cookie, int code, String message); void onExitCancelled(); void onFuncRequest(String func, String id, String arg); }
You can generate APK file with Android Studio after integrating with SGUtil library. You many also test your app on Android devices. However, the APK, has only a demo channel SDK. To bind a real channel SDK, please upload your APK to SG Studios website and download the final APK after repackaging is finished.