diff --git a/app/build.gradle b/app/build.gradle
index 3d6f06ddc..fac7b882f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -167,6 +167,7 @@ dependencies {
implementation 'com.google.apis:google-api-services-tasks:v1-rev47-1.22.0'
implementation 'com.google.api-client:google-api-client-android:1.22.0'
+ googleplayImplementation 'com.android.billingclient:billing:1.0'
googleplayImplementation "com.google.android.gms:play-services-location:${GPS_VERSION}"
googleplayImplementation "com.google.android.gms:play-services-analytics:${GPS_VERSION}"
googleplayImplementation "com.google.android.gms:play-services-auth:${GPS_VERSION}"
diff --git a/app/src/googleplay/AndroidManifest.xml b/app/src/googleplay/AndroidManifest.xml
index 44e8864af..a3e0d8ab8 100644
--- a/app/src/googleplay/AndroidManifest.xml
+++ b/app/src/googleplay/AndroidManifest.xml
@@ -3,11 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.tasks">
-
-
-
-
-
diff --git a/app/src/googleplay/aidl/com/android/vending/billing/IInAppBillingService.aidl b/app/src/googleplay/aidl/com/android/vending/billing/IInAppBillingService.aidl
deleted file mode 100644
index 0092998ad..000000000
--- a/app/src/googleplay/aidl/com/android/vending/billing/IInAppBillingService.aidl
+++ /dev/null
@@ -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 oldSkus, String newSku, String type, String developerPayload);
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/IabBroadcastReceiver.java b/app/src/googleplay/java/com/android/vending/billing/IabBroadcastReceiver.java
deleted file mode 100644
index 0bf9e38fb..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/IabBroadcastReceiver.java
+++ /dev/null
@@ -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.
- *
- *
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();
- }
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/IabException.java b/app/src/googleplay/java/com/android/vending/billing/IabException.java
deleted file mode 100644
index adbdd34c8..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/IabException.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/IabHelper.java b/app/src/googleplay/java/com/android/vending/billing/IabHelper.java
deleted file mode 100644
index 8c4c73ebf..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/IabHelper.java
+++ /dev/null
@@ -1,1095 +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.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentSender.SendIntentException;
-import android.content.ServiceConnection;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.text.TextUtils;
-import android.util.Log;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.Executor;
-import org.json.JSONException;
-
-/**
- * Provides convenience methods for in-app billing. You can create one instance of this class for
- * your application and use it to process in-app billing operations. It provides synchronous
- * (blocking) and asynchronous (non-blocking) methods for many common in-app billing operations, as
- * well as automatic signature verification.
- *
- *
After instantiating, you must perform setup in order to start using the object. To perform
- * setup, call the {@link #startSetup} method and provide a listener; that listener will be notified
- * when setup is complete, after which (and not before) you may call other methods.
- *
- *
After setup is complete, you will typically want to request an inventory of owned items and
- * subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} and related methods.
- *
- *
When you are done with this object, don't forget to call {@link #dispose} to ensure proper
- * cleanup. This object holds a binding to the in-app billing service, which will leak unless you
- * dispose of it correctly. If you created the object on an Activity's onCreate method, then the
- * recommended place to dispose of it is the Activity's onDestroy method.
- *
- *
A note about threading: When using this object from a background thread, you may call the
- * blocking versions of methods; when using from a UI thread, call only the asynchronous versions
- * and handle the results via callbacks. Also, notice that you can only call one asynchronous
- * operation at a time; attempting to start a second asynchronous operation while the first one has
- * not yet completed will result in an exception being thrown.
- */
-@SuppressWarnings("ALL")
-@SuppressLint("all")
-public class IabHelper {
-
- // Billing response codes
- public static final int BILLING_RESPONSE_RESULT_OK = 0;
- public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
- public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
- public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
- public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
- public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
- public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
- public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
- public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
- // IAB Helper error codes
- public static final int IABHELPER_ERROR_BASE = -1000;
- public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
- public static final int IABHELPER_BAD_RESPONSE = -1002;
- public static final int IABHELPER_VERIFICATION_FAILED = -1003;
- public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
- public static final int IABHELPER_USER_CANCELLED = -1005;
- public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
- public static final int IABHELPER_MISSING_TOKEN = -1007;
- public static final int IABHELPER_UNKNOWN_ERROR = -1008;
- public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
- public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
- public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011;
- // Keys for the responses from InAppBillingService
- public static final String RESPONSE_CODE = "RESPONSE_CODE";
- public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
- public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
- public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
- public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
- public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
- public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
- public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
- public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
- // Item types
- public static final String ITEM_TYPE_INAPP = "inapp";
- public static final String ITEM_TYPE_SUBS = "subs";
- // some fields on the getSkuDetails response bundle
- public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
- public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
- private final Executor executor;
- // Is debug logging enabled?
- boolean mDebugLog = false;
- String mDebugTag = "IabHelper";
- // Is setup done?
- boolean mSetupDone = false;
- // Has this object been disposed of? (If so, we should ignore callbacks, etc)
- boolean mDisposed = false;
- // Are subscriptions supported?
- boolean mSubscriptionsSupported = false;
- // Is subscription update supported?
- boolean mSubscriptionUpdateSupported = false;
- // Is an asynchronous operation in progress?
- // (only one at a time can be in progress)
- boolean mAsyncInProgress = false;
- // (for logging/debugging)
- // if mAsyncInProgress == true, what asynchronous operation is in progress?
- String mAsyncOperation = "";
- // Context we were passed during initialization
- Context mContext;
- // Connection to the service
- IInAppBillingService mService;
- ServiceConnection mServiceConn;
- // The request code used to launch purchase flow
- int mRequestCode;
- // The item type of the current purchase flow
- String mPurchasingItemType;
- // Public key for verifying signature, in base64 encoding
- String mSignatureBase64 = null;
- // The listener registered on launchPurchaseFlow, which we have to call back when
- // the purchase finishes
- OnIabPurchaseFinishedListener mPurchaseListener;
-
- /**
- * Creates an instance. After creation, it will not yet be ready to use. You must perform setup by
- * calling {@link #startSetup} and wait for setup to complete. This constructor does not block and
- * is safe to call from a UI thread.
- *
- * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
- * @param base64PublicKey Your application's public key, encoded in base64. This is used for
- * verification of purchase signatures. You can find your app's base64-encoded public key in
- * your application's page on Google Play Developer Console. Note that this is NOT your
- * "developer public key".
- */
- public IabHelper(Context ctx, String base64PublicKey, Executor executor) {
- mContext = ctx.getApplicationContext();
- mSignatureBase64 = base64PublicKey;
- this.executor = executor;
- logDebug("IAB helper created.");
- }
-
- /**
- * Returns a human-readable description for the given response code.
- *
- * @param code The response code
- * @return A human-readable string explaining the result code. It also includes the result code
- * numerically.
- */
- public static String getResponseDesc(int code) {
- String[] iab_msgs =
- ("0:OK/1:User Canceled/2:Unknown/"
- + "3:Billing Unavailable/4:Item unavailable/"
- + "5:Developer Error/6:Error/7:Item Already Owned/"
- + "8:Item not owned")
- .split("/");
- String[] iabhelper_msgs =
- ("0:OK/-1001:Remote exception during initialization/"
- + "-1002:Bad response received/"
- + "-1003:Purchase signature verification failed/"
- + "-1004:Send intent failed/"
- + "-1005:User cancelled/"
- + "-1006:Unknown purchase response/"
- + "-1007:Missing token/"
- + "-1008:Unknown error/"
- + "-1009:Subscriptions not available/"
- + "-1010:Invalid consumption attempt")
- .split("/");
-
- if (code <= IABHELPER_ERROR_BASE) {
- int index = IABHELPER_ERROR_BASE - code;
- if (index >= 0 && index < iabhelper_msgs.length) {
- return iabhelper_msgs[index];
- } else {
- return String.valueOf(code) + ":Unknown IAB Helper Error";
- }
- } else if (code < 0 || code >= iab_msgs.length) {
- return String.valueOf(code) + ":Unknown";
- } else {
- return iab_msgs[code];
- }
- }
-
- /** Enables or disable debug logging through LogCat. */
- public void enableDebugLogging(boolean enable, String tag) {
- checkNotDisposed();
- mDebugLog = enable;
- mDebugTag = tag;
- }
-
- public void enableDebugLogging(boolean enable) {
- checkNotDisposed();
- mDebugLog = enable;
- }
-
- /**
- * Starts the setup process. This will start up the setup process asynchronously. You will be
- * notified through the listener when the setup process is complete. This method is safe to call
- * from a UI thread.
- *
- * @param listener The listener to notify when the setup process is complete.
- */
- public void startSetup(final OnIabSetupFinishedListener listener) {
- // If already set up, can't do it again.
- checkNotDisposed();
- if (mSetupDone) {
- throw new IllegalStateException("IAB helper is already set up.");
- }
-
- // Connection to IAB service
- logDebug("Starting in-app billing setup.");
- mServiceConn =
- new ServiceConnection() {
- @Override
- public void onServiceDisconnected(ComponentName name) {
- logDebug("Billing service disconnected.");
- mService = null;
- }
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- if (mDisposed) {
- return;
- }
- logDebug("Billing service connected.");
- mService = IInAppBillingService.Stub.asInterface(service);
- String packageName = mContext.getPackageName();
- try {
- logDebug("Checking for in-app billing 3 support.");
-
- // check for in-app billing v3 support
- int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
- if (response != BILLING_RESPONSE_RESULT_OK) {
- if (listener != null) {
- listener.onIabSetupFinished(
- new IabResult(response, "Error checking for billing v3 support."));
- }
-
- // if in-app purchases aren't supported, neither are subscriptions
- mSubscriptionsSupported = false;
- mSubscriptionUpdateSupported = false;
- return;
- } else {
- logDebug("In-app billing version 3 supported for " + packageName);
- }
-
- // Check for v5 subscriptions support. This is needed for
- // getBuyIntentToReplaceSku which allows for subscription update
- response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS);
- if (response == BILLING_RESPONSE_RESULT_OK) {
- logDebug("Subscription re-signup AVAILABLE.");
- mSubscriptionUpdateSupported = true;
- } else {
- logDebug("Subscription re-signup not available.");
- mSubscriptionUpdateSupported = false;
- }
-
- if (mSubscriptionUpdateSupported) {
- mSubscriptionsSupported = true;
- } else {
- // check for v3 subscriptions support
- response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
- if (response == BILLING_RESPONSE_RESULT_OK) {
- logDebug("Subscriptions AVAILABLE.");
- mSubscriptionsSupported = true;
- } else {
- logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
- mSubscriptionsSupported = false;
- mSubscriptionUpdateSupported = false;
- }
- }
-
- mSetupDone = true;
- } catch (RemoteException e) {
- if (listener != null) {
- listener.onIabSetupFinished(
- new IabResult(
- IABHELPER_REMOTE_EXCEPTION,
- "RemoteException while setting up in-app billing."));
- }
- e.printStackTrace();
- return;
- }
-
- if (listener != null) {
- listener.onIabSetupFinished(
- new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
- }
- }
- };
-
- Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
- serviceIntent.setPackage("com.android.vending");
- List intentServices =
- mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
- if (intentServices != null && !intentServices.isEmpty()) {
- // service available to handle that Intent
- mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
- } else {
- // no service available to handle that Intent
- if (listener != null) {
- listener.onIabSetupFinished(
- new IabResult(
- BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
- "Billing service unavailable on device."));
- }
- }
- }
-
- /**
- * Dispose of object, releasing resources. It's very important to call this method when you are
- * done with this object. It will release any resources used by it such as service connections.
- * Naturally, once the object is disposed of, it can't be used again.
- */
- public void dispose() {
- logDebug("Disposing.");
- mSetupDone = false;
- if (mServiceConn != null) {
- logDebug("Unbinding from service.");
- if (mContext != null && mService != null) {
- mContext.unbindService(mServiceConn);
- }
- }
- mDisposed = true;
- mContext = null;
- mServiceConn = null;
- mService = null;
- mPurchaseListener = null;
- }
-
- private void checkNotDisposed() {
- if (mDisposed) {
- throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
- }
- }
-
- /** Returns whether subscriptions are supported. */
- public boolean subscriptionsSupported() {
- checkNotDisposed();
- return mSubscriptionsSupported;
- }
-
- public void launchPurchaseFlow(
- Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
- launchPurchaseFlow(act, sku, requestCode, listener, "");
- }
-
- public void launchPurchaseFlow(
- Activity act,
- String sku,
- int requestCode,
- OnIabPurchaseFinishedListener listener,
- String extraData) {
- launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData);
- }
-
- public void launchSubscriptionPurchaseFlow(
- Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
- launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
- }
-
- public void launchSubscriptionPurchaseFlow(
- Activity act,
- String sku,
- int requestCode,
- OnIabPurchaseFinishedListener listener,
- String extraData) {
- launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, requestCode, listener, extraData);
- }
-
- /**
- * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
- * which will involve bringing up the Google Play screen. The calling activity will be paused
- * while the user interacts with Google Play, and the result will be delivered via the activity's
- * {@link android.app.Activity#onActivityResult} method, at which point you must call this
- * object's {@link #handleActivityResult} method to continue the purchase flow. This method MUST
- * be called from the UI thread of the Activity.
- *
- * @param act The calling activity.
- * @param sku The sku of the item to purchase.
- * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or
- * ITEM_TYPE_SUBS)
- * @param oldSkus A list of SKUs which the new SKU is replacing or null if there are none
- * @param requestCode A request code (to differentiate from other responses -- as in {@link
- * android.app.Activity#startActivityForResult}).
- * @param listener The listener to notify when the purchase process finishes
- * @param extraData Extra data (developer payload), which will be returned with the purchase data
- * when the purchase completes. This extra data will be permanently bound to that purchase and
- * will always be returned when the purchase is queried.
- */
- public void launchPurchaseFlow(
- Activity act,
- String sku,
- String itemType,
- List oldSkus,
- int requestCode,
- OnIabPurchaseFinishedListener listener,
- String extraData) {
- checkNotDisposed();
- checkSetupDone("launchPurchaseFlow");
- flagStartAsync("launchPurchaseFlow");
- IabResult result;
-
- if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
- IabResult r =
- new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, "Subscriptions are not available.");
- flagEndAsync();
- if (listener != null) {
- listener.onIabPurchaseFinished(r, null);
- }
- return;
- }
-
- try {
- logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
- Bundle buyIntentBundle;
- if (oldSkus == null || oldSkus.isEmpty()) {
- // Purchasing a new item or subscription re-signup
- buyIntentBundle =
- mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
- } else {
- // Subscription upgrade/downgrade
- if (!mSubscriptionUpdateSupported) {
- IabResult r =
- new IabResult(
- IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE,
- "Subscription updates are not available.");
- flagEndAsync();
- if (listener != null) {
- listener.onIabPurchaseFinished(r, null);
- }
- return;
- }
- buyIntentBundle =
- mService.getBuyIntentToReplaceSkus(
- 5, mContext.getPackageName(), oldSkus, sku, itemType, extraData);
- }
- int response = getResponseCodeFromBundle(buyIntentBundle);
- if (response != BILLING_RESPONSE_RESULT_OK) {
- logError("Unable to buy item, Error response: " + getResponseDesc(response));
- flagEndAsync();
- result = new IabResult(response, "Unable to buy item");
- if (listener != null) {
- listener.onIabPurchaseFinished(result, null);
- }
- return;
- }
-
- PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
- logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
- mRequestCode = requestCode;
- mPurchaseListener = listener;
- mPurchasingItemType = itemType;
- act.startIntentSenderForResult(
- pendingIntent.getIntentSender(),
- requestCode,
- new Intent(),
- Integer.valueOf(0),
- Integer.valueOf(0),
- Integer.valueOf(0));
- } catch (SendIntentException e) {
- logError("SendIntentException while launching purchase flow for sku " + sku);
- e.printStackTrace();
- flagEndAsync();
-
- result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
- if (listener != null) {
- listener.onIabPurchaseFinished(result, null);
- }
- } catch (RemoteException e) {
- logError("RemoteException while launching purchase flow for sku " + sku);
- e.printStackTrace();
- flagEndAsync();
-
- result =
- new IabResult(
- IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
- if (listener != null) {
- listener.onIabPurchaseFinished(result, null);
- }
- }
- }
-
- /**
- * Handles an activity result that's part of the purchase flow in in-app billing. If you are
- * calling {@link #launchPurchaseFlow}, then you must call this method from your Activity's {@link
- * android.app.Activity@onActivityResult} method. This method MUST be called from the UI thread of
- * the Activity.
- *
- * @param requestCode The requestCode as you received it.
- * @param resultCode The resultCode as you received it.
- * @param data The data (Intent) as you received it.
- * @return Returns true if the result was related to a purchase flow and was handled; false if the
- * result was not related to a purchase, in which case you should handle it normally.
- */
- public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
- IabResult result;
- if (requestCode != mRequestCode) {
- return false;
- }
-
- checkNotDisposed();
- checkSetupDone("handleActivityResult");
-
- // end of async purchase operation that started on launchPurchaseFlow
- flagEndAsync();
-
- if (data == null) {
- logError("Null data in IAB activity result.");
- result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(result, null);
- }
- return true;
- }
-
- int responseCode = getResponseCodeFromIntent(data);
- String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
- String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
-
- if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
- logDebug("Successful resultcode from purchase activity.");
- logDebug("Purchase data: " + purchaseData);
- logDebug("Data signature: " + dataSignature);
- logDebug("Extras: " + data.getExtras());
- logDebug("Expected item type: " + mPurchasingItemType);
-
- if (purchaseData == null || dataSignature == null) {
- logError("BUG: either purchaseData or dataSignature is null.");
- logDebug("Extras: " + data.getExtras().toString());
- result =
- new IabResult(
- IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(result, null);
- }
- return true;
- }
-
- Purchase purchase = null;
- try {
- purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
- String sku = purchase.getSku();
-
- // Verify signature
- if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
- logError("Purchase signature verification FAILED for sku " + sku);
- result =
- new IabResult(
- IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(result, purchase);
- }
- return true;
- }
- logDebug("Purchase signature successfully verified.");
- } catch (JSONException e) {
- logError("Failed to parse purchase data.");
- e.printStackTrace();
- result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(result, null);
- }
- return true;
- }
-
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(
- new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
- }
- } else if (resultCode == Activity.RESULT_OK) {
- // result code was OK, but in-app billing response was not OK.
- logDebug(
- "Result code was OK but in-app billing response was not OK: "
- + getResponseDesc(responseCode));
- if (mPurchaseListener != null) {
- result = new IabResult(responseCode, "Problem purchashing item.");
- mPurchaseListener.onIabPurchaseFinished(result, null);
- }
- } else if (resultCode == Activity.RESULT_CANCELED) {
- logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
- result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(result, null);
- }
- } else {
- logError(
- "Purchase failed. Result code: "
- + Integer.toString(resultCode)
- + ". Response: "
- + getResponseDesc(responseCode));
- result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
- if (mPurchaseListener != null) {
- mPurchaseListener.onIabPurchaseFinished(result, null);
- }
- }
- return true;
- }
-
- public Inventory queryInventory(boolean querySkuDetails, List moreSkus)
- throws IabException {
- return queryInventory(querySkuDetails, moreSkus, null);
- }
-
- /**
- * Queries the inventory. This will query all owned items from the server, as well as information
- * on additional skus, if specified. This method may block or take long to execute. Do not call
- * from a UI thread. For that, use the non-blocking version {@link #queryInventoryAsync}.
- *
- * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
- * as purchase information.
- * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
- * Ignored if null or if querySkuDetails is false.
- * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of
- * ownership. Ignored if null or if querySkuDetails is false.
- * @throws IabException if a problem occurs while refreshing the inventory.
- */
- public Inventory queryInventory(
- boolean querySkuDetails, List moreItemSkus, List moreSubsSkus)
- throws IabException {
- checkNotDisposed();
- checkSetupDone("queryInventory");
- try {
- Inventory inv = new Inventory();
- int r = queryPurchases(inv, ITEM_TYPE_INAPP);
- if (r != BILLING_RESPONSE_RESULT_OK) {
- throw new IabException(r, "Error refreshing inventory (querying owned items).");
- }
-
- if (querySkuDetails) {
- r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
- if (r != BILLING_RESPONSE_RESULT_OK) {
- throw new IabException(r, "Error refreshing inventory (querying prices of items).");
- }
- }
-
- // if subscriptions are supported, then also query for subscriptions
- if (mSubscriptionsSupported) {
- r = queryPurchases(inv, ITEM_TYPE_SUBS);
- if (r != BILLING_RESPONSE_RESULT_OK) {
- throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
- }
-
- if (querySkuDetails) {
- r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
- if (r != BILLING_RESPONSE_RESULT_OK) {
- throw new IabException(
- r, "Error refreshing inventory (querying prices of subscriptions).");
- }
- }
- }
-
- return inv;
- } catch (RemoteException e) {
- throw new IabException(
- IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
- } catch (JSONException e) {
- throw new IabException(
- IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
- }
- }
-
- /**
- * Asynchronous wrapper for inventory query. This will perform an inventory query as described in
- * {@link #queryInventory}, but will do so asynchronously and call back the specified listener
- * upon completion. This method is safe to call from a UI thread.
- *
- * @param querySkuDetails as in {@link #queryInventory}
- * @param moreSkus as in {@link #queryInventory}
- * @param listener The listener to notify when the refresh operation completes.
- */
- public void queryInventoryAsync(
- final boolean querySkuDetails,
- final List moreSkus,
- final QueryInventoryFinishedListener listener) {
- final Handler handler = new Handler();
- checkNotDisposed();
- checkSetupDone("queryInventory");
- flagStartAsync("refresh inventory");
- executor.execute(
- () -> {
- IabResult result =
- new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
- Inventory inv = null;
- try {
- inv = queryInventory(querySkuDetails, moreSkus);
- } catch (IabException ex) {
- result = ex.getResult();
- }
-
- flagEndAsync();
-
- final IabResult result_f = result;
- final Inventory inv_f = inv;
- if (!mDisposed && listener != null) {
- handler.post(() -> listener.onQueryInventoryFinished(result_f, inv_f));
- }
- });
- }
-
- public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
- queryInventoryAsync(true, null, listener);
- }
-
- public void queryInventoryAsync(
- boolean querySkuDetails, QueryInventoryFinishedListener listener) {
- queryInventoryAsync(querySkuDetails, null, listener);
- }
-
- /**
- * Consumes a given in-app product. Consuming can only be done on an item that's owned, and as a
- * result of consumption, the user will no longer own it. This method may block or take long to
- * return. Do not call from the UI thread. For that, see {@link #consumeAsync}.
- *
- * @param itemInfo The PurchaseInfo that represents the item to consume.
- * @throws IabException if there is a problem during consumption.
- */
- void consume(Purchase itemInfo) throws IabException {
- checkNotDisposed();
- checkSetupDone("consume");
-
- if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
- throw new IabException(
- IABHELPER_INVALID_CONSUMPTION,
- "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
- }
-
- try {
- String token = itemInfo.getToken();
- String sku = itemInfo.getSku();
- if (token == null || token.equals("")) {
- logError("Can't consume " + sku + ". No token.");
- throw new IabException(
- IABHELPER_MISSING_TOKEN,
- "PurchaseInfo is missing token for sku: " + sku + " " + itemInfo);
- }
-
- logDebug("Consuming sku: " + sku + ", token: " + token);
- int response = mService.consumePurchase(3, mContext.getPackageName(), token);
- if (response == BILLING_RESPONSE_RESULT_OK) {
- logDebug("Successfully consumed sku: " + sku);
- } else {
- logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
- throw new IabException(response, "Error consuming sku " + sku);
- }
- } catch (RemoteException e) {
- throw new IabException(
- IABHELPER_REMOTE_EXCEPTION,
- "Remote exception while consuming. PurchaseInfo: " + itemInfo,
- e);
- }
- }
-
- /**
- * Asynchronous wrapper to item consumption. Works like {@link #consume}, but performs the
- * consumption in the background and notifies completion through the provided listener. This
- * method is safe to call from a UI thread.
- *
- * @param purchase The purchase to be consumed.
- * @param listener The listener to notify when the consumption operation finishes.
- */
- public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
- checkNotDisposed();
- checkSetupDone("consume");
- List purchases = new ArrayList();
- purchases.add(purchase);
- consumeAsyncInternal(purchases, listener, null);
- }
-
- /**
- * Same as {@link #consumeAsync}, but for multiple items at once.
- *
- * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
- * @param listener The listener to notify when the consumption operation finishes.
- */
- public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) {
- checkNotDisposed();
- checkSetupDone("consume");
- consumeAsyncInternal(purchases, null, listener);
- }
-
- // Checks that setup was done; if not, throws an exception.
- void checkSetupDone(String operation) {
- if (!mSetupDone) {
- logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
- throw new IllegalStateException(
- "IAB helper is not set up. Can't perform operation: " + operation);
- }
- }
-
- // Workaround to bug where sometimes response codes come as Long instead of Integer
- int getResponseCodeFromBundle(Bundle b) {
- Object o = b.get(RESPONSE_CODE);
- if (o == null) {
- logDebug("Bundle with null response code, assuming OK (known issue)");
- return BILLING_RESPONSE_RESULT_OK;
- } else if (o instanceof Integer) {
- return ((Integer) o).intValue();
- } else if (o instanceof Long) {
- return (int) ((Long) o).longValue();
- } else {
- logError("Unexpected type for bundle response code.");
- logError(o.getClass().getName());
- throw new RuntimeException(
- "Unexpected type for bundle response code: " + o.getClass().getName());
- }
- }
-
- // Workaround to bug where sometimes response codes come as Long instead of Integer
- int getResponseCodeFromIntent(Intent i) {
- Object o = i.getExtras().get(RESPONSE_CODE);
- if (o == null) {
- logError("Intent with no response code, assuming OK (known issue)");
- return BILLING_RESPONSE_RESULT_OK;
- } else if (o instanceof Integer) {
- return ((Integer) o).intValue();
- } else if (o instanceof Long) {
- return (int) ((Long) o).longValue();
- } else {
- logError("Unexpected type for intent response code.");
- logError(o.getClass().getName());
- throw new RuntimeException(
- "Unexpected type for intent response code: " + o.getClass().getName());
- }
- }
-
- void flagStartAsync(String operation) {
- if (mAsyncInProgress) {
- throw new IllegalStateException(
- "Can't start async operation ("
- + operation
- + ") because another async operation("
- + mAsyncOperation
- + ") is in progress.");
- }
- mAsyncOperation = operation;
- mAsyncInProgress = true;
- logDebug("Starting async operation: " + operation);
- }
-
- void flagEndAsync() {
- logDebug("Ending async operation: " + mAsyncOperation);
- mAsyncOperation = "";
- mAsyncInProgress = false;
- }
-
- int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
- // Query purchases
- logDebug("Querying owned items, item type: " + itemType);
- logDebug("Package name: " + mContext.getPackageName());
- boolean verificationFailed = false;
- String continueToken = null;
-
- do {
- logDebug("Calling getPurchases with continuation token: " + continueToken);
- Bundle ownedItems =
- mService.getPurchases(3, mContext.getPackageName(), itemType, continueToken);
-
- int response = getResponseCodeFromBundle(ownedItems);
- logDebug("Owned items response: " + String.valueOf(response));
- if (response != BILLING_RESPONSE_RESULT_OK) {
- logDebug("getPurchases() failed: " + getResponseDesc(response));
- return response;
- }
- if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
- || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
- || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
- logError("Bundle returned from getPurchases() doesn't contain required fields.");
- return IABHELPER_BAD_RESPONSE;
- }
-
- ArrayList ownedSkus = ownedItems.getStringArrayList(RESPONSE_INAPP_ITEM_LIST);
- ArrayList purchaseDataList =
- ownedItems.getStringArrayList(RESPONSE_INAPP_PURCHASE_DATA_LIST);
- ArrayList signatureList =
- ownedItems.getStringArrayList(RESPONSE_INAPP_SIGNATURE_LIST);
-
- for (int i = 0; i < purchaseDataList.size(); ++i) {
- String purchaseData = purchaseDataList.get(i);
- String signature = signatureList.get(i);
- String sku = ownedSkus.get(i);
- if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
- logDebug("Sku is owned: " + sku);
- Purchase purchase = new Purchase(itemType, purchaseData, signature);
-
- if (TextUtils.isEmpty(purchase.getToken())) {
- logWarn("BUG: empty/null token!");
- logDebug("Purchase data: " + purchaseData);
- }
-
- // Record ownership and token
- inv.addPurchase(purchase);
- } else {
- logWarn("Purchase signature verification **FAILED**. Not adding item.");
- logDebug(" Purchase data: " + purchaseData);
- logDebug(" Signature: " + signature);
- verificationFailed = true;
- }
- }
-
- continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
- logDebug("Continuation token: " + continueToken);
- } while (!TextUtils.isEmpty(continueToken));
-
- return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
- }
-
- int querySkuDetails(String itemType, Inventory inv, List moreSkus)
- throws RemoteException, JSONException {
- logDebug("Querying SKU details.");
- ArrayList skuList = new ArrayList();
- skuList.addAll(inv.getAllOwnedSkus(itemType));
- if (moreSkus != null) {
- for (String sku : moreSkus) {
- if (!skuList.contains(sku)) {
- skuList.add(sku);
- }
- }
- }
-
- if (skuList.size() == 0) {
- logDebug("queryPrices: nothing to do because there are no SKUs.");
- return BILLING_RESPONSE_RESULT_OK;
- }
-
- // Split the sku list in blocks of no more than 20 elements.
- ArrayList> packs = new ArrayList>();
- ArrayList tempList;
- int n = skuList.size() / 20;
- int mod = skuList.size() % 20;
- for (int i = 0; i < n; i++) {
- tempList = new ArrayList();
- for (String s : skuList.subList(i * 20, i * 20 + 20)) {
- tempList.add(s);
- }
- packs.add(tempList);
- }
- if (mod != 0) {
- tempList = new ArrayList();
- for (String s : skuList.subList(n * 20, n * 20 + mod)) {
- tempList.add(s);
- }
- packs.add(tempList);
- }
-
- for (ArrayList skuPartList : packs) {
- Bundle querySkus = new Bundle();
- querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList);
- Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), itemType, querySkus);
-
- if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
- int response = getResponseCodeFromBundle(skuDetails);
- if (response != BILLING_RESPONSE_RESULT_OK) {
- logDebug("getSkuDetails() failed: " + getResponseDesc(response));
- return response;
- } else {
- logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
- return IABHELPER_BAD_RESPONSE;
- }
- }
-
- ArrayList responseList = skuDetails.getStringArrayList(RESPONSE_GET_SKU_DETAILS_LIST);
-
- for (String thisResponse : responseList) {
- SkuDetails d = new SkuDetails(itemType, thisResponse);
- logDebug("Got sku details: " + d);
- inv.addSkuDetails(d);
- }
- }
-
- return BILLING_RESPONSE_RESULT_OK;
- }
-
- void consumeAsyncInternal(
- final List purchases,
- final OnConsumeFinishedListener singleListener,
- final OnConsumeMultiFinishedListener multiListener) {
- final Handler handler = new Handler();
- flagStartAsync("consume");
- executor.execute(
- () -> {
- final List results = new ArrayList();
- for (Purchase purchase : purchases) {
- try {
- consume(purchase);
- results.add(
- new IabResult(
- BILLING_RESPONSE_RESULT_OK,
- "Successful consume of sku " + purchase.getSku()));
- } catch (IabException ex) {
- results.add(ex.getResult());
- }
- }
-
- flagEndAsync();
- if (!mDisposed && singleListener != null) {
- handler.post(() -> singleListener.onConsumeFinished(purchases.get(0), results.get(0)));
- }
- if (!mDisposed && multiListener != null) {
- handler.post(() -> multiListener.onConsumeMultiFinished(purchases, results));
- }
- });
- }
-
- void logDebug(String msg) {
- if (mDebugLog) {
- Log.d(mDebugTag, msg);
- }
- }
-
- void logError(String msg) {
- Log.e(mDebugTag, "In-app billing error: " + msg);
- }
-
- void logWarn(String msg) {
- Log.w(mDebugTag, "In-app billing warning: " + msg);
- }
-
- /**
- * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called when
- * the setup process is complete.
- */
- public interface OnIabSetupFinishedListener {
-
- /**
- * Called to notify that setup is complete.
- *
- * @param result The result of the setup process.
- */
- void onIabSetupFinished(IabResult result);
- }
-
- /** Callback that notifies when a purchase is finished. */
- public interface OnIabPurchaseFinishedListener {
-
- /**
- * Called to notify that an in-app purchase finished. If the purchase was successful, then the
- * sku parameter specifies which item was purchased. If the purchase failed, the sku and
- * extraData parameters may or may not be null, depending on how far the purchase process went.
- *
- * @param result The result of the purchase.
- * @param info The purchase information (null if purchase failed)
- */
- void onIabPurchaseFinished(IabResult result, Purchase info);
- }
-
- /** Listener that notifies when an inventory query operation completes. */
- public interface QueryInventoryFinishedListener {
-
- /**
- * Called to notify that an inventory query operation completed.
- *
- * @param result The result of the operation.
- * @param inv The inventory.
- */
- void onQueryInventoryFinished(IabResult result, Inventory inv);
- }
-
- /** Callback that notifies when a consumption operation finishes. */
- public interface OnConsumeFinishedListener {
-
- /**
- * Called to notify that a consumption has finished.
- *
- * @param purchase The purchase that was (or was to be) consumed.
- * @param result The result of the consumption operation.
- */
- void onConsumeFinished(Purchase purchase, IabResult result);
- }
-
- /** Callback that notifies when a multi-item consumption operation finishes. */
- public interface OnConsumeMultiFinishedListener {
-
- /**
- * Called to notify that a consumption of multiple items has finished.
- *
- * @param purchases The purchases that were (or were to be) consumed.
- * @param results The results of each consumption operation, corresponding to each sku.
- */
- void onConsumeMultiFinished(List purchases, List results);
- }
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/IabResult.java b/app/src/googleplay/java/com/android/vending/billing/IabResult.java
deleted file mode 100644
index cdbc125e9..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/IabResult.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/Inventory.java b/app/src/googleplay/java/com/android/vending/billing/Inventory.java
deleted file mode 100644
index 09010ad1f..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/Inventory.java
+++ /dev/null
@@ -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 mSkuMap = new HashMap();
- Map mPurchaseMap = new HashMap();
-
- 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 getAllOwnedSkus() {
- return new ArrayList(mPurchaseMap.keySet());
- }
-
- /** Returns a list of all owned product IDs of a given type */
- List getAllOwnedSkus(String itemType) {
- List result = new ArrayList();
- for (Purchase p : mPurchaseMap.values()) {
- if (p.getItemType().equals(itemType)) {
- result.add(p.getSku());
- }
- }
- return result;
- }
-
- /** Returns a list of all purchases. */
- List getAllPurchases() {
- return new ArrayList(mPurchaseMap.values());
- }
-
- void addSkuDetails(SkuDetails d) {
- mSkuMap.put(d.getSku(), d);
- }
-
- void addPurchase(Purchase p) {
- mPurchaseMap.put(p.getSku(), p);
- }
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/Purchase.java b/app/src/googleplay/java/com/android/vending/billing/Purchase.java
deleted file mode 100644
index 7d2e2395b..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/Purchase.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/SkuDetails.java b/app/src/googleplay/java/com/android/vending/billing/SkuDetails.java
deleted file mode 100644
index ced974234..000000000
--- a/app/src/googleplay/java/com/android/vending/billing/SkuDetails.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/googleplay/java/org/tasks/FlavorSetup.java b/app/src/googleplay/java/org/tasks/FlavorSetup.java
index 242af6183..3f23b1eda 100644
--- a/app/src/googleplay/java/org/tasks/FlavorSetup.java
+++ b/app/src/googleplay/java/org/tasks/FlavorSetup.java
@@ -1,22 +1,23 @@
package org.tasks;
import javax.inject.Inject;
-import org.tasks.billing.InventoryHelper;
+import org.tasks.billing.BillingClient;
import org.tasks.gtasks.PlayServices;
public class FlavorSetup {
- private final InventoryHelper inventoryHelper;
private final PlayServices playServices;
+ private final BillingClient billingClient;
@Inject
- public FlavorSetup(InventoryHelper inventoryHelper, PlayServices playServices) {
- this.inventoryHelper = inventoryHelper;
+ public FlavorSetup(PlayServices playServices,
+ BillingClient billingClient) {
this.playServices = playServices;
+ this.billingClient = billingClient;
}
public void setup() {
- inventoryHelper.initialize();
+ billingClient.initialize();
playServices.refresh();
}
}
diff --git a/app/src/googleplay/java/org/tasks/analytics/Tracker.java b/app/src/googleplay/java/org/tasks/analytics/Tracker.java
index 957460b22..10c0e120d 100644
--- a/app/src/googleplay/java/org/tasks/analytics/Tracker.java
+++ b/app/src/googleplay/java/org/tasks/analytics/Tracker.java
@@ -1,7 +1,9 @@
package org.tasks.analytics;
+import static org.tasks.billing.BillingClient.BillingResponseToString;
+
import android.content.Context;
-import com.android.vending.billing.IabResult;
+import com.android.billingclient.api.BillingClient.BillingResponse;
import com.google.android.gms.analytics.ExceptionParser;
import com.google.android.gms.analytics.ExceptionReporter;
import com.google.android.gms.analytics.GoogleAnalytics;
@@ -92,12 +94,14 @@ public class Tracker {
tracker.send(eventBuilder.build());
}
- public void reportIabResult(IabResult result, String sku) {
+ public void reportIabResult(@BillingResponse int response, String sku) {
tracker.send(
new HitBuilders.EventBuilder()
.setCategory(context.getString(R.string.tracking_category_iab))
.setAction(sku)
- .setLabel(result.getMessage())
+ .setLabel(BillingResponseToString(response))
.build());
}
+
+
}
diff --git a/app/src/googleplay/java/org/tasks/billing/InventoryHelper.java b/app/src/googleplay/java/org/tasks/billing/InventoryHelper.java
deleted file mode 100644
index 6f2c7aa72..000000000
--- a/app/src/googleplay/java/org/tasks/billing/InventoryHelper.java
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/src/googleplay/java/org/tasks/billing/PurchaseHelper.java b/app/src/googleplay/java/org/tasks/billing/PurchaseHelper.java
deleted file mode 100644
index 083775373..000000000
--- a/app/src/googleplay/java/org/tasks/billing/PurchaseHelper.java
+++ /dev/null
@@ -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 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);
- }
- }
-}
diff --git a/app/src/googleplay/res/values/keys.xml b/app/src/googleplay/res/values/keys.xml
index 8f2a01edb..133f35ea4 100644
--- a/app/src/googleplay/res/values/keys.xml
+++ b/app/src/googleplay/res/values/keys.xml
@@ -1,6 +1,5 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8mXRE3dDXwtinUILCEzKjov2rxs3kZbLRzNrcjFWXpG9OEsUzRGLzqEN+WwibVuMRpZLj/+IxbU2sJWq/M0q+90rOhmXn46ZPeNyr77IqX2pWKIAWpzBoWq/mshRwtm9m1FIiGdBNlXrhSE7u3TGB5FuEuuSqKWvWzxeqQ7fHmlM04Lqrh1mN3FaMne8rWv+DWVHDbLrtnXBuC36glOAj17HxrzaE2v6Pv7Df3QefJ3rM1+0fAp/5jNInaP0qHAlG8WTbUmDShQ5kG3urbv3HLByyx6TSqhmNudXUK/6TusvIj50OptAG7x+UFYf956dD3diXhY3yoICvyFWx1sNwIDAQAB
- themesplay_services_available
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b03547b0d..8ff7189f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -480,6 +480,10 @@
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustResize"/>
+
+
{
public static final int REQUEST_SETTINGS = 10123;
+ public static final int REQUEST_PURCHASE = 10124;
// --- instance variables
private static final int VIEW_TYPE_COUNT = FilterListItem.Type.values().length;
@@ -61,6 +65,7 @@ public class FilterAdapter extends ArrayAdapter {
private final Activity activity;
private final Theme theme;
private final Locale locale;
+ private final Inventory inventory;
private final Preferences preferences;
private final FilterListUpdateReceiver filterListUpdateReceiver = new FilterListUpdateReceiver();
private final List items = new ArrayList<>();
@@ -77,6 +82,7 @@ public class FilterAdapter extends ArrayAdapter {
Theme theme,
ThemeCache themeCache,
Locale locale,
+ Inventory inventory,
Preferences preferences) {
super(activity, 0);
this.filterProvider = filterProvider;
@@ -84,6 +90,7 @@ public class FilterAdapter extends ArrayAdapter {
this.activity = activity;
this.theme = theme;
this.locale = locale;
+ this.inventory = inventory;
this.preferences = preferences;
this.inflater = theme.getLayoutInflater(activity);
this.themeCache = themeCache;
@@ -325,6 +332,15 @@ public class FilterAdapter extends ArrayAdapter {
if (navigationDrawer) {
add(new NavigationDrawerSeparator());
+ if (!inventory.hasPro()) {
+ add(
+ new NavigationDrawerAction(
+ activity.getResources().getString(R.string.subscribe_to_pro),
+ R.drawable.ic_attach_money_black_24dp,
+ new Intent(activity, PurchaseActivity.class),
+ REQUEST_PURCHASE));
+ }
+
add(
new NavigationDrawerAction(
activity.getResources().getString(R.string.TLA_menu_settings),
diff --git a/app/src/main/java/org/tasks/LocalBroadcastManager.java b/app/src/main/java/org/tasks/LocalBroadcastManager.java
index 9b9cb0ffd..a6bbd46e3 100644
--- a/app/src/main/java/org/tasks/LocalBroadcastManager.java
+++ b/app/src/main/java/org/tasks/LocalBroadcastManager.java
@@ -4,6 +4,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import com.android.billingclient.api.BillingClient.BillingResponse;
import com.todoroo.astrid.api.AstridApiConstants;
import javax.inject.Inject;
import org.tasks.injection.ForApplication;
@@ -14,6 +15,7 @@ public class LocalBroadcastManager {
public static final String REFRESH = BuildConfig.APPLICATION_ID + ".REFRESH";
public static final String REFRESH_LIST = BuildConfig.APPLICATION_ID + ".REFRESH_LIST";
private static final String REPEAT = BuildConfig.APPLICATION_ID + ".REPEAT";
+ private static final String REFRESH_PURCHASES = BuildConfig.APPLICATION_ID + ".REFRESH_PURCHASES";
private final android.support.v4.content.LocalBroadcastManager localBroadcastManager;
private final AppWidgetManager appWidgetManager;
@@ -36,6 +38,10 @@ public class LocalBroadcastManager {
localBroadcastManager.registerReceiver(broadcastReceiver, new IntentFilter(REPEAT));
}
+ public void registerPurchaseReceiver(BroadcastReceiver broadcastReceiver) {
+ localBroadcastManager.registerReceiver(broadcastReceiver, new IntentFilter(REFRESH_PURCHASES));
+ }
+
public void broadcastRefresh() {
localBroadcastManager.sendBroadcast(new Intent(REFRESH));
appWidgetManager.updateWidgets();
@@ -62,4 +68,8 @@ public class LocalBroadcastManager {
public void unregisterReceiver(BroadcastReceiver broadcastReceiver) {
localBroadcastManager.unregisterReceiver(broadcastReceiver);
}
+
+ public void broadcastPurchasesUpdated() {
+ localBroadcastManager.sendBroadcast(new Intent(REFRESH_PURCHASES));
+ }
}
diff --git a/app/src/main/java/org/tasks/activities/ColorPickerActivity.java b/app/src/main/java/org/tasks/activities/ColorPickerActivity.java
index 22fb36239..32742ede8 100644
--- a/app/src/main/java/org/tasks/activities/ColorPickerActivity.java
+++ b/app/src/main/java/org/tasks/activities/ColorPickerActivity.java
@@ -6,9 +6,9 @@ import android.content.Intent;
import android.os.Bundle;
import java.util.List;
import javax.inject.Inject;
-import org.tasks.R;
-import org.tasks.billing.PurchaseHelper;
-import org.tasks.billing.PurchaseHelperCallback;
+import org.tasks.billing.BillingClient;
+import org.tasks.billing.Inventory;
+import org.tasks.billing.PurchaseActivity;
import org.tasks.dialogs.ColorPickerDialog;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
@@ -16,16 +16,18 @@ import org.tasks.themes.Theme;
import org.tasks.themes.ThemeCache;
public class ColorPickerActivity extends ThemedInjectingAppCompatActivity
- implements ColorPickerDialog.ThemePickerCallback, PurchaseHelperCallback {
+ implements ColorPickerDialog.ThemePickerCallback {
public static final String EXTRA_PALETTE = "extra_palette";
public static final String EXTRA_SHOW_NONE = "extra_show_none";
public static final String EXTRA_THEME_INDEX = "extra_index";
private static final String FRAG_TAG_COLOR_PICKER = "frag_tag_color_picker";
- private static final int REQUEST_PURCHASE = 1006;
- @Inject PurchaseHelper purchaseHelper;
+ private static final int REQUEST_SUBSCRIPTION = 10101;
@Inject Theme theme;
@Inject ThemeCache themeCache;
+ @Inject BillingClient billingClient;
+ @Inject Inventory inventory;
+
private ColorPalette palette;
@Override
@@ -59,7 +61,7 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity
case WIDGET_BACKGROUND:
return themeCache.getWidgetThemes();
default:
- throw new RuntimeException("Un");
+ throw new IllegalArgumentException("Unsupported palette: " + palette);
}
}
@@ -79,12 +81,7 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity
@Override
public void initiateThemePurchase() {
- purchaseHelper.purchase(
- this,
- getString(R.string.sku_themes),
- getString(R.string.p_purchased_themes),
- REQUEST_PURCHASE,
- this);
+ startActivityForResult(new Intent(this, PurchaseActivity.class), REQUEST_SUBSCRIPTION);
}
@Override
@@ -94,20 +91,15 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == REQUEST_PURCHASE) {
- purchaseHelper.handleActivityResult(null, requestCode, resultCode, data);
+ if (requestCode == REQUEST_SUBSCRIPTION) {
+ if (!inventory.purchasedThemes()) {
+ finish();
+ }
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
- @Override
- public void purchaseCompleted(boolean success, String sku) {
- if (!success) {
- finish();
- }
- }
-
private int getCurrentSelection(ColorPalette palette) {
switch (palette) {
case COLORS:
diff --git a/app/src/main/java/org/tasks/billing/BillingClient.java b/app/src/main/java/org/tasks/billing/BillingClient.java
new file mode 100644
index 000000000..f18c42f08
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/BillingClient.java
@@ -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 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
+ *
+ *
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 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";
+ }
+ }
+}
diff --git a/app/src/main/java/org/tasks/billing/CardsWithHeadersDecoration.java b/app/src/main/java/org/tasks/billing/CardsWithHeadersDecoration.java
new file mode 100644
index 000000000..a9352460a
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/CardsWithHeadersDecoration.java
@@ -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;
+ }
+ }
+}
diff --git a/app/src/main/java/org/tasks/billing/Inventory.java b/app/src/main/java/org/tasks/billing/Inventory.java
new file mode 100644
index 000000000..89d06edb0
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/Inventory.java
@@ -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 SKU_SUBS = ImmutableList.of(SKU_PRO);
+
+ private final Preferences preferences;
+ private final String billingKey;
+
+ private Map 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 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 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);
+ }
+}
diff --git a/app/src/main/java/org/tasks/billing/PurchaseActivity.java b/app/src/main/java/org/tasks/billing/PurchaseActivity.java
new file mode 100644
index 000000000..c231965f0
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/PurchaseActivity.java
@@ -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 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 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 data,
+ List 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;
+ }
+ }
+}
diff --git a/app/src/main/java/org/tasks/billing/PurchaseHelperCallback.java b/app/src/main/java/org/tasks/billing/PurchaseHelperCallback.java
deleted file mode 100644
index c7344a8da..000000000
--- a/app/src/main/java/org/tasks/billing/PurchaseHelperCallback.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.tasks.billing;
-
-public interface PurchaseHelperCallback {
-
- void purchaseCompleted(boolean success, String sku);
-}
diff --git a/app/src/googleplay/java/com/android/vending/billing/Security.java b/app/src/main/java/org/tasks/billing/Security.java
similarity index 64%
rename from app/src/googleplay/java/com/android/vending/billing/Security.java
rename to app/src/main/java/org/tasks/billing/Security.java
index 03479ed81..c2b4c4cde 100644
--- a/app/src/googleplay/java/com/android/vending/billing/Security.java
+++ b/app/src/main/java/org/tasks/billing/Security.java
@@ -1,4 +1,5 @@
-/* Copyright (c) 2012 Google Inc.
+/*
+ * 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.
@@ -13,12 +14,12 @@
* limitations under the License.
*/
-package com.android.vending.billing;
+package org.tasks.billing;
-import android.annotation.SuppressLint;
import android.text.TextUtils;
import android.util.Base64;
-import android.util.Log;
+import com.android.billingclient.util.BillingHelper;
+import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
@@ -30,64 +31,60 @@ import java.security.spec.X509EncodedKeySpec;
/**
* Security-related methods. For a secure implementation, all of this code should be implemented on
- * a server that communicates with the application on the device. For the sake of simplicity and
- * clarity of this example, this code is included here and is executed on the device. If you must
- * verify the purchases on the phone, you should obfuscate this code to make it harder for an
- * attacker to replace the code with stubs that treat all purchases as verified.
+ * a server that communicates with the application on the device.
*/
-@SuppressWarnings("ALL")
-@SuppressLint("all")
public class Security {
-
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
- * Verifies that the data was signed with the given signature, and returns the verified purchase.
- * The data is in JSON format and signed with a private key. The data also contains the {@link
- * PurchaseState} and product ID of the purchase.
- *
+ * Verifies that the data was signed with the given signature, and returns the verified
+ * purchase.
* @param base64PublicKey the base64-encoded public key to use for verifying.
* @param signedData the signed JSON string (signed, not encrypted)
* @param signature the signature for the data, signed with the private key
+ * @throws IOException if encoding algorithm is not supported or key specification
+ * is invalid
*/
- public static boolean verifyPurchase(
- String base64PublicKey, String signedData, String signature) {
- if (TextUtils.isEmpty(signedData)
- || TextUtils.isEmpty(base64PublicKey)
+ public static boolean verifyPurchase(String base64PublicKey, String signedData,
+ String signature) throws IOException {
+ if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey)
|| TextUtils.isEmpty(signature)) {
- Log.e(TAG, "Purchase verification failed: missing data.");
+ BillingHelper.logWarn(TAG, "Purchase verification failed: missing data.");
return false;
}
- PublicKey key = Security.generatePublicKey(base64PublicKey);
- return Security.verify(key, signedData, signature);
+ PublicKey key = generatePublicKey(base64PublicKey);
+ return verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the Base64-encoded public key.
*
* @param encodedPublicKey Base64-encoded public key
- * @throws IllegalArgumentException if encodedPublicKey is invalid
+ * @throws IOException if encoding algorithm is not supported or key specification
+ * is invalid
*/
- public static PublicKey generatePublicKey(String encodedPublicKey) {
+ public static PublicKey generatePublicKey(String encodedPublicKey) throws IOException {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
+ // "RSA" is guaranteed to be available.
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
- Log.e(TAG, "Invalid key specification.");
- throw new IllegalArgumentException(e);
+ String msg = "Invalid key specification: " + e;
+ BillingHelper.logWarn(TAG, msg);
+ throw new IOException(msg);
}
}
/**
- * Verifies that the signature from the server matches the computed signature on the data. Returns
- * true if the data is correctly signed.
+ * Verifies that the signature from the server matches the computed signature on the data.
+ * Returns true if the data is correctly signed.
*
* @param publicKey public key associated with the developer account
* @param signedData signed data from server
@@ -99,24 +96,25 @@ public class Security {
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
- Log.e(TAG, "Base64 decoding failed.");
+ BillingHelper.logWarn(TAG, "Base64 decoding failed.");
return false;
}
try {
- Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
- sig.initVerify(publicKey);
- sig.update(signedData.getBytes());
- if (!sig.verify(signatureBytes)) {
- Log.e(TAG, "Signature verification failed.");
+ Signature signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM);
+ signatureAlgorithm.initVerify(publicKey);
+ signatureAlgorithm.update(signedData.getBytes());
+ if (!signatureAlgorithm.verify(signatureBytes)) {
+ BillingHelper.logWarn(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
- Log.e(TAG, "NoSuchAlgorithmException.");
+ // "RSA" is guaranteed to be available.
+ throw new RuntimeException(e);
} catch (InvalidKeyException e) {
- Log.e(TAG, "Invalid key specification.");
+ BillingHelper.logWarn(TAG, "Invalid key specification.");
} catch (SignatureException e) {
- Log.e(TAG, "Signature exception.");
+ BillingHelper.logWarn(TAG, "Signature exception.");
}
return false;
}
diff --git a/app/src/main/java/org/tasks/billing/SkusAdapter.java b/app/src/main/java/org/tasks/billing/SkusAdapter.java
new file mode 100644
index 000000000..9565030cd
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/SkusAdapter.java
@@ -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 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 data = ImmutableList.of();
+
+ SkusAdapter(Context context, Inventory inventory, OnClickHandler onClickHandler) {
+ this.context = context;
+ this.inventory = inventory;
+ this.onClickHandler = onClickHandler;
+ }
+
+ public void setData(List 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 {}
+}
diff --git a/app/src/main/java/org/tasks/billing/row/RowDataProvider.java b/app/src/main/java/org/tasks/billing/row/RowDataProvider.java
new file mode 100644
index 000000000..c4fa6f180
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/row/RowDataProvider.java
@@ -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);
+}
+
diff --git a/app/src/main/java/org/tasks/billing/row/RowViewHolder.java b/app/src/main/java/org/tasks/billing/row/RowViewHolder.java
new file mode 100644
index 000000000..74f5d1d70
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/row/RowViewHolder.java
@@ -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()));
+ }
+ }
+}
diff --git a/app/src/main/java/org/tasks/billing/row/SkuRowData.java b/app/src/main/java/org/tasks/billing/row/SkuRowData.java
new file mode 100644
index 000000000..b59149bbf
--- /dev/null
+++ b/app/src/main/java/org/tasks/billing/row/SkuRowData.java
@@ -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;
+ }
+}
diff --git a/app/src/main/java/org/tasks/dashclock/DashClockExtension.java b/app/src/main/java/org/tasks/dashclock/DashClockExtension.java
index 20a127acc..d622a3210 100644
--- a/app/src/main/java/org/tasks/dashclock/DashClockExtension.java
+++ b/app/src/main/java/org/tasks/dashclock/DashClockExtension.java
@@ -12,6 +12,7 @@ import java.util.List;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
+import org.tasks.billing.Inventory;
import org.tasks.injection.InjectingApplication;
import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.preferences.Preferences;
@@ -22,6 +23,8 @@ public class DashClockExtension extends com.google.android.apps.dashclock.api.Da
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject TaskDao taskDao;
@Inject Preferences preferences;
+ @Inject LocalBroadcastManager localBroadcastManager;
+ @Inject Inventory inventory;
private final BroadcastReceiver refreshReceiver =
new BroadcastReceiver() {
@Override
@@ -29,7 +32,6 @@ public class DashClockExtension extends com.google.android.apps.dashclock.api.Da
refresh();
}
};
- @Inject LocalBroadcastManager localBroadcastManager;
@Override
public void onCreate() {
@@ -53,7 +55,7 @@ public class DashClockExtension extends com.google.android.apps.dashclock.api.Da
}
private void refresh() {
- if (preferences.hasPurchase(R.string.p_purchased_dashclock)) {
+ if (inventory.purchasedDashclock()) {
final String filterPreference = preferences.getStringValue(R.string.p_dashclock_filter);
Filter filter = defaultFilterProvider.getFilterFromPreference(filterPreference);
@@ -85,8 +87,8 @@ public class DashClockExtension extends com.google.android.apps.dashclock.api.Da
new ExtensionData()
.visible(true)
.icon(R.drawable.ic_check_white_24dp)
- .status(getString(R.string.buy))
- .expandedTitle(getString(R.string.buy_dashclock_extension))
+ .status(getString(R.string.subscribe_to_pro))
+ .expandedTitle(getString(R.string.subscribe_to_pro))
.clickIntent(new Intent(this, DashClockSettings.class)));
}
}
diff --git a/app/src/main/java/org/tasks/dashclock/DashClockSettings.java b/app/src/main/java/org/tasks/dashclock/DashClockSettings.java
index ea652cb2a..d1233f888 100644
--- a/app/src/main/java/org/tasks/dashclock/DashClockSettings.java
+++ b/app/src/main/java/org/tasks/dashclock/DashClockSettings.java
@@ -8,35 +8,27 @@ import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.activities.FilterSelectionActivity;
-import org.tasks.billing.PurchaseHelper;
-import org.tasks.billing.PurchaseHelperCallback;
+import org.tasks.billing.BillingClient;
+import org.tasks.billing.Inventory;
+import org.tasks.billing.PurchaseActivity;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.preferences.DefaultFilterProvider;
-import org.tasks.preferences.Preferences;
-public class DashClockSettings extends InjectingPreferenceActivity
- implements PurchaseHelperCallback {
+public class DashClockSettings extends InjectingPreferenceActivity {
- private static final String EXTRA_PURCHASE_INITIATED = "extra_purchase_initiated";
private static final int REQUEST_SELECT_FILTER = 1005;
- private static final int REQUEST_PURCHASE = 1006;
+ private static final int REQUEST_SUBSCRIPTION = 1006;
- @Inject Preferences preferences;
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject LocalBroadcastManager localBroadcastManager;
- @Inject PurchaseHelper purchaseHelper;
-
- private boolean purchaseInitiated;
+ @Inject BillingClient billingClient;
+ @Inject Inventory inventory;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (savedInstanceState != null) {
- purchaseInitiated = savedInstanceState.getBoolean(EXTRA_PURCHASE_INITIATED);
- }
-
addPreferencesFromResource(R.xml.preferences_dashclock);
findPreference(getString(R.string.p_dashclock_filter))
@@ -52,23 +44,8 @@ public class DashClockSettings extends InjectingPreferenceActivity
refreshPreferences();
- if (!preferences.hasPurchase(R.string.p_purchased_dashclock) && !purchaseInitiated) {
- purchaseHelper.purchase(
- this,
- getString(R.string.sku_dashclock),
- getString(R.string.p_purchased_dashclock),
- REQUEST_PURCHASE,
- this);
- purchaseInitiated = true;
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- if (!isChangingConfigurations()) {
- purchaseHelper.disposeIabHelper();
+ if (!inventory.purchasedDashclock()) {
+ startActivityForResult(new Intent(this, PurchaseActivity.class), REQUEST_SUBSCRIPTION);
}
}
@@ -86,29 +63,15 @@ public class DashClockSettings extends InjectingPreferenceActivity
refreshPreferences();
localBroadcastManager.broadcastRefresh();
}
- } else if (requestCode == REQUEST_PURCHASE) {
- purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
+ } else if (requestCode == REQUEST_SUBSCRIPTION) {
+ if (!inventory.purchasedDashclock()) {
+ finish();
+ }
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putBoolean(EXTRA_PURCHASE_INITIATED, purchaseInitiated);
- }
-
- @Override
- public void purchaseCompleted(boolean success, String sku) {
- if (success) {
- localBroadcastManager.broadcastRefresh();
- } else {
- finish();
- }
- }
-
private void refreshPreferences() {
Filter filter = defaultFilterProvider.getFilterFromPreference(R.string.p_dashclock_filter);
findPreference(getString(R.string.p_dashclock_filter)).setSummary(filter.listingTitle);
diff --git a/app/src/main/java/org/tasks/dialogs/ColorPickerDialog.java b/app/src/main/java/org/tasks/dialogs/ColorPickerDialog.java
index df3fd4755..69d91997a 100644
--- a/app/src/main/java/org/tasks/dialogs/ColorPickerDialog.java
+++ b/app/src/main/java/org/tasks/dialogs/ColorPickerDialog.java
@@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
+import org.tasks.billing.Inventory;
import org.tasks.injection.DialogFragmentComponent;
import org.tasks.injection.ForActivity;
import org.tasks.injection.InjectingDialogFragment;
@@ -30,6 +31,7 @@ public class ColorPickerDialog extends InjectingDialogFragment {
@Inject @ForActivity Context context;
@Inject Preferences preferences;
@Inject Theme theme;
+ @Inject Inventory inventory;
private ThemePickerCallback callback;
private SingleCheckedArrayAdapter adapter;
private Dialog dialog;
@@ -59,8 +61,7 @@ public class ColorPickerDialog extends InjectingDialogFragment {
context, transform(items, Pickable::getName), theme.getThemeAccent()) {
@Override
protected int getDrawable(int position) {
- return preferences.hasPurchase(R.string.p_purchased_themes)
- || items.get(position).isFree()
+ return inventory.purchasedThemes() || items.get(position).isFree()
? R.drawable.ic_lens_black_24dp
: R.drawable.ic_vpn_key_black_24dp;
}
@@ -79,7 +80,7 @@ public class ColorPickerDialog extends InjectingDialogFragment {
selected,
(dialog, which) -> {
Pickable picked = items.get(which);
- if (preferences.hasPurchase(R.string.p_purchased_themes) || picked.isFree()) {
+ if (inventory.purchasedThemes() || picked.isFree()) {
callback.themePicked(picked);
} else {
callback.initiateThemePurchase();
diff --git a/app/src/main/java/org/tasks/dialogs/DonationDialog.java b/app/src/main/java/org/tasks/dialogs/DonationDialog.java
deleted file mode 100644
index da5332b33..000000000
--- a/app/src/main/java/org/tasks/dialogs/DonationDialog.java
+++ /dev/null
@@ -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 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 getDonationValues() {
- List 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);
- }
-}
diff --git a/app/src/main/java/org/tasks/injection/ActivityComponent.java b/app/src/main/java/org/tasks/injection/ActivityComponent.java
index 799e71e83..d36f77c1c 100644
--- a/app/src/main/java/org/tasks/injection/ActivityComponent.java
+++ b/app/src/main/java/org/tasks/injection/ActivityComponent.java
@@ -22,6 +22,7 @@ import org.tasks.activities.FilterSettingsActivity;
import org.tasks.activities.GoogleTaskListSettingsActivity;
import org.tasks.activities.TagSettingsActivity;
import org.tasks.activities.TimePickerActivity;
+import org.tasks.billing.PurchaseActivity;
import org.tasks.caldav.CaldavSettingsActivity;
import org.tasks.dashclock.DashClockSettings;
import org.tasks.files.FileExplore;
@@ -134,4 +135,6 @@ public interface ActivityComponent {
void inject(TaskerCreateTaskActivity taskerCreateTaskActivity);
void inject(TaskListViewModel taskListViewModel);
+
+ void inject(PurchaseActivity purchaseActivity);
}
diff --git a/app/src/main/java/org/tasks/injection/NativeDialogFragmentComponent.java b/app/src/main/java/org/tasks/injection/NativeDialogFragmentComponent.java
index f3272eadc..ce553d414 100644
--- a/app/src/main/java/org/tasks/injection/NativeDialogFragmentComponent.java
+++ b/app/src/main/java/org/tasks/injection/NativeDialogFragmentComponent.java
@@ -2,7 +2,6 @@ package org.tasks.injection;
import dagger.Subcomponent;
import org.tasks.activities.RemoteListNativePicker;
-import org.tasks.dialogs.DonationDialog;
import org.tasks.dialogs.ExportTasksDialog;
import org.tasks.dialogs.ImportTasksDialog;
import org.tasks.dialogs.NativeDatePickerDialog;
@@ -26,6 +25,4 @@ public interface NativeDialogFragmentComponent {
void inject(ExportTasksDialog exportTasksDialog);
void inject(ImportTasksDialog importTasksDialog);
-
- void inject(DonationDialog donationDialog);
}
diff --git a/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java b/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java
index fc5af1d84..87049d82e 100755
--- a/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java
+++ b/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java
@@ -11,22 +11,25 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import javax.inject.Inject;
import net.dinglisch.android.tasker.TaskerPlugin;
+import org.tasks.LocalBroadcastManager;
import org.tasks.R;
-import org.tasks.billing.PurchaseHelper;
-import org.tasks.billing.PurchaseHelperCallback;
+import org.tasks.billing.BillingClient;
+import org.tasks.billing.Inventory;
+import org.tasks.billing.PurchaseActivity;
import org.tasks.injection.ActivityComponent;
import org.tasks.locale.bundle.TaskCreationBundle;
import org.tasks.preferences.Preferences;
import org.tasks.ui.MenuColorizer;
public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCompatActivity
- implements PurchaseHelperCallback, Toolbar.OnMenuItemClickListener {
+ implements Toolbar.OnMenuItemClickListener {
- private static final int REQUEST_PURCHASE = 10125;
- private static final String EXTRA_PURCHASE_INITIATED = "extra_purchase_initiated";
+ private static final int REQUEST_SUBSCRIPTION = 10101;
@Inject Preferences preferences;
- @Inject PurchaseHelper purchaseHelper;
+ @Inject BillingClient billingClient;
+ @Inject Inventory inventory;
+ @Inject LocalBroadcastManager localBroadcastManager;
@BindView(R.id.title)
TextInputEditText title;
@@ -47,7 +50,6 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
TextInputEditText description;
private Bundle previousBundle;
- private boolean purchaseInitiated;
@Override
public void onCreate(final Bundle savedInstanceState) {
@@ -76,19 +78,12 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
if (savedInstanceState != null) {
previousBundle = savedInstanceState.getParcelable(TaskCreationBundle.EXTRA_BUNDLE);
- purchaseInitiated = savedInstanceState.getBoolean(EXTRA_PURCHASE_INITIATED);
TaskCreationBundle bundle = new TaskCreationBundle(previousBundle);
title.setText(bundle.getTitle());
}
- if (!preferences.hasPurchase(R.string.p_purchased_tasker) && !purchaseInitiated) {
- purchaseInitiated =
- purchaseHelper.purchase(
- this,
- getString(R.string.sku_tasker),
- getString(R.string.p_purchased_tasker),
- REQUEST_PURCHASE,
- this);
+ if (!inventory.purchasedTasker()) {
+ startActivityForResult(new Intent(this, PurchaseActivity.class), REQUEST_SUBSCRIPTION);
}
}
@@ -137,15 +132,6 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
return title.getText().toString().trim();
}
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == REQUEST_PURCHASE) {
- purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
- } else {
- super.onActivityResult(requestCode, resultCode, data);
- }
- }
-
@Override
public void onBackPressed() {
final boolean backButtonSavesTask = preferences.backButtonSavesTask();
@@ -165,20 +151,10 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
finish();
}
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- if (!isChangingConfigurations()) {
- purchaseHelper.disposeIabHelper();
- }
- }
-
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(TaskCreationBundle.EXTRA_BUNDLE, previousBundle);
- outState.putBoolean(EXTRA_PURCHASE_INITIATED, purchaseInitiated);
}
@Override
@@ -186,13 +162,6 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
component.inject(this);
}
- @Override
- public void purchaseCompleted(boolean success, String sku) {
- if (!success) {
- discardButtonClick();
- }
- }
-
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
@@ -204,6 +173,17 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom
new Intent(Intent.ACTION_VIEW).setData(Uri.parse("http://tasks.org/help/tasker")));
return true;
}
- return super.onOptionsItemSelected(item);
+ return onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_SUBSCRIPTION) {
+ if (!inventory.purchasedTasker()) {
+ discardButtonClick();
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
}
}
diff --git a/app/src/main/java/org/tasks/locale/ui/activity/TaskerSettingsActivity.java b/app/src/main/java/org/tasks/locale/ui/activity/TaskerSettingsActivity.java
index ac6891709..95db8ac9a 100755
--- a/app/src/main/java/org/tasks/locale/ui/activity/TaskerSettingsActivity.java
+++ b/app/src/main/java/org/tasks/locale/ui/activity/TaskerSettingsActivity.java
@@ -8,28 +8,26 @@ import com.todoroo.astrid.api.Filter;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.activities.FilterSelectionActivity;
-import org.tasks.billing.PurchaseHelper;
-import org.tasks.billing.PurchaseHelperCallback;
+import org.tasks.billing.BillingClient;
+import org.tasks.billing.Inventory;
+import org.tasks.billing.PurchaseActivity;
import org.tasks.injection.ActivityComponent;
import org.tasks.locale.bundle.ListNotificationBundle;
import org.tasks.preferences.DefaultFilterProvider;
-import org.tasks.preferences.Preferences;
public final class TaskerSettingsActivity extends AbstractFragmentPluginPreferenceActivity
- implements PurchaseHelperCallback, Toolbar.OnMenuItemClickListener {
+ implements Toolbar.OnMenuItemClickListener {
private static final int REQUEST_SELECT_FILTER = 10124;
- private static final int REQUEST_PURCHASE = 10125;
+ private static final int REQUEST_SUBSCRIPTION = 10125;
private static final String EXTRA_FILTER = "extra_filter";
- private static final String EXTRA_PURCHASE_INITIATED = "extra_purchase_initiated";
- @Inject Preferences preferences;
@Inject DefaultFilterProvider defaultFilterProvider;
- @Inject PurchaseHelper purchaseHelper;
+ @Inject BillingClient billingClient;
+ @Inject Inventory inventory;
private Bundle previousBundle;
private Filter filter;
- private boolean purchaseInitiated;
@Override
public void onCreate(final Bundle savedInstanceState) {
@@ -41,7 +39,6 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginPreferen
previousBundle =
savedInstanceState.getParcelable(ListNotificationBundle.BUNDLE_EXTRA_PREVIOUS_BUNDLE);
filter = savedInstanceState.getParcelable(EXTRA_FILTER);
- purchaseInitiated = savedInstanceState.getBoolean(EXTRA_PURCHASE_INITIATED);
} else {
filter = defaultFilterProvider.getDefaultFilter();
}
@@ -59,14 +56,8 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginPreferen
refreshPreferences();
- if (!preferences.hasPurchase(R.string.p_purchased_tasker) && !purchaseInitiated) {
- purchaseInitiated =
- purchaseHelper.purchase(
- this,
- getString(R.string.sku_tasker),
- getString(R.string.p_purchased_tasker),
- REQUEST_PURCHASE,
- this);
+ if (!inventory.purchasedTasker()) {
+ startActivityForResult(new Intent(this, PurchaseActivity.class), REQUEST_SUBSCRIPTION);
}
}
@@ -108,28 +99,20 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginPreferen
filter = data.getParcelableExtra(FilterSelectionActivity.EXTRA_FILTER);
refreshPreferences();
}
- } else if (requestCode == REQUEST_PURCHASE) {
- purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
+ } else if (requestCode == REQUEST_SUBSCRIPTION) {
+ if (!inventory.purchasedTasker()) {
+ cancel();
+ }
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- if (!isChangingConfigurations()) {
- purchaseHelper.disposeIabHelper();
- }
- }
-
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(ListNotificationBundle.BUNDLE_EXTRA_PREVIOUS_BUNDLE, previousBundle);
outState.putParcelable(EXTRA_FILTER, filter);
- outState.putBoolean(EXTRA_PURCHASE_INITIATED, purchaseInitiated);
}
private void refreshPreferences() {
@@ -141,13 +124,6 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginPreferen
component.inject(this);
}
- @Override
- public void purchaseCompleted(boolean success, String sku) {
- if (!success) {
- cancel();
- }
- }
-
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
@@ -155,6 +131,6 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginPreferen
finish();
return true;
}
- return super.onOptionsItemSelected(item);
+ return onOptionsItemSelected(item);
}
}
diff --git a/app/src/main/java/org/tasks/preferences/BasicPreferences.java b/app/src/main/java/org/tasks/preferences/BasicPreferences.java
index b955eaa68..33cac69b6 100644
--- a/app/src/main/java/org/tasks/preferences/BasicPreferences.java
+++ b/app/src/main/java/org/tasks/preferences/BasicPreferences.java
@@ -1,17 +1,14 @@
package org.tasks.preferences;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
-import static org.tasks.dialogs.DonationDialog.newDonationDialog;
import static org.tasks.dialogs.ExportTasksDialog.newExportTasksDialog;
import static org.tasks.dialogs.ImportTasksDialog.newImportTasksDialog;
import static org.tasks.locale.LocalePickerDialog.newLocalePickerDialog;
import android.app.Activity;
import android.content.Intent;
-import android.net.Uri;
import android.os.Bundle;
import android.preference.Preference;
-import android.preference.TwoStatePreference;
import com.google.common.base.Strings;
import com.todoroo.astrid.core.OldTaskPreferences;
import com.todoroo.astrid.reminders.ReminderPreferences;
@@ -22,8 +19,8 @@ import org.tasks.R;
import org.tasks.activities.ColorPickerActivity;
import org.tasks.analytics.Tracker;
import org.tasks.analytics.Tracking;
-import org.tasks.billing.PurchaseHelper;
-import org.tasks.billing.PurchaseHelperCallback;
+import org.tasks.billing.BillingClient;
+import org.tasks.billing.Inventory;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.files.FileExplore;
import org.tasks.injection.ActivityComponent;
@@ -34,15 +31,12 @@ import org.tasks.themes.ThemeAccent;
import org.tasks.themes.ThemeBase;
import org.tasks.themes.ThemeCache;
import org.tasks.themes.ThemeColor;
-import timber.log.Timber;
public class BasicPreferences extends InjectingPreferenceActivity
- implements LocalePickerDialog.LocaleSelectionHandler, PurchaseHelperCallback {
+ implements LocalePickerDialog.LocaleSelectionHandler {
- public static final int REQUEST_PURCHASE = 10007;
private static final String EXTRA_RESULT = "extra_result";
private static final String FRAG_TAG_LOCALE_PICKER = "frag_tag_locale_picker";
- private static final String FRAG_TAG_DONATION = "frag_tag_donation";
private static final String FRAG_TAG_IMPORT_TASKS = "frag_tag_import_tasks";
private static final String FRAG_TAG_EXPORT_TASKS = "frag_tag_export_tasks";
private static final int RC_PREFS = 10001;
@@ -59,7 +53,8 @@ public class BasicPreferences extends InjectingPreferenceActivity
@Inject DialogBuilder dialogBuilder;
@Inject Locale locale;
@Inject ThemeCache themeCache;
- @Inject PurchaseHelper purchaseHelper;
+ @Inject BillingClient billingClient;
+ @Inject Inventory inventory;
private Bundle result;
@@ -70,8 +65,10 @@ public class BasicPreferences extends InjectingPreferenceActivity
result = savedInstanceState == null ? new Bundle() : savedInstanceState.getBundle(EXTRA_RESULT);
addPreferencesFromResource(R.xml.preferences);
- addPreferencesFromResource(R.xml.preferences_addons);
addPreferencesFromResource(R.xml.preferences_privacy);
+ if (BuildConfig.DEBUG) {
+ addPreferencesFromResource(R.xml.preferences_debug);
+ }
setupActivity(R.string.EPr_appearance_header, AppearancePreferences.class);
setupActivity(R.string.notifications, ReminderPreferences.class);
@@ -136,88 +133,6 @@ public class BasicPreferences extends InjectingPreferenceActivity
return false;
});
- findPreference(R.string.TLA_menu_donate)
- .setOnPreferenceClickListener(
- preference -> {
- if (BuildConfig.FLAVOR.equals("googleplay")) {
- newDonationDialog().show(getFragmentManager(), FRAG_TAG_DONATION);
- } else {
- startActivity(
- new Intent(Intent.ACTION_VIEW).setData(Uri.parse("http://tasks.org/donate")));
- }
- return false;
- });
-
- findPreference(R.string.p_purchased_themes)
- .setOnPreferenceChangeListener(
- (preference, newValue) -> {
- if (newValue != null
- && (boolean) newValue
- && !preferences.hasPurchase(R.string.p_purchased_themes)) {
- purchaseHelper.purchase(
- BasicPreferences.this,
- getString(R.string.sku_themes),
- getString(R.string.p_purchased_themes),
- REQUEST_PURCHASE,
- BasicPreferences.this);
- }
- return false;
- });
-
- findPreference(R.string.p_purchased_tasker)
- .setOnPreferenceChangeListener(
- (preference, newValue) -> {
- if (newValue != null
- && (boolean) newValue
- && !preferences.hasPurchase(R.string.p_purchased_tasker)) {
- purchaseHelper.purchase(
- BasicPreferences.this,
- getString(R.string.sku_tasker),
- getString(R.string.p_purchased_tasker),
- REQUEST_PURCHASE,
- BasicPreferences.this);
- }
- return false;
- });
-
- findPreference(R.string.p_purchased_dashclock)
- .setOnPreferenceChangeListener(
- (preference, newValue) -> {
- if (newValue != null
- && (boolean) newValue
- && !preferences.hasPurchase(R.string.p_purchased_dashclock)) {
- purchaseHelper.purchase(
- BasicPreferences.this,
- getString(R.string.sku_dashclock),
- getString(R.string.p_purchased_dashclock),
- REQUEST_PURCHASE,
- BasicPreferences.this);
- }
- return false;
- });
-
- if (BuildConfig.DEBUG) {
- addPreferencesFromResource(R.xml.preferences_debug);
-
- findPreference(getString(R.string.debug_unlock_purchases))
- .setOnPreferenceClickListener(
- preference -> {
- preferences.setBoolean(R.string.p_purchased_dashclock, true);
- preferences.setBoolean(R.string.p_purchased_tasker, true);
- preferences.setBoolean(R.string.p_purchased_themes, true);
- recreate();
- return true;
- });
-
- findPreference(getString(R.string.debug_consume_purchases))
- .setOnPreferenceClickListener(
- preference -> {
- purchaseHelper.consumePurchases();
- recreate();
- return true;
- });
- }
-
findPreference(R.string.backup_BAc_import)
.setOnPreferenceClickListener(
preference -> {
@@ -237,15 +152,14 @@ public class BasicPreferences extends InjectingPreferenceActivity
initializeBackupDirectory();
- requires(R.string.get_plugins, atLeastJellybeanMR1(), R.string.p_purchased_dashclock);
requires(
R.string.settings_localization,
atLeastJellybeanMR1(),
R.string.p_language,
R.string.p_layout_direction);
+ //noinspection ConstantConditions
if (!BuildConfig.FLAVOR.equals("googleplay")) {
- requires(R.string.settings_general, false, R.string.synchronization);
requires(R.string.privacy, false, R.string.p_collect_statistics);
}
}
@@ -307,8 +221,6 @@ public class BasicPreferences extends InjectingPreferenceActivity
newImportTasksDialog(data.getStringExtra(FileExplore.EXTRA_FILE))
.show(getFragmentManager(), FRAG_TAG_IMPORT_TASKS);
}
- } else if (requestCode == REQUEST_PURCHASE) {
- purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
@@ -344,32 +256,6 @@ public class BasicPreferences extends InjectingPreferenceActivity
super.finish();
}
- @Override
- public void purchaseCompleted(final boolean success, final String sku) {
- runOnUiThread(
- () -> {
- if (getString(R.string.sku_tasker).equals(sku)) {
- ((TwoStatePreference) findPreference(R.string.p_purchased_tasker)).setChecked(success);
- } else if (getString(R.string.sku_dashclock).equals(sku)) {
- ((TwoStatePreference) findPreference(R.string.p_purchased_dashclock))
- .setChecked(success);
- } else if (getString(R.string.sku_themes).equals(sku)) {
- ((TwoStatePreference) findPreference(R.string.p_purchased_themes)).setChecked(success);
- } else {
- Timber.d("Unhandled sku: %s", sku);
- }
- });
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
-
- if (!isChangingConfigurations()) {
- purchaseHelper.disposeIabHelper();
- }
- }
-
private void initializeBackupDirectory() {
findPreference(getString(R.string.p_backup_dir))
.setOnPreferenceClickListener(
diff --git a/app/src/main/java/org/tasks/preferences/Preferences.java b/app/src/main/java/org/tasks/preferences/Preferences.java
index 5b9c272c1..4d8410bb5 100644
--- a/app/src/main/java/org/tasks/preferences/Preferences.java
+++ b/app/src/main/java/org/tasks/preferences/Preferences.java
@@ -1,7 +1,10 @@
package org.tasks.preferences;
import static android.content.SharedPreferences.Editor;
+import static com.google.common.collect.Iterables.transform;
+import static com.google.common.collect.Sets.newHashSet;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybean;
+import static java.util.Collections.emptySet;
import android.content.Context;
import android.content.SharedPreferences;
@@ -11,11 +14,14 @@ import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
+import com.android.billingclient.api.Purchase;
+import com.google.gson.GsonBuilder;
import com.todoroo.astrid.activity.BeastModePreferences;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.core.SortHelper;
import com.todoroo.astrid.data.Task;
import java.io.File;
+import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
@@ -123,6 +129,30 @@ public class Preferences {
return getMillisPerDayPref(R.string.p_date_shortcut_evening, R.integer.default_evening);
}
+ public Iterable getPurchases() {
+ try {
+ return transform(
+ prefs.getStringSet(context.getString(R.string.p_purchases), emptySet()),
+ p ->
+ new GsonBuilder().create().fromJson(p, com.android.billingclient.api.Purchase.class));
+ } catch (Exception e) {
+ Timber.e(e, e.getMessage());
+ return emptySet();
+ }
+ }
+
+ public void setPurchases(Collection purchases) {
+ try {
+ Editor editor = prefs.edit();
+ editor.putStringSet(
+ context.getString(R.string.p_purchases),
+ newHashSet(transform(purchases, p -> new GsonBuilder().create().toJson(p))));
+ editor.apply();
+ } catch (Exception e) {
+ Timber.e(e, e.getMessage());
+ }
+ }
+
public int getDateShortcutNight() {
return getMillisPerDayPref(R.string.p_date_shortcut_night, R.integer.default_night);
}
@@ -265,10 +295,6 @@ public class Preferences {
&& permissionChecker.canAccessMissedCallPermissions();
}
- public boolean hasPurchase(int keyResource) {
- return getBoolean(keyResource, false);
- }
-
public boolean getBoolean(int keyResources, boolean defValue) {
return getBoolean(context.getString(keyResources), defValue);
}
diff --git a/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java b/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java
index a07bf3a4d..db186f893 100644
--- a/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java
+++ b/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java
@@ -65,12 +65,15 @@ public class NavigationDrawerFragment extends InjectingFragment {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == FilterAdapter.REQUEST_SETTINGS
- && resultCode == Activity.RESULT_OK
- && data != null) {
- if (data.getBooleanExtra(AppearancePreferences.EXTRA_RESTART, false)) {
- TaskListActivity activity = (TaskListActivity) getActivity();
- activity.restart();
+ if (requestCode == FilterAdapter.REQUEST_SETTINGS) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
+ if (data.getBooleanExtra(AppearancePreferences.EXTRA_RESTART, false)) {
+ ((TaskListActivity) getActivity()).restart();
+ }
+ }
+ } else if (requestCode == FilterAdapter.REQUEST_PURCHASE) {
+ if (resultCode == Activity.RESULT_OK) {
+ ((TaskListActivity) getActivity()).restart();
}
} else if (requestCode == REQUEST_NEW_LIST
|| requestCode == ACTIVITY_REQUEST_NEW_FILTER
diff --git a/app/src/main/res/drawable/ic_attach_money_black_24dp.xml b/app/src/main/res/drawable/ic_attach_money_black_24dp.xml
new file mode 100644
index 000000000..4c88aeeb5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_attach_money_black_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_purchase.xml b/app/src/main/res/layout/activity_purchase.xml
new file mode 100644
index 000000000..5876926fa
--- /dev/null
+++ b/app/src/main/res/layout/activity_purchase.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/loading_indicator.xml b/app/src/main/res/layout/loading_indicator.xml
new file mode 100644
index 000000000..6e2823aa3
--- /dev/null
+++ b/app/src/main/res/layout/loading_indicator.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/app/src/main/res/layout/sku_details_row.xml b/app/src/main/res/layout/sku_details_row.xml
new file mode 100644
index 000000000..5511a3ca2
--- /dev/null
+++ b/app/src/main/res/layout/sku_details_row.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/sku_details_row_header.xml b/app/src/main/res/layout/sku_details_row_header.xml
new file mode 100644
index 000000000..809da518a
--- /dev/null
+++ b/app/src/main/res/layout/sku_details_row_header.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_purchase_activity.xml b/app/src/main/res/menu/menu_purchase_activity.xml
new file mode 100644
index 000000000..67ca9660a
--- /dev/null
+++ b/app/src/main/res/menu/menu_purchase_activity.xml
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index ef961da24..e00389c1d 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -164,7 +164,6 @@
إعدادات التصفيةعرض المهام المخفيةإظهار المكتملة
- تاسكس هو مشروع مفتوح المصدر مدموع من طرف مطور واحد. بعض الخيارات متوفرة عن الطريق الدفع من داخل التطبيق من أجل دعم التطويرالتعتيمتخصيص اللغة و الجهةإتجاه التنسيق
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
index 4d60efa65..91257cbf1 100644
--- a/app/src/main/res/values-bg-rBG/strings.xml
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -365,7 +365,6 @@
Покажи скритиПокажи завършениОбратно
- Покупки в приложениетоНе е намерено приложение за отваряне на прикачения файлДобавяне на прикачен файлЗаснемане
@@ -386,20 +385,12 @@
Бутон \"Назад\" запазва задачатаСписък по подразбиранеСинхронизация по подразбиране
- Tasks е проект с отворен код поддържан от един разработчик. Някои от функциите се предлагат като покупки в приложението за да се подпомогне процеса на разработка.
- Отключете всички теми и да добавите малко цвят към Tasks
- Context-Aware списък уведомления. Изисква Tasker или LocaleДаренията са добре дошли
- Показване на броя активни задачи. Изисква DashClock Widget
- Купи
- Купи разширение
- Услугата за таксуване в приложението е заета, опитайте отново по-късноФилтърНепрозрачностТемаЦвятАкцент
- Допълнителни темиЧервенРозовЛилав
@@ -499,4 +490,5 @@
Създай задачаСписък с нотификацииПомощ
+ Допълнителни теми
\ No newline at end of file
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 880dae0bc..41861e867 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -266,9 +266,7 @@
(Bez názvu)Tlačítko zpět uloží úkolVýchozí seznam
- Tasks je open source projekt udržovaný jedním programátorem. Některé funkce jsou nabízeny za nákup v aplikaci, což slouží k podpoře dalšího vývoje.Dary jsou velmi ceněny
- KoupitFiltrPrůhlednostVzhled
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index f5ed5b752..d8b31751a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -358,7 +358,6 @@
Ausgeblendete anzeigenErledigte anzeigenRückgangig
- In App-KäufeKeine Anwendung zum Öffnen des Anhangs gefundenAnhang hinzufügenBild aufnehmen
@@ -377,18 +376,10 @@
(kein Titel)Zurück-Button speichert die AufgabeStandard-Liste
- Tasks ist ein Open Source-Projekt eines einzelnen Entwicklers. Manche Funktionen werden daher als In App-Käufe angeboten, um die Entwicklung zu unterstützen.
- Entsperre alle Themes
- Kontext-bezogene Listenbenachrichtigungen. Benötigt Tasker oder LocaleSpenden sind sehr willkommen
- Anzahl aktiver Aufgaben anzeigen (DashClock-Widget benötigt).
- Kaufen
- Erweiterung kaufen
- Dienst für In-App Käufe ist beschäftigt, versuchen Sie es später noch einmalDurchsichtigkeitFarbeAkzent
- Zusätzliche ThemesRotLilaDunkel-Lila
@@ -474,4 +465,5 @@
vierteNachnameHilfe
+ Zusätzliche Themes
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 786c27ef7..ed57d61e1 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -361,7 +361,6 @@
Mostrar ocultoMostrar completadoInvertir
- Compras en la aplicaciónNo se ha encontrado una aplicación para abrir el archivo adjuntoAdjuntar archivoTomar fotografía
@@ -382,19 +381,11 @@
Botón atrás guarda la tareaLista por defectoSincronización predeterminada
- Tasks es un proyecto de código abierto mantenido por un único desarrollador. Algunas características especializadas se podrán comprar desde la aplicación para mantener su desarrollo.
- Desbloquea todos los temas y añade color a Tasks
- Notificaciones dependientes del contexto. Requiere Tasker o Locale.Se agradecen enormemente las donaciones
- Muestra una cuenta de las tareas activas. Requiere DashClock Widget
- Comprar
- Comprar extensión
- El servicio de compra desde la aplicación está ocupado, inténtelo más tarde.FiltroOpacidadTemaAcentuado
- Temas adicionalesRojoRosaMorado
@@ -493,4 +484,5 @@
Crear tareaListar notificaciónAyuda
+ Temas adicionales
\ No newline at end of file
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index aba4756d2..258cf784e 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -249,14 +249,12 @@
نمایش مخفی هانمایش انجام شده هابرعکس
- خرید داخل برنامهالصاق پیوستگرفتن عکسبهبود وظیفهاین تگ قبلاً ایجاد شده است(بدون عنوان)لیست پیش فرض
- Tasks پروژهای متنباز است که عمدتاً توسط یکنفر توسعه داده میشود. برای حمایت از این تلاش، برخی ویژگیها بهصورت خریدهای داخل برنامه ارائه شدهاند.شفافیتتمرنگ
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index cfe493cb8..a5a248d8d 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -366,7 +366,6 @@
Näytä piilotetutNäytä valmiitKäänteinen
- Sovelluksen sisäiset ostotLiitteen avaamiseksi ei löydy sovellustaLisää liiteOta kuva
@@ -382,20 +381,12 @@
Takaisin -painike tallentaa tehtävänOletuslistaOletus synkronointi
- Tasks on avoimen koodin projekti jota ylläpitää yksi kehittäjä. Jotkut ominaisuudet ovat saatavina sovelluksen sisäisinä ostoksina joilla tuetaan kehitystyötä.
- Avaa kaikki teemat ja lisää väriä Tasks -ohjelmaan
- Kontekstiriippuvainen listaus ilmoituksille. Vaatii Tasker tai Locale -sovelluksenLahjoitukset ovat erittäin tervetulleita
- Näytä aktiivisten tehtävien määrä. Vaatii DashClock Widgetin
- Osta
- Osta laajennusosa
- Sovelluksen sisäinen laskutuspalvelu on varattu, yritä myöhemmin uudelleenSuodatinLäpinäkyvyysTeemaVäriSävy
- LisäteematPun.PinkkiPurppura
@@ -490,4 +481,5 @@
Luo tehtäväIlmoituslistaApua
+ Lisäteemat
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index e9ba67d83..7a5fce46f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -348,7 +348,6 @@
Afficher les tâches cachéesAfficher les tâches terminéesInverser
- Achats intégrés à l\'applicationAucune application trouvée pour ouvrir la pièce jointeAjouter une pièce jointePrendre une photo
@@ -369,19 +368,11 @@
Faire un retour-arrière sauvegarde la tâcheListe par défautSynchronisation par défaut
- Tasks est un projet à source ouverte entretenu par un développeur. Quelques contenus payants sont disponible dans l\'application pour supporter le développement.
- Débloquer tous les thèmes et quelques couleurs à Tasks
- Notifications de la sensibilité du contexte de la liste. Tasker ou Local est requis.Les donations sont grandement appréciées.
- Affiche le nombre de tâches actives. Requiert le widget DashClock
- Acheter
- Acheter l\'extension
- Le service de facturation intégré à l\'application est surchargé, réessayez plus tard.FiltreTransparenceThèmeCouleur
- Thèmes additionnelsRougeRoseViolet
@@ -480,4 +471,5 @@
AideCalendrier manquantConnexion échouée
+ Thèmes additionnels
\ No newline at end of file
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 98fa31421..86cb0525d 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -288,7 +288,6 @@
Mostrar ocultoMostrar completadoInvertir
- Compras en la aplicaciónNo se ha encontrado una aplicación para abrir el archivo adjuntoAdjuntar archivoTomar fotografía
@@ -303,20 +302,12 @@
(Sin título)Botón atrás guarda la tareaLista por defecto
- Tasks es un proyecto de código abierto mantenido por un único desarrollador. Algunas características especializadas se podrán comprar desde la aplicación para mantener su desarrollo.
- Desbloquea todos los temas y añade color a Tasks
- Notificaciones dependientes del contexto. Requiere Tasker o Locale.Se agradecen enormemente las donaciones
- Muestra una cuenta de las tareas activas. Requiere DashClock Widget
- Comprar
- Comprar extensión
- El servicio de compra desde la aplicación está ocupado, inténtelo más tarde.FiltroOpacidadTemaCorAcentuado
- Temas adicionalesRojoRosaMorado
@@ -367,4 +358,5 @@
¿Borrar tareas seleccionadas?¿Copiar tareas seleccionadas?Escoller data e hora
+ Temas adicionales
\ No newline at end of file
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index a223cbd66..7fe1f288d 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -365,7 +365,6 @@
Rejtettek megjelenítéseElvégzettek megjelenítéseVisszafelé
- Alkalmazáson belüli vásárlásokA csatolmány megnyitására alkalmas program nem találhatóCsatolmány hozzáadásaKép készítése
@@ -386,20 +385,12 @@
A vissza gomb elmenti a feladatotAlapértelmezett listaAlapértelmezett szinkronizáció
- A Tasks egyetlen fejlesztő által karbantartott nyílt forráskódú projekt. Néhány funkció csak alkalmazáson belüli vásárlással érhető el, ezzel is támogatva a fejlesztést.
- Az összes Tasks téma és szín elérhetővé tétele
- Kontextus-függő lista emlékeztető. Használatához Tasker vagy Locale szükséges.Támogatását nagyra értékeljük
- Aktív feladatok számának megjelenítése. DashClock Widget megléte szükséges.
- Vásárlás
- Kiterjesztés vásárlása
- Az alkalmazáson belüli vásárlások szolgáltatás nem elérhető, kérem, próbálja később.SzűrőÁtlátszóságTémaSzínKiemelés
- További témákPirosRózsaszínLila
@@ -500,4 +491,5 @@
Feladat létrehozásaLista értesítésSúgó
+ További témák
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 9f4a70e6e..a61cf3a80 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -362,7 +362,6 @@
Mostra nascosteMostra completateContrario
- Acquisti nell\'app.Nessuna applicazione in grado di aprire l\'allegato Aggiungi allegatoScatta un foto
@@ -383,20 +382,12 @@
Il tasto indietro salva l\'attivitàLista predefinitaSincronizz. predefinita
- Tasks è un progetto open source manutenuto da un singolo sviluppatore. Per supportarne lo sviluppo all\'interno dell\'applicazione sono offerte alcune funzioni di nicchia a pagamento...
- Sblocca tutti i temi e aggiungi colori alle attività
- Lista notifiche in base al contesto. Richiede Tasker o LocaleSono gradite offerte
- Mostra conteggio attività attive. Richiede Widget DashClock
- Acquista
- Acquista estensione
- Servizio fatturazione in-app è occupato, prova più tardiFiltraOpacitàTemaColoreEvidenzia
- Ulteriori temiRossoRosaPorpora
@@ -495,4 +486,5 @@
Crea attivitàElenca notificheAiuto
+ Ulteriori temi
\ No newline at end of file
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 3b22acd2b..864a50a7a 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -313,7 +313,6 @@
הצגת משימות נסתרותהצגת משימות שבוצעואחורה
- רכישות בתוך הישוםלא נמצאה אפליקצה לפתיחת הקובץהוספ/י קובץצלם תמונה
@@ -334,20 +333,12 @@
כפתור \"חזרה\" שומר שינויים במשימהרשימת ברירת מחדלסנכרון ברירת מחדל
- זו אפליקציית קוד פתוח, מתוחזקת ע\"י מפתח אחד. מספר מאפיינים מוצעים לרכישה בתוך האפליקציה בכדי לתמוך בפיתוח.
- בטל נעילת ערכות נושא והוסף צבע למשימות
- Context-aware רשימת משימות. נדרש Tasker או Localeתרומות יתקבלו בברכה
- הצג כמות של משימות פעילות. נחוץ ישומון DashClock
- רכישה
- רכוש תוספת
- שרות רכישה עמוס, נסה מאוחר יותרסינוןאטימותערכת נושאצבעצבע משני
- ערכות נושא נוספותאדוםורודסגול
@@ -424,4 +415,5 @@
ללא סנכרוןיומן לא נמצאהחיבור נכשל
-
+ ערכות נושא נוספות
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 377b1a837..54dabf99e 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -363,7 +363,6 @@
非表示を表示完了を表示逆順
- アプリ内課金添付ファイルを開くアプリケーションが見つかりません添付ファイルを追加写真を撮影
@@ -384,20 +383,12 @@
戻るボタンでタスクを保存しますデフォルトリストデフォルトの同期
- Tasks は、1人の開発者が維持しているオープンソース プロジェクトです。開発をサポートするために、ニッチの機能はアプリ内課金で提供されています。
- すべてのテーマのロックを解除して Tasks に色を追加します
- コンテキスト アウェア リストの通知。Tasker または Locale が必要です寄付は大歓迎です
- アクティブなタスクの数を表示します。DashClock ウィジェットが必要です。
- 購入
- 購入エクステンション
- アプリ内課金サービスがビジー状態です。後でもう一度試してくださいフィルター透明度テーマ色アクセント
- 追加のテーマ赤ピンク紫
@@ -497,4 +488,5 @@
タスクを作成通知のリストヘルプ
+ 追加のテーマ
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 45d8761b2..11cb6788d 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -367,7 +367,6 @@
숨겨진 할일 표시완료한 할일 표시역순
- 인앱 결제첨부 파일을 열 수 있는 앱이 발견되지 않았습니다첨부파일 추가사진 촬영
@@ -387,20 +386,12 @@
(제목 없음)뒤로가기 버튼으로 할일 저장기본 목록
- Tasks는 한 명의 개발자에 의해 유지되고 있는 오픈 소스 프로젝트입니다. 개발을 지속하기 위해 몇몇 기능들은 인앱 결제를 통해 제공되고 있습니다.
- 모든 테마 잠금 해제 및 색상 추가
- 컨텍스트 기반의 리스트 알림. Tasker 또는 Locale이 필요합니다.기부를 해주시면 감사하겠습니다
- 활성화된 할일의 개수를 표시합니다. DashClock Widget을 필요로 합니다.
- 구매
- 확장팩 구매
- 인앱 결제 서비스가 혼잡합니다, 나중에 다시 시도하세요필터불투명도테마색상강조
- 추가적인 테마빨강분홍보라
@@ -496,4 +487,5 @@
세번째네번째마지막
+ 추가적인 테마
\ No newline at end of file
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index c2722033e..4cd0a1389 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -362,7 +362,6 @@
Rodyti paslėptusRodyti užbaigtusAtvirkščiai
- Apsipirkimas programojeNerasta programa, kuri galėtų atidaryti prisegtą failąPridėti failąNufotografuoti
@@ -383,20 +382,12 @@
Mygtukas \"Atgal\" išsaugo pakitimusNumatytasis sąrašasNumatytoji sinchronizacija
- Tasks yra atviro pirminio kodo projektas, išlaikomas vienintelio programuotojo. \'Kai kurios funkcijos yra siūlomos kaip papildomos ir mokamos, kad būtų išlaikytas projekto vystymas.
- Atrakinti visas Tasks temas ir pridėti kelias spalvas
- Pranešimai atsižvelgiant į sąrašo turinį. Reikalingas Tasker arba Locale.Paaukojimai yra ypač vertinami
- Rodyti aktyvių užduočių skaičių. Reikalingas DashClock Widget
- Pirkti
- Pirkti plėtinį
- Programos sąskaitų paslauga užimta, pabandykite vėliauFiltrasPermatomumasTemaSpalvaAkcentas
- Papildomos temosRaudonaRožinėVioletinė
@@ -498,4 +489,5 @@
PagalbaKalendorius nerastasSujungimas nepavyko
+ Papildomos temos
\ No newline at end of file
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 9c388d899..7cecce794 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -157,8 +157,6 @@
Vis skjulteVis fullførteTagg finnes allerede
- Tasks er et åpent kildekode-prosjekt vedlikeholdt av en utvikler. Enkelte funksjoner blir tilbudt som en betalt oppgradering i appen for å støtte videre utvikling.
- Kontekstbevisste listevarsler. Krever Tasker eller LocaleUgjennomsiktighetSpråkTasks må startes om for at endringene skal ta effekt
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index cfa63e565..1a2056c9e 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -358,7 +358,6 @@
Toon verborgenToon voltooideOmgekeerde
- In-app aankopenGeen applicatie gevonden om de bijlage te openenBijlage toevoegenMaak een foto
@@ -377,18 +376,10 @@
(geen titel)Terug knop slaat taak opStandaard lijst
- Tasks is een open source project onderhouden bij een ontwikkelaar. Sommige functionaliteit worden als in-app aankopen aangeboden om de ontwikkeling te ondersteunen.
- Ontlock alle thema\'s en voeg kleuren toe aan Tasks
- Contextbewuste notificaties. Vereist Tasker of LocaleDonaties worden erg gewaardeerd
- Toon een teller van actieve taken. Vereist DashClock Widget
- Kopen
- Koop uitbreiding
- In-app rekening service is bezig, probeer later opnieuwTransparantieThemaKleur
- Extra thema\'sRoodRozePaars
@@ -484,4 +475,5 @@
laatsteTaak makenNotificatie lijst
+ Extra thema\'s
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index a38214321..880b84a53 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -341,7 +341,6 @@
Pokaż ukrytePokaż ukończoneOdwrotnie
- Zakupy w aplikacjiNie znaleziono aplikacji do otwarcia załącznikaDodaj załącznikWybierz obrazek
@@ -361,19 +360,12 @@
(Bez tytułu)Przycisk Cofnij zapisuje zadanieDomyślna lista
- Tasks jest projektem na licencji open source utrzymywanym przez jednego developera. Płatne funkcje wspierają rozwój aplikacji.
- Odblokuj wszystkie motywy i dodaj trochę kolorów do TasksDotacje są bardzo mile widziane
- Wyświetla liczbę aktywnych zadań. Wymaga DashClock Widget
- Kup
- Kup rozszerzenie
- Serwis zakupów w aplikacji jest zajęty, spróbuj późniejFiltrNieprzezroczystośćMotywKolorAkcent
- Dodatkowe motywyCzerwonyRóżowyFioletowy
@@ -451,4 +443,5 @@
trzeciczwartyostatni
+ Dodatkowe motywy
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index ad31ee2f6..51600b8b1 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -282,7 +282,6 @@
Configurações de filtroMostrar ocultasMostrar completas
- Compras no appNenhuma aplicação encontrada para abrir o anexoAdicionar anexoTirar uma foto
@@ -296,20 +295,12 @@
(Sem título)Botão voltar salva a tarefaLista padrão
- Tasks é um projeto de código aberto mantido por um desenvolvedor. Algumas funções são oferecidas como compras dentro do app a fim de apoiar o desenvolvimento.
- Desbloquear todos os temas e adicionar um pouco de cor ao Tasks
- Notificações contextuadas de listas. Necessita Tasker ou Locale.Doações são muito valiosas
- Mostra um contador de tarefas ativas. Necessita DashClock Widget.
- Comprar
- Comprar extensão
- O serviço de compras no app está ocupado. Tente novamente mais tarde.FiltrarOpacidadeTemaCorCor de realce
- Temas adicionaisVermelhoRosaRoxo
@@ -364,4 +355,5 @@
Usar calendário nativoNão inserir no calendárioCalendário padrão
+ Temas adicionais
\ No newline at end of file
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index fc988be47..b714db8a4 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -363,7 +363,6 @@
Mostrar ocultasMostrar terminadasReverter
- Compras na aplicaçãoNenhuma aplicação encontrada para abrir o anexoAdicionar anexoTirar uma foto
@@ -378,17 +377,12 @@
(Sem título)O botão voltar guarda a tarefaLista padrão
- Tasks é um projeto de código aberto mantido por um programador. Alguns recursos são oferecidos através de compras no aplicativo para apoiar o desenvolvimento.
- Desbloqueie todos os temas e adicione alguma cor ao TasksOs donativos são muito apreciados
- Comprar
- Comprar extensãoFiltroOpacidadeTemaCorRealçe
- Temas adicionaisVermelhoRosaPúrpura
@@ -465,4 +459,5 @@
terceiroquartoúltimo
+ Temas adicionais
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index affc1fc0f..c60801dbb 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -363,7 +363,6 @@
Показать скрытыеПоказать выполненныеНаоборот
- Покупки в приложенииНе найдено приложение для открытия прикреплённого файлаПрикрепить файлСделать снимок
@@ -384,20 +383,12 @@
Кнопка «Назад» сохраняет задачуСписок по умолчаниюСинхронизация по-умолчанию
- Tasks развивается как проект с открытым исходным кодом и поддерживается единственным разработчиком. Некоторые функции приложения предлагаются как платные для дальнейшего развития программы
- Разблокировать все темы и добавить цвета в оформление Tasks
- Уведомления списка контекстного подбора. Требуется Tasker или LocaleБлагодарю за поддержку
- Вывести счетчик активных задач. Нужен DashClock-виджет
- Купить
- Купить расширение
- Встроенный сервис перегружен, попробуйте позжеФильтрПрозрачностьЦветовая темаЦветАкцент
- Дополнительные темыКрасныйРозовыйПурпурный
@@ -497,4 +488,5 @@
Создать задачуСписок уведомленийПомощь
+ Дополнительные темы
\ No newline at end of file
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 819f5fcda..a828e534f 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -363,7 +363,6 @@
Zobraziť skrytéZobraziť dokončenéOpačné
- Aplikácia obsahuje platené prvkyNa otvorenie tohto súboru nebola nájdená vhodná aplikácia Pridať prílohu Spraviť obrázok
@@ -384,19 +383,11 @@
Uložiť úlohu tlačidlom SpäťPredvolený zoznamPredvolené synchronizácia
- Úlohy sú open-source projektom, ktorý udržiava jeden vývojár. Pre podporu ďalšieho vývoja sú niektoré funkcie ponúkané ako platené.
- Odomknúť všetky témy a pridať farby do Úloh
- Zoznam s upozorňovaním na polohu. Vyžaduje sa Tasker alebo LocaleSme vďační za Vaše dary
- Zobraziť počet aktívnych úloh. Vyžaduje sa DashClock Widget.
- Kúpiť
- Kúpiť rozšírenie
- Služba platenia v aplikácii je zaneprázdnená, skúste znovu neskôrNejasnosťTémaFarbaZvýraznenie
- Ďaľšie témyČervenáRužováFialová
@@ -496,4 +487,5 @@
Vytvor úlohuZoznam upozorneníPomoc
+ Ďaľšie témy
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 50da755f5..bd13873cd 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -286,7 +286,6 @@
Visa doldaVisa slutfördaOmvänt
- Köp i appIngen applikation hittades för att öppna bilaganBifoga filerTa en bild
@@ -301,18 +300,10 @@
(Ingen titel)Bakåtknapp sparar uppgiftStandardlista
- Tasks är ett open source projekt som drivs av en ensam utvecklare. Några av funktionerna blir tillgängliga via köp i appar för att stödja fortsatt utveckling.
- Lås upp alla teman och sätt lite färg på Tasks
- Innehållsbaserade listpåminnelser. Kräver Tasker eller LocaleDonationer uppskattas varmt
- Visa antalet aktiva uppgifter. Kräver DashClock Widget
- Köp
- Köp tillägg
- Köp i appar tjänsten är upptagen, försök igen senareOpacitetTemaFärg
- Fler temanRödRosaLila
@@ -351,4 +342,5 @@
Tar bort listaByter namn på listanRensa bort slutförda uppgifter?
+ Fler teman
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index e55b41505..068471eef 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -367,7 +367,6 @@
Gizlenenleri gösterTamamlananları gösterTers
- Uygulama içi satın almalarEk dosyasını açmak için hiçbir uygulama bulunamadıEk ekleBir fotoğraf çek
@@ -388,20 +387,12 @@
Geri düğmesi görevi kaydederÖntanımlı listeÖntanımlı eşzamanlama
- Tasks, bir geliştirici tarafından sürdürülen açık kaynaklı tasarıdır. Bazı özellikleri gelişimi desteklemek için uygulama içi satın alma olarak sunulmaktadır.
- Tüm gövdeleri açar ve Tasks\'e birkaç renk ekler
- Bağlam bilinçli liste bildirimleri. Tasker veya Locale gerekirBağışlar makbule geçer
- Etkin görevlerin sayısını gösterir. DashClock Widget gerekir
- Satın al
- Eklenti satın al
- Uygulama içi faturalama hizmeti meşgul, daha sonra yeniden deneyinSüzgeçŞeffaflıkGövdeRenkAra renk
- Ek gövdelerKırmızıPembeMor
@@ -504,4 +495,5 @@
YardımTakvim bulunamadıBağlantı başarısız
+ Ek gövdeler
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 8b72602a9..b6881edb5 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -290,7 +290,6 @@
Показати прихованіПоказати завершеніРеверс
- Покупки в застосункуНе знайдено програми для відкриття вкладенняДодати вкладенняЗробити фото
@@ -305,20 +304,12 @@
(без назви)Зберігати завдання кнопкою НазадТиповий список
- Tasks це проект з відкритим кодом, що обслуговується одним розробником. З метою підтримки розробки, деякі функції доступні лише після придбання через додаток.
- Розблокувати усі схеми та додати кольори до Tasks
- Контекстні сповіщення списку. Потрібен Tasker або LocaleПожертви щиро вітаються
- Показати кількість активних завдань. Потрібен DashClock Widget
- Придбати
- Придбати розширення
- Сервіс оплати застосунку перевантажено, спробуйте пізніше.ФільтрПрозорістьСхемаКолірАкцент
- Додаткові схемиЧервонийРожевийПурпуровий
@@ -368,4 +359,5 @@
%s видаленоВидалити вибрані завдання?Копіювати вибрані завдання?
+ Додаткові схеми
\ No newline at end of file
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
index 7173796a6..bc8a6fa2b 100644
--- a/app/src/main/res/values-v21/styles.xml
+++ b/app/src/main/res/values-v21/styles.xml
@@ -10,4 +10,10 @@
gone
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 1fd967e92..90a2e6ff9 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -362,7 +362,6 @@
显示隐藏的任务显示已完成任务反向
- 应用内购买没有能打开附件的应用添加附件拍张照片
@@ -383,20 +382,12 @@
返回键保存任务默认列表默认同步
- Tasks是一个开发人员维护的开源项目。有些功能是作为应用内购提供的,以此支持开发。
- 解锁所有Tasks的主题和颜色
- 上下文感知列表通知。需要Tasks或Locale不胜感激
- 显示活动任务的计数。需要安装DashClock Widget
- 购买
- 购买扩展
- 应用内结算服务繁忙,请稍后重试过滤器不透明度主题颜色强调色
- 其他主题红色粉色紫色
@@ -497,4 +488,5 @@
创建任务列出通知帮助
+ 其他主题
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index c6d3f75b9..182b536b6 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -221,15 +221,12 @@
過濾器設定顯示隱藏顯示已完成
- 程式內購買預設清單
- 解鎖全部主題並為工作增加一些色彩非常歡迎您的贊助!透明度主題色彩強調色
- 其他主題一般語言Tasks 必須重新啟動以使變更生效
@@ -248,4 +245,5 @@
每月每年每 %s 重複
+ 其他主題
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index ead47a633..ed8a9870f 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -224,4 +224,12 @@
12
+
+
+ @string/themes
+ @string/pro_caldav_sync
+ @string/pro_multiple_google_task_accounts
+ @string/pro_tasker_plugins
+ @string/pro_dashclock_extension
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 55918dfbe..c807134a0 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -25,4 +25,17 @@
10dp6dp48dp
+
+ 14sp
+
+ 8dp
+ 2dp
+ 2sp
+
+ 152dp
+ 48dp
+
+ 8dp
+ 1px
+
\ No newline at end of file
diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml
index 91284e2ad..0696cb818 100644
--- a/app/src/main/res/values/keys.xml
+++ b/app/src/main/res/values/keys.xml
@@ -258,11 +258,7 @@
Multiselect clonebadges_enabledbadge_list
- purchased_tasker
- purchased_dashclock
- purchased_themesTasker/Locale
- DashClock extensiontheme_styletheme_colortheme_accent
@@ -282,11 +278,10 @@
dashclock_filterdefault_remote_list
- tasker
- dashclock
- Unlock purchases
- Consume purchases
+ Purchases
+ ConsumeStrict mode
+ BuyDebugstart_of_weekuse_native_datetime_pickers
@@ -296,5 +291,6 @@
warned_play_servicessync_caldavbackground_sync_unmetered_only
+ purchases
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a85fd8f06..10537255d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -744,7 +744,6 @@ File %1$s contained %2$s.\n\n
Show hiddenShow completedReverse
- In-app purchasesNo application found to open attachmentAdd attachmentTake a picture
@@ -765,21 +764,12 @@ File %1$s contained %2$s.\n\n
Back button saves taskDefault listDefault sync
- Tasks is an open source project maintained by one developer. Some features are offered as in-app purchases in order to support development.
- Unlock all themes and add some color to Tasks
- Context-aware list notifications. Requires Tasker or Locale
- Donations are greatly appreciated
- Display a count of active tasks. Requires DashClock Widget
- Buy
- Buy extension
- In-app billing service is busy, try again laterFilterOpacityThemeColorAccent
- Additional themesRedPinkPurple
@@ -889,4 +879,20 @@ File %1$s contained %2$s.\n\n
Calendar not foundConnection failedOnly on unmetered connections
+ Upgrade
+ Subscribe to pro
+ Refresh purchases
+ Subscribed
+ Subscribe
+ Owned
+ Billing unavailable. Make sure your Google Play app
+ is setup correctly
+ Billing unavailable. Please check your device.
+
+ Additional themes
+ CalDAV synchronization
+ Multiple Google Task accounts
+ Tasker plugins
+ Dashclock extension
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index ff7628967..50721f4d5 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -111,4 +111,28 @@
@android:color/transparent
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences_addons.xml b/app/src/main/res/xml/preferences_addons.xml
deleted file mode 100644
index 0db6b4851..000000000
--- a/app/src/main/res/xml/preferences_addons.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_debug.xml b/app/src/main/res/xml/preferences_debug.xml
index 5b247a1da..a632e5dfc 100644
--- a/app/src/main/res/xml/preferences_debug.xml
+++ b/app/src/main/res/xml/preferences_debug.xml
@@ -4,13 +4,11 @@
-
-
-
+
+
+