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;
|
package org.tasks;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import org.tasks.billing.InventoryHelper;
|
import org.tasks.billing.BillingClient;
|
||||||
import org.tasks.gtasks.PlayServices;
|
import org.tasks.gtasks.PlayServices;
|
||||||
|
|
||||||
public class FlavorSetup {
|
public class FlavorSetup {
|
||||||
|
|
||||||
private final InventoryHelper inventoryHelper;
|
|
||||||
private final PlayServices playServices;
|
private final PlayServices playServices;
|
||||||
|
private final BillingClient billingClient;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public FlavorSetup(InventoryHelper inventoryHelper, PlayServices playServices) {
|
public FlavorSetup(PlayServices playServices,
|
||||||
this.inventoryHelper = inventoryHelper;
|
BillingClient billingClient) {
|
||||||
this.playServices = playServices;
|
this.playServices = playServices;
|
||||||
|
this.billingClient = billingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setup() {
|
public void setup() {
|
||||||
inventoryHelper.initialize();
|
billingClient.initialize();
|
||||||
playServices.refresh();
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="gp_key">MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8mXRE3dDXwtinUILCEzKjov2rxs3kZbLRzNrcjFWXpG9OEsUzRGLzqEN+WwibVuMRpZLj/+IxbU2sJWq/M0q+90rOhmXn46ZPeNyr77IqX2pWKIAWpzBoWq/mshRwtm9m1FIiGdBNlXrhSE7u3TGB5FuEuuSqKWvWzxeqQ7fHmlM04Lqrh1mN3FaMne8rWv+DWVHDbLrtnXBuC36glOAj17HxrzaE2v6Pv7Df3QefJ3rM1+0fAp/5jNInaP0qHAlG8WTbUmDShQ5kG3urbv3HLByyx6TSqhmNudXUK/6TusvIj50OptAG7x+UFYf956dD3diXhY3yoICvyFWx1sNwIDAQAB</string>
|
<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>
|
<string name="play_services_available">play_services_available</string>
|
||||||
</resources>
|
</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