Checkout是Android In-App Billing API(v3 +)的一個封裝庫。In-App Billing 是一項 Google Play提供的內購服務,可以讓咱們在本身的應用內出售數字內容。咱們可使用該服務來出售衆多內容,包括可下載內容(例如媒體文件或照片)和虛擬內容(例如遊戲關卡或魔藥、高級服務和功能,等等)Checkout的主要目標是儘量簡單直接地集成應用內產品:開發人員不該該花太多時間來實現乏味的應用內結算API,而應該關注更重要的事情 - 他們的應用。 Checkout的github地址是:https://github.com/serso/android-checkoutjava
Billing: Checkout的核心類,實現了Android's Billing API。主要負責:android
Request: 表示Billing結算請求的實體類,具體實現類有BillingSupportedRequest,GetPurchaseHistoryRequest,GetPurchasesRequest,ChangePurchaseRequest,ConsumePurchaseRequest,GetSkuDetailsRequest,PurchaseRequest,分別表明具體的請求操做。git
OnConnectedServiceRunnable: Request的包裝類,實現了RequestRunnable接口,核心方法是run()
github
Checkout: Billing類的幫助類,維護了Billing實例,用於主線程中,生命週期需與activity/fragment/service綁定,對應的子類有FragmentCheckout,ActivityCheckout和CustomUiCheckout等。api
PendingRequests: 該類表示待處理的請求,維護了一個RequestRunnable的集合,全部的請求順序執行。緩存
Configuration: 表示Billing結算的配置接口,須要實現Configuration接口自定義配置。安全
Cache: 表示緩存的接口,具體實現類爲MapCache。服務器
ServiceConnector: 鏈接服務的接口,默認實現類爲DefaultServiceConnector,負責Google play app的綁定和解綁。併發
Purchase: 表示購買信息的類,成員變量與getBuyIntent()
返回的INAPP_DATA_SIGNATURE數據的 JSON 字段對應,也就是說Purchase都是根據這個JSON字段的內容定義的。app
Purchases: 表示購買信息列表的類。維護了一個Purchase集合。
PurchaseFlow: 表示從用戶請求購買之時起直到購買完成爲止的一個購買流程的類
PurchaseVerifier: 驗證購買接口,實現類爲BasePurchaseVerifier,該類爲抽象類,可繼承它實現本身的驗證類。驗證過程一般在後臺服務器進行。
Inventory: 用於加載產品,SKU和購買相關信息的類,其生命週期與Checkout的相關。子類有FallingBackInventory,CheckoutInventory和RobotmediaInventory。
Checkout是一個工具類,主要是對Billing結算流程的一個封裝和對Inventory
的處理。根據Context
環境的不一樣,構建一個Checkout類的非抽象子類(FragmentCheckout
、ActivityCheckout
、CustomUiCheckout
)對象,啓動結算流程。 注意:Checkout要與activity/fragment/service等生命週期相綁定,在onDestroy()
中調用mCheckout.stop()
,取消待執行的請求,解綁service.
1.主要成員變量
Billing mBilling
主類實例
Billing.Requests mRequests
表明各類結算方法的對象。
2.構造對象
根據如下幾個靜態方法構造出子類實例,對應ui/activity/fragment/service,並將Billing做爲參數傳進來。
public static UiCheckout forUi(@Nonnull IntentStarter intentStarter, @Nonnull Object tag, @Nonnull Billing billing);
public static UiCheckout forFragment(@Nonnull Fragment fragment, @Nonnull Billing billing);
public static ActivityCheckout forActivity(@Nonnull Activity activity, @Nonnull Billing billing);
public static Checkout forService(@Nonnull Service service, @Nonnull Billing billing);
複製代碼
2. 主要方法
做爲Checkout庫的調用入口,建立出 Checkout 之後,調用 start 方法
public void start() {
start(null);
}
public void start(@Nullable final Listener listener) {
Check.isMainThread();
synchronized (mLock) {
Check.isFalse(mState == State.STARTED, "Already started");
Check.isNull(mRequests, "Already started");
mState = State.STARTED;
mBilling.onCheckoutStarted();
mRequests = mBilling.getRequests(mTag);
}
whenReady(listener == null ? new EmptyListener() {} : listener);
}
複製代碼
start有兩重載方法,無參方法調用帶有listener
的方法,由第二個方法可見,主要是經過mBilling
獲取mRequests
,而後調用whenReady()
方法。
public void whenReady(@Nonnull final Listener listener) {
Check.isMainThread();
synchronized (mLock) {
Check.isNotNull(mRequests);
final Billing.Requests requests = mRequests;
@Nonnull
final Set<String> loadingProducts = new HashSet<>(ProductTypes.ALL);
for (final String product : ProductTypes.ALL) {
requests.isBillingSupported(product, new RequestListener<Object>() {
private void onBillingSupported(boolean supported) {
listener.onReady(requests, product, supported);
loadingProducts.remove(product);
if (loadingProducts.isEmpty()) {
listener.onReady(requests);
}
}
@Override
public void onSuccess(@Nonnull Object result) {
onBillingSupported(true);
}
@Override
public void onError(int response, @Nonnull Exception e) {
onBillingSupported(false);
}
});
}
}
}
複製代碼
whenReady()
方法的目的是檢查是否支持Billing API,也就是最終會調用service.isBillingSupported()
方法,而後返回回調處理結果。
當離開頁面時,須要調用stop()
方法釋放資源
public void stop() {
Check.isMainThread();
synchronized (mLock) {
if (mState != State.INITIAL) {
mState = State.STOPPED;
}
if (mRequests != null) {
mRequests.cancelAll();
mRequests = null;
}
if (mState == State.STOPPED) {
mBilling.onCheckoutStopped();
}
}
}
複製代碼
當調用stop()
時,將Request隊列中的請求取消,而mBilling.onCheckoutStopped();
主要作的事是斷開與Google Play服務的鏈接。
3.使用流程
在分析Billing類以前,咱們先分析Billing中幾個成員變量對應的類。
表示Billing請求的實體類,該類爲抽象類,具體實現類有BillingSupportedRequest
,GetSkuDetailsRequest
,ConsumePurchaseRequest
等,子類須要實現抽象方法
abstract void start(@Nonnull IInAppBillingService service, @Nonnull String packageName)
throws RemoteException, RequestException;
abstract String getCacheKey();
複製代碼
子類的start()
調用service相關的Billing API方法。
主要成員變量
int mApiVersion
In-app Billing的api版本
int mId
做爲請求獨一無二的id
RequestType mType
請求的類型
Object mTag
標籤
RequestListener<R> mListener
請求的回調接口
該類實現了RequestRunnable
接口,主要是對Request
的行爲進行包裝,增長緩存檢查和異常處理
1.成員變量
Request mRequest
被包裝的請求2.核心方法
@Override
public boolean run() {
final Request localRequest = getRequest();
if (localRequest == null) {
// request was cancelled => finish here
return true;
}
if (checkCache(localRequest)) return true;
// request is alive, let's check the service state
final State localState;
final IInAppBillingService localService;
synchronized (mLock) {
localState = mState;
localService = mService;
}
if (localState == State.CONNECTED) {
Check.isNotNull(localService);
// service is connected, let's start request
try {
localRequest.start(localService, mContext.getPackageName());
} catch (RemoteException | RuntimeException | RequestException e) {
localRequest.onError(e);
}
} else {
// service is not connected, let's check why
if (localState != State.FAILED) {
// service was disconnected
connect();
return false;
} else {
// service was not connected in the first place => can't do anything, aborting the request
localRequest.onError(ResponseCodes.SERVICE_NOT_CONNECTED);
}
}
return true;
}
複製代碼
該方法的邏輯也很清楚,先檢查是否有緩存,若是有緩存,直接返回(注意:checkCache()
會將緩存返回給request),不然檢查狀態,若是處於已鏈接狀態,執行request的start()
,不然嘗試創建起鏈接。
該類表示待處理的請求,並實現了Runnable
接口,其維護了一個RequestRunnable
列表mList
,全部請求需添加至mList
才能被處理。核心方法爲run()
,經過循環取出RequestRunnable
,並執行RequestRunnable
的run()
方法。
該類實現了BillingRequests
接口,Requests
做爲Billing的內部類,持有Billing實例的引用,並調用了其實例方法。BillingRequests
定義一系列關於Billing api相關的方法
Billing API的配置接口,定義了以下方法 String getPublicKey();
獲取公鑰,用於購買過程當中的簽名。
Cache getCache();
獲取緩存對象
PurchaseVerifier getPurchaseVerifier();
返回PurchaseVerifier
Inventory getFallbackInventory(@Nonnull Checkout checkout, @Nonnull Executor onLoadExecutor);
返回後備庫存,用於恢復購買
boolean isAutoConnect();
是否自動鏈接
該類可對其餘Configuration
進行包裝,獲得其mPublicKey
和mPurchaseVerifier
的引用。StaticConfiguration
實現了Configuration
的方法。通常狀況下,咱們須要實現本身的Configuration
1.成員變量
Configuration mOriginal
原始的Configuration
String mPublicKey;
公鑰字符串
PurchaseVerifier mPurchaseVerifier
驗證購買類對象
實現Configuration
部分方法的類,該類經過newCache()
獲取緩存對象,經過newPurchaseVerifier()
獲取購買驗證對象,isAutoConnect()
直接返回true。而getFallbackInventory()
則返回null,其子類須要實現getPublicKey()
緩存接口,表明了一個能夠獲取請求結果,存儲請求結果的緩存。
1.主要方法
Entry get(Key key);
經過 key 獲取請求的緩存實體
void put(Key key, Entry entry);
存入一個請求的緩存實體
void init();
初始化
void remove(Key key);
移除指定的緩存實體
void removeAll(int type);
清除某一類型的緩存實體
void clear();
清空緩存
2.表明鍵實體的內部類Key
成員變量
int type
類型
String key
鍵值字符串
2.表明緩存實體的內部類Entry
成員變量
Object data
緩存的對象
long expiresAt
緩存到期時間
Cache
接口的實現類,經過維護一個Map<Key, Entry> mMap
對象,實現了Cache
的緩存功能。
Cache
接口的實現類,該類對其餘Cache
實現類進行包裝,經過synchronized
同步鎖達到線程安全的效果
該類對Cache
接口的實現類,該類對其餘Cache
實現類進行包裝,捕獲異常。
該類實現了ServiceConnector
接口,實現了connect()
和disconnect()
,用於處理服務創建與斷開。DefaultServiceConnector
持有Billing對象的引用。
1.成員變量
ServiceConnection mConnection
ServiceConnection實例,當創建鏈接後,會調用Billing的setService()
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
setService(null, false);
}
@Override
public void onServiceConnected(ComponentName name,
IBinder service) {
setService(IInAppBillingService.Stub.asInterface(service), true);
}
};
複製代碼
2.實現方法
@Override
public boolean connect() {
try {
final Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
intent.setPackage("com.android.vending");
return mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
} catch (IllegalArgumentException e) {
// some devices throw IllegalArgumentException (Service Intent must be explicit)
// even though we set package name explicitly. Let's not crash the app and catch
// such exceptions here, the billing on such devices will not work.
return false;
} catch (NullPointerException e) {
// Meizu M3s phones might throw an NPE in Context#bindService (Attempt to read from field 'int com.android.server.am.ProcessRecord.uid' on a null object reference).
// As in-app purchases don't work if connection to the billing service can't be
// established let's not crash and allow users to continue using the app
return false;
}
}
@Override
public void disconnect() {
mContext.unbindService(mConnection);
}
複製代碼
connect()
負責綁定服務,disconnect()
解綁服務。
接下來重點分析Billing類。做爲Checkout的核心類,Billing封裝告終算流程的主要邏輯。
1.構造對象
爲避免與Google Play app重複鏈接,因此只能有一個Billing對象,因此咱們採起在application中構建單例的形式。
@Nonnull
private final Billing mBilling = new Billing(this, new Conguration());
複製代碼
2.主要成員變量
StaticConfiguration mConfiguration
配置類,主要是對publicKey,Cache等配置
ConcurrentCache mCache
緩存類,表明了一個能夠獲取請求結果,存儲請求結果的緩存
PendingRequests mPendingRequests
表示待執行的請求隊列。
BillingRequests mRequests
定義了全部的billing結算方法的接口
IInAppBillingService mService
billing服務實例對象
State mState
表示結算過程當中的狀態
CancellableExecutor mMainThread
表示主線程,用於處理服務鏈接創建和取消的過程。
Executor mBackground
表示子線程,用於處理結算流程。
ServiceConnector mConnector
服務鏈接類。
3.state狀態切換流程
state表示鏈接過程當中的狀態的枚舉類,具備INITIAL
,CONNECTING
,CONNECTED
,DISCONNECTING
,DISCONNECTED
, FAILED
6個狀態。state的轉換方式須要按照下圖:
經過setState()
方法改變State
狀態,若是傳入的值爲CONNECTED,則開始執行Request
隊列
void setState(@Nonnull State newState) {
synchronized (mLock) {
if (mState == newState) {
return;
}
Check.isTrue(sPreviousStates.get(newState).contains(mState), "State " + newState + " can't come right after " + mState + " state");
mState = newState;
switch (mState) {
case DISCONNECTING:
// as we can jump directly from DISCONNECTING to CONNECTED state let's remove
// the listener here instead of in DISCONNECTED state. That also will protect
// us from getting in the following trap: CONNECTED->DISCONNECTING->CONNECTING->FAILED
mPlayStoreBroadcastReceiver.removeListener(mPlayStoreListener);
break;
case CONNECTED:
// CONNECTED is the only state when we know for sure that Play Store is available.
// Registering the listener here also means that it should be never registered
// in the FAILED state
mPlayStoreBroadcastReceiver.addListener(mPlayStoreListener);
executePendingRequests();
break;
case FAILED:
// the play store listener should not be registered in the receiver in case of
// failure as FAILED state can't occur after CONNECTED
Check.isTrue(!mPlayStoreBroadcastReceiver.contains(mPlayStoreListener), "Leaking the listener");
mMainThread.execute(new Runnable() {
@Override
public void run() {
mPendingRequests.onConnectionFailed();
}
});
break;
}
}
}
複製代碼
4.創建鏈接
public void connect() {
synchronized (mLock) {
if (mState == State.CONNECTED) {
executePendingRequests();
return;
}
if (mState == State.CONNECTING) {
return;
}
if (mConfiguration.isAutoConnect() && mCheckoutCount <= 0) {
warning("Auto connection feature is turned on. There is no need in calling Billing.connect() manually. See Billing.Configuration.isAutoConnect");
}
setState(State.CONNECTING);
mMainThread.execute(new Runnable() {
@Override
public void run() {
connectOnMainThread();
}
});
}
}
複製代碼
經過上面看出,connect()
方法主要是設置state
爲CONNECTING
,並經過mMainThread
調用了connectOnMainThread()
方法,該方法又調用了mConnector
的connect()
方法,並返回mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
的結果。
須要注意的是,每次執行請求流程時,connect()都會被調用,確保服務是鏈接上的。
5.執行request
當創建起鏈接後,state被置爲CONNECTED,並調用executePendingRequests()
方法,該方法經過一個單線程的線程池,執行mPendingRequests
的run()
方法,循環的取出request
(其實是RequestRunnable)並執行。
private void executePendingRequests() {
mBackground.execute(mPendingRequests);
}
複製代碼
當開啓某一類型的請求時,Billing類中的runWhenConnected()
會被調用,這個方法會調用到connect()
,並最終執行executePendingRequests()
方法。 接着咱們來重點看一下這個方法,這是個重載方法。
private int runWhenConnected(@Nonnull Request request, @Nullable Object tag) {
return runWhenConnected(request, null, tag);
}
<R> int runWhenConnected(@Nonnull Request<R> request, @Nullable RequestListener<R> listener, @Nullable Object tag) {
if (listener != null) {
if (mCache.hasCache()) {
listener = new CachingRequestListener<>(request, listener);
}
request.setListener(listener);
}
if (tag != null) {
request.setTag(tag);
}
mPendingRequests.add(onConnectedService(request));
connect();
return request.getId();
}
複製代碼
能夠看出runWhenConnected()
作的事情就是傳進一個request對象,並將其加到mPendingRequests
中,而後在connect()
中執行request任務。
6.request執行過程當中的調用棧
咱們來了解一下一個請求執行的過程,以獲取購買的商品爲例
7.斷開鏈接
public void disconnect() {
synchronized (mLock) {
if (mState == State.DISCONNECTED || mState == State.DISCONNECTING || mState == State.INITIAL) {
return;
}
if (mState == State.FAILED) {
// it would be strange to change the state from FAILED to DISCONNECTING/DISCONNECTED,
// thus, just cancelling all pending the requested here and returning without updating
// the state
mPendingRequests.cancelAll();
return;
}
if (mState == State.CONNECTED) {
setState(State.DISCONNECTING);
mMainThread.execute(new Runnable() {
@Override
public void run() {
disconnectOnMainThread();
}
});
} else {
// if we're still CONNECTING - skip DISCONNECTING state
setState(State.DISCONNECTED);
}
// requests should be cancelled only when Billing#disconnect() is called explicitly as
// it's only then we know for sure that no more work should be done
mPendingRequests.cancelAll();
}
}
複製代碼
針對不一樣狀態作不一樣處理。當mState
爲CONNECTED時,經過mMainThread
調用disconnectOnMainThread()
。來看下這個方法。
private void disconnectOnMainThread() {
Check.isMainThread();
mConnector.disconnect();
}
複製代碼
邏輯很簡單,經過mConnector
斷開service鏈接。
表示購買信息的類
成員變量
String sku
表示商品項名稱
String orderId
表示訂單標識符
String packageName
應用包名
long time
購買的時間
String payload
一個開發人員指定的字符串,該字段在儲值的時候填入,在Google Play儲值完成後返回
String token
State state
購買的狀態,有PURCHASED,CANCELLED,REFUNDED,EXPIRED四個狀態
boolean autoRenewing
是否自動更新訂閱。
String data
購買的原始數據
String signature
數據簽名
表示購買信息列表的類。維護了一個Purchase集合。
成員變量
String product
產品類型
List<Purchase> list
購買過的商品列表
String continuationToken
用於查詢更多產品的token
表示從用戶請求購買之時起直到購買完成爲止的一個購買流程的類,該類實現了CancellableRequestListener接口,重寫了onSuccess()
回調方法。
1.核心方法
@Override
public void onSuccess(@Nonnull PendingIntent purchaseIntent) {
if (mListener == null) {
// request was cancelled => stop here
return;
}
try {
mIntentStarter.startForResult(purchaseIntent.getIntentSender(), mRequestCode, new Intent());
} catch (RuntimeException | IntentSender.SendIntentException e) {
handleError(e);
}
複製代碼
當PurchaseRequest獲取到BuyIntent
後,調用了RequestListener的onSuccess()
並把purchaseIntent
傳進來,啓動購買頁面。而後在activity的onActivityResult()
執行購買結果流程,PurchaseFlow
把這個流程封裝在本類中的onActivityResult()
方法中
void onActivityResult(int requestCode, int resultCode, Intent intent) {
try {
Check.equals(mRequestCode, requestCode);
if (intent == null) {
// sometimes intent is null (it's not obvious when it happens but it happens from time to time)
handleError(NULL_INTENT);
return;
}
final int responseCode = intent.getIntExtra(EXTRA_RESPONSE, OK);
if (resultCode != RESULT_OK || responseCode != OK) {
handleError(responseCode);
return;
}
final String data = intent.getStringExtra(EXTRA_PURCHASE_DATA);
final String signature = intent.getStringExtra(EXTRA_PURCHASE_SIGNATURE);
Check.isNotNull(data);
Check.isNotNull(signature);
final Purchase purchase = Purchase.fromJson(data, signature);
mVerifier.verify(singletonList(purchase), new VerificationListener());
} catch (RuntimeException | JSONException e) {
handleError(e);
}
}
複製代碼
2.PurchaseFlow的流程
表示加載關於products,SKUs和purchases相關信息的接口。
1.構造對象
這個類不能直接被實例化,須要經過調用Checkout的loadInventory()
或makeInventory()
@Nonnull
public Inventory loadInventory(@Nonnull Inventory.Request request, @Nonnull Inventory.Callback callback) {
final Inventory inventory = makeInventory();
inventory.load(request, callback);
return inventory;
}
@Nonnull
public Inventory makeInventory() {
Check.isMainThread();
synchronized (mLock) {
checkIsNotStopped();
}
final Inventory inventory;
final Inventory fallbackInventory = mBilling.getConfiguration().getFallbackInventory(this, mOnLoadExecutor);
if (fallbackInventory == null) {
inventory = new CheckoutInventory(this);
} else {
inventory = new FallingBackInventory(this, fallbackInventory);
}
return inventory;
}
複製代碼
能夠看出loadInventory()
又調用了makeInventory()
,Inventory
的實例化是在makeInventory()
中進行的。先獲取FallingBackInventory
對象,若是不存在,則實例化CheckoutInventory
對象。
2.主要方法
int load(@Nonnull Request request, @Nonnull Callback callback);//加載Products而且異步傳遞到Callback中,這是核心方法。
void cancel();//取消全部加載任務
void cancel(int id);//根據id取消指定的任務。
boolean isLoading();//判斷是否至少有一個任務在加載中
複製代碼
BaseInventory
實現了Inventory
接口,做爲基類。子類須要實現protected abstract Runnable createWorker(@Nonnull Task task);
抽象方法。
1.主要成員變量
List<Task> mTasks
維護了一個Task列表,用於對任務的管理
Checkout mCheckout
持有Checkout引用。
2.核心方法
@Override
public int load(@Nonnull Request request, @Nonnull Callback callback) {
synchronized (mLock) {
final Task task = new Task(request, callback);
mTasks.add(task);
task.run();
return task.mId;
}
}
複製代碼
能夠看出load()
根據request和callback實例化task對象,並添加到mTasks中,再執行task的run()
BaseInventory
的子類,用於加載購買流程的相關信息,實現了BaseInventory
的抽象方法
protected Runnable createWorker(@Nonnull Task task) {
return new Worker(task);
}
複製代碼
可見createWorker()
方法返回了Worker對象,Worker是CheckoutInventory的內部類。
1.內部類Worker
Worker
實現了Runnable
接口和Checkout.Listener
接口,做爲CheckoutInventory
的內部類,持有外部類引用,因此也就持有Checkout
引用。run()
方法調用了checkout的whenReady()
方法.咱們來看一下whenReady()
中又調用了 Checkout.Listener
回調方法。咱們看一下回調方法的實現。
@Override
public void onReady(@Nonnull BillingRequests requests) {
}
@Override
public void onReady(@Nonnull BillingRequests requests, @Nonnull String productId,
boolean billingSupported) {
final Product product = new Product(productId, billingSupported);
synchronized (mLock) {
countDown();
mProducts.add(product);
if (!mTask.isCancelled() && product.supported && mTask.getRequest().shouldLoadPurchases(productId)) {
loadPurchases(requests, product);
} else {
countDown(1);
}
if (!mTask.isCancelled() && product.supported && mTask.getRequest().shouldLoadSkus(productId)) {
loadSkus(requests, product);
} else {
countDown(1);
}
}
}
複製代碼
能夠看出onReady()
回調方法判斷是否加載購買信息或者加載SKU,分別調用了loadPurchases()
和loadSkus()
,而兩個方法右分別調用了requests.getAllPurchases()
和requests.getSkus()
,從而實現了獲取信息的流程。
2.查詢信息流程
咱們經過時序圖來理清整個流程,這裏以獲取購買信息爲例
一樣的集成了BaseInventory
,該類持有CheckoutInventory
引用。表示若是其中一個產品不被支持,則庫存回退到後備庫存。
Billing API中支持的Product類型,目前有IN_APP和SUBSCRIPTION兩種
表示在Inventory中的一種Product,包含了purchase列表和SKUS列表(若是有的話),Product可根據ProductTypes分爲IN_APP和SUBSCRIPTION。
1.成員變量
String id
Product ID
boolean supported
product是否被支持
List<Purchase> mPurchases
purchase列表
List<Sku> mSkus
SKU列表
表示Product的集合,維護了一個存儲Product的map。
工具類,做用是確保Runnable在主線程執行
主要方法
@Override
public void execute(@Nonnull Runnable runnable) {
if (MainThread.isMainThread()) {
runnable.run();
} else {
mHandler.post(runnable);
}
}
複製代碼
本文首發在公衆號:三七互娛技術中心。歡迎關注~~