Fix some IAB lifecycle issues

pull/384/head
Alex Baker 9 years ago
parent 927fbc7205
commit 7a3796527c

@ -4,6 +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.InventoryHelper;
import org.tasks.billing.PurchaseHelper;
import org.tasks.preferences.Preferences;
import org.tasks.receivers.TeslaUnreadReceiver;
@ -16,22 +17,23 @@ public class FlavorSetup {
private final GtasksSyncService gtasksSyncService;
private final GtasksPreferenceService gtasksPreferenceService;
private final TeslaUnreadReceiver teslaUnreadReceiver;
private final PurchaseHelper purchaseHelper;
private final InventoryHelper inventoryHelper;
@Inject
public FlavorSetup(Preferences preferences,
@SuppressWarnings("UnusedParameters") GtasksTaskListUpdater gtasksTaskListUpdater,
@SuppressWarnings("UnusedParameters") PurchaseHelper purchaseHelper,
GtasksSyncService gtasksSyncService, GtasksPreferenceService gtasksPreferenceService,
TeslaUnreadReceiver teslaUnreadReceiver, PurchaseHelper purchaseHelper) {
TeslaUnreadReceiver teslaUnreadReceiver, InventoryHelper inventoryHelper) {
this.preferences = preferences;
this.gtasksSyncService = gtasksSyncService;
this.gtasksPreferenceService = gtasksPreferenceService;
this.teslaUnreadReceiver = teslaUnreadReceiver;
this.purchaseHelper = purchaseHelper;
this.inventoryHelper = inventoryHelper;
}
public void setup() {
purchaseHelper.initialize();
inventoryHelper.initialize();
teslaUnreadReceiver.setEnabled(preferences.getBoolean(R.string.p_tesla_unread_enabled, false));
gtasksPreferenceService.stopOngoing(); // if sync ongoing flag was set, clear it
gtasksSyncService.initialize();

@ -81,6 +81,15 @@ public class DonationActivity extends InjectingAppCompatActivity implements Purc
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
purchaseHelper.disposeIabHelper();
}
}
@Override
public void purchaseCompleted(boolean success, String sku) {
finish();

@ -0,0 +1,102 @@
package org.tasks.billing;
import android.content.Context;
import android.content.IntentFilter;
import com.android.vending.billing.IabBroadcastReceiver;
import com.android.vending.billing.IabHelper;
import com.android.vending.billing.IabResult;
import com.android.vending.billing.Inventory;
import com.android.vending.billing.Purchase;
import org.tasks.Broadcaster;
import org.tasks.R;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import javax.inject.Inject;
import javax.inject.Singleton;
import timber.log.Timber;
@Singleton
public class InventoryHelper implements IabBroadcastReceiver.IabBroadcastListener {
private final Context context;
private final Preferences preferences;
private final Broadcaster broadcaster;
private Inventory inventory;
@Inject
public InventoryHelper(@ForApplication Context context, Preferences preferences, Broadcaster broadcaster) {
this.context = context;
this.preferences = preferences;
this.broadcaster = broadcaster;
}
public void initialize() {
context.registerReceiver(new IabBroadcastReceiver(this), new IntentFilter(IabBroadcastReceiver.ACTION));
refreshInventory();
}
public void refreshInventory() {
final IabHelper helper = new IabHelper(context, context.getString(R.string.gp_key));
helper.startSetup(getSetupListener(helper));
}
private IabHelper.OnIabSetupFinishedListener getSetupListener(final IabHelper helper) {
return new IabHelper.OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
helper.queryInventoryAsync(getQueryListener(helper));
} else {
Timber.e("setup failed: %s", result.getMessage());
helper.dispose();
}
}
};
}
private IabHelper.QueryInventoryFinishedListener getQueryListener(final IabHelper helper) {
return new IabHelper.QueryInventoryFinishedListener() {
@Override
public void onQueryInventoryFinished(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);
broadcaster.refresh();
} else {
Timber.e("query inventory failed: %s", result.getMessage());
}
helper.dispose();
}
};
}
@Override
public void receivedBroadcast() {
refreshInventory();
}
private void checkPurchase(int skuRes, final int prefRes) {
final String sku = context.getString(skuRes);
if (inventory.hasPurchase(sku)) {
Timber.d("Found purchase: %s", sku);
preferences.setBoolean(prefRes, true);
} else {
Timber.d("No purchase: %s", sku);
}
}
public void erasePurchase(String sku) {
inventory.erasePurchase(sku);
}
public Purchase getPurchase(String sku) {
return inventory.getPurchase(sku);
}
}

@ -4,13 +4,10 @@ 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;
@ -33,73 +30,36 @@ import timber.log.Timber;
import static com.todoroo.andlib.utility.AndroidUtilities.isAppInstalled;
@Singleton
public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener, IabHelper.QueryInventoryFinishedListener, IabBroadcastReceiver.IabBroadcastListener {
public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener {
private final IabHelper iabHelper;
private final Context context;
private final Preferences preferences;
private final Tracker tracker;
private final Broadcaster broadcaster;
private final InventoryHelper inventory;
private Inventory inventory;
private PurchaseHelperCallback activityResultCallback;
private IabHelper iabHelper;
@Inject
public PurchaseHelper(@ForApplication Context context, Preferences preferences, Tracker tracker,
Broadcaster broadcaster) {
Broadcaster broadcaster, InventoryHelper inventory) {
Timber.d("Injecting new PurchaseHelper");
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));
this.inventory = inventory;
}
@Override
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
iabHelper.queryInventoryAsync(this);
} else {
if (result.isFailure()) {
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) {
public boolean 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)
@ -116,14 +76,16 @@ public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener, Iab
}
})
.show();
return false;
} else {
launchPurchaseFlow(activity, sku, pref, requestCode, callback);
return true;
}
}
public void consumePurchases() {
if (BuildConfig.DEBUG) {
List<Purchase> purchases = new ArrayList<>();
final List<Purchase> purchases = new ArrayList<>();
final Purchase tasker = inventory.getPurchase(context.getString(R.string.sku_tasker));
final Purchase dashclock = inventory.getPurchase(context.getString(R.string.sku_dashclock));
final Purchase teslaUnread = inventory.getPurchase(context.getString(R.string.sku_tesla_unread));
@ -136,63 +98,110 @@ public class PurchaseHelper implements IabHelper.OnIabSetupFinishedListener, Iab
if (teslaUnread != null) {
purchases.add(teslaUnread);
}
iabHelper.consumeAsync(purchases, new IabHelper.OnConsumeMultiFinishedListener() {
final IabHelper iabHelper = new IabHelper(context, context.getString(R.string.gp_key));
iabHelper.enableDebugLogging(true);
iabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
@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);
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
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);
}
}
iabHelper.dispose();
}
inventory.erasePurchase(purchase.getSku());
Timber.d("Consumed %s", purchase);
} else {
Timber.e("Consume failed: %s, %s", purchase, iabResult);
}
});
} else {
Timber.e("setup failed: %s", result.getMessage());
iabHelper.dispose();
}
}
});
}
}
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);
private void launchPurchaseFlow(final Activity activity, final String sku, final String pref, final int requestCode, final PurchaseHelperCallback callback) {
if (iabHelper != null) {
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
return;
}
iabHelper = new IabHelper(context, context.getString(R.string.gp_key));
iabHelper.enableDebugLogging(BuildConfig.DEBUG);
Timber.d("%s: startSetup", iabHelper);
iabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
@Override
public void onIabSetupFinished(IabResult result) {
if (result.isSuccess()) {
try {
Timber.d("%s: launchPurchaseFlow for %s", iabHelper, sku);
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();
}
inventory.refreshInventory();
} 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);
disposeIabHelper();
}
});
} catch (IllegalStateException e) {
tracker.reportException(e);
Toast.makeText(activity, R.string.billing_service_busy, Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
disposeIabHelper();
}
} else {
Timber.e(result.toString());
Toast.makeText(activity, result.getMessage(), Toast.LENGTH_LONG).show();
callback.purchaseCompleted(false, sku);
disposeIabHelper();
}
}
});
}
public void disposeIabHelper() {
if (iabHelper != null) {
Timber.d("%s: dispose", iabHelper);
iabHelper.dispose();
iabHelper = null;
}
}
public void handleActivityResult(PurchaseHelperCallback callback, int requestCode, int resultCode, Intent data) {
this.activityResultCallback = callback;
iabHelper.handleActivityResult(requestCode, resultCode, data);
if (iabHelper != null) {
iabHelper.handleActivityResult(requestCode, resultCode, data);
}
}
}

@ -62,6 +62,15 @@ public class DashClockSettings extends InjectingPreferenceActivity implements Pu
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
purchaseHelper.disposeIabHelper();
}
}
@Override
public void inject(ActivityComponent component) {
component.inject(this);

@ -38,7 +38,7 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
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";
private static final String EXTRA_PURCHASE_INITIATED = "extra_purchase_initiated";
@Bind(R.id.toolbar) Toolbar toolbar;
@ -49,7 +49,7 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
private Bundle previousBundle;
private Filter filter;
private boolean purchaseInProgress;
private boolean purchaseInitiated;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@ -61,7 +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);
purchaseInitiated = savedInstanceState.getBoolean(EXTRA_PURCHASE_INITIATED);
} else {
filter = defaultFilterProvider.getDefaultFilter();
}
@ -77,8 +77,8 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
supportActionBar.setDisplayShowTitleEnabled(false);
}
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);
if (!preferences.hasPurchase(R.string.p_purchased_tasker) && !purchaseInitiated) {
purchaseInitiated = purchaseHelper.purchase(dialogBuilder, this, getString(R.string.sku_tasker), getString(R.string.p_purchased_tasker), REQUEST_PURCHASE, this);
}
}
@ -203,12 +203,21 @@ public final class TaskerSettingsActivity extends AbstractFragmentPluginAppCompa
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
purchaseHelper.disposeIabHelper();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(PluginBundleValues.BUNDLE_EXTRA_PREVIOUS_BUNDLE, previousBundle);
outState.putParcelable(EXTRA_FILTER, filter);
outState.putBoolean(EXTRA_PURCHASE_IN_PROGRESS, purchaseInProgress);
outState.putBoolean(EXTRA_PURCHASE_INITIATED, purchaseInitiated);
}
private void updateView() {

@ -3,7 +3,8 @@ package org.tasks.preferences;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.SwitchPreference;
import android.preference.PreferenceCategory;
import android.preference.TwoStatePreference;
import org.tasks.BuildConfig;
import org.tasks.R;
@ -18,6 +19,8 @@ import javax.inject.Inject;
import timber.log.Timber;
import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1;
public class BasicPreferences extends BaseBasicPreferences implements PurchaseHelperCallback {
private static final int REQUEST_PURCHASE = 10005;
@ -57,15 +60,21 @@ public class BasicPreferences extends BaseBasicPreferences implements PurchaseHe
}
});
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);
Preference dashClock = getPref(R.string.p_purchased_dashclock);
if (atLeastJellybeanMR1()) {
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;
}
return false;
}
});
});
} else {
PreferenceCategory iapCategory = (PreferenceCategory) findPreference(getString(R.string.get_plugins));
iapCategory.removePreference(dashClock);
}
findPreference(getString(R.string.p_collect_statistics)).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -106,21 +115,33 @@ public class BasicPreferences extends BaseBasicPreferences implements PurchaseHe
}
@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);
public void purchaseCompleted(final boolean success, final String sku) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (getString(R.string.sku_tasker).equals(sku)) {
((TwoStatePreference) getPref(R.string.p_purchased_tasker)).setChecked(success);
} else if (getString(R.string.sku_tesla_unread).equals(sku)) {
((TwoStatePreference) getPref(R.string.p_tesla_unread_enabled)).setChecked(success);
} else if (getString(R.string.sku_dashclock).equals(sku)) {
((TwoStatePreference) getPref(R.string.p_purchased_dashclock)).setChecked(success);
} else {
Timber.e("Unhandled sku: %s", sku);
}
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
purchaseHelper.disposeIabHelper();
}
}
private SwitchPreference getPref(int resId) {
return (SwitchPreference) findPreference(getString(resId));
private Preference getPref(int resId) {
return findPreference(getString(resId));
}
}

@ -304,6 +304,10 @@ public class AndroidUtilities {
return !atLeastLollipop();
}
public static boolean atLeastJellybeanMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
}
public static boolean atLeastJellybean() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}

@ -2,7 +2,8 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="@string/get_plugins">
android:title="@string/get_plugins"
android:key="@string/get_plugins">
<Preference
android:selectable="false"
@ -17,21 +18,21 @@
android:targetClass="org.tasks.activities.DonationActivity" />
</Preference>
<SwitchPreference
<CheckBoxPreference
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
<CheckBoxPreference
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
<CheckBoxPreference
android:key="@string/p_tesla_unread_enabled"
android:title="@string/tesla_unread"
android:summary="@string/tesla_unread_description"/>

@ -9,7 +9,7 @@
android:data="http://tasks.org/privacy.html" />
</Preference>
<SwitchPreference
<CheckBoxPreference
android:defaultValue="true"
android:key="@string/p_collect_statistics"
android:summary="@string/send_anonymous_statistics_summary"

Loading…
Cancel
Save