mirror of https://github.com/tasks/tasks
Add new subscription pricing
parent
0df0c11c30
commit
f4ae1100bd
@ -1,192 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
||||
* This service provides the following features:
|
||||
* 1. Provides a new API to get details of in-app items published for the app including
|
||||
* price, type, title and description.
|
||||
* 2. The purchase flow is synchronous and purchase information is available immediately
|
||||
* after it completes.
|
||||
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
||||
* till the purchase is consumed.
|
||||
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
||||
* in-app items are consumable and thereafter can be purchased again.
|
||||
* 5. An API to get current purchases of the user immediately. This will not contain any
|
||||
* consumed purchases.
|
||||
*
|
||||
* All calls will give a response code with the following possible values
|
||||
* RESULT_OK = 0 - success
|
||||
* RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
|
||||
* RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
|
||||
* RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
|
||||
* RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
|
||||
* RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
|
||||
* RESULT_ERROR = 6 - Fatal error during the API action
|
||||
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
||||
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
||||
*/
|
||||
interface IInAppBillingService {
|
||||
/**
|
||||
* Checks support for the requested billing API version, package and in-app type.
|
||||
* Minimum API version supported by this interface is 3.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param type type of the in-app item being purchased ("inapp" for one-time purchases
|
||||
* and "subs" for subscriptions)
|
||||
* @return RESULT_OK(0) on success and appropriate response code on failures.
|
||||
*/
|
||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
||||
|
||||
/**
|
||||
* Provides details of a list of SKUs
|
||||
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
||||
* with a list JSON strings containing the productId, price, title and description.
|
||||
* This API can be called with a maximum of 20 SKUs.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param type of the in-app items ("inapp" for one-time purchases
|
||||
* and "subs" for subscriptions)
|
||||
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
|
||||
* on failures.
|
||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||
* in JSON format similar to:
|
||||
* '{ "productId" : "exampleSku",
|
||||
* "type" : "inapp",
|
||||
* "price" : "$5.00",
|
||||
* "price_currency": "USD",
|
||||
* "price_amount_micros": 5000000,
|
||||
* "title : "Example Title",
|
||||
* "description" : "This is an example description" }'
|
||||
*/
|
||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
||||
|
||||
/**
|
||||
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
||||
* the type, a unique purchase token and an optional developer payload.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param sku the SKU of the in-app item as published in the developer console
|
||||
* @param type of the in-app item being purchased ("inapp" for one-time purchases
|
||||
* and "subs" for subscriptions)
|
||||
* @param developerPayload optional argument to be sent back with the purchase information
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
|
||||
* on failures.
|
||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||
*
|
||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
|
||||
* codes on failures.
|
||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||
* "packageName":"com.example.app",
|
||||
* "productId":"exampleSku",
|
||||
* "purchaseTime":1345678900000,
|
||||
* "purchaseToken" : "122333444455555",
|
||||
* "developerPayload":"example developer payload" }'
|
||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||
* was signed with the private key of the developer
|
||||
* TODO: change this to app-specific keys.
|
||||
*/
|
||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
||||
String developerPayload);
|
||||
|
||||
/**
|
||||
* Returns the current SKUs owned by the user of the type and package name specified along with
|
||||
* purchase information and a signature of the data to be validated.
|
||||
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
||||
* V1 and V2 that have not been consumed.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param type of the in-app items being requested ("inapp" for one-time purchases
|
||||
* and "subs" for subscriptions)
|
||||
* @param continuationToken to be set as null for the first call, if the number of owned
|
||||
* skus are too many, a continuationToken is returned in the response bundle.
|
||||
* This method can be called again with the continuation token to get the next set of
|
||||
* owned skus.
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
|
||||
on failures.
|
||||
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
||||
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
||||
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
||||
* of the purchase information
|
||||
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
||||
* next set of in-app purchases. Only set if the
|
||||
* user has more owned skus than the current list.
|
||||
*/
|
||||
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
|
||||
|
||||
/**
|
||||
* Consume the last purchase of the given SKU. This will result in this item being removed
|
||||
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
||||
* to be consumed
|
||||
* @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
|
||||
*/
|
||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
||||
|
||||
/**
|
||||
* This API is currently under development.
|
||||
*/
|
||||
int stub(int apiVersion, String packageName, String type);
|
||||
|
||||
/**
|
||||
* Returns a pending intent to launch the purchase flow for upgrading or downgrading a
|
||||
* subscription. The existing owned SKU(s) should be provided along with the new SKU that
|
||||
* the user is upgrading or downgrading to.
|
||||
* @param apiVersion billing API version that the app is using, must be 5 or later
|
||||
* @param packageName package name of the calling app
|
||||
* @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
|
||||
* if null or empty this method will behave like {@link #getBuyIntent}
|
||||
* @param newSku the SKU that the user is upgrading or downgrading to
|
||||
* @param type of the item being purchased, currently must be "subs"
|
||||
* @param developerPayload optional argument to be sent back with the purchase information
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
|
||||
* on failures.
|
||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||
*
|
||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
|
||||
* codes on failures.
|
||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||
* "packageName":"com.example.app",
|
||||
* "productId":"exampleSku",
|
||||
* "purchaseTime":1345678900000,
|
||||
* "purchaseToken" : "122333444455555",
|
||||
* "developerPayload":"example developer payload" }'
|
||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||
* was signed with the private key of the developer
|
||||
* TODO: change this to app-specific keys.
|
||||
*/
|
||||
Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
|
||||
in List<String> oldSkus, String newSku, String type, String developerPayload);
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
/* Copyright (c) 2014 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Receiver for the "com.android.vending.billing.PURCHASES_UPDATED" Action from the Play Store.
|
||||
*
|
||||
* <p>It is possible that an in-app item may be acquired without the application calling
|
||||
* getBuyIntent(), for example if the item can be redeemed from inside the Play Store using a
|
||||
* promotional code. If this application isn't running at the time, then when it is started a call
|
||||
* to getPurchases() will be sufficient notification. However, if the application is already running
|
||||
* in the background when the item is acquired, a message to this BroadcastReceiver will indicate
|
||||
* that the an item has been acquired.
|
||||
*/
|
||||
public class IabBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
/** The Intent action that this Receiver should filter for. */
|
||||
public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED";
|
||||
|
||||
private final IabBroadcastListener mListener;
|
||||
|
||||
public IabBroadcastReceiver(IabBroadcastListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (mListener != null) {
|
||||
mListener.receivedBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
/** Listener interface for received broadcast messages. */
|
||||
public interface IabBroadcastListener {
|
||||
|
||||
void receivedBroadcast();
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
/**
|
||||
* Exception thrown when something went wrong with in-app billing. An IabException has an associated
|
||||
* IabResult (an error). To get the IAB result that caused this exception to be thrown, call {@link
|
||||
* #getResult()}.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class IabException extends Exception {
|
||||
|
||||
IabResult mResult;
|
||||
|
||||
public IabException(IabResult r) {
|
||||
this(r, null);
|
||||
}
|
||||
|
||||
public IabException(int response, String message) {
|
||||
this(new IabResult(response, message));
|
||||
}
|
||||
|
||||
public IabException(IabResult r, Exception cause) {
|
||||
super(r.getMessage(), cause);
|
||||
mResult = r;
|
||||
}
|
||||
|
||||
public IabException(int response, String message, Exception cause) {
|
||||
this(new IabResult(response, message), cause);
|
||||
}
|
||||
|
||||
/** Returns the IAB result (error) that this exception signals. */
|
||||
public IabResult getResult() {
|
||||
return mResult;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,58 +0,0 @@
|
||||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
/**
|
||||
* Represents the result of an in-app billing operation. A result is composed of a response code (an
|
||||
* integer) and possibly a message (String). You can get those by calling {@link #getResponse} and
|
||||
* {@link #getMessage()}, respectively. You can also inquire whether a result is a success or a
|
||||
* failure by calling {@link #isSuccess()} and {@link #isFailure()}.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class IabResult {
|
||||
|
||||
int mResponse;
|
||||
String mMessage;
|
||||
|
||||
public IabResult(int response, String message) {
|
||||
mResponse = response;
|
||||
if (message == null || message.trim().length() == 0) {
|
||||
mMessage = IabHelper.getResponseDesc(response);
|
||||
} else {
|
||||
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public int getResponse() {
|
||||
return mResponse;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK;
|
||||
}
|
||||
|
||||
public boolean isFailure() {
|
||||
return !isSuccess();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "IabResult: " + getMessage();
|
||||
}
|
||||
}
|
||||
@ -1,96 +0,0 @@
|
||||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a block of information about in-app items. An Inventory is returned by such methods as
|
||||
* {@link IabHelper#queryInventory}.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class Inventory {
|
||||
|
||||
Map<String, SkuDetails> mSkuMap = new HashMap<String, SkuDetails>();
|
||||
Map<String, Purchase> mPurchaseMap = new HashMap<String, Purchase>();
|
||||
|
||||
Inventory() {}
|
||||
|
||||
/** Returns the listing details for an in-app product. */
|
||||
public SkuDetails getSkuDetails(String sku) {
|
||||
return mSkuMap.get(sku);
|
||||
}
|
||||
|
||||
/** Returns purchase information for a given product, or null if there is no purchase. */
|
||||
public Purchase getPurchase(String sku) {
|
||||
return mPurchaseMap.get(sku);
|
||||
}
|
||||
|
||||
/** Returns whether or not there exists a purchase of the given product. */
|
||||
public boolean hasPurchase(String sku) {
|
||||
return mPurchaseMap.containsKey(sku);
|
||||
}
|
||||
|
||||
/** Return whether or not details about the given product are available. */
|
||||
public boolean hasDetails(String sku) {
|
||||
return mSkuMap.containsKey(sku);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase a purchase (locally) from the inventory, given its product ID. This just modifies the
|
||||
* Inventory object locally and has no effect on the server! This is useful when you have an
|
||||
* existing Inventory object which you know to be up to date, and you have just consumed an item
|
||||
* successfully, which means that erasing its purchase data from the Inventory you already have is
|
||||
* quicker than querying for a new Inventory.
|
||||
*/
|
||||
public void erasePurchase(String sku) {
|
||||
if (mPurchaseMap.containsKey(sku)) {
|
||||
mPurchaseMap.remove(sku);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a list of all owned product IDs. */
|
||||
List<String> getAllOwnedSkus() {
|
||||
return new ArrayList<String>(mPurchaseMap.keySet());
|
||||
}
|
||||
|
||||
/** Returns a list of all owned product IDs of a given type */
|
||||
List<String> getAllOwnedSkus(String itemType) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
for (Purchase p : mPurchaseMap.values()) {
|
||||
if (p.getItemType().equals(itemType)) {
|
||||
result.add(p.getSku());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Returns a list of all purchases. */
|
||||
List<Purchase> getAllPurchases() {
|
||||
return new ArrayList<Purchase>(mPurchaseMap.values());
|
||||
}
|
||||
|
||||
void addSkuDetails(SkuDetails d) {
|
||||
mSkuMap.put(d.getSku(), d);
|
||||
}
|
||||
|
||||
void addPurchase(Purchase p) {
|
||||
mPurchaseMap.put(p.getSku(), p);
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Represents an in-app billing purchase. */
|
||||
@SuppressWarnings("ALL")
|
||||
public class Purchase {
|
||||
|
||||
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
|
||||
String mOrderId;
|
||||
String mPackageName;
|
||||
String mSku;
|
||||
long mPurchaseTime;
|
||||
int mPurchaseState;
|
||||
String mDeveloperPayload;
|
||||
String mToken;
|
||||
String mOriginalJson;
|
||||
String mSignature;
|
||||
boolean mIsAutoRenewing;
|
||||
|
||||
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
|
||||
mItemType = itemType;
|
||||
mOriginalJson = jsonPurchaseInfo;
|
||||
JSONObject o = new JSONObject(mOriginalJson);
|
||||
mOrderId = o.optString("orderId");
|
||||
mPackageName = o.optString("packageName");
|
||||
mSku = o.optString("productId");
|
||||
mPurchaseTime = o.optLong("purchaseTime");
|
||||
mPurchaseState = o.optInt("purchaseState");
|
||||
mDeveloperPayload = o.optString("developerPayload");
|
||||
mToken = o.optString("token", o.optString("purchaseToken"));
|
||||
mIsAutoRenewing = o.optBoolean("autoRenewing");
|
||||
mSignature = signature;
|
||||
}
|
||||
|
||||
public String getItemType() {
|
||||
return mItemType;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return mOrderId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return mPackageName;
|
||||
}
|
||||
|
||||
public String getSku() {
|
||||
return mSku;
|
||||
}
|
||||
|
||||
public long getPurchaseTime() {
|
||||
return mPurchaseTime;
|
||||
}
|
||||
|
||||
public int getPurchaseState() {
|
||||
return mPurchaseState;
|
||||
}
|
||||
|
||||
public String getDeveloperPayload() {
|
||||
return mDeveloperPayload;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return mToken;
|
||||
}
|
||||
|
||||
public String getOriginalJson() {
|
||||
return mOriginalJson;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return mSignature;
|
||||
}
|
||||
|
||||
public boolean isAutoRenewing() {
|
||||
return mIsAutoRenewing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson;
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
/* Copyright (c) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Represents an in-app product's listing details. */
|
||||
@SuppressWarnings("ALL")
|
||||
public class SkuDetails {
|
||||
|
||||
private final String mItemType;
|
||||
private final String mSku;
|
||||
private final String mType;
|
||||
private final String mPrice;
|
||||
private final long mPriceAmountMicros;
|
||||
private final String mPriceCurrencyCode;
|
||||
private final String mTitle;
|
||||
private final String mDescription;
|
||||
private final String mJson;
|
||||
|
||||
public SkuDetails(String jsonSkuDetails) throws JSONException {
|
||||
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
|
||||
}
|
||||
|
||||
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
|
||||
mItemType = itemType;
|
||||
mJson = jsonSkuDetails;
|
||||
JSONObject o = new JSONObject(mJson);
|
||||
mSku = o.optString("productId");
|
||||
mType = o.optString("type");
|
||||
mPrice = o.optString("price");
|
||||
mPriceAmountMicros = o.optLong("price_amount_micros");
|
||||
mPriceCurrencyCode = o.optString("price_currency_code");
|
||||
mTitle = o.optString("title");
|
||||
mDescription = o.optString("description");
|
||||
}
|
||||
|
||||
public String getSku() {
|
||||
return mSku;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public String getPrice() {
|
||||
return mPrice;
|
||||
}
|
||||
|
||||
public long getPriceAmountMicros() {
|
||||
return mPriceAmountMicros;
|
||||
}
|
||||
|
||||
public String getPriceCurrencyCode() {
|
||||
return mPriceCurrencyCode;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SkuDetails:" + mJson;
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,23 @@
|
||||
package org.tasks;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.billing.InventoryHelper;
|
||||
import org.tasks.billing.BillingClient;
|
||||
import org.tasks.gtasks.PlayServices;
|
||||
|
||||
public class FlavorSetup {
|
||||
|
||||
private final InventoryHelper inventoryHelper;
|
||||
private final PlayServices playServices;
|
||||
private final BillingClient billingClient;
|
||||
|
||||
@Inject
|
||||
public FlavorSetup(InventoryHelper inventoryHelper, PlayServices playServices) {
|
||||
this.inventoryHelper = inventoryHelper;
|
||||
public FlavorSetup(PlayServices playServices,
|
||||
BillingClient billingClient) {
|
||||
this.playServices = playServices;
|
||||
this.billingClient = billingClient;
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
inventoryHelper.initialize();
|
||||
billingClient.initialize();
|
||||
playServices.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
package org.tasks.billing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.IntentFilter;
|
||||
import com.android.vending.billing.IabBroadcastReceiver;
|
||||
import com.android.vending.billing.IabHelper;
|
||||
import com.android.vending.billing.Inventory;
|
||||
import com.android.vending.billing.Purchase;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.tasks.LocalBroadcastManager;
|
||||
import org.tasks.R;
|
||||
import org.tasks.injection.ApplicationScope;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import org.tasks.preferences.Preferences;
|
||||
import timber.log.Timber;
|
||||
|
||||
@ApplicationScope
|
||||
public class InventoryHelper implements IabBroadcastReceiver.IabBroadcastListener {
|
||||
|
||||
private final Context context;
|
||||
private final Preferences preferences;
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
private final Executor executor;
|
||||
|
||||
private Inventory inventory;
|
||||
|
||||
@Inject
|
||||
public InventoryHelper(
|
||||
@ForApplication Context context,
|
||||
Preferences preferences,
|
||||
LocalBroadcastManager localBroadcastManager,
|
||||
@Named("iab-executor") Executor executor) {
|
||||
this.context = context;
|
||||
this.preferences = preferences;
|
||||
this.localBroadcastManager = localBroadcastManager;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
context.registerReceiver(
|
||||
new IabBroadcastReceiver(this), new IntentFilter(IabBroadcastReceiver.ACTION));
|
||||
refreshInventory();
|
||||
}
|
||||
|
||||
public void refreshInventory() {
|
||||
final IabHelper helper = new IabHelper(context, context.getString(R.string.gp_key), executor);
|
||||
helper.startSetup(getSetupListener(helper));
|
||||
}
|
||||
|
||||
private IabHelper.OnIabSetupFinishedListener getSetupListener(final IabHelper helper) {
|
||||
return result -> {
|
||||
if (result.isSuccess()) {
|
||||
helper.queryInventoryAsync(getQueryListener(helper));
|
||||
} else {
|
||||
Timber.e("setup failed: %s", result.getMessage());
|
||||
helper.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private IabHelper.QueryInventoryFinishedListener getQueryListener(final IabHelper helper) {
|
||||
return (result, inv) -> {
|
||||
if (result.isSuccess()) {
|
||||
inventory = inv;
|
||||
checkPurchase(R.string.sku_tasker, R.string.p_purchased_tasker);
|
||||
checkPurchase(R.string.sku_dashclock, R.string.p_purchased_dashclock);
|
||||
checkPurchase(R.string.sku_themes, R.string.p_purchased_themes);
|
||||
localBroadcastManager.broadcastRefresh();
|
||||
} else {
|
||||
Timber.e("query inventory failed: %s", result.getMessage());
|
||||
}
|
||||
helper.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receivedBroadcast() {
|
||||
refreshInventory();
|
||||
}
|
||||
|
||||
private void checkPurchase(int skuRes, final int prefRes) {
|
||||
final String sku = context.getString(skuRes);
|
||||
if (inventory.hasPurchase(sku)) {
|
||||
Timber.d("Found purchase: %s", sku);
|
||||
preferences.setBoolean(prefRes, true);
|
||||
} else {
|
||||
Timber.d("No purchase: %s", sku);
|
||||
}
|
||||
}
|
||||
|
||||
public void erasePurchase(String sku) {
|
||||
inventory.erasePurchase(sku);
|
||||
}
|
||||
|
||||
public Purchase getPurchase(String sku) {
|
||||
return inventory.getPurchase(sku);
|
||||
}
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
package org.tasks.billing;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.widget.Toast;
|
||||
import com.android.vending.billing.IabHelper;
|
||||
import com.android.vending.billing.IabResult;
|
||||
import com.android.vending.billing.Purchase;
|
||||
import com.google.common.base.Strings;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import org.tasks.BuildConfig;
|
||||
import org.tasks.LocalBroadcastManager;
|
||||
import org.tasks.R;
|
||||
import org.tasks.analytics.Tracker;
|
||||
import org.tasks.injection.ApplicationScope;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import org.tasks.preferences.Preferences;
|
||||
import timber.log.Timber;
|
||||
|
||||
@ApplicationScope
|
||||
public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener {
|
||||
|
||||
private final Context context;
|
||||
private final Preferences preferences;
|
||||
private final Tracker tracker;
|
||||
private final InventoryHelper inventory;
|
||||
private final Executor executor;
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
|
||||
private PurchaseHelperCallback activityResultCallback;
|
||||
private IabHelper iabHelper;
|
||||
|
||||
@Inject
|
||||
public PurchaseHelper(
|
||||
@ForApplication Context context,
|
||||
Preferences preferences,
|
||||
Tracker tracker,
|
||||
InventoryHelper inventory,
|
||||
@Named("iab-executor") Executor executor,
|
||||
LocalBroadcastManager localBroadcastManager) {
|
||||
this.context = context;
|
||||
this.preferences = preferences;
|
||||
this.tracker = tracker;
|
||||
this.inventory = inventory;
|
||||
this.executor = executor;
|
||||
this.localBroadcastManager = localBroadcastManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIabSetupFinished(IabResult result) {
|
||||
if (result.isFailure()) {
|
||||
Timber.e("in-app billing setup failed: %s", result.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean purchase(
|
||||
final Activity activity,
|
||||
final String sku,
|
||||
final String pref,
|
||||
final int requestCode,
|
||||
final PurchaseHelperCallback callback) {
|
||||
launchPurchaseFlow(activity, sku, pref, requestCode, callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void consumePurchases() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
final List<Purchase> purchases = new ArrayList<>();
|
||||
final Purchase tasker = inventory.getPurchase(context.getString(R.string.sku_tasker));
|
||||
final Purchase dashclock = inventory.getPurchase(context.getString(R.string.sku_dashclock));
|
||||
final Purchase themes = inventory.getPurchase(context.getString(R.string.sku_themes));
|
||||
if (tasker != null) {
|
||||
purchases.add(tasker);
|
||||
}
|
||||
if (dashclock != null) {
|
||||
purchases.add(dashclock);
|
||||
}
|
||||
if (themes != null) {
|
||||
purchases.add(themes);
|
||||
}
|
||||
final IabHelper iabHelper =
|
||||
new IabHelper(context, context.getString(R.string.gp_key), executor);
|
||||
iabHelper.enableDebugLogging(true);
|
||||
iabHelper.startSetup(
|
||||
result -> {
|
||||
if (result.isSuccess()) {
|
||||
iabHelper.consumeAsync(
|
||||
purchases,
|
||||
(purchases1, results) -> {
|
||||
for (int i = 0; i < purchases1.size(); i++) {
|
||||
Purchase purchase = purchases1.get(i);
|
||||
IabResult iabResult = results.get(i);
|
||||
if (iabResult.isSuccess()) {
|
||||
if (purchase.equals(tasker)) {
|
||||
preferences.setBoolean(R.string.p_purchased_tasker, false);
|
||||
} else if (purchase.equals(dashclock)) {
|
||||
preferences.setBoolean(R.string.p_purchased_dashclock, false);
|
||||
} else if (purchase.equals(themes)) {
|
||||
preferences.setBoolean(R.string.p_purchased_themes, false);
|
||||
} else {
|
||||
Timber.e("Unhandled consumption for purchase: %s", purchase);
|
||||
}
|
||||
inventory.erasePurchase(purchase.getSku());
|
||||
Timber.d("Consumed %s", purchase);
|
||||
} else {
|
||||
Timber.e("Consume failed: %s, %s", purchase, iabResult);
|
||||
}
|
||||
}
|
||||
iabHelper.dispose();
|
||||
});
|
||||
} else {
|
||||
Timber.e("setup failed: %s", result.getMessage());
|
||||
iabHelper.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void launchPurchaseFlow(
|
||||
final Activity activity,
|
||||
final String sku,
|
||||
final String pref,
|
||||
final int requestCode,
|
||||
final PurchaseHelperCallback callback) {
|
||||
if (iabHelper != null) {
|
||||
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
|
||||
callback.purchaseCompleted(false, sku);
|
||||
return;
|
||||
}
|
||||
iabHelper = new IabHelper(context, context.getString(R.string.gp_key), executor);
|
||||
iabHelper.enableDebugLogging(BuildConfig.DEBUG);
|
||||
Timber.d("%s: startSetup", iabHelper);
|
||||
iabHelper.startSetup(
|
||||
result -> {
|
||||
if (result.isSuccess()) {
|
||||
try {
|
||||
Timber.d("%s: launchPurchaseFlow for %s", iabHelper, sku);
|
||||
iabHelper.launchPurchaseFlow(
|
||||
activity,
|
||||
sku,
|
||||
requestCode,
|
||||
(result1, info) -> {
|
||||
Timber.d(result1.toString());
|
||||
tracker.reportIabResult(result1, sku);
|
||||
if (result1.isSuccess()) {
|
||||
if (!Strings.isNullOrEmpty(pref)) {
|
||||
preferences.setBoolean(pref, true);
|
||||
localBroadcastManager.broadcastRefresh();
|
||||
}
|
||||
inventory.refreshInventory();
|
||||
} else if (result1.getResponse()
|
||||
!= IabHelper.BILLING_RESPONSE_RESULT_USER_CANCELED
|
||||
&& result1.getResponse() != IabHelper.IABHELPER_USER_CANCELLED) {
|
||||
Toast.makeText(activity, result1.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
if (activityResultCallback != null) {
|
||||
activityResultCallback.purchaseCompleted(result1.isSuccess(), sku);
|
||||
}
|
||||
disposeIabHelper();
|
||||
});
|
||||
} catch (IllegalStateException e) {
|
||||
tracker.reportException(e);
|
||||
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
|
||||
callback.purchaseCompleted(false, sku);
|
||||
disposeIabHelper();
|
||||
}
|
||||
} else {
|
||||
Timber.e(result.toString());
|
||||
Toast.makeText(activity, result.getMessage(), Toast.LENGTH_LONG).show();
|
||||
callback.purchaseCompleted(false, sku);
|
||||
disposeIabHelper();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void disposeIabHelper() {
|
||||
if (iabHelper != null) {
|
||||
Timber.d("%s: dispose", iabHelper);
|
||||
iabHelper.dispose();
|
||||
iabHelper = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleActivityResult(
|
||||
PurchaseHelperCallback callback, int requestCode, int resultCode, Intent data) {
|
||||
this.activityResultCallback = callback;
|
||||
|
||||
if (iabHelper != null) {
|
||||
iabHelper.handleActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="gp_key">MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8mXRE3dDXwtinUILCEzKjov2rxs3kZbLRzNrcjFWXpG9OEsUzRGLzqEN+WwibVuMRpZLj/+IxbU2sJWq/M0q+90rOhmXn46ZPeNyr77IqX2pWKIAWpzBoWq/mshRwtm9m1FIiGdBNlXrhSE7u3TGB5FuEuuSqKWvWzxeqQ7fHmlM04Lqrh1mN3FaMne8rWv+DWVHDbLrtnXBuC36glOAj17HxrzaE2v6Pv7Df3QefJ3rM1+0fAp/5jNInaP0qHAlG8WTbUmDShQ5kG3urbv3HLByyx6TSqhmNudXUK/6TusvIj50OptAG7x+UFYf956dD3diXhY3yoICvyFWx1sNwIDAQAB</string>
|
||||
<string name="sku_themes">themes</string>
|
||||
<string name="play_services_available">play_services_available</string>
|
||||
</resources>
|
||||
@ -0,0 +1,262 @@
|
||||
package org.tasks.billing;
|
||||
|
||||
import static com.google.common.collect.Iterables.transform;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import com.android.billingclient.api.BillingClient.BillingResponse;
|
||||
import com.android.billingclient.api.BillingClient.FeatureType;
|
||||
import com.android.billingclient.api.BillingClient.SkuType;
|
||||
import com.android.billingclient.api.BillingFlowParams;
|
||||
import com.android.billingclient.api.ConsumeResponseListener;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.Purchase.PurchasesResult;
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener;
|
||||
import com.android.billingclient.api.SkuDetailsParams;
|
||||
import com.android.billingclient.api.SkuDetailsParams.Builder;
|
||||
import com.android.billingclient.api.SkuDetailsResponseListener;
|
||||
import com.google.common.base.Joiner;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.BuildConfig;
|
||||
import org.tasks.LocalBroadcastManager;
|
||||
import org.tasks.analytics.Tracker;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BillingClient implements PurchasesUpdatedListener {
|
||||
|
||||
private final Inventory inventory;
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
private final Tracker tracker;
|
||||
|
||||
private com.android.billingclient.api.BillingClient billingClient;
|
||||
private boolean connected;
|
||||
private int billingClientResponseCode = -1;
|
||||
|
||||
@Inject
|
||||
public BillingClient(
|
||||
@ForApplication Context context,
|
||||
Inventory inventory,
|
||||
LocalBroadcastManager localBroadcastManager,
|
||||
Tracker tracker) {
|
||||
this.inventory = inventory;
|
||||
this.localBroadcastManager = localBroadcastManager;
|
||||
this.tracker = tracker;
|
||||
billingClient =
|
||||
com.android.billingclient.api.BillingClient.newBuilder(context).setListener(this).build();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
startServiceConnection(this::queryPurchases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query purchases across various use cases and deliver the result in a formalized way through a
|
||||
* listener
|
||||
*/
|
||||
public void queryPurchases() {
|
||||
Runnable queryToExecute =
|
||||
() -> {
|
||||
long time = System.currentTimeMillis();
|
||||
PurchasesResult purchasesResult = billingClient.queryPurchases(SkuType.INAPP);
|
||||
Timber.i("Querying purchases elapsed time: %sms", System.currentTimeMillis() - time);
|
||||
// If there are subscriptions supported, we add subscription rows as well
|
||||
if (areSubscriptionsSupported()) {
|
||||
PurchasesResult subscriptionResult = billingClient.queryPurchases(SkuType.SUBS);
|
||||
Timber.i(
|
||||
"Querying purchases and subscriptions elapsed time: %sms",
|
||||
System.currentTimeMillis() - time);
|
||||
Timber.i(
|
||||
"Querying subscriptions result code: %s res: %s",
|
||||
subscriptionResult.getResponseCode(), subscriptionResult.getPurchasesList().size());
|
||||
if (subscriptionResult.getResponseCode() == BillingResponse.OK) {
|
||||
purchasesResult.getPurchasesList().addAll(subscriptionResult.getPurchasesList());
|
||||
} else {
|
||||
Timber.e("Got an error response trying to query subscription purchases");
|
||||
}
|
||||
} else if (purchasesResult.getResponseCode() == BillingResponse.OK) {
|
||||
Timber.i("Skipped subscription purchases query since they are not supported");
|
||||
} else {
|
||||
Timber.w(
|
||||
"queryPurchases() got an error response code: %s",
|
||||
purchasesResult.getResponseCode());
|
||||
}
|
||||
onQueryPurchasesFinished(purchasesResult);
|
||||
};
|
||||
|
||||
executeServiceRequest(queryToExecute);
|
||||
}
|
||||
|
||||
/** Handle a result from querying of purchases and report an updated list to the listener */
|
||||
private void onQueryPurchasesFinished(PurchasesResult result) {
|
||||
// Have we been disposed of in the meantime? If so, or bad result code, then quit
|
||||
if (billingClient == null || result.getResponseCode() != BillingResponse.OK) {
|
||||
Timber.w(
|
||||
"Billing client was null or result code (%s) was bad - quitting",
|
||||
result.getResponseCode());
|
||||
return;
|
||||
}
|
||||
|
||||
Timber.d("Query inventory was successful.");
|
||||
|
||||
// Update the UI and purchases inventory with new list of purchases
|
||||
inventory.clear();
|
||||
onPurchasesUpdated(BillingResponse.OK, result.getPurchasesList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(@BillingResponse int resultCode, List<Purchase> purchases) {
|
||||
if (resultCode == BillingResponse.OK) {
|
||||
inventory.add(purchases);
|
||||
localBroadcastManager.broadcastPurchasesUpdated();
|
||||
} else {
|
||||
String skus =
|
||||
purchases == null ? "null" : Joiner.on(";").join(transform(purchases, Purchase::getSku));
|
||||
Timber.i("onPurchasesUpdate(%s, %s)", BillingResponseToString(resultCode), skus);
|
||||
tracker.reportIabResult(resultCode, skus);
|
||||
}
|
||||
}
|
||||
|
||||
/** Start a purchase flow */
|
||||
void initiatePurchaseFlow(
|
||||
Activity activity, final String skuId, final @SkuType String billingType) {
|
||||
Runnable purchaseFlowRequest =
|
||||
() -> {
|
||||
Timber.d("Launching in-app purchase flow");
|
||||
BillingFlowParams purchaseParams =
|
||||
BillingFlowParams.newBuilder()
|
||||
.setSku(skuId)
|
||||
.setType(billingType)
|
||||
.setOldSkus(null)
|
||||
.build();
|
||||
billingClient.launchBillingFlow(activity, purchaseParams);
|
||||
};
|
||||
|
||||
executeServiceRequest(purchaseFlowRequest);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
Timber.d("Destroying the manager.");
|
||||
|
||||
if (billingClient != null && billingClient.isReady()) {
|
||||
billingClient.endConnection();
|
||||
billingClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startServiceConnection(final Runnable executeOnSuccess) {
|
||||
billingClient.startConnection(
|
||||
new com.android.billingclient.api.BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(@BillingResponse int billingResponseCode) {
|
||||
Timber.d("onBillingSetupFinished(%s)", billingResponseCode);
|
||||
|
||||
if (billingResponseCode == BillingResponse.OK) {
|
||||
connected = true;
|
||||
if (executeOnSuccess != null) {
|
||||
executeOnSuccess.run();
|
||||
}
|
||||
}
|
||||
billingClientResponseCode = billingResponseCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
Timber.d("onBillingServiceDisconnected()");
|
||||
connected = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void executeServiceRequest(Runnable runnable) {
|
||||
if (connected) {
|
||||
runnable.run();
|
||||
} else {
|
||||
// If billing service was disconnected, we try to reconnect 1 time.
|
||||
// (feel free to introduce your retry policy here).
|
||||
startServiceConnection(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if subscriptions are supported for current client
|
||||
*
|
||||
* <p>Note: This method does not automatically retry for RESULT_SERVICE_DISCONNECTED. It is only
|
||||
* used in unit tests and after queryPurchases execution, which already has a retry-mechanism
|
||||
* implemented.
|
||||
*/
|
||||
private boolean areSubscriptionsSupported() {
|
||||
int responseCode = billingClient.isFeatureSupported(FeatureType.SUBSCRIPTIONS);
|
||||
if (responseCode != BillingResponse.OK) {
|
||||
Timber.d("areSubscriptionsSupported() got an error response: %s", responseCode);
|
||||
}
|
||||
return responseCode == BillingResponse.OK;
|
||||
}
|
||||
|
||||
public void querySkuDetailsAsync(
|
||||
@SkuType final String itemType,
|
||||
final List<String> skuList,
|
||||
final SkuDetailsResponseListener listener) {
|
||||
Runnable request =
|
||||
() -> {
|
||||
Builder params = SkuDetailsParams.newBuilder();
|
||||
params.setSkusList(skuList).setType(itemType);
|
||||
billingClient.querySkuDetailsAsync(params.build(), listener);
|
||||
};
|
||||
executeServiceRequest(request);
|
||||
}
|
||||
|
||||
public void consume(String sku) {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!inventory.purchased(sku)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
final ConsumeResponseListener onConsumeListener =
|
||||
(responseCode, purchaseToken1) -> {
|
||||
Timber.d("onConsumeResponse(%s, %s)", responseCode, purchaseToken1);
|
||||
queryPurchases();
|
||||
};
|
||||
|
||||
Runnable request =
|
||||
() ->
|
||||
billingClient.consumeAsync(
|
||||
inventory.getPurchase(sku).getPurchaseToken(), onConsumeListener);
|
||||
executeServiceRequest(request);
|
||||
}
|
||||
|
||||
public int getBillingClientResponseCode() {
|
||||
return billingClientResponseCode;
|
||||
}
|
||||
|
||||
public static String BillingResponseToString(@BillingResponse int response) {
|
||||
switch (response) {
|
||||
case BillingResponse.FEATURE_NOT_SUPPORTED:
|
||||
return "FEATURE_NOT_SUPPORTED";
|
||||
case BillingResponse.SERVICE_DISCONNECTED:
|
||||
return "SERVICE_DISCONNECTED";
|
||||
case BillingResponse.OK:
|
||||
return "OK";
|
||||
case BillingResponse.USER_CANCELED:
|
||||
return "USER_CANCELED";
|
||||
case BillingResponse.SERVICE_UNAVAILABLE:
|
||||
return "SERVICE_UNAVAILABLE";
|
||||
case BillingResponse.BILLING_UNAVAILABLE:
|
||||
return "BILLING_UNAVAILABLE";
|
||||
case BillingResponse.ITEM_UNAVAILABLE:
|
||||
return "ITEM_UNAVAILABLE";
|
||||
case BillingResponse.DEVELOPER_ERROR:
|
||||
return "DEVELOPER_ERROR";
|
||||
case BillingResponse.ERROR:
|
||||
return "ERROR";
|
||||
case BillingResponse.ITEM_ALREADY_OWNED:
|
||||
return "ITEM_ALREADY_OWNED";
|
||||
case BillingResponse.ITEM_NOT_OWNED:
|
||||
return "ITEM_NOT_OWNED";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
// Copyright 2017 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package org.tasks.billing;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import org.tasks.billing.row.RowDataProvider;
|
||||
import org.tasks.billing.row.SkuRowData;
|
||||
|
||||
/**
|
||||
* A separator for RecyclerView that keeps the specified spaces between headers and the cards.
|
||||
*/
|
||||
public class CardsWithHeadersDecoration extends RecyclerView.ItemDecoration {
|
||||
|
||||
private final RowDataProvider mRowDataProvider;
|
||||
private final int mHeaderGap, mRowGap;
|
||||
|
||||
public CardsWithHeadersDecoration(RowDataProvider rowDataProvider, int headerGap,
|
||||
int rowGap) {
|
||||
this.mRowDataProvider = rowDataProvider;
|
||||
this.mHeaderGap = headerGap;
|
||||
this.mRowGap = rowGap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
|
||||
RecyclerView.State state) {
|
||||
|
||||
final int position = parent.getChildAdapterPosition(view);
|
||||
final SkuRowData data = mRowDataProvider.getData(position);
|
||||
|
||||
// We should add a space on top of every header card
|
||||
if (data.getRowType() == SkusAdapter.TYPE_HEADER || position == 0) {
|
||||
outRect.top = mHeaderGap;
|
||||
}
|
||||
|
||||
// Adding a space under the last item
|
||||
if (position == parent.getAdapter().getItemCount() - 1) {
|
||||
outRect.bottom = mHeaderGap;
|
||||
} else {
|
||||
outRect.bottom = mRowGap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package org.tasks.billing;
|
||||
|
||||
import android.content.Context;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.R;
|
||||
import org.tasks.injection.ApplicationScope;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import org.tasks.preferences.Preferences;
|
||||
import timber.log.Timber;
|
||||
|
||||
@ApplicationScope
|
||||
public class Inventory {
|
||||
|
||||
private static final String SKU_PRO = "annual_499";
|
||||
static final String SKU_VIP = "vip";
|
||||
static final String SKU_TASKER = "tasker";
|
||||
static final String SKU_THEMES = "themes";
|
||||
static final String SKU_DASHCLOCK = "dashclock";
|
||||
|
||||
public static final List<String> SKU_SUBS = ImmutableList.of(SKU_PRO);
|
||||
|
||||
private final Preferences preferences;
|
||||
private final String billingKey;
|
||||
|
||||
private Map<String, Purchase> purchases = new HashMap<>();
|
||||
|
||||
@Inject
|
||||
public Inventory(@ForApplication Context context, Preferences preferences) {
|
||||
this.preferences = preferences;
|
||||
billingKey = context.getString(R.string.gp_key);
|
||||
for (Purchase purchase : preferences.getPurchases()) {
|
||||
add(purchase);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
Timber.d("clear()");
|
||||
purchases.clear();
|
||||
}
|
||||
|
||||
public void add(List<Purchase> purchases) {
|
||||
for (Purchase purchase : purchases) {
|
||||
add(purchase);
|
||||
}
|
||||
preferences.setPurchases(this.purchases.values());
|
||||
}
|
||||
|
||||
private void add(Purchase purchase) {
|
||||
if (verifySignature(purchase)) {
|
||||
Timber.d("add(%s)", purchase);
|
||||
purchases.put(purchase.getSku(), purchase);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean purchasedTasker() {
|
||||
return hasPro() || purchases.containsKey(SKU_TASKER);
|
||||
}
|
||||
|
||||
public boolean purchasedDashclock() {
|
||||
return hasPro() || purchases.containsKey(SKU_DASHCLOCK);
|
||||
}
|
||||
|
||||
public boolean purchasedThemes() {
|
||||
return hasPro() || purchases.containsKey(SKU_THEMES);
|
||||
}
|
||||
|
||||
public List<Purchase> getPurchases() {
|
||||
return ImmutableList.copyOf(purchases.values());
|
||||
}
|
||||
|
||||
public boolean hasPro() {
|
||||
return purchases.containsKey(SKU_PRO) || purchases.containsKey(SKU_VIP);
|
||||
}
|
||||
|
||||
public boolean purchased(String sku) {
|
||||
return purchases.containsKey(sku);
|
||||
}
|
||||
|
||||
private boolean verifySignature(Purchase purchase) {
|
||||
try {
|
||||
return Security.verifyPurchase(
|
||||
billingKey, purchase.getOriginalJson(), purchase.getSignature());
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Purchase getPurchase(String sku) {
|
||||
return purchases.get(sku);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
package org.tasks.billing;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static com.google.common.collect.Iterables.any;
|
||||
import static com.google.common.collect.Iterables.filter;
|
||||
import static com.google.common.collect.Lists.newArrayList;
|
||||
import static com.google.common.collect.Lists.transform;
|
||||
import static org.tasks.billing.Inventory.SKU_DASHCLOCK;
|
||||
import static org.tasks.billing.Inventory.SKU_TASKER;
|
||||
import static org.tasks.billing.Inventory.SKU_THEMES;
|
||||
import static org.tasks.billing.Inventory.SKU_VIP;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.android.billingclient.api.BillingClient.BillingResponse;
|
||||
import com.android.billingclient.api.BillingClient.SkuType;
|
||||
import com.android.billingclient.api.Purchase;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.BuildConfig;
|
||||
import org.tasks.LocalBroadcastManager;
|
||||
import org.tasks.R;
|
||||
import org.tasks.billing.SkusAdapter.OnClickHandler;
|
||||
import org.tasks.billing.row.SkuRowData;
|
||||
import org.tasks.injection.ActivityComponent;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import org.tasks.injection.ThemedInjectingAppCompatActivity;
|
||||
import org.tasks.preferences.HelpAndFeedbackActivity;
|
||||
import org.tasks.ui.MenuColorizer;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class PurchaseActivity extends ThemedInjectingAppCompatActivity
|
||||
implements OnClickHandler, OnMenuItemClickListener {
|
||||
|
||||
private static final List<String> DEBUG_SKUS =
|
||||
ImmutableList.of(SKU_THEMES, SKU_TASKER, SKU_DASHCLOCK, SKU_VIP);
|
||||
|
||||
@Inject @ForApplication Context context;
|
||||
@Inject BillingClient billingClient;
|
||||
@Inject Inventory inventory;
|
||||
@Inject LocalBroadcastManager localBroadcastManager;
|
||||
|
||||
private SkusAdapter adapter;
|
||||
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
|
||||
@BindView(R.id.list)
|
||||
RecyclerView recyclerView;
|
||||
|
||||
@BindView(R.id.screen_wait)
|
||||
View loadingView;
|
||||
|
||||
@BindView(R.id.error_textview)
|
||||
TextView errorTextView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_purchase);
|
||||
|
||||
ButterKnife.bind(this);
|
||||
|
||||
toolbar.setTitle(R.string.upgrade);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp);
|
||||
toolbar.setNavigationOnClickListener(v -> onBackPressed());
|
||||
toolbar.inflateMenu(R.menu.menu_purchase_activity);
|
||||
toolbar.setOnMenuItemClickListener(this);
|
||||
MenuColorizer.colorToolbar(this, toolbar);
|
||||
|
||||
adapter = new SkusAdapter(context, inventory, this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
Resources res = getResources();
|
||||
recyclerView.addItemDecoration(
|
||||
new CardsWithHeadersDecoration(
|
||||
adapter,
|
||||
(int) res.getDimension(R.dimen.header_gap),
|
||||
(int) res.getDimension(R.dimen.row_gap)));
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(context));
|
||||
setWaitScreen(true);
|
||||
querySkuDetails();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
querySkuDetails();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
localBroadcastManager.unregisterReceiver(purchaseReceiver);
|
||||
}
|
||||
|
||||
/** Queries for in-app and subscriptions SKU details and updates an adapter with new data */
|
||||
private void querySkuDetails() {
|
||||
if (!isFinishing()) {
|
||||
List<SkuRowData> data = new ArrayList<>();
|
||||
String owned = getString(R.string.owned);
|
||||
String debug = getString(R.string.debug);
|
||||
Runnable addDebug =
|
||||
BuildConfig.DEBUG
|
||||
? () ->
|
||||
addSkuRows(
|
||||
data,
|
||||
newArrayList(
|
||||
filter(DEBUG_SKUS, sku -> !any(data, row -> sku.equals(row.getSku())))),
|
||||
debug,
|
||||
SkuType.INAPP,
|
||||
null)
|
||||
: null;
|
||||
Runnable addIaps =
|
||||
() ->
|
||||
addSkuRows(
|
||||
data,
|
||||
newArrayList(
|
||||
filter(
|
||||
transform(inventory.getPurchases(), Purchase::getSku),
|
||||
sku1 -> !Inventory.SKU_SUBS.contains(sku1))),
|
||||
owned,
|
||||
SkuType.INAPP,
|
||||
addDebug);
|
||||
addSkuRows(data, Inventory.SKU_SUBS, null, SkuType.SUBS, addIaps);
|
||||
}
|
||||
}
|
||||
|
||||
private void addSkuRows(
|
||||
List<SkuRowData> data,
|
||||
List<String> skus,
|
||||
String title,
|
||||
@SkuType String skuType,
|
||||
Runnable whenFinished) {
|
||||
billingClient.querySkuDetailsAsync(
|
||||
skuType,
|
||||
skus,
|
||||
(responseCode, skuDetailsList) -> {
|
||||
if (responseCode != BillingResponse.OK) {
|
||||
Timber.w("Unsuccessful query for type: " + skuType + ". Error code: " + responseCode);
|
||||
} else if (skuDetailsList != null && skuDetailsList.size() > 0) {
|
||||
if (!isEmpty(title)) {
|
||||
data.add(new SkuRowData(title));
|
||||
}
|
||||
Timber.d("Adding %s skus", skuDetailsList.size());
|
||||
// Then fill all the other rows
|
||||
for (SkuDetails details : skuDetailsList) {
|
||||
Timber.i("Adding sku: %s", details);
|
||||
data.add(new SkuRowData(details, SkusAdapter.TYPE_NORMAL, skuType));
|
||||
}
|
||||
|
||||
if (data.size() == 0) {
|
||||
displayAnErrorIfNeeded();
|
||||
} else {
|
||||
adapter.setData(data);
|
||||
setWaitScreen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (whenFinished != null) {
|
||||
whenFinished.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayAnErrorIfNeeded() {
|
||||
if (!isFinishing()) {
|
||||
loadingView.setVisibility(View.GONE);
|
||||
errorTextView.setVisibility(View.VISIBLE);
|
||||
errorTextView.setText(
|
||||
billingClient.getBillingClientResponseCode() == BillingResponse.BILLING_UNAVAILABLE
|
||||
? R.string.error_billing_unavailable
|
||||
: R.string.error_billing_default);
|
||||
}
|
||||
}
|
||||
|
||||
private void setWaitScreen(boolean set) {
|
||||
recyclerView.setVisibility(set ? View.GONE : View.VISIBLE);
|
||||
loadingView.setVisibility(set ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inject(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(SkuRowData skuRowData) {
|
||||
String sku = skuRowData.getSku();
|
||||
String skuType = skuRowData.getSkuType();
|
||||
if (inventory.purchased(sku)) {
|
||||
if (BuildConfig.DEBUG && SkuType.INAPP.equals(skuType)) {
|
||||
billingClient.consume(sku);
|
||||
}
|
||||
} else {
|
||||
billingClient.initiatePurchaseFlow(this, sku, skuType);
|
||||
}
|
||||
}
|
||||
|
||||
private BroadcastReceiver purchaseReceiver =
|
||||
new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
querySkuDetails();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_help:
|
||||
startActivity(new Intent(this, HelpAndFeedbackActivity.class));
|
||||
return true;
|
||||
case R.id.menu_refresh_purchases:
|
||||
billingClient.queryPurchases();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
package org.tasks.billing;
|
||||
|
||||
public interface PurchaseHelperCallback {
|
||||
|
||||
void purchaseCompleted(boolean success, String sku);
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2017 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.tasks.billing;
|
||||
|
||||
import static com.google.common.collect.Lists.transform;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.android.billingclient.api.BillingClient.SkuType;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.tasks.BuildConfig;
|
||||
import org.tasks.R;
|
||||
import org.tasks.billing.row.RowDataProvider;
|
||||
import org.tasks.billing.row.RowViewHolder;
|
||||
import org.tasks.billing.row.SkuRowData;
|
||||
|
||||
public class SkusAdapter extends RecyclerView.Adapter<RowViewHolder> implements RowDataProvider {
|
||||
|
||||
public static final int TYPE_HEADER = 0;
|
||||
public static final int TYPE_NORMAL = 1;
|
||||
private final Context context;
|
||||
private final Inventory inventory;
|
||||
private final OnClickHandler onClickHandler;
|
||||
private List<SkuRowData> data = ImmutableList.of();
|
||||
|
||||
SkusAdapter(Context context, Inventory inventory, OnClickHandler onClickHandler) {
|
||||
this.context = context;
|
||||
this.inventory = inventory;
|
||||
this.onClickHandler = onClickHandler;
|
||||
}
|
||||
|
||||
public void setData(List<SkuRowData> data) {
|
||||
this.data = data;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @RowTypeDef int getItemViewType(int position) {
|
||||
return data.isEmpty() ? TYPE_HEADER : data.get(position).getRowType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public RowViewHolder onCreateViewHolder(@Nonnull ViewGroup parent, @RowTypeDef int viewType) {
|
||||
// Selecting a flat layout for header rows
|
||||
if (viewType == SkusAdapter.TYPE_HEADER) {
|
||||
View item =
|
||||
LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.sku_details_row_header, parent, false);
|
||||
return new RowViewHolder(item, null);
|
||||
} else {
|
||||
View item =
|
||||
LayoutInflater.from(parent.getContext()).inflate(R.layout.sku_details_row, parent, false);
|
||||
return new RowViewHolder(item, row -> onClickHandler.click(getData(row)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@Nonnull RowViewHolder holder, int position) {
|
||||
SkuRowData data = getData(position);
|
||||
if (data != null) {
|
||||
holder.title.setText(data.getTitle());
|
||||
if (getItemViewType(position) != SkusAdapter.TYPE_HEADER) {
|
||||
|
||||
String sku = data.getSku();
|
||||
if (SkuType.SUBS.equals(data.getSkuType())) {
|
||||
String[] rows = context.getResources().getStringArray(R.array.pro_description);
|
||||
holder.description.setText(
|
||||
Joiner.on('\n').join(transform(asList(rows), item -> "\u2022 " + item)));
|
||||
holder.button.setVisibility(View.VISIBLE);
|
||||
holder.price.setVisibility(View.VISIBLE);
|
||||
holder.price.setText(data.getPrice());
|
||||
holder.button.setText(
|
||||
inventory.purchased(sku) ? R.string.button_subscribed : R.string.button_subscribe);
|
||||
} else {
|
||||
holder.description.setText(data.getDescription());
|
||||
holder.button.setVisibility(View.GONE);
|
||||
holder.price.setVisibility(View.GONE);
|
||||
if (BuildConfig.DEBUG) {
|
||||
holder.button.setVisibility(View.VISIBLE);
|
||||
holder.button.setText(
|
||||
inventory.purchased(sku) ? R.string.debug_consume : R.string.debug_buy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SkuRowData getData(int position) {
|
||||
return data.isEmpty() ? null : data.get(position);
|
||||
}
|
||||
|
||||
public interface OnClickHandler {
|
||||
void click(SkuRowData skuRowData);
|
||||
}
|
||||
|
||||
/** Types for adapter rows */
|
||||
@Retention(SOURCE)
|
||||
@IntDef({TYPE_HEADER, TYPE_NORMAL})
|
||||
public @interface RowTypeDef {}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2017 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.tasks.billing.row;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Provider for data that corresponds to a particular row
|
||||
*/
|
||||
public interface RowDataProvider {
|
||||
SkuRowData getData(int position);
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package org.tasks.billing.row;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import org.tasks.R;
|
||||
|
||||
public final class RowViewHolder extends RecyclerView.ViewHolder {
|
||||
public final TextView title;
|
||||
public final TextView description;
|
||||
public final TextView price;
|
||||
public final Button button;
|
||||
|
||||
public interface ButtonClick {
|
||||
void onClick(int row);
|
||||
}
|
||||
|
||||
public RowViewHolder(final View itemView, final ButtonClick onClick) {
|
||||
super(itemView);
|
||||
title = itemView.findViewById(R.id.title);
|
||||
price = itemView.findViewById(R.id.price);
|
||||
description = itemView.findViewById(R.id.description);
|
||||
button = itemView.findViewById(R.id.buy_button);
|
||||
if (button != null) {
|
||||
button.setOnClickListener(view -> onClick.onClick(getAdapterPosition()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2017 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.tasks.billing.row;
|
||||
|
||||
import com.android.billingclient.api.BillingClient.SkuType;
|
||||
import com.android.billingclient.api.SkuDetails;
|
||||
import org.tasks.billing.SkusAdapter;
|
||||
import org.tasks.billing.SkusAdapter.RowTypeDef;
|
||||
|
||||
/**
|
||||
* A model for SkusAdapter's row
|
||||
*/
|
||||
public class SkuRowData {
|
||||
private String sku, title, price, description;
|
||||
private @RowTypeDef int type;
|
||||
private @SkuType String billingType;
|
||||
|
||||
public SkuRowData(SkuDetails details, @RowTypeDef int rowType,
|
||||
@SkuType String billingType) {
|
||||
this.sku = details.getSku();
|
||||
this.title = details.getTitle();
|
||||
this.price = details.getPrice();
|
||||
this.description = details.getDescription();
|
||||
this.type = rowType;
|
||||
this.billingType = billingType;
|
||||
}
|
||||
|
||||
public SkuRowData(String title) {
|
||||
this.title = title;
|
||||
this.type = SkusAdapter.TYPE_HEADER;
|
||||
}
|
||||
|
||||
public String getSku() {
|
||||
return sku;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public @RowTypeDef int getRowType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public @SkuType
|
||||
String getSkuType() {
|
||||
return billingType;
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package org.tasks.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.inject.Inject;
|
||||
import org.tasks.R;
|
||||
import org.tasks.billing.PurchaseHelper;
|
||||
import org.tasks.injection.InjectingNativeDialogFragment;
|
||||
import org.tasks.injection.NativeDialogFragmentComponent;
|
||||
import org.tasks.preferences.BasicPreferences;
|
||||
|
||||
public class DonationDialog extends InjectingNativeDialogFragment {
|
||||
|
||||
@Inject DialogBuilder dialogBuilder;
|
||||
@Inject PurchaseHelper purchaseHelper;
|
||||
|
||||
public static DonationDialog newDonationDialog() {
|
||||
return new DonationDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final List<String> donationValues = getDonationValues();
|
||||
return dialogBuilder
|
||||
.newDialog()
|
||||
.setTitle(R.string.select_amount)
|
||||
.setItems(
|
||||
donationValues,
|
||||
(dialog, which) -> {
|
||||
String value = donationValues.get(which);
|
||||
Pattern pattern = Pattern.compile("\\$(\\d+) USD");
|
||||
Matcher matcher = pattern.matcher(value);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
matcher.matches();
|
||||
String sku =
|
||||
String.format(
|
||||
java.util.Locale.ENGLISH, "%03d", Integer.parseInt(matcher.group(1)));
|
||||
purchaseHelper.purchase(
|
||||
getActivity(),
|
||||
sku,
|
||||
null,
|
||||
BasicPreferences.REQUEST_PURCHASE,
|
||||
(BasicPreferences) getActivity());
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private List<String> getDonationValues() {
|
||||
List<String> values = new ArrayList<>();
|
||||
for (int i = 1; i <= 100; i++) {
|
||||
values.add(String.format("$%s USD", Integer.toString(i)));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void inject(NativeDialogFragmentComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
|
||||
</vector>
|
||||
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/window_background">
|
||||
|
||||
<include layout="@layout/loading_indicator"/>
|
||||
|
||||
<include
|
||||
layout="@layout/toolbar"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_gravity="top" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/error_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="@dimen/card_view_margin"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
||||
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017 Google Inc. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<ProgressBar
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/screen_wait"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"/>
|
||||
@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017 Google Inc. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<android.support.v7.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card_view"
|
||||
style="@style/CardViewStyle"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/content_background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="@dimen/keyline_first"
|
||||
android:layout_marginBottom="@dimen/keyline_first">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingLeft="@dimen/keyline_first"
|
||||
android:paddingStart="@dimen/keyline_first"
|
||||
android:paddingRight="@dimen/keyline_first"
|
||||
android:paddingEnd="@dimen/keyline_first"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="@dimen/sku_details_row_text_size"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/price"
|
||||
android:textStyle="bold"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="0dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingRight="@dimen/keyline_first"
|
||||
android:paddingEnd="@dimen/keyline_first"
|
||||
android:textAlignment="viewStart"
|
||||
android:layout_gravity="start"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="@dimen/sku_details_row_text_size"/>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="@dimen/keyline_first"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="@dimen/sku_details_row_text_size"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/buy_button"
|
||||
style="@style/ButtonStyle"
|
||||
android:layout_marginTop="@dimen/card_view_margin"
|
||||
android:layout_marginBottom="@dimen/card_view_margin"
|
||||
android:layout_marginStart="@dimen/card_view_margin"
|
||||
android:layout_marginEnd="@dimen/card_view_margin"
|
||||
android:contentDescription="@string/button_subscribe"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v7.widget.CardView>
|
||||
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2017 Google Inc. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<android.support.v7.widget.CardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card_view"
|
||||
style="@style/CardViewStyle"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="@color/content_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/TextAppearance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/keyline_first"
|
||||
android:layout_marginBottom="@dimen/keyline_first"
|
||||
android:layout_marginStart="@dimen/keyline_first"
|
||||
android:layout_marginEnd="@dimen/keyline_first"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textSize="@dimen/sku_details_row_text_size"
|
||||
app:fontFamily="sans-serif-medium"/>
|
||||
|
||||
</android.support.v7.widget.CardView>
|
||||
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tasks="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_help"
|
||||
android:icon="@drawable/ic_help_24dp"
|
||||
android:title="@string/help"
|
||||
tasks:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_refresh_purchases"
|
||||
android:title="@string/refresh_purchases"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/get_plugins"
|
||||
android:title="@string/get_plugins">
|
||||
|
||||
<Preference
|
||||
android:selectable="false"
|
||||
android:summary="@string/plugin_description"/>
|
||||
|
||||
<Preference
|
||||
android:key="@string/TLA_menu_donate"
|
||||
android:summary="@string/donate_summary"
|
||||
android:title="@string/TLA_menu_donate"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:dependency="@string/p_purchased_themes"
|
||||
android:disableDependentsState="true"
|
||||
android:key="@string/p_purchased_themes"
|
||||
android:summary="@string/themes_purchase_description"
|
||||
android:title="@string/themes"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:dependency="@string/p_purchased_dashclock"
|
||||
android:disableDependentsState="true"
|
||||
android:key="@string/p_purchased_dashclock"
|
||||
android:summary="@string/dashclock_purchase_description"
|
||||
android:title="@string/dashclock"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:dependency="@string/p_purchased_tasker"
|
||||
android:disableDependentsState="true"
|
||||
android:key="@string/p_purchased_tasker"
|
||||
android:summary="@string/tasker_description"
|
||||
android:title="@string/tasker_locale"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
Loading…
Reference in New Issue