Add DashClock extension

Closes #125
pull/384/head
Alex Baker 10 years ago
parent 39fea551b6
commit 927fbc7205

@ -96,6 +96,7 @@ dependencies {
exclude group: 'com.android.support', module: 'support-v4'
}
googleplayCompile 'com.google.android.apps.dashclock:dashclock-api:2.0.0'
googleplayCompile 'com.twofortyfouram:android-plugin-api-for-locale:[1.0.1,2.0['
googleplayCompile 'com.google.android.gms:play-services-location:8.4.0'
googleplayCompile 'com.google.android.gms:play-services-analytics:8.4.0'

@ -0,0 +1,8 @@
package org.tasks.injection;
import dagger.Subcomponent;
@Subcomponent(modules = ServiceModule.class)
public interface ServiceComponent extends BaseServiceComponent {
}

@ -15,7 +15,8 @@ public class BasicPreferences extends BaseBasicPreferences {
PreferenceScreen preferenceScreen = getPreferenceScreen();
preferenceScreen.removePreference(findPreference(getString(R.string.synchronization)));
preferenceScreen.removePreference(findPreference(getString(R.string.p_tesla_unread_enabled)));
preferenceScreen.removePreference(findPreference(getString(R.string.p_tasker_enabled)));
preferenceScreen.removePreference(findPreference(getString(R.string.p_purchased_tasker)));
preferenceScreen.removePreference(findPreference(getString(R.string.p_purchased_dashclock)));
preferenceScreen.removePreference(findPreference(getString(R.string.p_collect_statistics)));
}

@ -86,6 +86,8 @@
<service android:name="com.google.android.gms.analytics.CampaignTrackingService" />
<!-- Tasker/Locale -->
<activity
android:name=".locale.ui.activity.TaskerSettingsActivity"
android:exported="false"
@ -93,15 +95,7 @@
android:theme="@style/Tasks"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustResize"/>
<!--
This is the "edit" Activity. Note that the host will reject plug-in
Activities for the following reasons:
- Missing "android:label=[...]"
- Missing "android:icon=[...]"
- The Activity isn't exported (e.g. android:exported="false")
- The Activity isn't enabled (e.g. android:enabled="false")
- The Activity requires permissions not available to the host
-->
<activity-alias
android:name="com.twofortyfouram.locale.example.setting.toast.ui.activity.PluginActivity"
android:exported="true"
@ -110,33 +104,43 @@
android:label="@string/app_name"
android:targetActivity=".locale.ui.activity.TaskerSettingsActivity"
tools:ignore="ExportedActivity">
<!-- this Intent filter allows the plug-in to be discovered by the host. -->
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity-alias>
<!--
This is the "fire" BroadcastReceiver. Note that the host will reject plug-in
BroadcastReceivers for the following reasons:
- The BroadcastReceiver isn't exported (e.g. android:exported="false")
- The BroadcastReceiver isn't enabled (e.g. android:enabled="false")
- The BroadcastReceiver requires permissions not available to the host
- There are multiple BroadcastReceivers for com.twofortyfouram.locale.intent.action.FIRE_SETTING
-->
<receiver
android:name=".locale.receiver.FireReceiver"
android:exported="true"
android:enabled="true"
android:process=":background"
tools:ignore="ExportedReceiver">
<!-- this Intent filter allows the plug-in to discovered by the host. -->
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter>
</receiver>
<!-- DashClock extension -->
<service android:name=".dashclock.DashClockExtension"
android:icon="@drawable/ic_check_white_24dp"
android:label="@string/app_name"
android:permission="com.google.android.apps.dashclock.permission.READ_EXTENSION_DATA">
<intent-filter>
<action android:name="com.google.android.apps.dashclock.Extension" />
</intent-filter>
<meta-data android:name="protocolVersion" android:value="2" />
<meta-data android:name="worldReadable" android:value="true" />
<meta-data android:name="description"
android:value="@string/dashclock_description" />
<meta-data android:name="settingsActivity"
android:value=".dashclock.DashClockSettings" />
</service>
<activity android:name=".dashclock.DashClockSettings"
android:label="@string/app_name"
android:exported="true" />
</application>
</manifest>

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 The Android Open Source Project
* 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.
@ -34,10 +34,11 @@ import android.os.Bundle;
*
* 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_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_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
@ -46,11 +47,11 @@ 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 the billing version which the app is using
* @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 subscription.
* @return RESULT_OK(0) on success, corresponding result code on failures
* @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);
@ -59,16 +60,23 @@ interface IInAppBillingService {
* 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 Third-party is using
* @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, other response codes on
* failure as listed above.
* "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",
* "title : "Example Title", "description" : "This is an example description" }'
* 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);
@ -78,26 +86,26 @@ interface IInAppBillingService {
* @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 the type of the in-app item ("inapp" for one-time purchases
* and "subs" for subscription).
* @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, other response codes on
* failure as listed above.
* "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, other response codes on
* failure as listed above.
* "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" }'
* '{"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.
@ -112,15 +120,15 @@ interface IInAppBillingService {
* 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 the type of the in-app items being requested
* ("inapp" for one-time purchases and "subs" for subscription).
* @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, other response codes on
* failure as listed above.
* "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
@ -138,7 +146,47 @@ interface IInAppBillingService {
* @param packageName package name of the calling app
* @param purchaseToken token in the purchase information JSON that identifies the purchase
* to be consumed
* @return 0 if consumption succeeded. Appropriate error values for failures.
* @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);
}

@ -0,0 +1,60 @@
/* 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.</p>
*/
public class IabBroadcastReceiver extends BroadcastReceiver {
/**
* Listener interface for received broadcast messages.
*/
public interface IabBroadcastListener {
void receivedBroadcast();
}
/**
* 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();
}
}
}

@ -13,7 +13,7 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
/**
* Exception thrown when something went wrong with in-app billing.

@ -13,7 +13,7 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
import android.app.Activity;
import android.app.PendingIntent;
@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@ -32,19 +33,10 @@ import android.util.Log;
import com.android.vending.billing.IInAppBillingService;
import org.json.JSONException;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import timber.log.Timber;
/**
* Provides convenience methods for in-app billing. You can create one instance of this
@ -75,14 +67,11 @@ import timber.log.Timber;
* attempting to start a second asynchronous operation while the first one
* has not yet completed will result in an exception being thrown.
*
* @author Bruno Oliveira (Google)
*
*/
@Singleton
public class IabHelper {
// Is debug logging enabled?
boolean mDebugLog = BuildConfig.DEBUG;
String mDebugTag = BuildConfig.APPLICATION_ID;
boolean mDebugLog = false;
String mDebugTag = "IabHelper";
// Is setup done?
boolean mSetupDone = false;
@ -93,6 +82,9 @@ public class IabHelper {
// Are subscriptions supported?
boolean mSubscriptionsSupported = false;
// Is subscription update supported?
boolean mSubscriptionUpdateSupported = false;
// Is an asynchronous operation in progress?
// (only one at a time can be in progress)
boolean mAsyncInProgress = false;
@ -120,6 +112,7 @@ public class IabHelper {
// Billing response codes
public static final int BILLING_RESPONSE_RESULT_OK = 0;
public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
@ -139,6 +132,7 @@ public class IabHelper {
public static final int IABHELPER_UNKNOWN_ERROR = -1008;
public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011;
// Keys for the responses from InAppBillingService
public static final String RESPONSE_CODE = "RESPONSE_CODE";
@ -158,23 +152,59 @@ public class IabHelper {
// some fields on the getSkuDetails response bundle
public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
private Preferences preferences;
@Inject
public IabHelper(@ForApplication Context ctx, Preferences preferences) {
this.preferences = preferences;
/**
* Creates an instance. After creation, it will not yet be ready to use. You must perform
* setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
* block and is safe to call from a UI thread.
*
* @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
* @param base64PublicKey Your application's public key, encoded in base64.
* This is used for verification of purchase signatures. You can find your app's base64-encoded
* public key in your application's page on Google Play Developer Console. Note that this
* is NOT your "developer public key".
*/
public IabHelper(Context ctx, String base64PublicKey) {
mContext = ctx.getApplicationContext();
mSignatureBase64 = ctx.getString(R.string.gp_key);
mSignatureBase64 = base64PublicKey;
logDebug("IAB helper created.");
}
/**
* Enables or disable debug logging through LogCat.
*/
public void enableDebugLogging(boolean enable, String tag) {
checkNotDisposed();
mDebugLog = enable;
mDebugTag = tag;
}
public void enableDebugLogging(boolean enable) {
checkNotDisposed();
mDebugLog = enable;
}
/**
* Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
* when the setup process is complete.
*/
public interface OnIabSetupFinishedListener {
/**
* Called to notify that setup is complete.
*
* @param result The result of the setup process.
*/
void onIabSetupFinished(IabResult result);
}
/**
* Starts the setup process. This will start up the setup process asynchronously.
* You will be notified through the listener when the setup process is complete.
* This method is safe to call from a UI thread.
*
* @param listener The listener to notify when the setup process is complete.
*/
public void startSetup() {
public void startSetup(final OnIabSetupFinishedListener listener) {
// If already set up, can't do it again.
checkNotDisposed();
if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
@ -200,70 +230,73 @@ public class IabHelper {
// check for in-app billing v3 support
int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
if (response != BILLING_RESPONSE_RESULT_OK) {
onIabSetupFinished(new IabResult(response,
if (listener != null) listener.onIabSetupFinished(new IabResult(response,
"Error checking for billing v3 support."));
// if in-app purchases aren't supported, neither are subscriptions.
// if in-app purchases aren't supported, neither are subscriptions
mSubscriptionsSupported = false;
mSubscriptionUpdateSupported = false;
return;
} else {
logDebug("In-app billing version 3 supported for " + packageName);
}
logDebug("In-app billing version 3 supported for " + packageName);
// check for v3 subscriptions support
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
// Check for v5 subscriptions support. This is needed for
// getBuyIntentToReplaceSku which allows for subscription update
response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Subscriptions AVAILABLE.");
mSubscriptionsSupported = true;
logDebug("Subscription re-signup AVAILABLE.");
mSubscriptionUpdateSupported = true;
} else {
logDebug("Subscription re-signup not available.");
mSubscriptionUpdateSupported = false;
}
else {
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
if (mSubscriptionUpdateSupported) {
mSubscriptionsSupported = true;
} else {
// check for v3 subscriptions support
response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Subscriptions AVAILABLE.");
mSubscriptionsSupported = true;
} else {
logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
mSubscriptionsSupported = false;
mSubscriptionUpdateSupported = false;
}
}
mSetupDone = true;
}
catch (RemoteException e) {
onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
"RemoteException while setting up in-app billing."));
if (listener != null) {
listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
"RemoteException while setting up in-app billing."));
}
e.printStackTrace();
return;
}
onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
if (listener != null) {
listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
}
}
};
Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
serviceIntent.setPackage("com.android.vending");
if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
List<ResolveInfo> intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
if (intentServices != null && !intentServices.isEmpty()) {
// service available to handle that Intent
mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
}
else {
// no service available to handle that Intent
onIabSetupFinished(
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
"Billing service unavailable on device."));
}
}
private void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
Timber.d("IAB setup successful");
queryInventoryAsync();
} else {
Timber.e(result.getMessage());
}
}
private void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (result.isFailure()) {
Timber.e("Query inventory failed: %s", result);
} else {
if (inventory.hasPurchase(mContext.getString(R.string.sku_tesla_unread))) {
preferences.setBoolean(R.string.p_purchased_tesla_unread, false);
}
if (inventory.hasPurchase(mContext.getString(R.string.sku_tasker))) {
preferences.setBoolean(R.string.p_purchased_tasker, false);
if (listener != null) {
listener.onIabSetupFinished(
new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
"Billing service unavailable on device."));
}
}
}
@ -298,6 +331,7 @@ public class IabHelper {
return mSubscriptionsSupported;
}
/**
* Callback that notifies when a purchase is finished.
*/
@ -311,7 +345,7 @@ public class IabHelper {
* @param result The result of the purchase.
* @param info The purchase information (null if purchase failed)
*/
public void onIabPurchaseFinished(IabResult result, Purchase info);
void onIabPurchaseFinished(IabResult result, Purchase info);
}
// The listener registered on launchPurchaseFlow, which we have to call back when
@ -324,7 +358,7 @@ public class IabHelper {
public void launchPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData) {
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData);
}
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
@ -334,29 +368,31 @@ public class IabHelper {
public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData) {
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, requestCode, listener, extraData);
}
/**
* Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
* which will involve bringing up the Google Play screen. The calling activity will be paused while
* the user interacts with Google Play, and the result will be delivered via the activity's
* {@link android.app.Activity#onActivityResult} method, at which point you must call
* which will involve bringing up the Google Play screen. The calling activity will be paused
* while the user interacts with Google Play, and the result will be delivered via the
* activity's {@link android.app.Activity#onActivityResult} method, at which point you must call
* this object's {@link #handleActivityResult} method to continue the purchase flow. This method
* MUST be called from the UI thread of the Activity.
*
* @param act The calling activity.
* @param sku The sku of the item to purchase.
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
* @param requestCode A request code (to differentiate from other responses --
* as in {@link android.app.Activity#startActivityForResult}).
* @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or
* ITEM_TYPE_SUBS)
* @param oldSkus A list of SKUs which the new SKU is replacing or null if there are none
* @param requestCode A request code (to differentiate from other responses -- as in
* {@link android.app.Activity#startActivityForResult}).
* @param listener The listener to notify when the purchase process finishes
* @param extraData Extra data (developer payload), which will be returned with the purchase data
* when the purchase completes. This extra data will be permanently bound to that purchase
* and will always be returned when the purchase is queried.
* @param extraData Extra data (developer payload), which will be returned with the purchase
* data when the purchase completes. This extra data will be permanently bound to that
* purchase and will always be returned when the purchase is queried.
*/
public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
OnIabPurchaseFinishedListener listener, String extraData) {
public void launchPurchaseFlow(Activity act, String sku, String itemType, List<String> oldSkus,
int requestCode, OnIabPurchaseFinishedListener listener, String extraData) {
checkNotDisposed();
checkSetupDone("launchPurchaseFlow");
flagStartAsync("launchPurchaseFlow");
@ -372,7 +408,23 @@ public class IabHelper {
try {
logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
Bundle buyIntentBundle;
if (oldSkus == null || oldSkus.isEmpty()) {
// Purchasing a new item or subscription re-signup
buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType,
extraData);
} else {
// Subscription upgrade/downgrade
if (!mSubscriptionUpdateSupported) {
IabResult r = new IabResult(IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE,
"Subscription updates are not available.");
flagEndAsync();
if (listener != null) listener.onIabPurchaseFinished(r, null);
return;
}
buyIntentBundle = mService.getBuyIntentToReplaceSkus(5, mContext.getPackageName(),
oldSkus, sku, itemType, extraData);
}
int response = getResponseCodeFromBundle(buyIntentBundle);
if (response != BILLING_RESPONSE_RESULT_OK) {
logError("Unable to buy item, Error response: " + getResponseDesc(response));
@ -388,9 +440,9 @@ public class IabHelper {
mPurchaseListener = listener;
mPurchasingItemType = itemType;
act.startIntentSenderForResult(pendingIntent.getIntentSender(),
requestCode, new Intent(),
Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
requestCode, new Intent(),
Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
}
catch (SendIntentException e) {
logError("SendIntentException while launching purchase flow for sku " + sku);
@ -525,7 +577,7 @@ public class IabHelper {
* @throws IabException if a problem occurs while refreshing the inventory.
*/
public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
List<String> moreSubsSkus) throws IabException {
List<String> moreSubsSkus) throws IabException {
checkNotDisposed();
checkSetupDone("queryInventory");
try {
@ -550,7 +602,7 @@ public class IabHelper {
}
if (querySkuDetails) {
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
if (r != BILLING_RESPONSE_RESULT_OK) {
throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
}
@ -567,6 +619,20 @@ public class IabHelper {
}
}
/**
* Listener that notifies when an inventory query operation completes.
*/
public interface QueryInventoryFinishedListener {
/**
* Called to notify that an inventory query operation completed.
*
* @param result The result of the operation.
* @param inv The inventory.
*/
void onQueryInventoryFinished(IabResult result, Inventory inv);
}
/**
* Asynchronous wrapper for inventory query. This will perform an inventory
* query as described in {@link #queryInventory}, but will do so asynchronously
@ -575,8 +641,10 @@ public class IabHelper {
*
* @param querySkuDetails as in {@link #queryInventory}
* @param moreSkus as in {@link #queryInventory}
* @param listener The listener to notify when the refresh operation completes.
*/
public void queryInventoryAsync(final boolean querySkuDetails, final List<String> moreSkus) {
public void queryInventoryAsync(final boolean querySkuDetails, final List<String> moreSkus,
final QueryInventoryFinishedListener listener) {
final Handler handler = new Handler();
checkNotDisposed();
checkSetupDone("queryInventory");
@ -596,10 +664,10 @@ public class IabHelper {
final IabResult result_f = result;
final Inventory inv_f = inv;
if (!mDisposed) {
if (!mDisposed && listener != null) {
handler.post(new Runnable() {
public void run() {
onQueryInventoryFinished(result_f, inv_f);
listener.onQueryInventoryFinished(result_f, inv_f);
}
});
}
@ -607,14 +675,15 @@ public class IabHelper {
})).start();
}
public void queryInventoryAsync() {
queryInventoryAsync(true, null);
public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
queryInventoryAsync(true, null, listener);
}
public void queryInventoryAsync(boolean querySkuDetails) {
queryInventoryAsync(querySkuDetails, null);
public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
queryInventoryAsync(querySkuDetails, null, listener);
}
/**
* Consumes a given in-app product. Consuming can only be done on an item
* that's owned, and as a result of consumption, the user will no longer own it.
@ -637,19 +706,19 @@ public class IabHelper {
String token = itemInfo.getToken();
String sku = itemInfo.getSku();
if (token == null || token.equals("")) {
logError("Can't consume "+ sku + ". No token.");
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ sku + " " + itemInfo);
logError("Can't consume "+ sku + ". No token.");
throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+ sku + " " + itemInfo);
}
logDebug("Consuming sku: " + sku + ", token: " + token);
int response = mService.consumePurchase(3, mContext.getPackageName(), token);
if (response == BILLING_RESPONSE_RESULT_OK) {
logDebug("Successfully consumed sku: " + sku);
logDebug("Successfully consumed sku: " + sku);
}
else {
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
throw new IabException(response, "Error consuming sku " + sku);
logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
throw new IabException(response, "Error consuming sku " + sku);
}
}
catch (RemoteException e) {
@ -827,11 +896,11 @@ public class IabHelper {
}
ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
RESPONSE_INAPP_ITEM_LIST);
RESPONSE_INAPP_ITEM_LIST);
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
RESPONSE_INAPP_PURCHASE_DATA_LIST);
RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = ownedItems.getStringArrayList(
RESPONSE_INAPP_SIGNATURE_LIST);
RESPONSE_INAPP_SIGNATURE_LIST);
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
@ -865,7 +934,7 @@ public class IabHelper {
}
int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
throws RemoteException, JSONException {
throws RemoteException, JSONException {
logDebug("Querying SKU details.");
ArrayList<String> skuList = new ArrayList<String>();
skuList.addAll(inv.getAllOwnedSkus(itemType));
@ -882,35 +951,56 @@ public class IabHelper {
return BILLING_RESPONSE_RESULT_OK;
}
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
itemType, querySkus);
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
int response = getResponseCodeFromBundle(skuDetails);
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
return response;
// Split the sku list in blocks of no more than 20 elements.
ArrayList<ArrayList<String>> packs = new ArrayList<ArrayList<String>>();
ArrayList<String> tempList;
int n = skuList.size() / 20;
int mod = skuList.size() % 20;
for (int i = 0; i < n; i++) {
tempList = new ArrayList<String>();
for (String s : skuList.subList(i * 20, i * 20 + 20)) {
tempList.add(s);
}
else {
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
return IABHELPER_BAD_RESPONSE;
packs.add(tempList);
}
if (mod != 0) {
tempList = new ArrayList<String>();
for (String s : skuList.subList(n * 20, n * 20 + mod)) {
tempList.add(s);
}
packs.add(tempList);
}
ArrayList<String> responseList = skuDetails.getStringArrayList(
RESPONSE_GET_SKU_DETAILS_LIST);
for (ArrayList<String> skuPartList : packs) {
Bundle querySkus = new Bundle();
querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList);
Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
itemType, querySkus);
if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
int response = getResponseCodeFromBundle(skuDetails);
if (response != BILLING_RESPONSE_RESULT_OK) {
logDebug("getSkuDetails() failed: " + getResponseDesc(response));
return response;
} else {
logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
return IABHELPER_BAD_RESPONSE;
}
}
ArrayList<String> responseList = skuDetails.getStringArrayList(
RESPONSE_GET_SKU_DETAILS_LIST);
for (String thisResponse : responseList) {
SkuDetails d = new SkuDetails(itemType, thisResponse);
logDebug("Got sku details: " + d);
inv.addSkuDetails(d);
for (String thisResponse : responseList) {
SkuDetails d = new SkuDetails(itemType, thisResponse);
logDebug("Got sku details: " + d);
inv.addSkuDetails(d);
}
}
return BILLING_RESPONSE_RESULT_OK;
}
void consumeAsyncInternal(final List<Purchase> purchases,
final OnConsumeFinishedListener singleListener,
final OnConsumeMultiFinishedListener multiListener) {

@ -13,7 +13,7 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
/**
* Represents the result of an in-app billing operation.

@ -13,7 +13,7 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
import java.util.ArrayList;
import java.util.HashMap;
@ -77,7 +77,7 @@ public class Inventory {
}
/** Returns a list of all purchases. */
public List<Purchase> getAllPurchases() {
List<Purchase> getAllPurchases() {
return new ArrayList<Purchase>(mPurchaseMap.values());
}

@ -13,7 +13,7 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
import org.json.JSONException;
import org.json.JSONObject;
@ -32,6 +32,7 @@ public class Purchase {
String mToken;
String mOriginalJson;
String mSignature;
boolean mIsAutoRenewing;
public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
mItemType = itemType;
@ -44,6 +45,7 @@ public class Purchase {
mPurchaseState = o.optInt("purchaseState");
mDeveloperPayload = o.optString("developerPayload");
mToken = o.optString("token", o.optString("purchaseToken"));
mIsAutoRenewing = o.optBoolean("autoRenewing");
mSignature = signature;
}
@ -57,6 +59,7 @@ public class Purchase {
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; }

@ -13,15 +13,12 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
@ -75,7 +72,7 @@ public class Security {
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey);
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
@ -83,9 +80,6 @@ public class Security {
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
throw new IllegalArgumentException(e);
}
}
@ -99,12 +93,18 @@ public class Security {
* @return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
Signature sig;
byte[] signatureBytes;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
return false;
}
try {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
if (!sig.verify(signatureBytes)) {
Log.e(TAG, "Signature verification failed.");
return false;
}
@ -115,8 +115,6 @@ public class Security {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}

@ -13,7 +13,7 @@
* limitations under the License.
*/
package org.tasks.billing;
package com.android.vending.billing;
import org.json.JSONException;
import org.json.JSONObject;
@ -22,13 +22,15 @@ import org.json.JSONObject;
* Represents an in-app product's listing details.
*/
public class SkuDetails {
String mItemType;
String mSku;
String mType;
String mPrice;
String mTitle;
String mDescription;
String mJson;
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);
@ -41,6 +43,8 @@ public class SkuDetails {
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");
}
@ -48,6 +52,8 @@ public class SkuDetails {
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; }

@ -4,7 +4,7 @@ import com.todoroo.astrid.gtasks.GtasksPreferenceService;
import com.todoroo.astrid.gtasks.GtasksTaskListUpdater;
import com.todoroo.astrid.gtasks.sync.GtasksSyncService;
import org.tasks.billing.IabHelper;
import org.tasks.billing.PurchaseHelper;
import org.tasks.preferences.Preferences;
import org.tasks.receivers.TeslaUnreadReceiver;
@ -16,22 +16,22 @@ public class FlavorSetup {
private final GtasksSyncService gtasksSyncService;
private final GtasksPreferenceService gtasksPreferenceService;
private final TeslaUnreadReceiver teslaUnreadReceiver;
private final IabHelper iabHelper;
private final PurchaseHelper purchaseHelper;
@Inject
public FlavorSetup(Preferences preferences,
@SuppressWarnings("UnusedParameters") GtasksTaskListUpdater gtasksTaskListUpdater,
GtasksSyncService gtasksSyncService, GtasksPreferenceService gtasksPreferenceService,
TeslaUnreadReceiver teslaUnreadReceiver, IabHelper iabHelper) {
TeslaUnreadReceiver teslaUnreadReceiver, PurchaseHelper purchaseHelper) {
this.preferences = preferences;
this.gtasksSyncService = gtasksSyncService;
this.gtasksPreferenceService = gtasksPreferenceService;
this.teslaUnreadReceiver = teslaUnreadReceiver;
this.iabHelper = iabHelper;
this.purchaseHelper = purchaseHelper;
}
public void setup() {
iabHelper.startSetup();
purchaseHelper.initialize();
teslaUnreadReceiver.setEnabled(preferences.getBoolean(R.string.p_tesla_unread_enabled, false));
gtasksPreferenceService.stopOngoing(); // if sync ongoing flag was set, clear it
gtasksSyncService.initialize();

@ -1,36 +1,32 @@
package org.tasks.activities;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import org.tasks.R;
import org.tasks.billing.IabHelper;
import org.tasks.billing.IabResult;
import org.tasks.billing.Purchase;
import org.tasks.billing.PurchaseHelper;
import org.tasks.billing.PurchaseHelperCallback;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingAppCompatActivity;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import timber.log.Timber;
public class DonationActivity extends InjectingAppCompatActivity implements IabHelper.OnIabPurchaseFinishedListener {
public class DonationActivity extends InjectingAppCompatActivity implements PurchaseHelperCallback {
private static final int RC_REQUEST = 10001;
private boolean itemSelected;
@Inject DialogBuilder dialogBuilder;
@Inject IabHelper iabHelper;
@Inject PurchaseHelper purchaseHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -46,11 +42,10 @@ public class DonationActivity extends InjectingAppCompatActivity implements IabH
String value = donationValues[which];
Pattern pattern = Pattern.compile("\\$(\\d+) USD");
Matcher matcher = pattern.matcher(value);
if (matcher.matches()) {
initiateDonation(Integer.parseInt(matcher.group(1)));
} else {
error(getString(R.string.error));
}
//noinspection ResultOfMethodCallIgnored
matcher.matches();
String sku = String.format(Locale.ENGLISH, "%03d", Integer.parseInt(matcher.group(1)));
purchaseHelper.purchase(dialogBuilder, DonationActivity.this, sku, null, RC_REQUEST, DonationActivity.this);
}
})
.setOnDismissListener(new DialogInterface.OnDismissListener() {
@ -69,14 +64,6 @@ public class DonationActivity extends InjectingAppCompatActivity implements IabH
component.inject(this);
}
private void initiateDonation(int amount) {
launchPurchaseFlow(String.format("%03d", amount));
}
private void launchPurchaseFlow(String sku) {
iabHelper.launchPurchaseFlow(this, sku, RC_REQUEST, this);
}
private String[] getValues() {
List<String> values = new ArrayList<>();
for (int i = 1 ; i <= 100 ; i++) {
@ -85,30 +72,17 @@ public class DonationActivity extends InjectingAppCompatActivity implements IabH
return values.toArray(new String[values.size()]);
}
private void error(String message) {
Timber.e(message);
Toast.makeText(DonationActivity.this, message, Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
if (result.isSuccess()) {
Timber.d("Purchased %s", purchase);
} else {
error(result.getMessage());
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RC_REQUEST) {
iabHelper.handleActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
finish();
}
purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
public void purchaseCompleted(boolean success, String sku) {
finish();
}
}

@ -2,6 +2,8 @@ package org.tasks.analytics;
import android.content.Context;
import com.android.vending.billing.IabResult;
import com.android.vending.billing.Purchase;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.StandardExceptionParser;
@ -56,4 +58,13 @@ public class Tracker {
.setLabel(context.getString(event.label))
.build());
}
public void reportIabResult(IabResult result, Purchase info) {
tracker.send(new HitBuilders.EventBuilder()
.setCategory(context.getString(R.string.tracking_category_iab))
.setAction(context.getString(R.string.tracking_action_purchase))
.setLabel(info != null ? info.getSku() : "")
.setValue(result.getResponse())
.build());
}
}

@ -1,570 +0,0 @@
// Portions copyright 2002, 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;
// This code was converted from code at http://iharder.sourceforge.net/base64/
// Lots of extraneous features were removed.
/* The original code said:
* <p>
* I am placing this code in the Public Domain. Do with it as you will.
* This software comes with no guarantees or warranties but with
* plenty of well-wishing instead!
* Please visit
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
* periodically to check for updates or to contribute improvements.
* </p>
*
* @author Robert Harder
* @author rharder@usa.net
* @version 1.3
*/
/**
* Base64 converter class. This code is not a complete MIME encoder;
* it simply converts binary data to base64 data and back.
*
* <p>Note {@link CharBase64} is a GWT-compatible implementation of this
* class.
*/
public class Base64 {
/** Specify encoding (value is {@code true}). */
public final static boolean ENCODE = true;
/** Specify decoding (value is {@code false}). */
public final static boolean DECODE = false;
/** The equals sign (=) as a byte. */
private final static byte EQUALS_SIGN = (byte) '=';
/** The new line character (\n) as a byte. */
private final static byte NEW_LINE = (byte) '\n';
/**
* The 64 valid Base64 values.
*/
private final static byte[] ALPHABET =
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
(byte) '9', (byte) '+', (byte) '/'};
/**
* The 64 valid web safe Base64 values.
*/
private final static byte[] WEBSAFE_ALPHABET =
{(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
(byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
(byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
(byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
(byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
(byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
(byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
(byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
(byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
(byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
(byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
(byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
(byte) '9', (byte) '-', (byte) '_'};
/**
* Translates a Base64 value to either its 6-bit reconstruction value
* or a negative number indicating some other meaning.
**/
private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
62, // Plus sign at decimal 43
-9, -9, -9, // Decimal 44 - 46
63, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
};
/** The web safe decodabet */
private final static byte[] WEBSAFE_DECODABET =
{-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
62, // Dash '-' sign at decimal 45
-9, -9, // Decimal 46-47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
-9, -9, -9, -9, // Decimal 91-94
63, // Underscore '_' at decimal 95
-9, // Decimal 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
/* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
};
// Indicates white space in encoding
private final static byte WHITE_SPACE_ENC = -5;
// Indicates equals sign in encoding
private final static byte EQUALS_SIGN_ENC = -1;
/** Defeats instantiation. */
private Base64() {
}
/* ******** E N C O D I N G M E T H O D S ******** */
/**
* Encodes up to three bytes of the array <var>source</var>
* and writes the resulting four Base64 bytes to <var>destination</var>.
* The source and destination arrays can be manipulated
* anywhere along their length by specifying
* <var>srcOffset</var> and <var>destOffset</var>.
* This method does not check to make sure your arrays
* are large enough to accommodate <var>srcOffset</var> + 3 for
* the <var>source</var> array or <var>destOffset</var> + 4 for
* the <var>destination</var> array.
* The actual number of significant bytes in your array is
* given by <var>numSigBytes</var>.
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param numSigBytes the number of significant bytes in your array
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @param alphabet is the encoding alphabet
* @return the <var>destination</var> array
* @since 1.3
*/
private static byte[] encode3to4(byte[] source, int srcOffset,
int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
// 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
// --------| || || || | Six bit groups to index alphabet
// >>18 >>12 >> 6 >> 0 Right shift necessary
// 0x3f 0x3f 0x3f Additional AND
// Create buffer with zero-padding if there are only one or two
// significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear
// when Java treats a value as negative that is cast from a byte to an int.
int inBuff =
(numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
switch (numSigBytes) {
case 3:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
return destination;
case 2:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
case 1:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = EQUALS_SIGN;
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
default:
return destination;
} // end switch
} // end encode3to4
/**
* Encodes a byte array into Base64 notation.
* Equivalent to calling
* {@code encodeBytes(source, 0, source.length)}
*
* @param source The data to convert
* @since 1.4
*/
public static String encode(byte[] source) {
return encode(source, 0, source.length, ALPHABET, true);
}
/**
* Encodes a byte array into web safe Base64 notation.
*
* @param source The data to convert
* @param doPadding is {@code true} to pad result with '=' chars
* if it does not fall on 3 byte boundaries
*/
public static String encodeWebSafe(byte[] source, boolean doPadding) {
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
}
/**
* Encodes a byte array into Base64 notation.
*
* @param source the data to convert
* @param off offset in array where conversion should begin
* @param len length of data to convert
* @param alphabet the encoding alphabet
* @param doPadding is {@code true} to pad result with '=' chars
* if it does not fall on 3 byte boundaries
* @since 1.4
*/
public static String encode(byte[] source, int off, int len, byte[] alphabet,
boolean doPadding) {
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
int outLen = outBuff.length;
// If doPadding is false, set length to truncate '='
// padding characters
while (doPadding == false && outLen > 0) {
if (outBuff[outLen - 1] != '=') {
break;
}
outLen -= 1;
}
return new String(outBuff, 0, outLen);
}
/**
* Encodes a byte array into Base64 notation.
*
* @param source the data to convert
* @param off offset in array where conversion should begin
* @param len length of data to convert
* @param alphabet is the encoding alphabet
* @param maxLineLength maximum length of one line.
* @return the BASE64-encoded byte array
*/
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
int maxLineLength) {
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
int len43 = lenDiv3 * 4;
byte[] outBuff = new byte[len43 // Main 4:3
+ (len43 / maxLineLength)]; // New lines
int d = 0;
int e = 0;
int len2 = len - 2;
int lineLength = 0;
for (; d < len2; d += 3, e += 4) {
// The following block of code is the same as
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
// but inlined for faster encoding (~20% improvement)
int inBuff =
((source[d + off] << 24) >>> 8)
| ((source[d + 1 + off] << 24) >>> 16)
| ((source[d + 2 + off] << 24) >>> 24);
outBuff[e] = alphabet[(inBuff >>> 18)];
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
lineLength += 4;
if (lineLength == maxLineLength) {
outBuff[e + 4] = NEW_LINE;
e++;
lineLength = 0;
} // end if: end of line
} // end for: each piece of array
if (d < len) {
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
lineLength += 4;
if (lineLength == maxLineLength) {
// Add a last newline
outBuff[e + 4] = NEW_LINE;
e++;
}
e += 4;
}
assert (e == outBuff.length);
return outBuff;
}
/* ******** D E C O D I N G M E T H O D S ******** */
/**
* Decodes four bytes from array <var>source</var>
* and writes the resulting bytes (up to three of them)
* to <var>destination</var>.
* The source and destination arrays can be manipulated
* anywhere along their length by specifying
* <var>srcOffset</var> and <var>destOffset</var>.
* This method does not check to make sure your arrays
* are large enough to accommodate <var>srcOffset</var> + 4 for
* the <var>source</var> array or <var>destOffset</var> + 3 for
* the <var>destination</var> array.
* This method returns the actual number of bytes that
* were converted from the Base64 encoding.
*
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
* @param decodabet the decodabet for decoding Base64 content
* @return the number of decoded bytes converted
* @since 1.3
*/
private static int decode4to3(byte[] source, int srcOffset,
byte[] destination, int destOffset, byte[] decodabet) {
// Example: Dk==
if (source[srcOffset + 2] == EQUALS_SIGN) {
int outBuff =
((decodabet[source[srcOffset]] << 24) >>> 6)
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
destination[destOffset] = (byte) (outBuff >>> 16);
return 1;
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
// Example: DkL=
int outBuff =
((decodabet[source[srcOffset]] << 24) >>> 6)
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
destination[destOffset] = (byte) (outBuff >>> 16);
destination[destOffset + 1] = (byte) (outBuff >>> 8);
return 2;
} else {
// Example: DkLE
int outBuff =
((decodabet[source[srcOffset]] << 24) >>> 6)
| ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
| ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
destination[destOffset] = (byte) (outBuff >> 16);
destination[destOffset + 1] = (byte) (outBuff >> 8);
destination[destOffset + 2] = (byte) (outBuff);
return 3;
}
} // end decodeToBytes
/**
* Decodes data from Base64 notation.
*
* @param s the string to decode (decoded in default encoding)
* @return the decoded data
* @since 1.4
*/
public static byte[] decode(String s) throws Base64DecoderException {
byte[] bytes = s.getBytes();
return decode(bytes, 0, bytes.length);
}
/**
* Decodes data from web safe Base64 notation.
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
*
* @param s the string to decode (decoded in default encoding)
* @return the decoded data
*/
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
byte[] bytes = s.getBytes();
return decodeWebSafe(bytes, 0, bytes.length);
}
/**
* Decodes Base64 content in byte array format and returns
* the decoded byte array.
*
* @param source The Base64 encoded data
* @return decoded data
* @since 1.3
* @throws Base64DecoderException
*/
public static byte[] decode(byte[] source) throws Base64DecoderException {
return decode(source, 0, source.length);
}
/**
* Decodes web safe Base64 content in byte array format and returns
* the decoded data.
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
*
* @param source the string to decode (decoded in default encoding)
* @return the decoded data
*/
public static byte[] decodeWebSafe(byte[] source)
throws Base64DecoderException {
return decodeWebSafe(source, 0, source.length);
}
/**
* Decodes Base64 content in byte array format and returns
* the decoded byte array.
*
* @param source the Base64 encoded data
* @param off the offset of where to begin decoding
* @param len the length of characters to decode
* @return decoded data
* @since 1.3
* @throws Base64DecoderException
*/
public static byte[] decode(byte[] source, int off, int len)
throws Base64DecoderException {
return decode(source, off, len, DECODABET);
}
/**
* Decodes web safe Base64 content in byte array format and returns
* the decoded byte array.
* Web safe encoding uses '-' instead of '+', '_' instead of '/'
*
* @param source the Base64 encoded data
* @param off the offset of where to begin decoding
* @param len the length of characters to decode
* @return decoded data
*/
public static byte[] decodeWebSafe(byte[] source, int off, int len)
throws Base64DecoderException {
return decode(source, off, len, WEBSAFE_DECODABET);
}
/**
* Decodes Base64 content using the supplied decodabet and returns
* the decoded byte array.
*
* @param source the Base64 encoded data
* @param off the offset of where to begin decoding
* @param len the length of characters to decode
* @param decodabet the decodabet for decoding Base64 content
* @return decoded data
*/
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
throws Base64DecoderException {
int len34 = len * 3 / 4;
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
int outBuffPosn = 0;
byte[] b4 = new byte[4];
int b4Posn = 0;
int i = 0;
byte sbiCrop = 0;
byte sbiDecode = 0;
for (i = 0; i < len; i++) {
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
sbiDecode = decodabet[sbiCrop];
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
if (sbiDecode >= EQUALS_SIGN_ENC) {
// An equals sign (for padding) must not occur at position 0 or 1
// and must be the last byte[s] in the encoded value
if (sbiCrop == EQUALS_SIGN) {
int bytesLeft = len - i;
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
if (b4Posn == 0 || b4Posn == 1) {
throw new Base64DecoderException(
"invalid padding byte '=' at byte offset " + i);
} else if ((b4Posn == 3 && bytesLeft > 2)
|| (b4Posn == 4 && bytesLeft > 1)) {
throw new Base64DecoderException(
"padding byte '=' falsely signals end of encoded value "
+ "at offset " + i);
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
throw new Base64DecoderException(
"encoded value has invalid trailing byte");
}
break;
}
b4[b4Posn++] = sbiCrop;
if (b4Posn == 4) {
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
b4Posn = 0;
}
}
} else {
throw new Base64DecoderException("Bad Base64 input character at " + i
+ ": " + source[i + off] + "(decimal)");
}
}
// Because web safe encoding allows non padding base64 encodes, we
// need to pad the rest of the b4 buffer with equal signs when
// b4Posn != 0. There can be at most 2 equal signs at the end of
// four characters, so the b4 buffer must have two or three
// characters. This also catches the case where the input is
// padded with EQUALS_SIGN
if (b4Posn != 0) {
if (b4Posn == 1) {
throw new Base64DecoderException("single trailing character at offset "
+ (len - 1));
}
b4[b4Posn++] = EQUALS_SIGN;
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
}
byte[] out = new byte[outBuffPosn];
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
return out;
}
}

@ -1,32 +0,0 @@
// Copyright 2002, 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;
/**
* Exception thrown when encountering an invalid Base64 input character.
*
* @author nelson
*/
public class Base64DecoderException extends Exception {
public Base64DecoderException() {
super();
}
public Base64DecoderException(String s) {
super(s);
}
private static final long serialVersionUID = 1L;
}

@ -0,0 +1,198 @@
package org.tasks.billing;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.widget.Toast;
import com.android.vending.billing.IabBroadcastReceiver;
import com.android.vending.billing.IabHelper;
import com.android.vending.billing.IabResult;
import com.android.vending.billing.Inventory;
import com.android.vending.billing.Purchase;
import com.google.common.base.Strings;
import org.tasks.Broadcaster;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.analytics.Tracker;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import timber.log.Timber;
import static com.todoroo.andlib.utility.AndroidUtilities.isAppInstalled;
@Singleton
public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener, IabHelper.QueryInventoryFinishedListener, IabBroadcastReceiver.IabBroadcastListener {
private final IabHelper iabHelper;
private final Context context;
private final Preferences preferences;
private final Tracker tracker;
private final Broadcaster broadcaster;
private Inventory inventory;
private PurchaseHelperCallback activityResultCallback;
@Inject
public PurchaseHelper(@ForApplication Context context, Preferences preferences, Tracker tracker,
Broadcaster broadcaster) {
this.context = context;
this.preferences = preferences;
this.tracker = tracker;
this.broadcaster = broadcaster;
iabHelper = new IabHelper(context, context.getString(R.string.gp_key));
}
public void initialize() {
iabHelper.startSetup(this);
context.registerReceiver(new IabBroadcastReceiver(this), new IntentFilter(IabBroadcastReceiver.ACTION));
}
@Override
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
iabHelper.queryInventoryAsync(this);
} else {
Timber.e("in-app billing setup failed: %s", result.getMessage());
}
}
@Override
public void onQueryInventoryFinished(final IabResult result, Inventory inv) {
if (result.isSuccess()) {
inventory = inv;
checkPurchase(R.string.sku_tasker, R.string.p_purchased_tasker);
checkPurchase(R.string.sku_tesla_unread, R.string.p_purchased_tesla_unread);
checkPurchase(R.string.sku_dashclock, R.string.p_purchased_dashclock);
} else {
Timber.e("in-app billing inventory query failed: %s", result.getMessage());
}
}
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);
}
}
@Override
public void receivedBroadcast() {
try {
iabHelper.queryInventoryAsync(this);
} catch(IllegalStateException e) {
tracker.reportException(e);
}
}
public void purchase(DialogBuilder dialogBuilder, final Activity activity, final String sku, final String pref, final int requestCode, final PurchaseHelperCallback callback) {
if (activity.getString(R.string.sku_tasker).equals(sku) && isAppInstalled(activity, "org.tasks.locale")) {
dialogBuilder.newMessageDialog(R.string.tasker_message)
.setCancelable(false)
.setPositiveButton(R.string.buy, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
launchPurchaseFlow(activity, sku, pref, requestCode, callback);
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
callback.purchaseCompleted(false, sku);
}
})
.show();
} else {
launchPurchaseFlow(activity, sku, pref, requestCode, callback);
}
}
public void consumePurchases() {
if (BuildConfig.DEBUG) {
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 teslaUnread = inventory.getPurchase(context.getString(R.string.sku_tesla_unread));
if (tasker != null) {
purchases.add(tasker);
}
if (dashclock != null) {
purchases.add(dashclock);
}
if (teslaUnread != null) {
purchases.add(teslaUnread);
}
iabHelper.consumeAsync(purchases, new IabHelper.OnConsumeMultiFinishedListener() {
@Override
public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results) {
for (int i = 0 ; i < purchases.size() ; i++) {
Purchase purchase = purchases.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(teslaUnread)) {
preferences.setBoolean(R.string.p_purchased_tesla_unread, false);
preferences.setBoolean(R.string.p_tesla_unread_enabled, 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);
}
}
}
});
}
}
private void launchPurchaseFlow(final Activity activity, final String sku, final String pref, int requestCode, PurchaseHelperCallback callback) {
try {
iabHelper.launchPurchaseFlow(activity, sku, requestCode, new IabHelper.OnIabPurchaseFinishedListener() {
@Override
public void onIabPurchaseFinished(IabResult result, Purchase info) {
Timber.d(result.toString());
tracker.reportIabResult(result, info);
if (result.isSuccess()) {
if (!Strings.isNullOrEmpty(pref)) {
preferences.setBoolean(pref, true);
broadcaster.refresh();
}
} else if (result.getResponse() != IabHelper.BILLING_RESPONSE_RESULT_USER_CANCELED &&
result.getResponse() != IabHelper.IABHELPER_USER_CANCELLED) {
Toast.makeText(activity, result.getMessage(), Toast.LENGTH_LONG).show();
}
activityResultCallback.purchaseCompleted(result.isSuccess(), sku);
}
});
} catch (IllegalStateException e) {
tracker.reportException(e);
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
}
}
public void handleActivityResult(PurchaseHelperCallback callback, int requestCode, int resultCode, Intent data) {
this.activityResultCallback = callback;
iabHelper.handleActivityResult(requestCode, resultCode, data);
}
}

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

@ -0,0 +1,100 @@
package org.tasks.dashclock;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import com.google.android.apps.dashclock.api.ExtensionData;
import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import org.tasks.R;
import org.tasks.injection.InjectingDashClockExtension;
import org.tasks.injection.ServiceComponent;
import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.preferences.Preferences;
import java.util.List;
import javax.inject.Inject;
public class DashClockExtension extends InjectingDashClockExtension {
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject TaskDao taskDao;
@Inject Preferences preferences;
private final BroadcastReceiver refreshReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
refresh();
}
};
@Override
public void onCreate() {
super.onCreate();
registerReceiver(refreshReceiver, new IntentFilter(AstridApiConstants.BROADCAST_EVENT_REFRESH));
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(refreshReceiver);
}
@Override
protected void onUpdateData(int i) {
refresh();
}
@Override
protected void inject(ServiceComponent component) {
component.inject(this);
}
private void refresh() {
if (preferences.hasPurchase(R.string.p_purchased_dashclock)) {
final String filterPreference = preferences.getStringValue(R.string.p_dashclock_filter);
Filter filter = defaultFilterProvider.getFilterFromPreference(filterPreference);
int count = taskDao.count(filter);
if (count == 0) {
publishUpdate(null);
} else {
ExtensionData extensionData = new ExtensionData()
.visible(true)
.icon(R.drawable.ic_check_white_24dp)
.status(Integer.toString(count))
.expandedTitle(getString(R.string.task_count, count))
.expandedBody(filter.listingTitle)
.clickIntent(new Intent(this, TaskListActivity.class) {{
putExtra(TaskListActivity.LOAD_FILTER, filterPreference);
}});
if (count == 1) {
List<Task> tasks = taskDao.query(filter);
if (!tasks.isEmpty()) {
extensionData.expandedTitle(tasks.get(0).getTitle());
}
}
publishUpdate(extensionData);
}
} else {
publishUpdate(new ExtensionData()
.visible(true)
.icon(R.drawable.ic_check_white_24dp)
.status(getString(R.string.buy))
.expandedTitle(getString(R.string.buy_dashclock_extension))
.clickIntent(new Intent(this, DashClockSettings.class) {{
setFlags(FLAG_ACTIVITY_NEW_TASK);
}}));
}
}
}

@ -0,0 +1,107 @@
package org.tasks.dashclock;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import com.todoroo.astrid.api.Filter;
import org.tasks.Broadcaster;
import org.tasks.R;
import org.tasks.activities.FilterSelectionActivity;
import org.tasks.billing.PurchaseHelper;
import org.tasks.billing.PurchaseHelperCallback;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.preferences.Preferences;
import javax.inject.Inject;
public class DashClockSettings extends InjectingPreferenceActivity implements PurchaseHelperCallback {
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;
@Inject Preferences preferences;
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject Broadcaster broadcaster;
@Inject PurchaseHelper purchaseHelper;
@Inject DialogBuilder dialogBuilder;
private boolean purchaseInitiated;
@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)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivityForResult(new Intent(DashClockSettings.this, FilterSelectionActivity.class) {{
putExtra(FilterSelectionActivity.EXTRA_RETURN_FILTER, true);
}}, REQUEST_SELECT_FILTER);
return false;
}
});
refreshPreferences();
if (!preferences.hasPurchase(R.string.p_purchased_dashclock) && !purchaseInitiated) {
purchaseHelper.purchase(dialogBuilder, this, getString(R.string.sku_dashclock), getString(R.string.p_purchased_dashclock), REQUEST_PURCHASE, this);
purchaseInitiated = true;
}
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_SELECT_FILTER) {
if (resultCode == Activity.RESULT_OK) {
Filter filter = data.getParcelableExtra(FilterSelectionActivity.EXTRA_FILTER);
String filterPreference = defaultFilterProvider.getFilterPreferenceValue(filter);
preferences.setString(R.string.p_dashclock_filter, filterPreference);
refreshPreferences();
broadcaster.refresh();
}
} else if (requestCode == REQUEST_PURCHASE) {
purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
} 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) {
broadcaster.refresh();
} else {
finish();
}
}
private void refreshPreferences() {
Filter filter = defaultFilterProvider.getFilterFromPreference(R.string.p_dashclock_filter);
findPreference(getString(R.string.p_dashclock_filter)).setSummary(filter.listingTitle);
}
}

@ -3,6 +3,7 @@ package org.tasks.injection;
import com.todoroo.astrid.gtasks.GtasksPreferences;
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity;
import org.tasks.dashclock.DashClockSettings;
import org.tasks.locale.ui.activity.TaskerSettingsActivity;
import javax.inject.Singleton;
@ -17,4 +18,6 @@ public interface ActivityComponent extends BaseActivityComponent {
void inject(TaskerSettingsActivity taskerSettingsActivity);
void inject(GtasksLoginActivity gtasksLoginActivity);
void inject(DashClockSettings dashClockSettings);
}

@ -0,0 +1,16 @@
package org.tasks.injection;
import com.google.android.apps.dashclock.api.DashClockExtension;
public abstract class InjectingDashClockExtension extends DashClockExtension {
@Override
public void onCreate() {
super.onCreate();
inject(((InjectingApplication) getApplication())
.getComponent()
.plus(new ServiceModule()));
}
protected abstract void inject(ServiceComponent component);
}

@ -0,0 +1,10 @@
package org.tasks.injection;
import org.tasks.dashclock.DashClockExtension;
import dagger.Subcomponent;
@Subcomponent(modules = ServiceModule.class)
public interface ServiceComponent extends BaseServiceComponent {
void inject(DashClockExtension dashClockExtension);
}

@ -16,11 +16,12 @@ import com.todoroo.astrid.api.Filter;
import org.tasks.R;
import org.tasks.activities.FilterSelectionActivity;
import org.tasks.billing.PurchaseHelper;
import org.tasks.billing.PurchaseHelperCallback;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent;
import org.tasks.locale.bundle.PluginBundleValues;
import org.tasks.preferences.ActivityPreferences;
import org.tasks.preferences.BasicPreferences;
import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.ui.MenuColorizer;
@ -32,19 +33,23 @@ import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompatActivity {
public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompatActivity implements PurchaseHelperCallback {
private static final int REQUEST_SELECT_FILTER = 10124;
private static final int REQUEST_PURCHASE = 10125;
private static final String EXTRA_FILTER = "extra_filter";
private static final String EXTRA_PURCHASE_IN_PROGRESS = "extra_purchase_in_progress";
@Bind(R.id.toolbar) Toolbar toolbar;
@Inject ActivityPreferences preferences;
@Inject DialogBuilder dialogBuilder;
@Inject DefaultFilterProvider defaultFilterProvider;
@Inject PurchaseHelper purchaseHelper;
@Inject DialogBuilder dialogBuilder;
private Bundle previousBundle;
private Filter filter;
private boolean purchaseInProgress;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@ -56,6 +61,7 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
if (savedInstanceState != null) {
previousBundle = savedInstanceState.getParcelable(PluginBundleValues.BUNDLE_EXTRA_PREVIOUS_BUNDLE);
filter = savedInstanceState.getParcelable(EXTRA_FILTER);
purchaseInProgress = savedInstanceState.getBoolean(EXTRA_PURCHASE_IN_PROGRESS);
} else {
filter = defaultFilterProvider.getDefaultFilter();
}
@ -71,22 +77,8 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
supportActionBar.setDisplayShowTitleEnabled(false);
}
if (!preferences.getBoolean(R.string.p_tasker_enabled, false)) {
dialogBuilder.newMessageDialog(R.string.tasker_disabled_warning)
.setPositiveButton(R.string.TLA_menu_settings, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startActivity(new Intent(TaskerSettingsActivity.this, BasicPreferences.class));
cancel();
}
})
.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
cancel();
}
})
.show();
if (!preferences.hasPurchase(R.string.p_purchased_tasker) && !purchaseInProgress) {
purchaseHelper.purchase(dialogBuilder, this, getString(R.string.sku_tasker), getString(R.string.p_purchased_tasker), REQUEST_PURCHASE, this);
}
}
@ -204,10 +196,11 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
filter = data.getParcelableExtra(FilterSelectionActivity.EXTRA_FILTER);
updateView();
}
return;
} else if (requestCode == REQUEST_PURCHASE) {
purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
@ -215,6 +208,7 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
super.onSaveInstanceState(outState);
outState.putParcelable(PluginBundleValues.BUNDLE_EXTRA_PREVIOUS_BUNDLE, previousBundle);
outState.putParcelable(EXTRA_FILTER, filter);
outState.putBoolean(EXTRA_PURCHASE_IN_PROGRESS, purchaseInProgress);
}
private void updateView() {
@ -226,4 +220,11 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
public void inject(ActivityComponent component) {
component.inject(this);
}
@Override
public void purchaseCompleted(boolean success, String sku) {
if (!success) {
cancel();
}
}
}

@ -3,38 +3,41 @@ package org.tasks.preferences;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.SwitchPreference;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.analytics.Tracker;
import org.tasks.billing.IabHelper;
import org.tasks.billing.IabResult;
import org.tasks.billing.Purchase;
import org.tasks.billing.PurchaseHelper;
import org.tasks.billing.PurchaseHelperCallback;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent;
import org.tasks.receivers.TeslaUnreadReceiver;
import javax.inject.Inject;
public class BasicPreferences extends BaseBasicPreferences implements IabHelper.OnIabPurchaseFinishedListener {
import timber.log.Timber;
private static final int REQUEST_PURCHASE_TESLA_UNREAD = 10002;
private static final int REQUEST_PURCHASE_TASKER = 10003;
public class BasicPreferences extends BaseBasicPreferences implements PurchaseHelperCallback {
private static final int REQUEST_PURCHASE = 10005;
@Inject Tracker tracker;
@Inject IabHelper iabHelper;
@Inject TeslaUnreadReceiver teslaUnreadReceiver;
@Inject Preferences preferences;
@Inject PurchaseHelper purchaseHelper;
@Inject DialogBuilder dialogBuilder;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
findPreference(getString(R.string.p_tesla_unread_enabled)).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
getPref(R.string.p_tesla_unread_enabled).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue != null) {
if ((boolean) newValue && !preferences.getBoolean(R.string.p_purchased_tesla_unread, BuildConfig.DEBUG)) {
iabHelper.launchPurchaseFlow(BasicPreferences.this, getString(R.string.sku_tesla_unread), REQUEST_PURCHASE_TESLA_UNREAD, BasicPreferences.this);
if ((boolean) newValue && !preferences.hasPurchase(R.string.p_purchased_tesla_unread)) {
purchaseHelper.purchase(dialogBuilder, BasicPreferences.this, getString(R.string.sku_tesla_unread), getString(R.string.p_purchased_tesla_unread), REQUEST_PURCHASE, BasicPreferences.this);
} else {
teslaUnreadReceiver.setEnabled((boolean) newValue);
return true;
@ -44,15 +47,21 @@ public class BasicPreferences extends BaseBasicPreferences implements IabHelper.
}
});
findPreference(getString(R.string.p_tasker_enabled)).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
getPref(R.string.p_purchased_tasker).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue != null) {
if ((boolean) newValue && !preferences.getBoolean(R.string.p_purchased_tasker, BuildConfig.DEBUG)) {
iabHelper.launchPurchaseFlow(BasicPreferences.this, getString(R.string.sku_tasker), REQUEST_PURCHASE_TASKER, BasicPreferences.this);
} else {
return true;
}
if (newValue != null && (boolean) newValue && !preferences.hasPurchase(R.string.p_purchased_tasker)) {
purchaseHelper.purchase(dialogBuilder, BasicPreferences.this, getString(R.string.sku_tasker), getString(R.string.p_purchased_tasker), REQUEST_PURCHASE, BasicPreferences.this);
}
return false;
}
});
getPref(R.string.p_purchased_dashclock).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue != null && (boolean) newValue && !preferences.hasPurchase(R.string.p_purchased_dashclock)) {
purchaseHelper.purchase(dialogBuilder, BasicPreferences.this, getString(R.string.sku_dashclock), getString(R.string.p_purchased_dashclock), REQUEST_PURCHASE, BasicPreferences.this);
}
return false;
}
@ -68,21 +77,15 @@ public class BasicPreferences extends BaseBasicPreferences implements IabHelper.
return false;
}
});
}
@Override
public void onIabPurchaseFinished(IabResult result, final Purchase info) {
if (result.isSuccess()) {
runOnUiThread(new Runnable() {
if (BuildConfig.DEBUG) {
addPreferencesFromResource(R.xml.preferences_debug);
findPreference(getString(R.string.debug_consume_purchases)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public void run() {
if (info.getSku().equals(getString(R.string.sku_tasker))) {
preferences.setBoolean(R.string.p_purchased_tasker, true);
findPreference(getString(R.string.p_tasker_enabled)).setEnabled(true);
} else if (info.getSku().equals(getString(R.string.sku_tesla_unread))) {
preferences.setBoolean(R.string.p_purchased_tesla_unread, true);
findPreference(getString(R.string.p_tesla_unread_enabled)).setEnabled(true);
}
public boolean onPreferenceClick(Preference preference) {
purchaseHelper.consumePurchases();
return true;
}
});
}
@ -90,8 +93,8 @@ public class BasicPreferences extends BaseBasicPreferences implements IabHelper.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_PURCHASE_TASKER || requestCode == REQUEST_PURCHASE_TESLA_UNREAD) {
iabHelper.handleActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_PURCHASE) {
purchaseHelper.handleActivityResult(this, requestCode, resultCode, data);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
@ -101,4 +104,23 @@ public class BasicPreferences extends BaseBasicPreferences implements IabHelper.
public void inject(ActivityComponent component) {
component.inject(this);
}
@Override
public void purchaseCompleted(boolean success, String sku) {
if (success) {
if (getString(R.string.sku_tasker).equals(sku)) {
getPref(R.string.p_purchased_tasker).setChecked(true);
} else if (getString(R.string.sku_tesla_unread).equals(sku)) {
getPref(R.string.p_tesla_unread_enabled).setChecked(true);
} else if (getString(R.string.sku_dashclock).equals(sku)) {
getPref(R.string.p_purchased_dashclock).setChecked(true);
} else {
Timber.e("Unhandled sku: %s", sku);
}
}
}
private SwitchPreference getPref(int resId) {
return (SwitchPreference) findPreference(getString(resId));
}
}

@ -6,12 +6,9 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import org.tasks.Broadcaster;
import org.tasks.BuildConfig;
@ -55,8 +52,7 @@ public class TeslaUnreadReceiver extends InjectingBroadcastReceiver {
super.onReceive(context, intent);
Filter defaultFilter = defaultFilterProvider.getDefaultFilter();
String query = PermaSql.replacePlaceholders(defaultFilter.getSqlQuery());
publishCount(taskDao.count(Query.select(Task.ID).withQueryTemplate(query)));
publishCount(taskDao.count(defaultFilter));
}
@Override

@ -3,4 +3,7 @@
<string name="gp_key">MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk8mXRE3dDXwtinUILCEzKjov2rxs3kZbLRzNrcjFWXpG9OEsUzRGLzqEN+WwibVuMRpZLj/+IxbU2sJWq/M0q+90rOhmXn46ZPeNyr77IqX2pWKIAWpzBoWq/mshRwtm9m1FIiGdBNlXrhSE7u3TGB5FuEuuSqKWvWzxeqQ7fHmlM04Lqrh1mN3FaMne8rWv+DWVHDbLrtnXBuC36glOAj17HxrzaE2v6Pv7Df3QefJ3rM1+0fAp/5jNInaP0qHAlG8WTbUmDShQ5kG3urbv3HLByyx6TSqhmNudXUK/6TusvIj50OptAG7x+UFYf956dD3diXhY3yoICvyFWx1sNwIDAQAB</string>
<string name="sku_tasker">tasker</string>
<string name="sku_tesla_unread">tesla_unread</string>
<string name="sku_dashclock">dashclock</string>
<string name="p_dashclock_filter">dashclock_filter</string>
<string name="tasker_message">If you purchased the stand-alone Tasker plugin please send your Google Play Store transaction ID to support@tasks.org and you will receive a promo code for this in-app purchase. You can find the transaction ID in your Google Play Store order confirmation e-mail or by visiting wallet.google.com</string>
</resources>

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="select_filter">Select Filter</string>
<string name="filter">Filter</string>
<string name="discard_changes">Discard changes?</string>
<string name="debug_consume_purchases">Consume purchases</string>
<string name="debug">Debug</string>
</resources>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<com.todoroo.astrid.ui.MultilinePreference
android:key="@string/p_dashclock_filter"
android:title="@string/filter" />
</PreferenceScreen>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/debug">
<Preference
android:title="@string/debug_consume_purchases"
android:key="@string/debug_consume_purchases" />
</PreferenceCategory>
</PreferenceScreen>

@ -401,4 +401,13 @@ public class AndroidUtilities {
}
return extension;
}
public static boolean isAppInstalled(Context context, String packageName) {
try {
context.getPackageManager().getPackageInfo(packageName, 0);
return true;
} catch (Exception ignored) {
return false;
}
}
}

@ -79,6 +79,7 @@ public class TaskListActivity extends InjectingAppCompatActivity implements
public static final String TOKEN_CREATE_NEW_LIST_NAME = "newListName"; //$NON-NLS-1$
public static final String OPEN_FILTER = "open_filter"; //$NON-NLS-1$
public static final String LOAD_FILTER = "load_filter";
public static final String OPEN_TASK = "open_task"; //$NON-NLS-1$
/**
@ -127,6 +128,10 @@ public class TaskListActivity extends InjectingAppCompatActivity implements
Filter filter = intent.getParcelableExtra(OPEN_FILTER);
intent.removeExtra(OPEN_FILTER);
taskListFragment = newTaskListFragment(filter);
} else if (intent.hasExtra(LOAD_FILTER)) {
Filter filter = defaultFilterProvider.getFilterFromPreference(intent.getStringExtra(LOAD_FILTER));
intent.removeExtra(LOAD_FILTER);
taskListFragment = newTaskListFragment(filter);
} else {
taskListFragment = getTaskListFragment();
if (taskListFragment == null) {

@ -17,6 +17,8 @@ import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.PermaSql;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.Task;
@ -79,10 +81,20 @@ public class TaskDao {
return dao.fetch(id, properties);
}
public int count(Filter filter) {
String query = PermaSql.replacePlaceholders(filter.getSqlQuery());
return count(Query.select(Task.ID).withQueryTemplate(query));
}
public int count(Query query) {
return dao.count(query);
}
public List<Task> query(Filter filter) {
String query = PermaSql.replacePlaceholders(filter.getSqlQuery());
return dao.toList(Query.select(Task.PROPERTIES).withQueryTemplate(query));
}
public TodorooCursor<Task> rawQuery(String selection, String[] selectionArgs, Property.LongProperty id) {
return dao.rawQuery(selection, selectionArgs, id);
}

@ -18,5 +18,5 @@ public interface ApplicationComponent {
IntentServiceComponent plus(IntentServiceModule module);
RemoteViewsServiceComponent plus(RemoteViewsServiceModule remoteViewsServiceModule);
ServiceComponent plus(ServiceModule serviceModule);
}

@ -4,7 +4,6 @@ import org.tasks.widget.ScrollableWidgetUpdateService;
import dagger.Subcomponent;
@Subcomponent(modules = RemoteViewsServiceModule.class)
public interface RemoteViewsServiceComponent {
public interface BaseServiceComponent {
void inject(ScrollableWidgetUpdateService scrollableWidgetUpdateService);
}

@ -9,8 +9,8 @@ public abstract class InjectingRemoteViewsService extends RemoteViewsService {
inject(((InjectingApplication) getApplication())
.getComponent()
.plus(new RemoteViewsServiceModule()));
.plus(new ServiceModule()));
}
protected abstract void inject(RemoteViewsServiceComponent component);
protected abstract void inject(ServiceComponent component);
}

@ -3,5 +3,5 @@ package org.tasks.injection;
import dagger.Module;
@Module
public class RemoteViewsServiceModule {
public class ServiceModule {
}

@ -64,7 +64,11 @@ public class DefaultFilterProvider {
}
public Filter getDefaultFilter() {
return getFilterFromPreference(preferences.getStringValue(R.string.p_default_list));
return getFilterFromPreference(R.string.p_default_list);
}
public Filter getFilterFromPreference(int resId) {
return getFilterFromPreference(preferences.getStringValue(resId));
}
public Filter getFilterFromPreference(String preferenceValue) {

@ -14,6 +14,7 @@ import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskAttachment;
import com.todoroo.astrid.widget.WidgetConfigActivity;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.injection.ForApplication;
import org.tasks.time.DateTime;
@ -201,6 +202,10 @@ 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);
}

@ -10,7 +10,7 @@ import com.todoroo.astrid.service.TaskService;
import com.todoroo.astrid.subtasks.SubtasksHelper;
import org.tasks.injection.InjectingRemoteViewsService;
import org.tasks.injection.RemoteViewsServiceComponent;
import org.tasks.injection.ServiceComponent;
import org.tasks.preferences.Preferences;
import javax.inject.Inject;
@ -50,7 +50,7 @@ public class ScrollableWidgetUpdateService extends InjectingRemoteViewsService {
}
@Override
protected void inject(RemoteViewsServiceComponent component) {
protected void inject(ServiceComponent component) {
component.inject(this);
}
}

@ -283,10 +283,15 @@
<string name="p_default_list">default_list</string>
<string name="tracking_category_preferences">Preferences</string>
<string name="tracking_category_iab">IAB</string>
<string name="tracking_action_set">Set</string>
<string name="tracking_action_purchase">Purchase</string>
<string name="p_tesla_unread_enabled">tesla_unread_enabled</string>
<string name="p_tasker_enabled">tasker_enabled</string>
<string name="p_purchased_tesla_unread">purchased_tesla_unread</string>
<string name="p_purchased_tasker">purchased_tasker</string>
<string name="p_purchased_dashclock">purchased_dashclock</string>
<string name="tesla_unread">TeslaUnread</string>
<string name="tasker_locale">Tasker/Locale</string>
<string name="dashclock">DashClock extension</string>
</resources>

@ -796,7 +796,6 @@ File %1$s contained %2$s.\n\n
<string name="rate_tasks">Rate Tasks</string>
<string name="quiet_hours_summary">No reminders during quiet hours</string>
<string name="TLA_menu_donate">Donate</string>
<string name="error">Error</string>
<string name="select_amount">Select amount</string>
<string name="notification_actions">Notification Actions</string>
<string name="notification_actions_summary">Show snooze and complete actions in notification</string>
@ -863,7 +862,13 @@ File %1$s contained %2$s.\n\n
<string name="tesla_unread_description">Display a badge for the number of active tasks in your default list. Requires TeslaUnread for Nova Launcher</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="tasker_disabled_warning">Enable Tasker integration in Tasks settings</string>
<string name="dashclock_description">Display a count of active tasks</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="select_filter">Select Filter</string>
<string name="filter">Filter</string>
<string-array name="sync_SPr_interval_entries">
<!-- sync_SPr_interval_entries: Synchronization Intervals -->

@ -18,15 +18,24 @@
</Preference>
<SwitchPreference
android:key="@string/p_tesla_unread_enabled"
android:title="TeslaUnread"
android:summary="@string/tesla_unread_description"/>
android:key="@string/p_purchased_dashclock"
android:title="@string/dashclock"
android:dependency="@string/p_purchased_dashclock"
android:disableDependentsState="true"
android:summary="@string/dashclock_purchase_description" />
<SwitchPreference
android:key="@string/p_tasker_enabled"
android:title="Tasker/Locale"
android:key="@string/p_purchased_tasker"
android:title="@string/tasker_locale"
android:dependency="@string/p_purchased_tasker"
android:disableDependentsState="true"
android:summary="@string/tasker_description"/>
<SwitchPreference
android:key="@string/p_tesla_unread_enabled"
android:title="@string/tesla_unread"
android:summary="@string/tesla_unread_description"/>
</PreferenceCategory>
</PreferenceScreen>
Loading…
Cancel
Save