Add new subscription pricing

pull/699/head
Alex Baker 8 years ago
parent 0df0c11c30
commit f4ae1100bd

@ -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}"

@ -3,11 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.tasks">
<!-- **************** -->
<!-- in-app donations -->
<!-- **************** -->
<uses-permission android:name="com.android.vending.BILLING"/>
<!-- ************************ -->
<!-- location based reminders -->
<!-- ************************ -->

@ -1,192 +0,0 @@
/*
* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.os.Bundle;
/**
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
* This service provides the following features:
* 1. Provides a new API to get details of in-app items published for the app including
* price, type, title and description.
* 2. The purchase flow is synchronous and purchase information is available immediately
* after it completes.
* 3. Purchase information of in-app purchases is maintained within the Google Play system
* till the purchase is consumed.
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
* in-app items are consumable and thereafter can be purchased again.
* 5. An API to get current purchases of the user immediately. This will not contain any
* consumed purchases.
*
* All calls will give a response code with the following possible values
* RESULT_OK = 0 - success
* RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
* RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
* RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
* RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
* RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
* RESULT_ERROR = 6 - Fatal error during the API action
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
*/
interface IInAppBillingService {
/**
* Checks support for the requested billing API version, package and in-app type.
* Minimum API version supported by this interface is 3.
* @param apiVersion billing API version that the app is using
* @param packageName the package name of the calling app
* @param type type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @return RESULT_OK(0) on success and appropriate response code on failures.
*/
int isBillingSupported(int apiVersion, String packageName, String type);
/**
* Provides details of a list of SKUs
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
* with a list JSON strings containing the productId, price, title and description.
* This API can be called with a maximum of 20 SKUs.
* @param apiVersion billing API version that the app is using
* @param packageName the package name of the calling app
* @param type of the in-app items ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "DETAILS_LIST" with a StringArrayList containing purchase information
* in JSON format similar to:
* '{ "productId" : "exampleSku",
* "type" : "inapp",
* "price" : "$5.00",
* "price_currency": "USD",
* "price_amount_micros": 5000000,
* "title : "Example Title",
* "description" : "This is an example description" }'
*/
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
/**
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
* the type, a unique purchase token and an optional developer payload.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param sku the SKU of the in-app item as published in the developer console
* @param type of the in-app item being purchased ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
* codes on failures.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
String developerPayload);
/**
* Returns the current SKUs owned by the user of the type and package name specified along with
* purchase information and a signature of the data to be validated.
* This will return all SKUs that have been purchased in V3 and managed items purchased using
* V1 and V2 that have not been consumed.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param type of the in-app items being requested ("inapp" for one-time purchases
* and "subs" for subscriptions)
* @param continuationToken to be set as null for the first call, if the number of owned
* skus are too many, a continuationToken is returned in the response bundle.
* This method can be called again with the continuation token to get the next set of
* owned skus.
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
on failures.
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
* of the purchase information
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
* next set of in-app purchases. Only set if the
* user has more owned skus than the current list.
*/
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
/**
* Consume the last purchase of the given SKU. This will result in this item being removed
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
* @param apiVersion billing API version that the app is using
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
*/
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
/**
* This API is currently under development.
*/
int stub(int apiVersion, String packageName, String type);
/**
* Returns a pending intent to launch the purchase flow for upgrading or downgrading a
* subscription. The existing owned SKU(s) should be provided along with the new SKU that
* the user is upgrading or downgrading to.
* @param apiVersion billing API version that the app is using, must be 5 or later
* @param packageName package name of the calling app
* @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
* if null or empty this method will behave like {@link #getBuyIntent}
* @param newSku the SKU that the user is upgrading or downgrading to
* @param type of the item being purchased, currently must be "subs"
* @param developerPayload optional argument to be sent back with the purchase information
* @return Bundle containing the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
* on failures.
* "BUY_INTENT" - PendingIntent to start the purchase flow
*
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
* If the purchase is successful, the result data will contain the following key-value pairs
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
* codes on failures.
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
* '{"orderId":"12999763169054705758.1371079406387615",
* "packageName":"com.example.app",
* "productId":"exampleSku",
* "purchaseTime":1345678900000,
* "purchaseToken" : "122333444455555",
* "developerPayload":"example developer payload" }'
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
* was signed with the private key of the developer
* TODO: change this to app-specific keys.
*/
Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
in List<String> oldSkus, String newSku, String type, String developerPayload);
}

@ -1,55 +0,0 @@
/* Copyright (c) 2014 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Receiver for the "com.android.vending.billing.PURCHASES_UPDATED" Action from the Play Store.
*
* <p>It is possible that an in-app item may be acquired without the application calling
* getBuyIntent(), for example if the item can be redeemed from inside the Play Store using a
* promotional code. If this application isn't running at the time, then when it is started a call
* to getPurchases() will be sufficient notification. However, if the application is already running
* in the background when the item is acquired, a message to this BroadcastReceiver will indicate
* that the an item has been acquired.
*/
public class IabBroadcastReceiver extends BroadcastReceiver {
/** The Intent action that this Receiver should filter for. */
public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED";
private final IabBroadcastListener mListener;
public IabBroadcastReceiver(IabBroadcastListener listener) {
mListener = listener;
}
@Override
public void onReceive(Context context, Intent intent) {
if (mListener != null) {
mListener.receivedBroadcast();
}
}
/** Listener interface for received broadcast messages. */
public interface IabBroadcastListener {
void receivedBroadcast();
}
}

@ -1,49 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
/**
* Exception thrown when something went wrong with in-app billing. An IabException has an associated
* IabResult (an error). To get the IAB result that caused this exception to be thrown, call {@link
* #getResult()}.
*/
@SuppressWarnings("ALL")
public class IabException extends Exception {
IabResult mResult;
public IabException(IabResult r) {
this(r, null);
}
public IabException(int response, String message) {
this(new IabResult(response, message));
}
public IabException(IabResult r, Exception cause) {
super(r.getMessage(), cause);
mResult = r;
}
public IabException(int response, String message, Exception cause) {
this(new IabResult(response, message), cause);
}
/** Returns the IAB result (error) that this exception signals. */
public IabResult getResult() {
return mResult;
}
}

@ -1,58 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
/**
* Represents the result of an in-app billing operation. A result is composed of a response code (an
* integer) and possibly a message (String). You can get those by calling {@link #getResponse} and
* {@link #getMessage()}, respectively. You can also inquire whether a result is a success or a
* failure by calling {@link #isSuccess()} and {@link #isFailure()}.
*/
@SuppressWarnings("ALL")
public class IabResult {
int mResponse;
String mMessage;
public IabResult(int response, String message) {
mResponse = response;
if (message == null || message.trim().length() == 0) {
mMessage = IabHelper.getResponseDesc(response);
} else {
mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
}
}
public int getResponse() {
return mResponse;
}
public String getMessage() {
return mMessage;
}
public boolean isSuccess() {
return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK;
}
public boolean isFailure() {
return !isSuccess();
}
public String toString() {
return "IabResult: " + getMessage();
}
}

@ -1,96 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a block of information about in-app items. An Inventory is returned by such methods as
* {@link IabHelper#queryInventory}.
*/
@SuppressWarnings("ALL")
public class Inventory {
Map<String, SkuDetails> mSkuMap = new HashMap<String, SkuDetails>();
Map<String, Purchase> mPurchaseMap = new HashMap<String, Purchase>();
Inventory() {}
/** Returns the listing details for an in-app product. */
public SkuDetails getSkuDetails(String sku) {
return mSkuMap.get(sku);
}
/** Returns purchase information for a given product, or null if there is no purchase. */
public Purchase getPurchase(String sku) {
return mPurchaseMap.get(sku);
}
/** Returns whether or not there exists a purchase of the given product. */
public boolean hasPurchase(String sku) {
return mPurchaseMap.containsKey(sku);
}
/** Return whether or not details about the given product are available. */
public boolean hasDetails(String sku) {
return mSkuMap.containsKey(sku);
}
/**
* Erase a purchase (locally) from the inventory, given its product ID. This just modifies the
* Inventory object locally and has no effect on the server! This is useful when you have an
* existing Inventory object which you know to be up to date, and you have just consumed an item
* successfully, which means that erasing its purchase data from the Inventory you already have is
* quicker than querying for a new Inventory.
*/
public void erasePurchase(String sku) {
if (mPurchaseMap.containsKey(sku)) {
mPurchaseMap.remove(sku);
}
}
/** Returns a list of all owned product IDs. */
List<String> getAllOwnedSkus() {
return new ArrayList<String>(mPurchaseMap.keySet());
}
/** Returns a list of all owned product IDs of a given type */
List<String> getAllOwnedSkus(String itemType) {
List<String> result = new ArrayList<String>();
for (Purchase p : mPurchaseMap.values()) {
if (p.getItemType().equals(itemType)) {
result.add(p.getSku());
}
}
return result;
}
/** Returns a list of all purchases. */
List<Purchase> getAllPurchases() {
return new ArrayList<Purchase>(mPurchaseMap.values());
}
void addSkuDetails(SkuDetails d) {
mSkuMap.put(d.getSku(), d);
}
void addPurchase(Purchase p) {
mPurchaseMap.put(p.getSku(), p);
}
}

@ -1,100 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import org.json.JSONException;
import org.json.JSONObject;
/** Represents an in-app billing purchase. */
@SuppressWarnings("ALL")
public class Purchase {
String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
String mOrderId;
String mPackageName;
String mSku;
long mPurchaseTime;
int mPurchaseState;
String mDeveloperPayload;
String mToken;
String mOriginalJson;
String mSignature;
boolean mIsAutoRenewing;
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
mItemType = itemType;
mOriginalJson = jsonPurchaseInfo;
JSONObject o = new JSONObject(mOriginalJson);
mOrderId = o.optString("orderId");
mPackageName = o.optString("packageName");
mSku = o.optString("productId");
mPurchaseTime = o.optLong("purchaseTime");
mPurchaseState = o.optInt("purchaseState");
mDeveloperPayload = o.optString("developerPayload");
mToken = o.optString("token", o.optString("purchaseToken"));
mIsAutoRenewing = o.optBoolean("autoRenewing");
mSignature = signature;
}
public String getItemType() {
return mItemType;
}
public String getOrderId() {
return mOrderId;
}
public String getPackageName() {
return mPackageName;
}
public String getSku() {
return mSku;
}
public long getPurchaseTime() {
return mPurchaseTime;
}
public int getPurchaseState() {
return mPurchaseState;
}
public String getDeveloperPayload() {
return mDeveloperPayload;
}
public String getToken() {
return mToken;
}
public String getOriginalJson() {
return mOriginalJson;
}
public String getSignature() {
return mSignature;
}
public boolean isAutoRenewing() {
return mIsAutoRenewing;
}
@Override
public String toString() {
return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson;
}
}

@ -1,84 +0,0 @@
/* Copyright (c) 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.vending.billing;
import org.json.JSONException;
import org.json.JSONObject;
/** Represents an in-app product's listing details. */
@SuppressWarnings("ALL")
public class SkuDetails {
private final String mItemType;
private final String mSku;
private final String mType;
private final String mPrice;
private final long mPriceAmountMicros;
private final String mPriceCurrencyCode;
private final String mTitle;
private final String mDescription;
private final String mJson;
public SkuDetails(String jsonSkuDetails) throws JSONException {
this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
}
public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
mItemType = itemType;
mJson = jsonSkuDetails;
JSONObject o = new JSONObject(mJson);
mSku = o.optString("productId");
mType = o.optString("type");
mPrice = o.optString("price");
mPriceAmountMicros = o.optLong("price_amount_micros");
mPriceCurrencyCode = o.optString("price_currency_code");
mTitle = o.optString("title");
mDescription = o.optString("description");
}
public String getSku() {
return mSku;
}
public String getType() {
return mType;
}
public String getPrice() {
return mPrice;
}
public long getPriceAmountMicros() {
return mPriceAmountMicros;
}
public String getPriceCurrencyCode() {
return mPriceCurrencyCode;
}
public String getTitle() {
return mTitle;
}
public String getDescription() {
return mDescription;
}
@Override
public String toString() {
return "SkuDetails:" + mJson;
}
}

@ -1,22 +1,23 @@
package org.tasks;
import javax.inject.Inject;
import org.tasks.billing.InventoryHelper;
import org.tasks.billing.BillingClient;
import org.tasks.gtasks.PlayServices;
public class FlavorSetup {
private final InventoryHelper inventoryHelper;
private final PlayServices playServices;
private final BillingClient billingClient;
@Inject
public FlavorSetup(InventoryHelper inventoryHelper, PlayServices playServices) {
this.inventoryHelper = inventoryHelper;
public FlavorSetup(PlayServices playServices,
BillingClient billingClient) {
this.playServices = playServices;
this.billingClient = billingClient;
}
public void setup() {
inventoryHelper.initialize();
billingClient.initialize();
playServices.refresh();
}
}

@ -1,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());
}
}

@ -1,100 +0,0 @@
package org.tasks.billing;
import android.content.Context;
import android.content.IntentFilter;
import com.android.vending.billing.IabBroadcastReceiver;
import com.android.vending.billing.IabHelper;
import com.android.vending.billing.Inventory;
import com.android.vending.billing.Purchase;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ApplicationScope
public class InventoryHelper implements IabBroadcastReceiver.IabBroadcastListener {
private final Context context;
private final Preferences preferences;
private final LocalBroadcastManager localBroadcastManager;
private final Executor executor;
private Inventory inventory;
@Inject
public InventoryHelper(
@ForApplication Context context,
Preferences preferences,
LocalBroadcastManager localBroadcastManager,
@Named("iab-executor") Executor executor) {
this.context = context;
this.preferences = preferences;
this.localBroadcastManager = localBroadcastManager;
this.executor = executor;
}
public void initialize() {
context.registerReceiver(
new IabBroadcastReceiver(this), new IntentFilter(IabBroadcastReceiver.ACTION));
refreshInventory();
}
public void refreshInventory() {
final IabHelper helper = new IabHelper(context, context.getString(R.string.gp_key), executor);
helper.startSetup(getSetupListener(helper));
}
private IabHelper.OnIabSetupFinishedListener getSetupListener(final IabHelper helper) {
return result -> {
if (result.isSuccess()) {
helper.queryInventoryAsync(getQueryListener(helper));
} else {
Timber.e("setup failed: %s", result.getMessage());
helper.dispose();
}
};
}
private IabHelper.QueryInventoryFinishedListener getQueryListener(final IabHelper helper) {
return (result, inv) -> {
if (result.isSuccess()) {
inventory = inv;
checkPurchase(R.string.sku_tasker, R.string.p_purchased_tasker);
checkPurchase(R.string.sku_dashclock, R.string.p_purchased_dashclock);
checkPurchase(R.string.sku_themes, R.string.p_purchased_themes);
localBroadcastManager.broadcastRefresh();
} else {
Timber.e("query inventory failed: %s", result.getMessage());
}
helper.dispose();
};
}
@Override
public void receivedBroadcast() {
refreshInventory();
}
private void checkPurchase(int skuRes, final int prefRes) {
final String sku = context.getString(skuRes);
if (inventory.hasPurchase(sku)) {
Timber.d("Found purchase: %s", sku);
preferences.setBoolean(prefRes, true);
} else {
Timber.d("No purchase: %s", sku);
}
}
public void erasePurchase(String sku) {
inventory.erasePurchase(sku);
}
public Purchase getPurchase(String sku) {
return inventory.getPurchase(sku);
}
}

@ -1,197 +0,0 @@
package org.tasks.billing;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import com.android.vending.billing.IabHelper;
import com.android.vending.billing.IabResult;
import com.android.vending.billing.Purchase;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
import org.tasks.BuildConfig;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.analytics.Tracker;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ApplicationScope
public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener {
private final Context context;
private final Preferences preferences;
private final Tracker tracker;
private final InventoryHelper inventory;
private final Executor executor;
private final LocalBroadcastManager localBroadcastManager;
private PurchaseHelperCallback activityResultCallback;
private IabHelper iabHelper;
@Inject
public PurchaseHelper(
@ForApplication Context context,
Preferences preferences,
Tracker tracker,
InventoryHelper inventory,
@Named("iab-executor") Executor executor,
LocalBroadcastManager localBroadcastManager) {
this.context = context;
this.preferences = preferences;
this.tracker = tracker;
this.inventory = inventory;
this.executor = executor;
this.localBroadcastManager = localBroadcastManager;
}
@Override
public void onIabSetupFinished(IabResult result) {
if (result.isFailure()) {
Timber.e("in-app billing setup failed: %s", result.getMessage());
}
}
public boolean purchase(
final Activity activity,
final String sku,
final String pref,
final int requestCode,
final PurchaseHelperCallback callback) {
launchPurchaseFlow(activity, sku, pref, requestCode, callback);
return true;
}
public void consumePurchases() {
if (BuildConfig.DEBUG) {
final List<Purchase> purchases = new ArrayList<>();
final Purchase tasker = inventory.getPurchase(context.getString(R.string.sku_tasker));
final Purchase dashclock = inventory.getPurchase(context.getString(R.string.sku_dashclock));
final Purchase themes = inventory.getPurchase(context.getString(R.string.sku_themes));
if (tasker != null) {
purchases.add(tasker);
}
if (dashclock != null) {
purchases.add(dashclock);
}
if (themes != null) {
purchases.add(themes);
}
final IabHelper iabHelper =
new IabHelper(context, context.getString(R.string.gp_key), executor);
iabHelper.enableDebugLogging(true);
iabHelper.startSetup(
result -> {
if (result.isSuccess()) {
iabHelper.consumeAsync(
purchases,
(purchases1, results) -> {
for (int i = 0; i < purchases1.size(); i++) {
Purchase purchase = purchases1.get(i);
IabResult iabResult = results.get(i);
if (iabResult.isSuccess()) {
if (purchase.equals(tasker)) {
preferences.setBoolean(R.string.p_purchased_tasker, false);
} else if (purchase.equals(dashclock)) {
preferences.setBoolean(R.string.p_purchased_dashclock, false);
} else if (purchase.equals(themes)) {
preferences.setBoolean(R.string.p_purchased_themes, false);
} else {
Timber.e("Unhandled consumption for purchase: %s", purchase);
}
inventory.erasePurchase(purchase.getSku());
Timber.d("Consumed %s", purchase);
} else {
Timber.e("Consume failed: %s, %s", purchase, iabResult);
}
}
iabHelper.dispose();
});
} else {
Timber.e("setup failed: %s", result.getMessage());
iabHelper.dispose();
}
});
}
}
private void launchPurchaseFlow(
final Activity activity,
final String sku,
final String pref,
final int requestCode,
final PurchaseHelperCallback callback) {
if (iabHelper != null) {
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
return;
}
iabHelper = new IabHelper(context, context.getString(R.string.gp_key), executor);
iabHelper.enableDebugLogging(BuildConfig.DEBUG);
Timber.d("%s: startSetup", iabHelper);
iabHelper.startSetup(
result -> {
if (result.isSuccess()) {
try {
Timber.d("%s: launchPurchaseFlow for %s", iabHelper, sku);
iabHelper.launchPurchaseFlow(
activity,
sku,
requestCode,
(result1, info) -> {
Timber.d(result1.toString());
tracker.reportIabResult(result1, sku);
if (result1.isSuccess()) {
if (!Strings.isNullOrEmpty(pref)) {
preferences.setBoolean(pref, true);
localBroadcastManager.broadcastRefresh();
}
inventory.refreshInventory();
} else if (result1.getResponse()
!= IabHelper.BILLING_RESPONSE_RESULT_USER_CANCELED
&& result1.getResponse() != IabHelper.IABHELPER_USER_CANCELLED) {
Toast.makeText(activity, result1.getMessage(), Toast.LENGTH_LONG).show();
}
if (activityResultCallback != null) {
activityResultCallback.purchaseCompleted(result1.isSuccess(), sku);
}
disposeIabHelper();
});
} catch (IllegalStateException e) {
tracker.reportException(e);
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
disposeIabHelper();
}
} else {
Timber.e(result.toString());
Toast.makeText(activity, result.getMessage(), Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
disposeIabHelper();
}
});
}
public void disposeIabHelper() {
if (iabHelper != null) {
Timber.d("%s: dispose", iabHelper);
iabHelper.dispose();
iabHelper = null;
}
}
public void handleActivityResult(
PurchaseHelperCallback callback, int requestCode, int resultCode, Intent data) {
this.activityResultCallback = callback;
if (iabHelper != null) {
iabHelper.handleActivityResult(requestCode, resultCode, data);
}
}
}

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="gp_key">MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8mXRE3dDXwtinUILCEzKjov2rxs3kZbLRzNrcjFWXpG9OEsUzRGLzqEN+WwibVuMRpZLj/+IxbU2sJWq/M0q+90rOhmXn46ZPeNyr77IqX2pWKIAWpzBoWq/mshRwtm9m1FIiGdBNlXrhSE7u3TGB5FuEuuSqKWvWzxeqQ7fHmlM04Lqrh1mN3FaMne8rWv+DWVHDbLrtnXBuC36glOAj17HxrzaE2v6Pv7Df3QefJ3rM1+0fAp/5jNInaP0qHAlG8WTbUmDShQ5kG3urbv3HLByyx6TSqhmNudXUK/6TusvIj50OptAG7x+UFYf956dD3diXhY3yoICvyFWx1sNwIDAQAB</string>
<string name="sku_themes">themes</string>
<string name="play_services_available">play_services_available</string>
</resources>

@ -480,6 +480,10 @@
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".billing.PurchaseActivity"
android:theme="@style/Tasks"/>
<activity-alias
android:enabled="true"
android:exported="true"

@ -6,6 +6,7 @@
package com.todoroo.astrid.activity;
import static android.support.v4.content.ContextCompat.getColor;
import static com.todoroo.astrid.adapter.FilterAdapter.REQUEST_PURCHASE;
import android.app.Activity;
import android.arch.lifecycle.ViewModelProviders;
@ -53,6 +54,8 @@ import org.tasks.R;
import org.tasks.activities.FilterSettingsActivity;
import org.tasks.analytics.Tracker;
import org.tasks.analytics.Tracking;
import org.tasks.billing.Inventory;
import org.tasks.billing.PurchaseActivity;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.dialogs.SortDialog;
import org.tasks.injection.ForActivity;
@ -67,6 +70,7 @@ import org.tasks.ui.CheckBoxes;
import org.tasks.ui.MenuColorizer;
import org.tasks.ui.ProgressDialogAsyncTask;
import org.tasks.ui.TaskListViewModel;
import timber.log.Timber;
/**
* Primary activity for the Bente application. Shows a list of upcoming tasks and a user's coaches.
@ -100,6 +104,7 @@ public class TaskListFragment extends InjectingFragment
@Inject ViewHolderFactory viewHolderFactory;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject Device device;
@Inject Inventory inventory;
@BindView(R.id.swipe_layout)
SwipeRefreshLayout swipeRefreshLayout;
@ -227,6 +232,7 @@ public class TaskListFragment extends InjectingFragment
}
private void setupMenu(Menu menu) {
menu.findItem(R.id.menu_purchase).setVisible(!inventory.hasPro());
MenuItem hidden = menu.findItem(R.id.menu_show_hidden);
if (preferences.getBoolean(R.string.p_show_hidden_tasks, false)) {
hidden.setChecked(true);
@ -313,8 +319,11 @@ public class TaskListFragment extends InjectingFragment
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
case R.id.menu_purchase:
startActivityForResult(new Intent(context, PurchaseActivity.class), REQUEST_PURCHASE);
return true;
default:
return super.onOptionsItemSelected(item);
return onOptionsItemSelected(item);
}
}
@ -522,6 +531,10 @@ public class TaskListFragment extends InjectingFragment
activity.recreate();
}
}
} else if (requestCode == REQUEST_PURCHASE) {
if (inventory.hasPro()) {
((TaskListActivity) getActivity()).restart();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}

@ -33,9 +33,12 @@ import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.activities.GoogleTaskListSettingsActivity;
import org.tasks.activities.TagSettingsActivity;
import org.tasks.billing.Inventory;
import org.tasks.billing.PurchaseActivity;
import org.tasks.caldav.CaldavSettingsActivity;
import org.tasks.filters.FilterCounter;
import org.tasks.filters.FilterProvider;
@ -53,6 +56,7 @@ import org.tasks.ui.NavigationDrawerFragment;
public class FilterAdapter extends ArrayAdapter<FilterListItem> {
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<FilterListItem> {
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<FilterListItem> items = new ArrayList<>();
@ -77,6 +82,7 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
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<FilterListItem> {
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<FilterListItem> {
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),

@ -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));
}
}

@ -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:

@ -0,0 +1,262 @@
package org.tasks.billing;
import static com.google.common.collect.Iterables.transform;
import android.app.Activity;
import android.content.Context;
import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.BillingClient.FeatureType;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.Purchase.PurchasesResult;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsParams.Builder;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.google.common.base.Joiner;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.LocalBroadcastManager;
import org.tasks.analytics.Tracker;
import org.tasks.injection.ForApplication;
import timber.log.Timber;
public class BillingClient implements PurchasesUpdatedListener {
private final Inventory inventory;
private final LocalBroadcastManager localBroadcastManager;
private final Tracker tracker;
private com.android.billingclient.api.BillingClient billingClient;
private boolean connected;
private int billingClientResponseCode = -1;
@Inject
public BillingClient(
@ForApplication Context context,
Inventory inventory,
LocalBroadcastManager localBroadcastManager,
Tracker tracker) {
this.inventory = inventory;
this.localBroadcastManager = localBroadcastManager;
this.tracker = tracker;
billingClient =
com.android.billingclient.api.BillingClient.newBuilder(context).setListener(this).build();
}
public void initialize() {
startServiceConnection(this::queryPurchases);
}
/**
* Query purchases across various use cases and deliver the result in a formalized way through a
* listener
*/
public void queryPurchases() {
Runnable queryToExecute =
() -> {
long time = System.currentTimeMillis();
PurchasesResult purchasesResult = billingClient.queryPurchases(SkuType.INAPP);
Timber.i("Querying purchases elapsed time: %sms", System.currentTimeMillis() - time);
// If there are subscriptions supported, we add subscription rows as well
if (areSubscriptionsSupported()) {
PurchasesResult subscriptionResult = billingClient.queryPurchases(SkuType.SUBS);
Timber.i(
"Querying purchases and subscriptions elapsed time: %sms",
System.currentTimeMillis() - time);
Timber.i(
"Querying subscriptions result code: %s res: %s",
subscriptionResult.getResponseCode(), subscriptionResult.getPurchasesList().size());
if (subscriptionResult.getResponseCode() == BillingResponse.OK) {
purchasesResult.getPurchasesList().addAll(subscriptionResult.getPurchasesList());
} else {
Timber.e("Got an error response trying to query subscription purchases");
}
} else if (purchasesResult.getResponseCode() == BillingResponse.OK) {
Timber.i("Skipped subscription purchases query since they are not supported");
} else {
Timber.w(
"queryPurchases() got an error response code: %s",
purchasesResult.getResponseCode());
}
onQueryPurchasesFinished(purchasesResult);
};
executeServiceRequest(queryToExecute);
}
/** Handle a result from querying of purchases and report an updated list to the listener */
private void onQueryPurchasesFinished(PurchasesResult result) {
// Have we been disposed of in the meantime? If so, or bad result code, then quit
if (billingClient == null || result.getResponseCode() != BillingResponse.OK) {
Timber.w(
"Billing client was null or result code (%s) was bad - quitting",
result.getResponseCode());
return;
}
Timber.d("Query inventory was successful.");
// Update the UI and purchases inventory with new list of purchases
inventory.clear();
onPurchasesUpdated(BillingResponse.OK, result.getPurchasesList());
}
@Override
public void onPurchasesUpdated(@BillingResponse int resultCode, List<Purchase> purchases) {
if (resultCode == BillingResponse.OK) {
inventory.add(purchases);
localBroadcastManager.broadcastPurchasesUpdated();
} else {
String skus =
purchases == null ? "null" : Joiner.on(";").join(transform(purchases, Purchase::getSku));
Timber.i("onPurchasesUpdate(%s, %s)", BillingResponseToString(resultCode), skus);
tracker.reportIabResult(resultCode, skus);
}
}
/** Start a purchase flow */
void initiatePurchaseFlow(
Activity activity, final String skuId, final @SkuType String billingType) {
Runnable purchaseFlowRequest =
() -> {
Timber.d("Launching in-app purchase flow");
BillingFlowParams purchaseParams =
BillingFlowParams.newBuilder()
.setSku(skuId)
.setType(billingType)
.setOldSkus(null)
.build();
billingClient.launchBillingFlow(activity, purchaseParams);
};
executeServiceRequest(purchaseFlowRequest);
}
public void destroy() {
Timber.d("Destroying the manager.");
if (billingClient != null && billingClient.isReady()) {
billingClient.endConnection();
billingClient = null;
}
}
private void startServiceConnection(final Runnable executeOnSuccess) {
billingClient.startConnection(
new com.android.billingclient.api.BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@BillingResponse int billingResponseCode) {
Timber.d("onBillingSetupFinished(%s)", billingResponseCode);
if (billingResponseCode == BillingResponse.OK) {
connected = true;
if (executeOnSuccess != null) {
executeOnSuccess.run();
}
}
billingClientResponseCode = billingResponseCode;
}
@Override
public void onBillingServiceDisconnected() {
Timber.d("onBillingServiceDisconnected()");
connected = false;
}
});
}
private void executeServiceRequest(Runnable runnable) {
if (connected) {
runnable.run();
} else {
// If billing service was disconnected, we try to reconnect 1 time.
// (feel free to introduce your retry policy here).
startServiceConnection(runnable);
}
}
/**
* Checks if subscriptions are supported for current client
*
* <p>Note: This method does not automatically retry for RESULT_SERVICE_DISCONNECTED. It is only
* used in unit tests and after queryPurchases execution, which already has a retry-mechanism
* implemented.
*/
private boolean areSubscriptionsSupported() {
int responseCode = billingClient.isFeatureSupported(FeatureType.SUBSCRIPTIONS);
if (responseCode != BillingResponse.OK) {
Timber.d("areSubscriptionsSupported() got an error response: %s", responseCode);
}
return responseCode == BillingResponse.OK;
}
public void querySkuDetailsAsync(
@SkuType final String itemType,
final List<String> skuList,
final SkuDetailsResponseListener listener) {
Runnable request =
() -> {
Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(itemType);
billingClient.querySkuDetailsAsync(params.build(), listener);
};
executeServiceRequest(request);
}
public void consume(String sku) {
if (!BuildConfig.DEBUG) {
throw new IllegalStateException();
}
if (!inventory.purchased(sku)) {
throw new IllegalArgumentException();
}
final ConsumeResponseListener onConsumeListener =
(responseCode, purchaseToken1) -> {
Timber.d("onConsumeResponse(%s, %s)", responseCode, purchaseToken1);
queryPurchases();
};
Runnable request =
() ->
billingClient.consumeAsync(
inventory.getPurchase(sku).getPurchaseToken(), onConsumeListener);
executeServiceRequest(request);
}
public int getBillingClientResponseCode() {
return billingClientResponseCode;
}
public static String BillingResponseToString(@BillingResponse int response) {
switch (response) {
case BillingResponse.FEATURE_NOT_SUPPORTED:
return "FEATURE_NOT_SUPPORTED";
case BillingResponse.SERVICE_DISCONNECTED:
return "SERVICE_DISCONNECTED";
case BillingResponse.OK:
return "OK";
case BillingResponse.USER_CANCELED:
return "USER_CANCELED";
case BillingResponse.SERVICE_UNAVAILABLE:
return "SERVICE_UNAVAILABLE";
case BillingResponse.BILLING_UNAVAILABLE:
return "BILLING_UNAVAILABLE";
case BillingResponse.ITEM_UNAVAILABLE:
return "ITEM_UNAVAILABLE";
case BillingResponse.DEVELOPER_ERROR:
return "DEVELOPER_ERROR";
case BillingResponse.ERROR:
return "ERROR";
case BillingResponse.ITEM_ALREADY_OWNED:
return "ITEM_ALREADY_OWNED";
case BillingResponse.ITEM_NOT_OWNED:
return "ITEM_NOT_OWNED";
default:
return "Unknown";
}
}
}

@ -0,0 +1,56 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.tasks.billing;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.tasks.billing.row.RowDataProvider;
import org.tasks.billing.row.SkuRowData;
/**
* A separator for RecyclerView that keeps the specified spaces between headers and the cards.
*/
public class CardsWithHeadersDecoration extends RecyclerView.ItemDecoration {
private final RowDataProvider mRowDataProvider;
private final int mHeaderGap, mRowGap;
public CardsWithHeadersDecoration(RowDataProvider rowDataProvider, int headerGap,
int rowGap) {
this.mRowDataProvider = rowDataProvider;
this.mHeaderGap = headerGap;
this.mRowGap = rowGap;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
final int position = parent.getChildAdapterPosition(view);
final SkuRowData data = mRowDataProvider.getData(position);
// We should add a space on top of every header card
if (data.getRowType() == SkusAdapter.TYPE_HEADER || position == 0) {
outRect.top = mHeaderGap;
}
// Adding a space under the last item
if (position == parent.getAdapter().getItemCount() - 1) {
outRect.bottom = mHeaderGap;
} else {
outRect.bottom = mRowGap;
}
}
}

@ -0,0 +1,98 @@
package org.tasks.billing;
import android.content.Context;
import com.android.billingclient.api.Purchase;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
@ApplicationScope
public class Inventory {
private static final String SKU_PRO = "annual_499";
static final String SKU_VIP = "vip";
static final String SKU_TASKER = "tasker";
static final String SKU_THEMES = "themes";
static final String SKU_DASHCLOCK = "dashclock";
public static final List<String> SKU_SUBS = ImmutableList.of(SKU_PRO);
private final Preferences preferences;
private final String billingKey;
private Map<String, Purchase> purchases = new HashMap<>();
@Inject
public Inventory(@ForApplication Context context, Preferences preferences) {
this.preferences = preferences;
billingKey = context.getString(R.string.gp_key);
for (Purchase purchase : preferences.getPurchases()) {
add(purchase);
}
}
public void clear() {
Timber.d("clear()");
purchases.clear();
}
public void add(List<Purchase> purchases) {
for (Purchase purchase : purchases) {
add(purchase);
}
preferences.setPurchases(this.purchases.values());
}
private void add(Purchase purchase) {
if (verifySignature(purchase)) {
Timber.d("add(%s)", purchase);
purchases.put(purchase.getSku(), purchase);
}
}
public boolean purchasedTasker() {
return hasPro() || purchases.containsKey(SKU_TASKER);
}
public boolean purchasedDashclock() {
return hasPro() || purchases.containsKey(SKU_DASHCLOCK);
}
public boolean purchasedThemes() {
return hasPro() || purchases.containsKey(SKU_THEMES);
}
public List<Purchase> getPurchases() {
return ImmutableList.copyOf(purchases.values());
}
public boolean hasPro() {
return purchases.containsKey(SKU_PRO) || purchases.containsKey(SKU_VIP);
}
public boolean purchased(String sku) {
return purchases.containsKey(sku);
}
private boolean verifySignature(Purchase purchase) {
try {
return Security.verifyPurchase(
billingKey, purchase.getOriginalJson(), purchase.getSignature());
} catch (IOException e) {
Timber.e(e, e.getMessage());
return false;
}
}
public Purchase getPurchase(String sku) {
return purchases.get(sku);
}
}

@ -0,0 +1,245 @@
package org.tasks.billing;
import static android.text.TextUtils.isEmpty;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static org.tasks.billing.Inventory.SKU_DASHCLOCK;
import static org.tasks.billing.Inventory.SKU_TASKER;
import static org.tasks.billing.Inventory.SKU_THEMES;
import static org.tasks.billing.Inventory.SKU_VIP;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.Toolbar.OnMenuItemClickListener;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.android.billingclient.api.BillingClient.BillingResponse;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.SkuDetails;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.billing.SkusAdapter.OnClickHandler;
import org.tasks.billing.row.SkuRowData;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ForApplication;
import org.tasks.injection.ThemedInjectingAppCompatActivity;
import org.tasks.preferences.HelpAndFeedbackActivity;
import org.tasks.ui.MenuColorizer;
import timber.log.Timber;
public class PurchaseActivity extends ThemedInjectingAppCompatActivity
implements OnClickHandler, OnMenuItemClickListener {
private static final List<String> DEBUG_SKUS =
ImmutableList.of(SKU_THEMES, SKU_TASKER, SKU_DASHCLOCK, SKU_VIP);
@Inject @ForApplication Context context;
@Inject BillingClient billingClient;
@Inject Inventory inventory;
@Inject LocalBroadcastManager localBroadcastManager;
private SkusAdapter adapter;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.list)
RecyclerView recyclerView;
@BindView(R.id.screen_wait)
View loadingView;
@BindView(R.id.error_textview)
TextView errorTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_purchase);
ButterKnife.bind(this);
toolbar.setTitle(R.string.upgrade);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_24dp);
toolbar.setNavigationOnClickListener(v -> onBackPressed());
toolbar.inflateMenu(R.menu.menu_purchase_activity);
toolbar.setOnMenuItemClickListener(this);
MenuColorizer.colorToolbar(this, toolbar);
adapter = new SkusAdapter(context, inventory, this);
recyclerView.setAdapter(adapter);
Resources res = getResources();
recyclerView.addItemDecoration(
new CardsWithHeadersDecoration(
adapter,
(int) res.getDimension(R.dimen.header_gap),
(int) res.getDimension(R.dimen.row_gap)));
recyclerView.setLayoutManager(new LinearLayoutManager(context));
setWaitScreen(true);
querySkuDetails();
}
@Override
protected void onResume() {
super.onResume();
querySkuDetails();
}
@Override
protected void onStart() {
super.onStart();
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver);
}
@Override
protected void onStop() {
super.onStop();
localBroadcastManager.unregisterReceiver(purchaseReceiver);
}
/** Queries for in-app and subscriptions SKU details and updates an adapter with new data */
private void querySkuDetails() {
if (!isFinishing()) {
List<SkuRowData> data = new ArrayList<>();
String owned = getString(R.string.owned);
String debug = getString(R.string.debug);
Runnable addDebug =
BuildConfig.DEBUG
? () ->
addSkuRows(
data,
newArrayList(
filter(DEBUG_SKUS, sku -> !any(data, row -> sku.equals(row.getSku())))),
debug,
SkuType.INAPP,
null)
: null;
Runnable addIaps =
() ->
addSkuRows(
data,
newArrayList(
filter(
transform(inventory.getPurchases(), Purchase::getSku),
sku1 -> !Inventory.SKU_SUBS.contains(sku1))),
owned,
SkuType.INAPP,
addDebug);
addSkuRows(data, Inventory.SKU_SUBS, null, SkuType.SUBS, addIaps);
}
}
private void addSkuRows(
List<SkuRowData> data,
List<String> skus,
String title,
@SkuType String skuType,
Runnable whenFinished) {
billingClient.querySkuDetailsAsync(
skuType,
skus,
(responseCode, skuDetailsList) -> {
if (responseCode != BillingResponse.OK) {
Timber.w("Unsuccessful query for type: " + skuType + ". Error code: " + responseCode);
} else if (skuDetailsList != null && skuDetailsList.size() > 0) {
if (!isEmpty(title)) {
data.add(new SkuRowData(title));
}
Timber.d("Adding %s skus", skuDetailsList.size());
// Then fill all the other rows
for (SkuDetails details : skuDetailsList) {
Timber.i("Adding sku: %s", details);
data.add(new SkuRowData(details, SkusAdapter.TYPE_NORMAL, skuType));
}
if (data.size() == 0) {
displayAnErrorIfNeeded();
} else {
adapter.setData(data);
setWaitScreen(false);
}
}
if (whenFinished != null) {
whenFinished.run();
}
});
}
private void displayAnErrorIfNeeded() {
if (!isFinishing()) {
loadingView.setVisibility(View.GONE);
errorTextView.setVisibility(View.VISIBLE);
errorTextView.setText(
billingClient.getBillingClientResponseCode() == BillingResponse.BILLING_UNAVAILABLE
? R.string.error_billing_unavailable
: R.string.error_billing_default);
}
}
private void setWaitScreen(boolean set) {
recyclerView.setVisibility(set ? View.GONE : View.VISIBLE);
loadingView.setVisibility(set ? View.VISIBLE : View.GONE);
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);
}
@Override
public void click(SkuRowData skuRowData) {
String sku = skuRowData.getSku();
String skuType = skuRowData.getSkuType();
if (inventory.purchased(sku)) {
if (BuildConfig.DEBUG && SkuType.INAPP.equals(skuType)) {
billingClient.consume(sku);
}
} else {
billingClient.initiatePurchaseFlow(this, sku, skuType);
}
}
private BroadcastReceiver purchaseReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
querySkuDetails();
}
};
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_help:
startActivity(new Intent(this, HelpAndFeedbackActivity.class));
return true;
case R.id.menu_refresh_purchases:
billingClient.queryPurchases();
return true;
default:
return false;
}
}
}

@ -1,6 +0,0 @@
package org.tasks.billing;
public interface PurchaseHelperCallback {
void purchaseCompleted(boolean success, String sku);
}

@ -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;
}

@ -0,0 +1,131 @@
/*
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.billing;
import static com.google.common.collect.Lists.transform;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static java.util.Arrays.asList;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.billingclient.api.BillingClient.SkuType;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Retention;
import java.util.List;
import javax.annotation.Nonnull;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.billing.row.RowDataProvider;
import org.tasks.billing.row.RowViewHolder;
import org.tasks.billing.row.SkuRowData;
public class SkusAdapter extends RecyclerView.Adapter<RowViewHolder> implements RowDataProvider {
public static final int TYPE_HEADER = 0;
public static final int TYPE_NORMAL = 1;
private final Context context;
private final Inventory inventory;
private final OnClickHandler onClickHandler;
private List<SkuRowData> data = ImmutableList.of();
SkusAdapter(Context context, Inventory inventory, OnClickHandler onClickHandler) {
this.context = context;
this.inventory = inventory;
this.onClickHandler = onClickHandler;
}
public void setData(List<SkuRowData> data) {
this.data = data;
notifyDataSetChanged();
}
@Override
public @RowTypeDef int getItemViewType(int position) {
return data.isEmpty() ? TYPE_HEADER : data.get(position).getRowType();
}
@Override
@Nonnull
public RowViewHolder onCreateViewHolder(@Nonnull ViewGroup parent, @RowTypeDef int viewType) {
// Selecting a flat layout for header rows
if (viewType == SkusAdapter.TYPE_HEADER) {
View item =
LayoutInflater.from(parent.getContext())
.inflate(R.layout.sku_details_row_header, parent, false);
return new RowViewHolder(item, null);
} else {
View item =
LayoutInflater.from(parent.getContext()).inflate(R.layout.sku_details_row, parent, false);
return new RowViewHolder(item, row -> onClickHandler.click(getData(row)));
}
}
@Override
public void onBindViewHolder(@Nonnull RowViewHolder holder, int position) {
SkuRowData data = getData(position);
if (data != null) {
holder.title.setText(data.getTitle());
if (getItemViewType(position) != SkusAdapter.TYPE_HEADER) {
String sku = data.getSku();
if (SkuType.SUBS.equals(data.getSkuType())) {
String[] rows = context.getResources().getStringArray(R.array.pro_description);
holder.description.setText(
Joiner.on('\n').join(transform(asList(rows), item -> "\u2022 " + item)));
holder.button.setVisibility(View.VISIBLE);
holder.price.setVisibility(View.VISIBLE);
holder.price.setText(data.getPrice());
holder.button.setText(
inventory.purchased(sku) ? R.string.button_subscribed : R.string.button_subscribe);
} else {
holder.description.setText(data.getDescription());
holder.button.setVisibility(View.GONE);
holder.price.setVisibility(View.GONE);
if (BuildConfig.DEBUG) {
holder.button.setVisibility(View.VISIBLE);
holder.button.setText(
inventory.purchased(sku) ? R.string.debug_consume : R.string.debug_buy);
}
}
}
}
}
@Override
public int getItemCount() {
return data.size();
}
@Override
public SkuRowData getData(int position) {
return data.isEmpty() ? null : data.get(position);
}
public interface OnClickHandler {
void click(SkuRowData skuRowData);
}
/** Types for adapter rows */
@Retention(SOURCE)
@IntDef({TYPE_HEADER, TYPE_NORMAL})
public @interface RowTypeDef {}
}

@ -0,0 +1,26 @@
/*
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.billing.row;
/**
* Provider for data that corresponds to a particular row
*/
public interface RowDataProvider {
SkuRowData getData(int position);
}

@ -0,0 +1,29 @@
package org.tasks.billing.row;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.tasks.R;
public final class RowViewHolder extends RecyclerView.ViewHolder {
public final TextView title;
public final TextView description;
public final TextView price;
public final Button button;
public interface ButtonClick {
void onClick(int row);
}
public RowViewHolder(final View itemView, final ButtonClick onClick) {
super(itemView);
title = itemView.findViewById(R.id.title);
price = itemView.findViewById(R.id.price);
description = itemView.findViewById(R.id.description);
button = itemView.findViewById(R.id.buy_button);
if (button != null) {
button.setOnClickListener(view -> onClick.onClick(getAdapterPosition()));
}
}
}

@ -0,0 +1,70 @@
/*
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.billing.row;
import com.android.billingclient.api.BillingClient.SkuType;
import com.android.billingclient.api.SkuDetails;
import org.tasks.billing.SkusAdapter;
import org.tasks.billing.SkusAdapter.RowTypeDef;
/**
* A model for SkusAdapter's row
*/
public class SkuRowData {
private String sku, title, price, description;
private @RowTypeDef int type;
private @SkuType String billingType;
public SkuRowData(SkuDetails details, @RowTypeDef int rowType,
@SkuType String billingType) {
this.sku = details.getSku();
this.title = details.getTitle();
this.price = details.getPrice();
this.description = details.getDescription();
this.type = rowType;
this.billingType = billingType;
}
public SkuRowData(String title) {
this.title = title;
this.type = SkusAdapter.TYPE_HEADER;
}
public String getSku() {
return sku;
}
public String getTitle() {
return title;
}
public String getPrice() {
return price;
}
public String getDescription() {
return description;
}
public @RowTypeDef int getRowType() {
return type;
}
public @SkuType
String getSkuType() {
return billingType;
}
}

@ -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)));
}
}

@ -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);

@ -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();

@ -1,65 +0,0 @@
package org.tasks.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.billing.PurchaseHelper;
import org.tasks.injection.InjectingNativeDialogFragment;
import org.tasks.injection.NativeDialogFragmentComponent;
import org.tasks.preferences.BasicPreferences;
public class DonationDialog extends InjectingNativeDialogFragment {
@Inject DialogBuilder dialogBuilder;
@Inject PurchaseHelper purchaseHelper;
public static DonationDialog newDonationDialog() {
return new DonationDialog();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final List<String> donationValues = getDonationValues();
return dialogBuilder
.newDialog()
.setTitle(R.string.select_amount)
.setItems(
donationValues,
(dialog, which) -> {
String value = donationValues.get(which);
Pattern pattern = Pattern.compile("\\$(\\d+) USD");
Matcher matcher = pattern.matcher(value);
//noinspection ResultOfMethodCallIgnored
matcher.matches();
String sku =
String.format(
java.util.Locale.ENGLISH, "%03d", Integer.parseInt(matcher.group(1)));
purchaseHelper.purchase(
getActivity(),
sku,
null,
BasicPreferences.REQUEST_PURCHASE,
(BasicPreferences) getActivity());
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private List<String> getDonationValues() {
List<String> values = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
values.add(String.format("$%s USD", Integer.toString(i)));
}
return values;
}
@Override
protected void inject(NativeDialogFragmentComponent component) {
component.inject(this);
}
}

@ -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);
}

@ -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);
}

@ -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);
}
}
}

@ -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);
}
}

@ -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(

@ -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<Purchase> 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<Purchase> 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);
}

@ -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

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
</vector>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background">
<include layout="@layout/loading_indicator"/>
<include
layout="@layout/toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_gravity="top" />
<TextView
android:id="@+id/error_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/card_view_margin"
android:gravity="center"
android:visibility="gone"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:gravity="center"/>
</FrameLayout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ProgressBar
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/screen_wait"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"/>

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
style="@style/CardViewStyle"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/content_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:layout_marginTop="@dimen/keyline_first"
android:layout_marginBottom="@dimen/keyline_first">
<TextView
android:id="@+id/title"
android:textStyle="bold"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingLeft="@dimen/keyline_first"
android:paddingStart="@dimen/keyline_first"
android:paddingRight="@dimen/keyline_first"
android:paddingEnd="@dimen/keyline_first"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/text_primary"
android:textSize="@dimen/sku_details_row_text_size"/>
<TextView
android:id="@+id/price"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="0dp"
android:paddingStart="0dp"
android:paddingRight="@dimen/keyline_first"
android:paddingEnd="@dimen/keyline_first"
android:textAlignment="viewStart"
android:layout_gravity="start"
android:textColor="@color/text_secondary"
android:textSize="@dimen/sku_details_row_text_size"/>
</LinearLayout>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="@dimen/keyline_first"
android:textColor="@color/text_secondary"
android:textSize="@dimen/sku_details_row_text_size"/>
<Button
android:id="@+id/buy_button"
style="@style/ButtonStyle"
android:layout_marginTop="@dimen/card_view_margin"
android:layout_marginBottom="@dimen/card_view_margin"
android:layout_marginStart="@dimen/card_view_margin"
android:layout_marginEnd="@dimen/card_view_margin"
android:contentDescription="@string/button_subscribe"/>
</LinearLayout>
</android.support.v7.widget.CardView>

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card_view"
style="@style/CardViewStyle"
android:layout_height="wrap_content"
android:focusable="true"
app:cardBackgroundColor="@color/content_background">
<TextView
android:id="@+id/title"
style="@style/TextAppearance"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/keyline_first"
android:layout_marginBottom="@dimen/keyline_first"
android:layout_marginStart="@dimen/keyline_first"
android:layout_marginEnd="@dimen/keyline_first"
android:gravity="start|center_vertical"
android:textSize="@dimen/sku_details_row_text_size"
app:fontFamily="sans-serif-medium"/>
</android.support.v7.widget.CardView>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tasks="http://schemas.android.com/tools">
<item
android:id="@+id/menu_help"
android:icon="@drawable/ic_help_24dp"
android:title="@string/help"
tasks:showAsAction="ifRoom"/>
<item
android:id="@+id/menu_refresh_purchases"
android:title="@string/refresh_purchases"
app:showAsAction="never" />
</menu>

@ -164,7 +164,6 @@
<string name="filter_settings">إعدادات التصفية</string>
<string name="show_hidden">عرض المهام المخفية</string>
<string name="show_completed">إظهار المكتملة</string>
<string name="plugin_description">تاسكس هو مشروع مفتوح المصدر مدموع من طرف مطور واحد. بعض الخيارات متوفرة عن الطريق الدفع من داخل التطبيق من أجل دعم التطوير</string>
<string name="opacity">التعتيم</string>
<string name="settings_localization">تخصيص اللغة و الجهة</string>
<string name="layout_direction">إتجاه التنسيق</string>

@ -365,7 +365,6 @@
<string name="show_hidden">Покажи скрити</string>
<string name="show_completed">Покажи завършени</string>
<string name="reverse">Обратно</string>
<string name="get_plugins">Покупки в приложението</string>
<string name="no_application_found">Не е намерено приложение за отваряне на прикачения файл</string>
<string name="add_attachment">Добавяне на прикачен файл</string>
<string name="take_a_picture">Заснемане</string>
@ -386,20 +385,12 @@
<string name="back_button_saves_task">Бутон \"Назад\" запазва задачата</string>
<string name="default_list">Списък по подразбиране</string>
<string name="default_sync">Синхронизация по подразбиране</string>
<string name="plugin_description">Tasks е проект с отворен код поддържан от един разработчик. Някои от функциите се предлагат като покупки в приложението за да се подпомогне процеса на разработка.</string>
<string name="themes_purchase_description">Отключете всички теми и да добавите малко цвят към Tasks</string>
<string name="tasker_description">Context-Aware списък уведомления. Изисква Tasker или Locale</string>
<string name="donate_summary">Даренията са добре дошли</string>
<string name="dashclock_purchase_description">Показване на броя активни задачи. Изисква DashClock Widget</string>
<string name="buy">Купи</string>
<string name="buy_dashclock_extension">Купи разширение</string>
<string name="billing_service_busy">Услугата за таксуване в приложението е заета, опитайте отново по-късно</string>
<string name="filter">Филтър</string>
<string name="opacity">Непрозрачност</string>
<string name="theme">Тема</string>
<string name="color">Цвят</string>
<string name="accent">Акцент</string>
<string name="themes">Допълнителни теми</string>
<string name="theme_red">Червен</string>
<string name="theme_pink">Розов</string>
<string name="theme_purple">Лилав</string>
@ -499,4 +490,5 @@
<string name="tasker_create_task">Създай задача</string>
<string name="tasker_list_notification">Списък с нотификации</string>
<string name="help">Помощ</string>
<string name="themes">Допълнителни теми</string>
</resources>

@ -266,9 +266,7 @@
<string name="no_title">(Bez názvu)</string>
<string name="back_button_saves_task">Tlačítko zpět uloží úkol</string>
<string name="default_list">Výchozí seznam</string>
<string name="plugin_description">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.</string>
<string name="donate_summary">Dary jsou velmi ceněny</string>
<string name="buy">Koupit</string>
<string name="filter">Filtr</string>
<string name="opacity">Průhlednost</string>
<string name="theme">Vzhled</string>

@ -358,7 +358,6 @@
<string name="show_hidden">Ausgeblendete anzeigen</string>
<string name="show_completed">Erledigte anzeigen</string>
<string name="reverse">Rückgangig</string>
<string name="get_plugins">In App-Käufe</string>
<string name="no_application_found">Keine Anwendung zum Öffnen des Anhangs gefunden</string>
<string name="add_attachment">Anhang hinzufügen</string>
<string name="take_a_picture">Bild aufnehmen</string>
@ -377,18 +376,10 @@
<string name="no_title">(kein Titel)</string>
<string name="back_button_saves_task">Zurück-Button speichert die Aufgabe</string>
<string name="default_list">Standard-Liste</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Entsperre alle Themes</string>
<string name="tasker_description">Kontext-bezogene Listenbenachrichtigungen. Benötigt Tasker oder Locale</string>
<string name="donate_summary">Spenden sind sehr willkommen</string>
<string name="dashclock_purchase_description">Anzahl aktiver Aufgaben anzeigen (DashClock-Widget benötigt).</string>
<string name="buy">Kaufen</string>
<string name="buy_dashclock_extension">Erweiterung kaufen</string>
<string name="billing_service_busy">Dienst für In-App Käufe ist beschäftigt, versuchen Sie es später noch einmal</string>
<string name="opacity">Durchsichtigkeit</string>
<string name="color">Farbe</string>
<string name="accent">Akzent</string>
<string name="themes">Zusätzliche Themes</string>
<string name="theme_red">Rot</string>
<string name="theme_purple">Lila</string>
<string name="theme_deep_purple">Dunkel-Lila</string>
@ -474,4 +465,5 @@
<string name="repeat_monthly_fourth_week">vierte</string>
<string name="repeat_monthly_last_week">Nachname</string>
<string name="help">Hilfe</string>
<string name="themes">Zusätzliche Themes</string>
</resources>

@ -361,7 +361,6 @@
<string name="show_hidden">Mostrar oculto</string>
<string name="show_completed">Mostrar completado</string>
<string name="reverse">Invertir</string>
<string name="get_plugins">Compras en la aplicación</string>
<string name="no_application_found">No se ha encontrado una aplicación para abrir el archivo adjunto</string>
<string name="add_attachment">Adjuntar archivo</string>
<string name="take_a_picture">Tomar fotografía</string>
@ -382,19 +381,11 @@
<string name="back_button_saves_task">Botón atrás guarda la tarea</string>
<string name="default_list">Lista por defecto</string>
<string name="default_sync">Sincronización predeterminada</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Desbloquea todos los temas y añade color a Tasks</string>
<string name="tasker_description">Notificaciones dependientes del contexto. Requiere Tasker o Locale.</string>
<string name="donate_summary">Se agradecen enormemente las donaciones</string>
<string name="dashclock_purchase_description">Muestra una cuenta de las tareas activas. Requiere DashClock Widget</string>
<string name="buy">Comprar</string>
<string name="buy_dashclock_extension">Comprar extensión</string>
<string name="billing_service_busy">El servicio de compra desde la aplicación está ocupado, inténtelo más tarde.</string>
<string name="filter">Filtro</string>
<string name="opacity">Opacidad</string>
<string name="theme">Tema</string>
<string name="accent">Acentuado</string>
<string name="themes">Temas adicionales</string>
<string name="theme_red">Rojo</string>
<string name="theme_pink">Rosa</string>
<string name="theme_purple">Morado</string>
@ -493,4 +484,5 @@
<string name="tasker_create_task">Crear tarea</string>
<string name="tasker_list_notification">Listar notificación</string>
<string name="help">Ayuda</string>
<string name="themes">Temas adicionales</string>
</resources>

@ -249,14 +249,12 @@
<string name="show_hidden">نمایش مخفی ها</string>
<string name="show_completed">نمایش انجام شده ها</string>
<string name="reverse">برعکس</string>
<string name="get_plugins">خرید داخل برنامه</string>
<string name="add_attachment">الصاق پیوست</string>
<string name="take_a_picture">گرفتن عکس</string>
<string name="send_anonymous_statistics">بهبود وظیفه</string>
<string name="tag_already_exists">این تگ قبلاً ایجاد شده است</string>
<string name="no_title">(بدون عنوان)</string>
<string name="default_list">لیست پیش فرض</string>
<string name="plugin_description">Tasks پروژه‌ای متن‌باز است که عمدتاً توسط یک‌نفر توسعه داده می‌شود. برای حمایت از این تلاش، برخی ویژگی‌ها به‌صورت خریدهای داخل برنامه ارائه شده‌اند.</string>
<string name="opacity">شفافیت</string>
<string name="theme">تم</string>
<string name="color">رنگ</string>

@ -366,7 +366,6 @@
<string name="show_hidden">Näytä piilotetut</string>
<string name="show_completed">Näytä valmiit</string>
<string name="reverse">Käänteinen</string>
<string name="get_plugins">Sovelluksen sisäiset ostot</string>
<string name="no_application_found">Liitteen avaamiseksi ei löydy sovellusta</string>
<string name="add_attachment">Lisää liite</string>
<string name="take_a_picture">Ota kuva</string>
@ -382,20 +381,12 @@
<string name="back_button_saves_task">Takaisin -painike tallentaa tehtävän</string>
<string name="default_list">Oletuslista</string>
<string name="default_sync">Oletus synkronointi</string>
<string name="plugin_description">Tasks on avoimen koodin projekti jota ylläpitää yksi kehittäjä. Jotkut ominaisuudet ovat saatavina sovelluksen sisäisinä ostoksina joilla tuetaan kehitystyötä.</string>
<string name="themes_purchase_description">Avaa kaikki teemat ja lisää väriä Tasks -ohjelmaan</string>
<string name="tasker_description">Kontekstiriippuvainen listaus ilmoituksille. Vaatii Tasker tai Locale -sovelluksen</string>
<string name="donate_summary">Lahjoitukset ovat erittäin tervetulleita</string>
<string name="dashclock_purchase_description">Näytä aktiivisten tehtävien määrä. Vaatii DashClock Widgetin</string>
<string name="buy">Osta</string>
<string name="buy_dashclock_extension">Osta laajennusosa</string>
<string name="billing_service_busy">Sovelluksen sisäinen laskutuspalvelu on varattu, yritä myöhemmin uudelleen</string>
<string name="filter">Suodatin</string>
<string name="opacity">Läpinäkyvyys</string>
<string name="theme">Teema</string>
<string name="color">Väri</string>
<string name="accent">Sävy</string>
<string name="themes">Lisäteemat</string>
<string name="theme_red">Pun.</string>
<string name="theme_pink">Pinkki</string>
<string name="theme_purple">Purppura</string>
@ -490,4 +481,5 @@
<string name="tasker_create_task">Luo tehtävä</string>
<string name="tasker_list_notification">Ilmoituslista</string>
<string name="help">Apua</string>
<string name="themes">Lisäteemat</string>
</resources>

@ -348,7 +348,6 @@
<string name="show_hidden">Afficher les tâches cachées</string>
<string name="show_completed">Afficher les tâches terminées</string>
<string name="reverse">Inverser</string>
<string name="get_plugins">Achats intégrés à l\'application</string>
<string name="no_application_found">Aucune application trouvée pour ouvrir la pièce jointe</string>
<string name="add_attachment">Ajouter une pièce jointe</string>
<string name="take_a_picture">Prendre une photo</string>
@ -369,19 +368,11 @@
<string name="back_button_saves_task">Faire un retour-arrière sauvegarde la tâche</string>
<string name="default_list">Liste par défaut</string>
<string name="default_sync">Synchronisation par défaut</string>
<string name="plugin_description">Tasks est un projet à source ouverte entretenu par un développeur. Quelques contenus payants sont disponible dans l\'application pour supporter le développement.</string>
<string name="themes_purchase_description">Débloquer tous les thèmes et quelques couleurs à Tasks</string>
<string name="tasker_description">Notifications de la sensibilité du contexte de la liste. Tasker ou Local est requis.</string>
<string name="donate_summary">Les donations sont grandement appréciées.</string>
<string name="dashclock_purchase_description">Affiche le nombre de tâches actives. Requiert le widget DashClock</string>
<string name="buy">Acheter</string>
<string name="buy_dashclock_extension">Acheter l\'extension</string>
<string name="billing_service_busy">Le service de facturation intégré à l\'application est surchargé, réessayez plus tard.</string>
<string name="filter">Filtre</string>
<string name="opacity">Transparence</string>
<string name="theme">Thème</string>
<string name="color">Couleur</string>
<string name="themes">Thèmes additionnels</string>
<string name="theme_red">Rouge</string>
<string name="theme_pink">Rose</string>
<string name="theme_purple">Violet</string>
@ -480,4 +471,5 @@
<string name="help">Aide</string>
<string name="calendar_not_found">Calendrier manquant</string>
<string name="network_error">Connexion échouée</string>
<string name="themes">Thèmes additionnels</string>
</resources>

@ -288,7 +288,6 @@
<string name="show_hidden">Mostrar oculto</string>
<string name="show_completed">Mostrar completado</string>
<string name="reverse">Invertir</string>
<string name="get_plugins">Compras en la aplicación</string>
<string name="no_application_found">No se ha encontrado una aplicación para abrir el archivo adjunto</string>
<string name="add_attachment">Adjuntar archivo</string>
<string name="take_a_picture">Tomar fotografía</string>
@ -303,20 +302,12 @@
<string name="no_title">(Sin título)</string>
<string name="back_button_saves_task">Botón atrás guarda la tarea</string>
<string name="default_list">Lista por defecto</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Desbloquea todos los temas y añade color a Tasks</string>
<string name="tasker_description">Notificaciones dependientes del contexto. Requiere Tasker o Locale.</string>
<string name="donate_summary">Se agradecen enormemente las donaciones</string>
<string name="dashclock_purchase_description">Muestra una cuenta de las tareas activas. Requiere DashClock Widget</string>
<string name="buy">Comprar</string>
<string name="buy_dashclock_extension">Comprar extensión</string>
<string name="billing_service_busy">El servicio de compra desde la aplicación está ocupado, inténtelo más tarde.</string>
<string name="filter">Filtro</string>
<string name="opacity">Opacidad</string>
<string name="theme">Tema</string>
<string name="color">Cor</string>
<string name="accent">Acentuado</string>
<string name="themes">Temas adicionales</string>
<string name="theme_red">Rojo</string>
<string name="theme_pink">Rosa</string>
<string name="theme_purple">Morado</string>
@ -367,4 +358,5 @@
<string name="delete_selected_tasks">¿Borrar tareas seleccionadas?</string>
<string name="copy_selected_tasks">¿Copiar tareas seleccionadas?</string>
<string name="use_native_datetime_pickers">Escoller data e hora</string>
<string name="themes">Temas adicionales</string>
</resources>

@ -365,7 +365,6 @@
<string name="show_hidden">Rejtettek megjelenítése</string>
<string name="show_completed">Elvégzettek megjelenítése</string>
<string name="reverse">Visszafelé</string>
<string name="get_plugins">Alkalmazáson belüli vásárlások</string>
<string name="no_application_found">A csatolmány megnyitására alkalmas program nem található</string>
<string name="add_attachment">Csatolmány hozzáadása</string>
<string name="take_a_picture">Kép készítése</string>
@ -386,20 +385,12 @@
<string name="back_button_saves_task">A vissza gomb elmenti a feladatot</string>
<string name="default_list">Alapértelmezett lista</string>
<string name="default_sync">Alapértelmezett szinkronizáció</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Az összes Tasks téma és szín elérhetővé tétele</string>
<string name="tasker_description">Kontextus-függő lista emlékeztető. Használatához Tasker vagy Locale szükséges.</string>
<string name="donate_summary">Támogatását nagyra értékeljük</string>
<string name="dashclock_purchase_description">Aktív feladatok számának megjelenítése. DashClock Widget megléte szükséges.</string>
<string name="buy">Vásárlás</string>
<string name="buy_dashclock_extension">Kiterjesztés vásárlása</string>
<string name="billing_service_busy">Az alkalmazáson belüli vásárlások szolgáltatás nem elérhető, kérem, próbálja később.</string>
<string name="filter">Szűrő</string>
<string name="opacity">Átlátszóság</string>
<string name="theme">Téma</string>
<string name="color">Szín</string>
<string name="accent">Kiemelés</string>
<string name="themes">További témák</string>
<string name="theme_red">Piros</string>
<string name="theme_pink">Rózsaszín</string>
<string name="theme_purple">Lila</string>
@ -500,4 +491,5 @@
<string name="tasker_create_task">Feladat létrehozása</string>
<string name="tasker_list_notification">Lista értesítés</string>
<string name="help">Súgó</string>
<string name="themes">További témák</string>
</resources>

@ -362,7 +362,6 @@
<string name="show_hidden">Mostra nascoste</string>
<string name="show_completed">Mostra completate</string>
<string name="reverse">Contrario</string>
<string name="get_plugins">Acquisti nell\'app.</string>
<string name="no_application_found">Nessuna applicazione in grado di aprire l\'allegato </string>
<string name="add_attachment">Aggiungi allegato</string>
<string name="take_a_picture">Scatta un foto</string>
@ -383,20 +382,12 @@
<string name="back_button_saves_task">Il tasto indietro salva l\'attività</string>
<string name="default_list">Lista predefinita</string>
<string name="default_sync">Sincronizz. predefinita</string>
<string name="plugin_description">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...</string>
<string name="themes_purchase_description">Sblocca tutti i temi e aggiungi colori alle attività</string>
<string name="tasker_description">Lista notifiche in base al contesto. Richiede Tasker o Locale</string>
<string name="donate_summary">Sono gradite offerte</string>
<string name="dashclock_purchase_description">Mostra conteggio attività attive. Richiede Widget DashClock</string>
<string name="buy">Acquista</string>
<string name="buy_dashclock_extension">Acquista estensione</string>
<string name="billing_service_busy">Servizio fatturazione in-app è occupato, prova più tardi</string>
<string name="filter">Filtra</string>
<string name="opacity">Opacità</string>
<string name="theme">Tema</string>
<string name="color">Colore</string>
<string name="accent">Evidenzia</string>
<string name="themes">Ulteriori temi</string>
<string name="theme_red">Rosso</string>
<string name="theme_pink">Rosa</string>
<string name="theme_purple">Porpora</string>
@ -495,4 +486,5 @@
<string name="tasker_create_task">Crea attività</string>
<string name="tasker_list_notification">Elenca notifiche</string>
<string name="help">Aiuto</string>
<string name="themes">Ulteriori temi</string>
</resources>

@ -313,7 +313,6 @@
<string name="show_hidden">הצגת משימות נסתרות</string>
<string name="show_completed">הצגת משימות שבוצעו</string>
<string name="reverse">אחורה</string>
<string name="get_plugins">רכישות בתוך הישום</string>
<string name="no_application_found">לא נמצאה אפליקצה לפתיחת הקובץ</string>
<string name="add_attachment">הוספ/י קובץ</string>
<string name="take_a_picture">צלם תמונה</string>
@ -334,20 +333,12 @@
<string name="back_button_saves_task">כפתור \"חזרה\" שומר שינויים במשימה</string>
<string name="default_list">רשימת ברירת מחדל</string>
<string name="default_sync">סנכרון ברירת מחדל</string>
<string name="plugin_description">זו אפליקציית קוד פתוח, מתוחזקת ע\"י מפתח אחד. מספר מאפיינים מוצעים לרכישה בתוך האפליקציה בכדי לתמוך בפיתוח.</string>
<string name="themes_purchase_description">בטל נעילת ערכות נושא והוסף צבע למשימות</string>
<string name="tasker_description">Context-aware רשימת משימות. נדרש Tasker או Locale</string>
<string name="donate_summary">תרומות יתקבלו בברכה</string>
<string name="dashclock_purchase_description">הצג כמות של משימות פעילות. נחוץ ישומון DashClock</string>
<string name="buy">רכישה</string>
<string name="buy_dashclock_extension">רכוש תוספת</string>
<string name="billing_service_busy">שרות רכישה עמוס, נסה מאוחר יותר</string>
<string name="filter">סינון</string>
<string name="opacity">אטימות</string>
<string name="theme">ערכת נושא</string>
<string name="color">צבע</string>
<string name="accent">צבע משני</string>
<string name="themes">ערכות נושא נוספות</string>
<string name="theme_red">אדום</string>
<string name="theme_pink">ורוד</string>
<string name="theme_purple">סגול</string>
@ -424,4 +415,5 @@
<string name="dont_sync">ללא סנכרון</string>
<string name="calendar_not_found">יומן לא נמצא</string>
<string name="network_error">החיבור נכשל</string>
<string name="themes">ערכות נושא נוספות</string>
</resources>

@ -363,7 +363,6 @@
<string name="show_hidden">非表示を表示</string>
<string name="show_completed">完了を表示</string>
<string name="reverse">逆順</string>
<string name="get_plugins">アプリ内課金</string>
<string name="no_application_found">添付ファイルを開くアプリケーションが見つかりません</string>
<string name="add_attachment">添付ファイルを追加</string>
<string name="take_a_picture">写真を撮影</string>
@ -384,20 +383,12 @@
<string name="back_button_saves_task">戻るボタンでタスクを保存します</string>
<string name="default_list">デフォルトリスト</string>
<string name="default_sync">デフォルトの同期</string>
<string name="plugin_description">Tasks は、1人の開発者が維持しているオープンソース プロジェクトです。開発をサポートするために、ニッチの機能はアプリ内課金で提供されています。</string>
<string name="themes_purchase_description">すべてのテーマのロックを解除して Tasks に色を追加します</string>
<string name="tasker_description">コンテキスト アウェア リストの通知。Tasker または Locale が必要です</string>
<string name="donate_summary">寄付は大歓迎です</string>
<string name="dashclock_purchase_description">アクティブなタスクの数を表示します。DashClock ウィジェットが必要です。</string>
<string name="buy">購入</string>
<string name="buy_dashclock_extension">購入エクステンション</string>
<string name="billing_service_busy">アプリ内課金サービスがビジー状態です。後でもう一度試してください</string>
<string name="filter">フィルター</string>
<string name="opacity">透明度</string>
<string name="theme">テーマ</string>
<string name="color"></string>
<string name="accent">アクセント</string>
<string name="themes">追加のテーマ</string>
<string name="theme_red"></string>
<string name="theme_pink">ピンク</string>
<string name="theme_purple"></string>
@ -497,4 +488,5 @@
<string name="tasker_create_task">タスクを作成</string>
<string name="tasker_list_notification">通知のリスト</string>
<string name="help">ヘルプ</string>
<string name="themes">追加のテーマ</string>
</resources>

@ -367,7 +367,6 @@
<string name="show_hidden">숨겨진 할일 표시</string>
<string name="show_completed">완료한 할일 표시</string>
<string name="reverse">역순</string>
<string name="get_plugins">인앱 결제</string>
<string name="no_application_found">첨부 파일을 열 수 있는 앱이 발견되지 않았습니다</string>
<string name="add_attachment">첨부파일 추가</string>
<string name="take_a_picture">사진 촬영</string>
@ -387,20 +386,12 @@
<string name="no_title">(제목 없음)</string>
<string name="back_button_saves_task">뒤로가기 버튼으로 할일 저장</string>
<string name="default_list">기본 목록</string>
<string name="plugin_description">Tasks는 한 명의 개발자에 의해 유지되고 있는 오픈 소스 프로젝트입니다. 개발을 지속하기 위해 몇몇 기능들은 인앱 결제를 통해 제공되고 있습니다.</string>
<string name="themes_purchase_description">모든 테마 잠금 해제 및 색상 추가</string>
<string name="tasker_description">컨텍스트 기반의 리스트 알림. Tasker 또는 Locale이 필요합니다.</string>
<string name="donate_summary">기부를 해주시면 감사하겠습니다</string>
<string name="dashclock_purchase_description">활성화된 할일의 개수를 표시합니다. DashClock Widget을 필요로 합니다.</string>
<string name="buy">구매</string>
<string name="buy_dashclock_extension">확장팩 구매</string>
<string name="billing_service_busy">인앱 결제 서비스가 혼잡합니다, 나중에 다시 시도하세요</string>
<string name="filter">필터</string>
<string name="opacity">불투명도</string>
<string name="theme">테마</string>
<string name="color">색상</string>
<string name="accent">강조</string>
<string name="themes">추가적인 테마</string>
<string name="theme_red">빨강</string>
<string name="theme_pink">분홍</string>
<string name="theme_purple">보라</string>
@ -496,4 +487,5 @@
<string name="repeat_monthly_third_week">세번째</string>
<string name="repeat_monthly_fourth_week">네번째</string>
<string name="repeat_monthly_last_week">마지막</string>
<string name="themes">추가적인 테마</string>
</resources>

@ -362,7 +362,6 @@
<string name="show_hidden">Rodyti paslėptus</string>
<string name="show_completed">Rodyti užbaigtus</string>
<string name="reverse">Atvirkščiai</string>
<string name="get_plugins">Apsipirkimas programoje</string>
<string name="no_application_found">Nerasta programa, kuri galėtų atidaryti prisegtą failą</string>
<string name="add_attachment">Pridėti failą</string>
<string name="take_a_picture">Nufotografuoti</string>
@ -383,20 +382,12 @@
<string name="back_button_saves_task">Mygtukas \"Atgal\" išsaugo pakitimus</string>
<string name="default_list">Numatytasis sąrašas</string>
<string name="default_sync">Numatytoji sinchronizacija</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Atrakinti visas Tasks temas ir pridėti kelias spalvas</string>
<string name="tasker_description">Pranešimai atsižvelgiant į sąrašo turinį. Reikalingas Tasker arba Locale.</string>
<string name="donate_summary">Paaukojimai yra ypač vertinami</string>
<string name="dashclock_purchase_description">Rodyti aktyvių užduočių skaičių. Reikalingas DashClock Widget</string>
<string name="buy">Pirkti</string>
<string name="buy_dashclock_extension">Pirkti plėtinį</string>
<string name="billing_service_busy">Programos sąskaitų paslauga užimta, pabandykite vėliau</string>
<string name="filter">Filtras</string>
<string name="opacity">Permatomumas</string>
<string name="theme">Tema</string>
<string name="color">Spalva</string>
<string name="accent">Akcentas</string>
<string name="themes">Papildomos temos</string>
<string name="theme_red">Raudona</string>
<string name="theme_pink">Rožinė</string>
<string name="theme_purple">Violetinė</string>
@ -498,4 +489,5 @@
<string name="help">Pagalba</string>
<string name="calendar_not_found">Kalendorius nerastas</string>
<string name="network_error">Sujungimas nepavyko</string>
<string name="themes">Papildomos temos</string>
</resources>

@ -157,8 +157,6 @@
<string name="show_hidden">Vis skjulte</string>
<string name="show_completed">Vis fullførte</string>
<string name="tag_already_exists">Tagg finnes allerede</string>
<string name="plugin_description">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.</string>
<string name="tasker_description">Kontekstbevisste listevarsler. Krever Tasker eller Locale</string>
<string name="opacity">Ugjennomsiktighet</string>
<string name="language">Språk</string>
<string name="restart_required">Tasks må startes om for at endringene skal ta effekt</string>

@ -358,7 +358,6 @@
<string name="show_hidden">Toon verborgen</string>
<string name="show_completed">Toon voltooide</string>
<string name="reverse">Omgekeerde</string>
<string name="get_plugins">In-app aankopen</string>
<string name="no_application_found">Geen applicatie gevonden om de bijlage te openen</string>
<string name="add_attachment">Bijlage toevoegen</string>
<string name="take_a_picture">Maak een foto</string>
@ -377,18 +376,10 @@
<string name="no_title">(geen titel)</string>
<string name="back_button_saves_task">Terug knop slaat taak op</string>
<string name="default_list">Standaard lijst</string>
<string name="plugin_description">Tasks is een open source project onderhouden bij een ontwikkelaar. Sommige functionaliteit worden als in-app aankopen aangeboden om de ontwikkeling te ondersteunen.</string>
<string name="themes_purchase_description">Ontlock alle thema\'s en voeg kleuren toe aan Tasks</string>
<string name="tasker_description"> Contextbewuste notificaties. Vereist Tasker of Locale</string>
<string name="donate_summary">Donaties worden erg gewaardeerd</string>
<string name="dashclock_purchase_description">Toon een teller van actieve taken. Vereist DashClock Widget</string>
<string name="buy">Kopen</string>
<string name="buy_dashclock_extension">Koop uitbreiding</string>
<string name="billing_service_busy">In-app rekening service is bezig, probeer later opnieuw</string>
<string name="opacity">Transparantie</string>
<string name="theme">Thema</string>
<string name="color">Kleur</string>
<string name="themes">Extra thema\'s</string>
<string name="theme_red">Rood</string>
<string name="theme_pink">Roze</string>
<string name="theme_purple">Paars</string>
@ -484,4 +475,5 @@
<string name="repeat_monthly_last_week">laatste</string>
<string name="tasker_create_task">Taak maken</string>
<string name="tasker_list_notification">Notificatie lijst</string>
<string name="themes">Extra thema\'s</string>
</resources>

@ -341,7 +341,6 @@
<string name="show_hidden">Pokaż ukryte</string>
<string name="show_completed">Pokaż ukończone</string>
<string name="reverse">Odwrotnie</string>
<string name="get_plugins">Zakupy w aplikacji</string>
<string name="no_application_found">Nie znaleziono aplikacji do otwarcia załącznika</string>
<string name="add_attachment">Dodaj załącznik</string>
<string name="take_a_picture">Wybierz obrazek</string>
@ -361,19 +360,12 @@
<string name="no_title">(Bez tytułu)</string>
<string name="back_button_saves_task">Przycisk Cofnij zapisuje zadanie</string>
<string name="default_list">Domyślna lista</string>
<string name="plugin_description">Tasks jest projektem na licencji open source utrzymywanym przez jednego developera. Płatne funkcje wspierają rozwój aplikacji.</string>
<string name="themes_purchase_description">Odblokuj wszystkie motywy i dodaj trochę kolorów do Tasks</string>
<string name="donate_summary">Dotacje są bardzo mile widziane</string>
<string name="dashclock_purchase_description">Wyświetla liczbę aktywnych zadań. Wymaga DashClock Widget</string>
<string name="buy">Kup</string>
<string name="buy_dashclock_extension">Kup rozszerzenie</string>
<string name="billing_service_busy">Serwis zakupów w aplikacji jest zajęty, spróbuj później</string>
<string name="filter">Filtr</string>
<string name="opacity">Nieprzezroczystość</string>
<string name="theme">Motyw</string>
<string name="color">Kolor</string>
<string name="accent">Akcent</string>
<string name="themes">Dodatkowe motywy</string>
<string name="theme_red">Czerwony</string>
<string name="theme_pink">Różowy</string>
<string name="theme_purple">Fioletowy</string>
@ -451,4 +443,5 @@
<string name="repeat_monthly_third_week">trzeci</string>
<string name="repeat_monthly_fourth_week">czwarty</string>
<string name="repeat_monthly_last_week">ostatni</string>
<string name="themes">Dodatkowe motywy</string>
</resources>

@ -282,7 +282,6 @@
<string name="filter_settings">Configurações de filtro</string>
<string name="show_hidden">Mostrar ocultas</string>
<string name="show_completed">Mostrar completas</string>
<string name="get_plugins">Compras no app</string>
<string name="no_application_found">Nenhuma aplicação encontrada para abrir o anexo</string>
<string name="add_attachment">Adicionar anexo</string>
<string name="take_a_picture">Tirar uma foto</string>
@ -296,20 +295,12 @@
<string name="no_title">(Sem título)</string>
<string name="back_button_saves_task">Botão voltar salva a tarefa</string>
<string name="default_list">Lista padrão</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Desbloquear todos os temas e adicionar um pouco de cor ao Tasks</string>
<string name="tasker_description">Notificações contextuadas de listas. Necessita Tasker ou Locale.</string>
<string name="donate_summary">Doações são muito valiosas</string>
<string name="dashclock_purchase_description">Mostra um contador de tarefas ativas. Necessita DashClock Widget.</string>
<string name="buy">Comprar</string>
<string name="buy_dashclock_extension">Comprar extensão</string>
<string name="billing_service_busy">O serviço de compras no app está ocupado. Tente novamente mais tarde.</string>
<string name="filter">Filtrar</string>
<string name="opacity">Opacidade</string>
<string name="theme">Tema</string>
<string name="color">Cor</string>
<string name="accent">Cor de realce</string>
<string name="themes">Temas adicionais</string>
<string name="theme_red">Vermelho</string>
<string name="theme_pink">Rosa</string>
<string name="theme_purple">Roxo</string>
@ -364,4 +355,5 @@
<string name="use_native_datetime_pickers">Usar calendário nativo</string>
<string name="dont_add_to_calendar">Não inserir no calendário</string>
<string name="default_calendar">Calendário padrão</string>
<string name="themes">Temas adicionais</string>
</resources>

@ -363,7 +363,6 @@
<string name="show_hidden">Mostrar ocultas</string>
<string name="show_completed">Mostrar terminadas</string>
<string name="reverse">Reverter</string>
<string name="get_plugins">Compras na aplicação</string>
<string name="no_application_found">Nenhuma aplicação encontrada para abrir o anexo</string>
<string name="add_attachment">Adicionar anexo</string>
<string name="take_a_picture">Tirar uma foto</string>
@ -378,17 +377,12 @@
<string name="no_title">(Sem título)</string>
<string name="back_button_saves_task">O botão voltar guarda a tarefa</string>
<string name="default_list">Lista padrão</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Desbloqueie todos os temas e adicione alguma cor ao Tasks</string>
<string name="donate_summary">Os donativos são muito apreciados</string>
<string name="buy">Comprar</string>
<string name="buy_dashclock_extension">Comprar extensão</string>
<string name="filter">Filtro</string>
<string name="opacity">Opacidade</string>
<string name="theme">Tema</string>
<string name="color">Cor</string>
<string name="accent">Realçe</string>
<string name="themes">Temas adicionais</string>
<string name="theme_red">Vermelho</string>
<string name="theme_pink">Rosa</string>
<string name="theme_purple">Púrpura</string>
@ -465,4 +459,5 @@
<string name="repeat_monthly_third_week">terceiro</string>
<string name="repeat_monthly_fourth_week">quarto</string>
<string name="repeat_monthly_last_week">último</string>
<string name="themes">Temas adicionais</string>
</resources>

@ -363,7 +363,6 @@
<string name="show_hidden">Показать скрытые</string>
<string name="show_completed">Показать выполненные</string>
<string name="reverse">Наоборот</string>
<string name="get_plugins">Покупки в приложении</string>
<string name="no_application_found">Не найдено приложение для открытия прикреплённого файла</string>
<string name="add_attachment">Прикрепить файл</string>
<string name="take_a_picture">Сделать снимок</string>
@ -384,20 +383,12 @@
<string name="back_button_saves_task">Кнопка «Назад» сохраняет задачу</string>
<string name="default_list">Список по умолчанию</string>
<string name="default_sync">Синхронизация по-умолчанию</string>
<string name="plugin_description">Tasks развивается как проект с открытым исходным кодом и поддерживается единственным разработчиком. Некоторые функции приложения предлагаются как платные для дальнейшего развития программы</string>
<string name="themes_purchase_description">Разблокировать все темы и добавить цвета в оформление Tasks</string>
<string name="tasker_description">Уведомления списка контекстного подбора. Требуется Tasker или Locale</string>
<string name="donate_summary">Благодарю за поддержку</string>
<string name="dashclock_purchase_description">Вывести счетчик активных задач. Нужен DashClock-виджет</string>
<string name="buy">Купить</string>
<string name="buy_dashclock_extension">Купить расширение</string>
<string name="billing_service_busy">Встроенный сервис перегружен, попробуйте позже</string>
<string name="filter">Фильтр</string>
<string name="opacity">Прозрачность</string>
<string name="theme">Цветовая тема</string>
<string name="color">Цвет</string>
<string name="accent">Акцент</string>
<string name="themes">Дополнительные темы</string>
<string name="theme_red">Красный</string>
<string name="theme_pink">Розовый</string>
<string name="theme_purple">Пурпурный</string>
@ -497,4 +488,5 @@
<string name="tasker_create_task">Создать задачу</string>
<string name="tasker_list_notification">Список уведомлений</string>
<string name="help">Помощь</string>
<string name="themes">Дополнительные темы</string>
</resources>

@ -363,7 +363,6 @@
<string name="show_hidden">Zobraziť skryté</string>
<string name="show_completed">Zobraziť dokončené</string>
<string name="reverse">Opačné</string>
<string name="get_plugins">Aplikácia obsahuje platené prvky</string>
<string name="no_application_found">Na otvorenie tohto súboru nebola nájdená vhodná aplikácia </string>
<string name="add_attachment">Pridať prílohu </string>
<string name="take_a_picture">Spraviť obrázok</string>
@ -384,19 +383,11 @@
<string name="back_button_saves_task">Uložiť úlohu tlačidlom Späť</string>
<string name="default_list">Predvolený zoznam</string>
<string name="default_sync">Predvolené synchronizácia </string>
<string name="plugin_description">Ú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é.</string>
<string name="themes_purchase_description">Odomknúť všetky témy a pridať farby do Úloh</string>
<string name="tasker_description">Zoznam s upozorňovaním na polohu. Vyžaduje sa Tasker alebo Locale</string>
<string name="donate_summary">Sme vďační za Vaše dary</string>
<string name="dashclock_purchase_description">Zobraziť počet aktívnych úloh. Vyžaduje sa DashClock Widget.</string>
<string name="buy">Kúpiť</string>
<string name="buy_dashclock_extension">Kúpiť rozšírenie</string>
<string name="billing_service_busy">Služba platenia v aplikácii je zaneprázdnená, skúste znovu neskôr</string>
<string name="opacity">Nejasnosť</string>
<string name="theme">Téma</string>
<string name="color">Farba</string>
<string name="accent">Zvýraznenie </string>
<string name="themes">Ďaľšie témy</string>
<string name="theme_red">Červená</string>
<string name="theme_pink">Ružová</string>
<string name="theme_purple">Fialová</string>
@ -496,4 +487,5 @@
<string name="tasker_create_task">Vytvor úlohu</string>
<string name="tasker_list_notification">Zoznam upozornení</string>
<string name="help">Pomoc</string>
<string name="themes">Ďaľšie témy</string>
</resources>

@ -286,7 +286,6 @@
<string name="show_hidden">Visa dolda</string>
<string name="show_completed">Visa slutförda</string>
<string name="reverse">Omvänt</string>
<string name="get_plugins">Köp i app</string>
<string name="no_application_found">Ingen applikation hittades för att öppna bilagan</string>
<string name="add_attachment">Bifoga filer</string>
<string name="take_a_picture">Ta en bild</string>
@ -301,18 +300,10 @@
<string name="no_title">(Ingen titel)</string>
<string name="back_button_saves_task">Bakåtknapp sparar uppgift</string>
<string name="default_list">Standardlista</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Lås upp alla teman och sätt lite färg på Tasks</string>
<string name="tasker_description">Innehållsbaserade listpåminnelser. Kräver Tasker eller Locale</string>
<string name="donate_summary">Donationer uppskattas varmt</string>
<string name="dashclock_purchase_description">Visa antalet aktiva uppgifter. Kräver DashClock Widget</string>
<string name="buy">Köp</string>
<string name="buy_dashclock_extension">Köp tillägg</string>
<string name="billing_service_busy">Köp i appar tjänsten är upptagen, försök igen senare</string>
<string name="opacity">Opacitet</string>
<string name="theme">Tema</string>
<string name="color">Färg</string>
<string name="themes">Fler teman</string>
<string name="theme_red">Röd</string>
<string name="theme_pink">Rosa</string>
<string name="theme_purple">Lila</string>
@ -351,4 +342,5 @@
<string name="deleting_list">Tar bort lista</string>
<string name="renaming_list">Byter namn på listan</string>
<string name="clear_completed_tasks_confirmation">Rensa bort slutförda uppgifter?</string>
<string name="themes">Fler teman</string>
</resources>

@ -367,7 +367,6 @@
<string name="show_hidden">Gizlenenleri göster</string>
<string name="show_completed">Tamamlananları göster</string>
<string name="reverse">Ters</string>
<string name="get_plugins">Uygulama içi satın almalar</string>
<string name="no_application_found">Ek dosyasını açmak için hiçbir uygulama bulunamadı</string>
<string name="add_attachment">Ek ekle</string>
<string name="take_a_picture">Bir fotoğraf çek</string>
@ -388,20 +387,12 @@
<string name="back_button_saves_task">Geri düğmesi görevi kaydeder</string>
<string name="default_list">Öntanımlı liste</string>
<string name="default_sync">Öntanımlı eşzamanlama</string>
<string name="plugin_description">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.</string>
<string name="themes_purchase_description">Tüm gövdeleri açar ve Tasks\'e birkaç renk ekler</string>
<string name="tasker_description">Bağlam bilinçli liste bildirimleri. Tasker veya Locale gerekir</string>
<string name="donate_summary">Bağışlar makbule geçer</string>
<string name="dashclock_purchase_description">Etkin görevlerin sayısını gösterir. DashClock Widget gerekir</string>
<string name="buy">Satın al</string>
<string name="buy_dashclock_extension">Eklenti satın al</string>
<string name="billing_service_busy">Uygulama içi faturalama hizmeti meşgul, daha sonra yeniden deneyin</string>
<string name="filter">Süzgeç</string>
<string name="opacity">Şeffaflık</string>
<string name="theme">Gövde</string>
<string name="color">Renk</string>
<string name="accent">Ara renk</string>
<string name="themes">Ek gövdeler</string>
<string name="theme_red">Kırmızı</string>
<string name="theme_pink">Pembe</string>
<string name="theme_purple">Mor</string>
@ -504,4 +495,5 @@
<string name="help">Yardım</string>
<string name="calendar_not_found">Takvim bulunamadı</string>
<string name="network_error">Bağlantı başarısız</string>
<string name="themes">Ek gövdeler</string>
</resources>

@ -290,7 +290,6 @@
<string name="show_hidden">Показати приховані</string>
<string name="show_completed">Показати завершені</string>
<string name="reverse">Реверс</string>
<string name="get_plugins">Покупки в застосунку</string>
<string name="no_application_found">Не знайдено програми для відкриття вкладення</string>
<string name="add_attachment">Додати вкладення</string>
<string name="take_a_picture">Зробити фото</string>
@ -305,20 +304,12 @@
<string name="no_title">(без назви)</string>
<string name="back_button_saves_task">Зберігати завдання кнопкою Назад</string>
<string name="default_list">Типовий список</string>
<string name="plugin_description">Tasks це проект з відкритим кодом, що обслуговується одним розробником. З метою підтримки розробки, деякі функції доступні лише після придбання через додаток.</string>
<string name="themes_purchase_description">Розблокувати усі схеми та додати кольори до Tasks</string>
<string name="tasker_description">Контекстні сповіщення списку. Потрібен Tasker або Locale</string>
<string name="donate_summary">Пожертви щиро вітаються</string>
<string name="dashclock_purchase_description">Показати кількість активних завдань. Потрібен DashClock Widget</string>
<string name="buy">Придбати</string>
<string name="buy_dashclock_extension">Придбати розширення</string>
<string name="billing_service_busy">Сервіс оплати застосунку перевантажено, спробуйте пізніше.</string>
<string name="filter">Фільтр</string>
<string name="opacity">Прозорість</string>
<string name="theme">Схема</string>
<string name="color">Колір</string>
<string name="accent">Акцент</string>
<string name="themes">Додаткові схеми</string>
<string name="theme_red">Червоний</string>
<string name="theme_pink">Рожевий</string>
<string name="theme_purple">Пурпуровий</string>
@ -368,4 +359,5 @@
<string name="delete_multiple_tasks_confirmation">%s видалено</string>
<string name="delete_selected_tasks">Видалити вибрані завдання?</string>
<string name="copy_selected_tasks">Копіювати вибрані завдання?</string>
<string name="themes">Додаткові схеми</string>
</resources>

@ -10,4 +10,10 @@
<item name="android:visibility">gone</item>
</style>
<style name="ButtonStyle" parent="BaseButtonStyle">
<item name="android:backgroundTint">?attr/colorAccent</item>
<item name="android:textColor">@android:color/white</item>
<item name="android:layout_gravity">bottom|end</item>
</style>
</resources>

@ -362,7 +362,6 @@
<string name="show_hidden">显示隐藏的任务</string>
<string name="show_completed">显示已完成任务</string>
<string name="reverse">反向</string>
<string name="get_plugins">应用内购买</string>
<string name="no_application_found">没有能打开附件的应用</string>
<string name="add_attachment">添加附件</string>
<string name="take_a_picture">拍张照片</string>
@ -383,20 +382,12 @@
<string name="back_button_saves_task">返回键保存任务</string>
<string name="default_list">默认列表</string>
<string name="default_sync">默认同步</string>
<string name="plugin_description">Tasks是一个开发人员维护的开源项目。有些功能是作为应用内购提供的以此支持开发。</string>
<string name="themes_purchase_description">解锁所有Tasks的主题和颜色</string>
<string name="tasker_description">上下文感知列表通知。需要Tasks或Locale</string>
<string name="donate_summary">不胜感激</string>
<string name="dashclock_purchase_description">显示活动任务的计数。需要安装DashClock Widget</string>
<string name="buy">购买</string>
<string name="buy_dashclock_extension">购买扩展</string>
<string name="billing_service_busy">应用内结算服务繁忙,请稍后重试</string>
<string name="filter">过滤器</string>
<string name="opacity">不透明度</string>
<string name="theme">主题</string>
<string name="color">颜色</string>
<string name="accent">强调色</string>
<string name="themes">其他主题</string>
<string name="theme_red">红色</string>
<string name="theme_pink">粉色</string>
<string name="theme_purple">紫色</string>
@ -497,4 +488,5 @@
<string name="tasker_create_task">创建任务</string>
<string name="tasker_list_notification">列出通知</string>
<string name="help">帮助</string>
<string name="themes">其他主题</string>
</resources>

@ -221,15 +221,12 @@
<string name="filter_settings">過濾器設定</string>
<string name="show_hidden">顯示隱藏</string>
<string name="show_completed">顯示已完成</string>
<string name="get_plugins">程式內購買</string>
<string name="default_list">預設清單</string>
<string name="themes_purchase_description">解鎖全部主題並為工作增加一些色彩</string>
<string name="donate_summary">非常歡迎您的贊助!</string>
<string name="opacity">透明度</string>
<string name="theme">主題</string>
<string name="color">色彩</string>
<string name="accent">強調色</string>
<string name="themes">其他主題</string>
<string name="settings_general">一般</string>
<string name="language">語言</string>
<string name="restart_required">Tasks 必須重新啟動以使變更生效</string>
@ -248,4 +245,5 @@
<string name="repeats_monthly">每月</string>
<string name="repeats_yearly">每年</string>
<string name="repeats_plural">每 %s 重複</string>
<string name="themes">其他主題</string>
</resources>

@ -224,4 +224,12 @@
<item>1</item>
<item>2</item>
</string-array>
<string-array name="pro_description">
<item>@string/themes</item>
<item>@string/pro_caldav_sync</item>
<item>@string/pro_multiple_google_task_accounts</item>
<item>@string/pro_tasker_plugins</item>
<item>@string/pro_dashclock_extension</item>
</string-array>
</resources>

@ -25,4 +25,17 @@
<dimen name="widget_padding">10dp</dimen>
<dimen name="week_button_inset">6dp</dimen>
<dimen name="week_button_state_on_circle_size">48dp</dimen>
<dimen name="sku_details_row_text_size">14sp</dimen>
<dimen name="card_view_margin">8dp</dimen>
<dimen name="card_view_card_corner_radius">2dp</dimen>
<dimen name="card_view_card_elevation">2sp</dimen>
<dimen name="button_width">152dp</dimen>
<dimen name="button_height">48dp</dimen>
<dimen name="header_gap">8dp</dimen>
<dimen name="row_gap">1px</dimen>
</resources>

@ -258,11 +258,7 @@
<string name="tracking_event_multiselect_clone">Multiselect clone</string>
<string name="p_badges_enabled">badges_enabled</string>
<string name="p_badge_list">badge_list</string>
<string name="p_purchased_tasker">purchased_tasker</string>
<string name="p_purchased_dashclock">purchased_dashclock</string>
<string name="p_purchased_themes">purchased_themes</string>
<string name="tasker_locale">Tasker/Locale</string>
<string name="dashclock">DashClock extension</string>
<string name="p_theme">theme_style</string>
<string name="p_theme_color">theme_color</string>
<string name="p_theme_accent">theme_accent</string>
@ -282,11 +278,10 @@
<string name="p_dashclock_filter">dashclock_filter</string>
<string name="p_default_remote_list">default_remote_list</string>
<string name="sku_tasker">tasker</string>
<string name="sku_dashclock">dashclock</string>
<string name="debug_unlock_purchases">Unlock purchases</string>
<string name="debug_consume_purchases">Consume purchases</string>
<string name="debug_display_purchases">Purchases</string>
<string name="debug_consume">Consume</string>
<string name="debug_strict_mode">Strict mode</string>
<string name="debug_buy">Buy</string>
<string name="debug">Debug</string>
<string name="p_start_of_week">start_of_week</string>
<string name="p_use_native_datetime_pickers">use_native_datetime_pickers</string>
@ -296,5 +291,6 @@
<string name="warned_play_services">warned_play_services</string>
<string name="p_sync_caldav">sync_caldav</string>
<string name="p_background_sync_unmetered_only">background_sync_unmetered_only</string>
<string name="p_purchases">purchases</string>
</resources>

@ -744,7 +744,6 @@ File %1$s contained %2$s.\n\n
<string name="show_hidden">Show hidden</string>
<string name="show_completed">Show completed</string>
<string name="reverse">Reverse</string>
<string name="get_plugins">In-app purchases</string>
<string name="no_application_found">No application found to open attachment</string>
<string name="add_attachment">Add attachment</string>
<string name="take_a_picture">Take a picture</string>
@ -765,21 +764,12 @@ File %1$s contained %2$s.\n\n
<string name="back_button_saves_task">Back button saves task</string>
<string name="default_list">Default list</string>
<string name="default_sync">Default sync</string>
<string name="plugin_description">Tasks is an open source project maintained by one developer. Some features are offered as in-app purchases in order to support development.</string>
<string name="themes_purchase_description">Unlock all themes and add some color to Tasks</string>
<string name="tasker_description">Context-aware list notifications. Requires Tasker or Locale</string>
<string name="donate_summary">Donations are greatly appreciated</string>
<string name="dashclock_purchase_description">Display a count of active tasks. Requires DashClock Widget</string>
<string name="buy">Buy</string>
<string name="buy_dashclock_extension">Buy extension</string>
<string name="billing_service_busy">In-app billing service is busy, try again later</string>
<string name="filter">Filter</string>
<string name="opacity">Opacity</string>
<string name="theme">Theme</string>
<string name="color">Color</string>
<string name="accent">Accent</string>
<string name="themes">Additional themes</string>
<string name="theme_red">Red</string>
<string name="theme_pink">Pink</string>
<string name="theme_purple">Purple</string>
@ -889,4 +879,20 @@ File %1$s contained %2$s.\n\n
<string name="calendar_not_found">Calendar not found</string>
<string name="network_error">Connection failed</string>
<string name="background_sync_unmetered_only">Only on unmetered connections</string>
<string name="upgrade">Upgrade</string>
<string name="subscribe_to_pro">Subscribe to pro</string>
<string name="refresh_purchases">Refresh purchases</string>
<string name="button_subscribed">Subscribed</string>
<string name="button_subscribe">Subscribe</string>
<string name="owned">Owned</string>
<string name="error_billing_unavailable">Billing unavailable. Make sure your Google Play app
is setup correctly</string>
<string name="error_billing_default">Billing unavailable. Please check your device.</string>
<string name="themes">Additional themes</string>
<string name="pro_caldav_sync">CalDAV synchronization</string>
<string name="pro_multiple_google_task_accounts">Multiple Google Task accounts</string>
<string name="pro_tasker_plugins">Tasker plugins</string>
<string name="pro_dashclock_extension">Dashclock extension</string>
</resources>

@ -111,4 +111,28 @@
<item name="android:background">@android:color/transparent</item>
</style>
<style name="CardViewStyle" parent="CardView">
<item name="android:layout_marginTop">0dp</item>
<item name="android:layout_marginBottom">0dp</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginRight">@dimen/card_view_margin</item>
<item name="android:layout_marginEnd" tools:ignore="NewApi">@dimen/card_view_margin</item>
<item name="android:layout_marginLeft">@dimen/card_view_margin</item>
<item name="android:layout_marginStart" tools:ignore="NewApi">@dimen/card_view_margin</item>
<item name="cardCornerRadius">@dimen/card_view_card_corner_radius</item>
<item name="cardElevation">@dimen/card_view_card_elevation</item>
<item name="cardPreventCornerOverlap">false</item>
</style>
<style name="BaseButtonStyle" parent="Widget.AppCompat.Button.Colored">
<item name="android:layout_width">@dimen/button_width</item>
<item name="android:layout_height">@dimen/button_height</item>
</style>
<style name="ButtonStyle" parent="BaseButtonStyle">
<item name="android:background">?attr/colorAccent</item>
<item name="android:textColor">@android:color/white</item>
<item name="android:layout_gravity">bottom|end</item>
</style>
</resources>

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="@string/get_plugins"
android:title="@string/get_plugins">
<Preference
android:selectable="false"
android:summary="@string/plugin_description"/>
<Preference
android:key="@string/TLA_menu_donate"
android:summary="@string/donate_summary"
android:title="@string/TLA_menu_donate"/>
<CheckBoxPreference
android:dependency="@string/p_purchased_themes"
android:disableDependentsState="true"
android:key="@string/p_purchased_themes"
android:summary="@string/themes_purchase_description"
android:title="@string/themes"/>
<CheckBoxPreference
android:dependency="@string/p_purchased_dashclock"
android:disableDependentsState="true"
android:key="@string/p_purchased_dashclock"
android:summary="@string/dashclock_purchase_description"
android:title="@string/dashclock"/>
<CheckBoxPreference
android:dependency="@string/p_purchased_tasker"
android:disableDependentsState="true"
android:key="@string/p_purchased_tasker"
android:summary="@string/tasker_description"
android:title="@string/tasker_locale"/>
</PreferenceCategory>
</PreferenceScreen>

@ -4,13 +4,11 @@
<PreferenceCategory
android:title="@string/debug">
<Preference
android:key="@string/debug_unlock_purchases"
android:title="@string/debug_unlock_purchases"/>
<Preference
android:key="@string/debug_consume_purchases"
android:title="@string/debug_consume_purchases"/>
<Preference android:title="@string/debug_display_purchases">
<intent
android:targetClass="org.tasks.billing.PurchaseActivity"
android:targetPackage="org.tasks"/>
</Preference>
<CheckBoxPreference
android:key="@string/p_strict_mode"

Loading…
Cancel
Save