diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8233a87..dd718e6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Change Log --- ### 6.8 (beta) +* Name your own subscription price! Upgrade, downgrade, or cancel at any time * Choose icons for lists (requires [subscription](http://tasks.org/subscribe)) * Choose color for custom filters * Performance improvements diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7a711c938..561a8f7cc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { defaultConfig { testApplicationId = "org.tasks.test" applicationId = "org.tasks" - versionCode = 600 - versionName = "6.8.0" + versionCode = 601 + versionName = "6.8" targetSdkVersion(Versions.compileSdk) minSdkVersion(Versions.minSdk) multiDexEnabled = true diff --git a/app/src/googleplay/java/org/tasks/billing/BillingClientImpl.java b/app/src/googleplay/java/org/tasks/billing/BillingClientImpl.java index f13e760fd..8f5941c6f 100644 --- a/app/src/googleplay/java/org/tasks/billing/BillingClientImpl.java +++ b/app/src/googleplay/java/org/tasks/billing/BillingClientImpl.java @@ -12,6 +12,7 @@ import static org.tasks.billing.Inventory.SKU_VIP; import android.app.Activity; import android.content.Context; +import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; @@ -19,6 +20,7 @@ import com.android.billingclient.api.BillingClient.BillingResponse; import com.android.billingclient.api.BillingClient.FeatureType; import com.android.billingclient.api.BillingClient.SkuType; import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingFlowParams.ProrationMode; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.Purchase.PurchasesResult; import com.android.billingclient.api.PurchasesUpdatedListener; @@ -51,6 +53,7 @@ public class BillingClientImpl implements BillingClient, PurchasesUpdatedListene private com.android.billingclient.api.BillingClient billingClient; private boolean connected; private int billingClientResponseCode = -1; + private OnPurchasesUpdated onPurchasesUpdated; @Inject public BillingClientImpl(@ForApplication Context context, Inventory inventory, Tracker tracker) { @@ -149,6 +152,9 @@ public class BillingClientImpl implements BillingClient, PurchasesUpdatedListene if (resultCode == BillingResponse.OK) { add(purchases); } + if (onPurchasesUpdated != null) { + onPurchasesUpdated.onPurchasesUpdated(); + } String skus = purchases == null ? "null" @@ -164,17 +170,23 @@ public class BillingClientImpl implements BillingClient, PurchasesUpdatedListene } @Override - public void initiatePurchaseFlow(Activity activity, String skuId, String billingType) { + public void initiatePurchaseFlow( + Activity activity, String skuId, String billingType, @Nullable String oldSku) { executeServiceRequest( - () -> { - billingClient.launchBillingFlow( - activity, - BillingFlowParams.newBuilder() - .setSku(skuId) - .setType(billingType) - .setOldSkus(null) - .build()); - }); + () -> + billingClient.launchBillingFlow( + activity, + BillingFlowParams.newBuilder() + .setSku(skuId) + .setType(billingType) + .setOldSkus(oldSku == null ? null : newArrayList(oldSku)) + .setReplaceSkusProrationMode(ProrationMode.IMMEDIATE_WITH_TIME_PRORATION) + .build())); + } + + @Override + public void addPurchaseCallback(OnPurchasesUpdated onPurchasesUpdated) { + this.onPurchasesUpdated = onPurchasesUpdated; } public void destroy() { diff --git a/app/src/googleplay/java/org/tasks/billing/Purchase.java b/app/src/googleplay/java/org/tasks/billing/Purchase.java index 1e4a0fbed..0bb7b5556 100644 --- a/app/src/googleplay/java/org/tasks/billing/Purchase.java +++ b/app/src/googleplay/java/org/tasks/billing/Purchase.java @@ -1,9 +1,13 @@ package org.tasks.billing; import com.google.gson.GsonBuilder; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Purchase { + private static final Pattern PATTERN = Pattern.compile("^(annual|monthly)_([0-1][0-9]|499)$"); + private final com.android.billingclient.api.Purchase purchase; public Purchase(String json) { @@ -38,6 +42,27 @@ public class Purchase { return !SkuDetails.SKU_SUBS.contains(getSku()); } + boolean isProSubscription() { + return PATTERN.matcher(getSku()).matches(); + } + + boolean isMonthly() { + return getSku().startsWith("monthly"); + } + + boolean isCanceled() { + return !purchase.isAutoRenewing(); + } + + Integer getSubscriptionPrice() { + Matcher matcher = PATTERN.matcher(getSku()); + if (matcher.matches()) { + int price = Integer.parseInt(matcher.group(2)); + return price == 499 ? 5 : price; + } + return null; + } + @Override public String toString() { return "Purchase{" + "purchase=" + purchase + '}'; diff --git a/app/src/googleplay/res/values/keys.xml b/app/src/googleplay/res/values/keys.xml index 6cf4c294c..d4677e815 100644 --- a/app/src/googleplay/res/values/keys.xml +++ b/app/src/googleplay/res/values/keys.xml @@ -4,6 +4,7 @@ play_services_available market://details?id=org.tasks purchases - https://play.google.com/store/account/subscriptions?sku=annual_499&package=org.tasks + https://play.google.com/store/account/subscriptions?sku=%s&package=org.tasks + https://tasks.org/subscribe play-support@tasks.org \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4da8aa513..b353dd7d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -505,7 +505,7 @@ + android:theme="@style/TranslucentDialog"/> { - public static final int REQUEST_SETTINGS = 10123; - public static final int REQUEST_PURCHASE = 10124; - private static final String TOKEN_SELECTED = "token_selected"; private final Activity activity; private final ThemeAccent accent; diff --git a/app/src/main/java/org/tasks/activities/ColorPickerActivity.java b/app/src/main/java/org/tasks/activities/ColorPickerActivity.java index f02d1a701..7b40d7311 100644 --- a/app/src/main/java/org/tasks/activities/ColorPickerActivity.java +++ b/app/src/main/java/org/tasks/activities/ColorPickerActivity.java @@ -1,14 +1,13 @@ package org.tasks.activities; +import static org.tasks.billing.PurchaseDialog.newPurchaseDialog; import static org.tasks.dialogs.ColorPickerDialog.newColorPickerDialog; import android.content.Intent; -import android.os.Bundle; import java.util.List; import javax.inject.Inject; import org.tasks.R; import org.tasks.billing.Inventory; -import org.tasks.billing.PurchaseActivity; import org.tasks.dialogs.ColorPickerDialog; import org.tasks.injection.ActivityComponent; import org.tasks.injection.ThemedInjectingAppCompatActivity; @@ -23,7 +22,7 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity public static final String EXTRA_SHOW_NONE = "extra_show_none"; public static final String EXTRA_THEME_INDEX = "extra_index"; private static final String FRAG_TAG_COLOR_PICKER = "frag_tag_color_picker"; - private static final int REQUEST_SUBSCRIPTION = 10101; + private static final String FRAG_TAG_PURCHASE = "frag_tag_purchase"; @Inject Theme theme; @Inject ThemeCache themeCache; @Inject Inventory inventory; @@ -31,11 +30,6 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity private ColorPalette palette; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - @Override protected void onPostResume() { super.onPostResume(); @@ -84,7 +78,7 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity @Override public void initiateThemePurchase() { - startActivityForResult(new Intent(this, PurchaseActivity.class), REQUEST_SUBSCRIPTION); + newPurchaseDialog().show(getSupportFragmentManager(), FRAG_TAG_PURCHASE); } @Override @@ -92,17 +86,6 @@ public class ColorPickerActivity extends ThemedInjectingAppCompatActivity finish(); } - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_SUBSCRIPTION) { - if (!inventory.purchasedThemes()) { - finish(); - } - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } - private int getCurrentSelection(ColorPalette palette) { switch (palette) { case COLORS: diff --git a/app/src/main/java/org/tasks/analytics/Tracking.java b/app/src/main/java/org/tasks/analytics/Tracking.java index 883f13af4..8b3d5295d 100644 --- a/app/src/main/java/org/tasks/analytics/Tracking.java +++ b/app/src/main/java/org/tasks/analytics/Tracking.java @@ -29,8 +29,6 @@ public class Tracking { CLEAR_COMPLETED(R.string.tracking_category_event, R.string.tracking_action_clear_completed), UPGRADE(R.string.tracking_category_event, R.string.tracking_event_upgrade), TASK_CREATION_FAILED(R.string.tracking_category_error, R.string.tracking_event_task_creation), - NIGHT_MODE_MISMATCH( - R.string.tracking_category_event, R.string.tracking_event_night_mode_mismatch), SET_PREFERENCE(R.string.tracking_category_preferences, 0), PLAY_SERVICES_WARNING( R.string.tracking_category_event, R.string.tracking_event_play_services_error), diff --git a/app/src/main/java/org/tasks/billing/BillingClient.java b/app/src/main/java/org/tasks/billing/BillingClient.java index 66ac8051f..596622792 100644 --- a/app/src/main/java/org/tasks/billing/BillingClient.java +++ b/app/src/main/java/org/tasks/billing/BillingClient.java @@ -1,6 +1,7 @@ package org.tasks.billing; import android.app.Activity; +import androidx.annotation.Nullable; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.Observer; import java.util.List; @@ -19,5 +20,7 @@ public interface BillingClient { void consume(String sku); - void initiatePurchaseFlow(Activity activity, String sku, String skuType); + void initiatePurchaseFlow(Activity activity, String sku, String skuType, @Nullable String oldSku); + + void addPurchaseCallback(OnPurchasesUpdated onPurchasesUpdated); } diff --git a/app/src/main/java/org/tasks/billing/CardsWithHeadersDecoration.java b/app/src/main/java/org/tasks/billing/CardsWithHeadersDecoration.java deleted file mode 100644 index 19317688a..000000000 --- a/app/src/main/java/org/tasks/billing/CardsWithHeadersDecoration.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package org.tasks.billing; - -import android.graphics.Rect; -import android.view.View; -import androidx.recyclerview.widget.RecyclerView; -import org.tasks.billing.row.RowDataProvider; -import org.tasks.billing.row.SkuRowData; - -/** A separator for RecyclerView that keeps the specified spaces between headers and the cards. */ -public class CardsWithHeadersDecoration extends RecyclerView.ItemDecoration { - - private final RowDataProvider mRowDataProvider; - private final int mHeaderGap, mRowGap; - - public CardsWithHeadersDecoration(RowDataProvider rowDataProvider, int headerGap, int rowGap) { - this.mRowDataProvider = rowDataProvider; - this.mHeaderGap = headerGap; - this.mRowGap = rowGap; - } - - @Override - public void getItemOffsets( - Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - - final int position = parent.getChildAdapterPosition(view); - final SkuRowData data = mRowDataProvider.getData(position); - - // We should add a space on top of every header card - if (data.getRowType() == SkusAdapter.TYPE_HEADER || position == 0) { - outRect.top = mHeaderGap; - } - - // Adding a space under the last item - if (position == parent.getAdapter().getItemCount() - 1) { - outRect.bottom = mHeaderGap; - } else { - outRect.bottom = mRowGap; - } - } -} diff --git a/app/src/main/java/org/tasks/billing/Inventory.java b/app/src/main/java/org/tasks/billing/Inventory.java index 32acccc92..2e599ddef 100644 --- a/app/src/main/java/org/tasks/billing/Inventory.java +++ b/app/src/main/java/org/tasks/billing/Inventory.java @@ -27,6 +27,7 @@ public class Inventory { private final LocalBroadcastManager localBroadcastManager; private Map purchases = new HashMap<>(); + private Purchase subscription = null; @Inject public Inventory( @@ -62,6 +63,9 @@ public class Inventory { if (signatureVerifier.verifySignature(purchase)) { Timber.d("add(%s)", purchase); purchases.put(purchase.getSku(), purchase); + if (purchase.isProSubscription() && (subscription == null || subscription.isCanceled())) { + subscription = purchase; + } } } @@ -81,9 +85,13 @@ public class Inventory { return ImmutableList.copyOf(purchases.values()); } + public Purchase getSubscription() { + return subscription; + } + public boolean hasPro() { //noinspection ConstantConditions - return purchases.containsKey(SkuDetails.SKU_PRO) + return subscription != null || purchases.containsKey(SKU_VIP) || BuildConfig.FLAVOR.equals("generic") || (BuildConfig.DEBUG && preferences.getBoolean(R.string.p_debug_pro, false)); diff --git a/app/src/main/java/org/tasks/billing/NameYourPriceDialog.java b/app/src/main/java/org/tasks/billing/NameYourPriceDialog.java new file mode 100644 index 000000000..7764cd3a3 --- /dev/null +++ b/app/src/main/java/org/tasks/billing/NameYourPriceDialog.java @@ -0,0 +1,266 @@ +package org.tasks.billing; + +import static com.google.common.collect.Lists.newArrayList; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import com.android.billingclient.api.BillingClient.SkuType; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.button.MaterialButtonToggleGroup; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.Range; +import javax.inject.Inject; +import org.tasks.LocalBroadcastManager; +import org.tasks.R; +import org.tasks.dialogs.DialogBuilder; +import org.tasks.dialogs.IconLayoutManager; +import org.tasks.injection.DialogFragmentComponent; +import org.tasks.injection.ForActivity; +import org.tasks.injection.InjectingDialogFragment; +import org.tasks.locale.Locale; + +public class NameYourPriceDialog extends InjectingDialogFragment implements OnPurchasesUpdated { + + private static final String EXTRA_MONTHLY = "extra_monthly"; + private static final String EXTRA_PRICE = "extra_price"; + + @Inject DialogBuilder dialogBuilder; + @Inject @ForActivity Context context; + @Inject BillingClient billingClient; + @Inject LocalBroadcastManager localBroadcastManager; + @Inject Inventory inventory; + @Inject Locale locale; + + @BindView(R.id.recycler_view) + RecyclerView recyclerView; + + @BindView(R.id.screen_wait) + View loadingView; + + @BindView(R.id.buttons) + MaterialButtonToggleGroup buttons; + + @BindView(R.id.subscribe) + MaterialButton subscribe; + + @BindView(R.id.unsubscribe) + MaterialButton unsubscribe; + + private PurchaseAdapter adapter; + private Purchase currentSubscription = null; + private BroadcastReceiver purchaseReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + setup(); + } + }; + private OnDismissListener listener; + + static NameYourPriceDialog newNameYourPriceDialog() { + return new NameYourPriceDialog(); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog dialog = + dialogBuilder + .newDialog() + .setTitle(R.string.name_your_price) + .setView(R.layout.activity_purchase) + .show(); + + ButterKnife.bind(this, dialog); + + setWaitScreen(true); + + adapter = new PurchaseAdapter((Activity) context, locale, this::onPriceChanged); + + buttons.addOnButtonCheckedListener(this::onButtonChecked); + + if (savedInstanceState != null) { + buttons.check( + savedInstanceState.getBoolean(EXTRA_MONTHLY) + ? R.id.button_monthly + : R.id.button_annually); + adapter.setSelected(savedInstanceState.getInt(EXTRA_PRICE)); + } + + return dialog; + } + + private void onButtonChecked(MaterialButtonToggleGroup group, int id, boolean checked) { + if (id == R.id.button_monthly) { + if (!checked && group.getCheckedButtonId() != R.id.button_annually) { + group.check(R.id.button_monthly); + } + } else { + if (!checked && group.getCheckedButtonId() != R.id.button_monthly) { + group.check(R.id.button_annually); + } + } + updateSubscribeButton(); + } + + private boolean isMonthly() { + return buttons.getCheckedButtonId() == R.id.button_monthly; + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putBoolean(EXTRA_MONTHLY, isMonthly()); + outState.putInt(EXTRA_PRICE, adapter.getSelected()); + } + + @SuppressLint("DefaultLocale") + @OnClick(R.id.subscribe) + protected void subscribe() { + if (currentSubscriptionSelected() && currentSubscription.isCanceled()) { + billingClient.initiatePurchaseFlow( + (Activity) context, currentSubscription.getSku(), SkuType.SUBS, null); + } else { + billingClient.initiatePurchaseFlow( + (Activity) context, + String.format("%s_%02d", isMonthly() ? "monthly" : "annual", adapter.getSelected()), + SkuType.SUBS, + currentSubscription == null ? null : currentSubscription.getSku()); + } + billingClient.addPurchaseCallback(this); + dismiss(); + } + + private void setup() { + currentSubscription = inventory.getSubscription(); + if (adapter.getSelected() == 0) { + if (currentSubscription == null) { + adapter.setSelected(1); + } else { + adapter.setSelected(currentSubscription.getSubscriptionPrice()); + buttons.check(currentSubscription.isMonthly() ? R.id.button_monthly : R.id.button_annually); + } + } + unsubscribe.setVisibility( + currentSubscription == null || currentSubscription.isCanceled() ? View.GONE : View.VISIBLE); + updateSubscribeButton(); + setWaitScreen(false); + adapter.submitList( + newArrayList(ContiguousSet.create(Range.closed(1, 10), DiscreteDomain.integers()))); + recyclerView.setLayoutManager(new IconLayoutManager(context)); + recyclerView.setAdapter(adapter); + } + + @OnClick(R.id.unsubscribe) + protected void manageSubscription() { + startActivity( + new Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.manage_subscription_url, currentSubscription.getSku())))); + dismiss(); + } + + private void onPriceChanged(Integer price) { + adapter.setSelected(price); + updateSubscribeButton(); + } + + private void updateSubscribeButton() { + subscribe.setEnabled(true); + if (currentSubscription == null) { + subscribe.setText(R.string.button_subscribe); + } else if (currentSubscriptionSelected()) { + if (currentSubscription.isCanceled()) { + subscribe.setText(R.string.button_restore_subscription); + } else { + subscribe.setText(R.string.button_current_subscription); + subscribe.setEnabled(false); + } + } else { + subscribe.setText(isUpgrade() ? R.string.button_upgrade : R.string.button_downgrade); + } + } + + private boolean isUpgrade() { + return isMonthly() == currentSubscription.isMonthly() + ? currentSubscription.getSubscriptionPrice() < adapter.getSelected() + : isMonthly(); + } + + private boolean currentSubscriptionSelected() { + return currentSubscription != null + && isMonthly() == currentSubscription.isMonthly() + && adapter.getSelected() == currentSubscription.getSubscriptionPrice(); + } + + @Override + protected void inject(DialogFragmentComponent component) { + component.inject(this); + } + + private void setWaitScreen(boolean isWaitScreen) { + recyclerView.setVisibility(isWaitScreen ? View.GONE : View.VISIBLE); + buttons.setVisibility(isWaitScreen ? View.GONE : View.VISIBLE); + subscribe.setVisibility(isWaitScreen ? View.GONE : View.VISIBLE); + loadingView.setVisibility(isWaitScreen ? View.VISIBLE : View.GONE); + } + + @Override + public void onStart() { + super.onStart(); + + localBroadcastManager.registerPurchaseReceiver(purchaseReceiver); + billingClient.queryPurchases(); + } + + @Override + public void onStop() { + super.onStop(); + + localBroadcastManager.unregisterReceiver(purchaseReceiver); + } + + NameYourPriceDialog setOnDismissListener(OnDismissListener listener) { + this.listener = listener; + return this; + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + + if (listener != null) { + listener.onDismiss(dialog); + } + } + + @Override + public void onPurchasesUpdated() { + dismiss(); + } + + @OnClick(R.id.button_more_info) + public void openDocumentation() { + startActivity( + new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.subscription_help_url)))); + dismiss(); + } +} diff --git a/app/src/main/java/org/tasks/billing/OnPurchasesUpdated.java b/app/src/main/java/org/tasks/billing/OnPurchasesUpdated.java new file mode 100644 index 000000000..35fbce9e0 --- /dev/null +++ b/app/src/main/java/org/tasks/billing/OnPurchasesUpdated.java @@ -0,0 +1,5 @@ +package org.tasks.billing; + +public interface OnPurchasesUpdated { + void onPurchasesUpdated(); +} diff --git a/app/src/main/java/org/tasks/billing/PurchaseActivity.java b/app/src/main/java/org/tasks/billing/PurchaseActivity.java index af6a62629..46df4f6db 100644 --- a/app/src/main/java/org/tasks/billing/PurchaseActivity.java +++ b/app/src/main/java/org/tasks/billing/PurchaseActivity.java @@ -1,191 +1,45 @@ package org.tasks.billing; -import static com.google.common.collect.Lists.transform; +import static org.tasks.billing.NameYourPriceDialog.newNameYourPriceDialog; +import static org.tasks.billing.PurchaseDialog.newPurchaseDialog; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.net.Uri; import android.os.Bundle; -import android.view.MenuItem; -import android.view.View; -import android.widget.TextView; -import androidx.appcompat.widget.Toolbar; -import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.ButterKnife; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import androidx.fragment.app.FragmentManager; import javax.inject.Inject; -import org.tasks.BuildConfig; -import org.tasks.LocalBroadcastManager; -import org.tasks.R; -import org.tasks.billing.SkusAdapter.OnClickHandler; -import org.tasks.billing.row.SkuRowData; import org.tasks.injection.ActivityComponent; -import org.tasks.injection.ForApplication; import org.tasks.injection.ThemedInjectingAppCompatActivity; -import org.tasks.ui.MenuColorizer; -public class PurchaseActivity extends ThemedInjectingAppCompatActivity - implements OnClickHandler, OnMenuItemClickListener { +public class PurchaseActivity extends ThemedInjectingAppCompatActivity { - @Inject @ForApplication Context context; - @Inject BillingClient billingClient; - @Inject Inventory inventory; - @Inject LocalBroadcastManager localBroadcastManager; - - @BindView(R.id.toolbar) - Toolbar toolbar; - - @BindView(R.id.list) - RecyclerView recyclerView; - - @BindView(R.id.screen_wait) - View loadingView; + private static final String FRAG_TAG_PURCHASE = "frag_tag_purchase"; + private static final String FRAG_TAG_PRICE = "frag_tag_price"; - @BindView(R.id.error_textview) - TextView errorTextView; - - private SkusAdapter adapter; - private BroadcastReceiver purchaseReceiver = - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - billingClient.querySkuDetails(); - } - }; - private List iaps = Collections.emptyList(); - private List subscriptions = Collections.emptyList(); + @Inject Inventory inventory; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_purchase); - - ButterKnife.bind(this); - - toolbar.setTitle(R.string.upgrade); - toolbar.setNavigationIcon(R.drawable.ic_outline_arrow_back_24px); - toolbar.setNavigationOnClickListener(v -> onBackPressed()); - toolbar.inflateMenu(R.menu.menu_purchase_activity); - toolbar.setOnMenuItemClickListener(this); - MenuColorizer.colorToolbar(this, toolbar); - - adapter = new SkusAdapter(context, inventory, this); - recyclerView.setAdapter(adapter); - Resources res = getResources(); - recyclerView.addItemDecoration( - new CardsWithHeadersDecoration( - adapter, - (int) res.getDimension(R.dimen.header_gap), - (int) res.getDimension(R.dimen.row_gap))); - recyclerView.setLayoutManager(new LinearLayoutManager(context)); - setWaitScreen(true); - } - - @Override - protected void onResume() { - super.onResume(); - - billingClient.observeSkuDetails(this, this::onSubscriptionsUpdated, this::onIapsUpdated); - billingClient.querySkuDetails(); - } - - @Override - protected void onStart() { - super.onStart(); - - localBroadcastManager.registerPurchaseReceiver(purchaseReceiver); - } - - @Override - protected void onStop() { - super.onStop(); - - localBroadcastManager.unregisterReceiver(purchaseReceiver); - } - - private void onIapsUpdated(List iaps) { - this.iaps = iaps; - updateSkuDetails(); - } - - private void onSubscriptionsUpdated(List subscriptions) { - this.subscriptions = subscriptions; - updateSkuDetails(); - } - - private void updateSkuDetails() { - List data = new ArrayList<>(transform(subscriptions, SkuRowData::new)); - if (iaps.size() > 0) { - data.add(new SkuRowData(context.getString(R.string.owned))); - data.addAll(transform(iaps, SkuRowData::new)); - } - if (data.isEmpty()) { - displayAnErrorIfNeeded(); + FragmentManager fragmentManager = getSupportFragmentManager(); + if (inventory.hasPro()) { + NameYourPriceDialog dialog = (NameYourPriceDialog) fragmentManager.findFragmentByTag(FRAG_TAG_PRICE); + if (dialog == null) { + dialog = newNameYourPriceDialog(); + dialog.show(fragmentManager, FRAG_TAG_PRICE); + } + dialog.setOnDismissListener(d -> finish()); } else { - adapter.setData(data); - setWaitScreen(false); - } - } - - private void displayAnErrorIfNeeded() { - if (!isFinishing()) { - loadingView.setVisibility(View.GONE); - errorTextView.setVisibility(View.VISIBLE); - errorTextView.setText(billingClient.getErrorMessage()); + PurchaseDialog dialog = (PurchaseDialog) fragmentManager.findFragmentByTag(FRAG_TAG_PURCHASE); + if (dialog == null) { + dialog = newPurchaseDialog(); + dialog.show(fragmentManager, FRAG_TAG_PURCHASE); + } + dialog.setOnDismissListener(d -> finish()); } } - private void setWaitScreen(boolean set) { - recyclerView.setVisibility(set ? View.GONE : View.VISIBLE); - loadingView.setVisibility(set ? View.VISIBLE : View.GONE); - } - @Override public void inject(ActivityComponent component) { component.inject(this); } - - @Override - public void clickAux(SkuRowData skuRowData) { - startSubscribeActivity(); - } - - @Override - public void click(SkuRowData skuRowData) { - String sku = skuRowData.getSku(); - String skuType = skuRowData.getSkuType(); - if (inventory.purchased(sku)) { - if (BuildConfig.DEBUG && SkuDetails.TYPE_INAPP.equals(skuType)) { - billingClient.consume(sku); - } - } else { - billingClient.initiatePurchaseFlow(this, sku, skuType); - } - } - - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_help: - startSubscribeActivity(); - return true; - case R.id.menu_refresh_purchases: - billingClient.queryPurchases(); - return true; - default: - return false; - } - } - - private void startSubscribeActivity() { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://tasks.org/subscribe"))); - } } diff --git a/app/src/main/java/org/tasks/billing/PurchaseAdapter.java b/app/src/main/java/org/tasks/billing/PurchaseAdapter.java new file mode 100644 index 000000000..bea55a71f --- /dev/null +++ b/app/src/main/java/org/tasks/billing/PurchaseAdapter.java @@ -0,0 +1,62 @@ +package org.tasks.billing; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil.ItemCallback; +import androidx.recyclerview.widget.ListAdapter; +import org.tasks.Callback; +import org.tasks.R; +import org.tasks.locale.Locale; + +public class PurchaseAdapter extends ListAdapter { + + private final Activity activity; + private final Locale locale; + private final Callback onPriceChanged; + private int selected; + + PurchaseAdapter(Activity activity, Locale locale, Callback onPriceChanged) { + super(new DiffCallback()); + this.activity = activity; + this.locale = locale; + this.onPriceChanged = onPriceChanged; + } + + public int getSelected() { + return selected; + } + + public void setSelected(int price) { + int previous = selected; + this.selected = price; + notifyItemChanged(previous - 1, null); + notifyItemChanged(price - 1, null); + } + + @NonNull + @Override + public PurchaseHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = activity.getLayoutInflater().inflate(R.layout.dialog_purchase_cell, parent, false); + return new PurchaseHolder(activity, view, onPriceChanged, locale); + } + + @Override + public void onBindViewHolder(@NonNull PurchaseHolder holder, int position) { + int price = position + 1; + holder.bind(price, price == selected); + } + + private static class DiffCallback extends ItemCallback { + @Override + public boolean areItemsTheSame(@NonNull Integer oldItem, @NonNull Integer newItem) { + return oldItem.equals(newItem); + } + + @Override + public boolean areContentsTheSame(@NonNull Integer oldItem, @NonNull Integer newItem) { + return true; + } + } +} diff --git a/app/src/main/java/org/tasks/billing/PurchaseDialog.java b/app/src/main/java/org/tasks/billing/PurchaseDialog.java new file mode 100644 index 000000000..6e759ceef --- /dev/null +++ b/app/src/main/java/org/tasks/billing/PurchaseDialog.java @@ -0,0 +1,77 @@ +package org.tasks.billing; + +import static com.google.common.collect.Lists.transform; +import static java.util.Arrays.asList; +import static org.tasks.billing.NameYourPriceDialog.newNameYourPriceDialog; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.base.Joiner; +import javax.inject.Inject; +import org.tasks.R; +import org.tasks.dialogs.DialogBuilder; +import org.tasks.injection.DialogFragmentComponent; +import org.tasks.injection.ForActivity; +import org.tasks.injection.InjectingDialogFragment; + +public class PurchaseDialog extends InjectingDialogFragment { + + private static final String FRAG_TAG_PRICE = "frag_tag_price"; + + @Inject DialogBuilder dialogBuilder; + @Inject @ForActivity Context context; + private OnDismissListener listener; + + public static PurchaseDialog newPurchaseDialog() { + return new PurchaseDialog(); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + View view = ((Activity) context).getLayoutInflater().inflate(R.layout.dialog_purchase, null); + TextView textView = view.findViewById(R.id.feature_list); + String[] rows = context.getResources().getStringArray(R.array.pro_description); + textView.setText(Joiner.on('\n').join(transform(asList(rows), item -> "\u2022 " + item))); + return dialogBuilder + .newDialog() + .setTitle(R.string.pro_support_development) + .setView(view) + .setPositiveButton( + R.string.name_your_price, + (dialog, which) -> { + newNameYourPriceDialog() + .setOnDismissListener(listener) + .show(getFragmentManager(), FRAG_TAG_PRICE); + listener = null; + dialog.dismiss(); + }) + .show(); + } + + void setOnDismissListener(OnDismissListener listener) { + this.listener = listener; + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + + if (listener != null) { + listener.onDismiss(dialog); + } + } + + @Override + protected void inject(DialogFragmentComponent component) { + component.inject(this); + } +} diff --git a/app/src/main/java/org/tasks/billing/PurchaseHolder.java b/app/src/main/java/org/tasks/billing/PurchaseHolder.java new file mode 100644 index 000000000..88d1f95e9 --- /dev/null +++ b/app/src/main/java/org/tasks/billing/PurchaseHolder.java @@ -0,0 +1,52 @@ +package org.tasks.billing; + +import static org.tasks.preferences.ResourceResolver.getData; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import org.tasks.Callback; +import org.tasks.R; +import org.tasks.locale.Locale; + +public class PurchaseHolder extends RecyclerView.ViewHolder { + + private final Context context; + private final Callback onClick; + private final Locale locale; + + @BindView(R.id.price) + TextView textView; + + private int price; + + PurchaseHolder(Context context, @NonNull View view, Callback onClick, Locale locale) { + super(view); + this.locale = locale; + + ButterKnife.bind(this, view); + + this.context = context; + this.onClick = onClick; + } + + @OnClick(R.id.price) + void onClick() { + onClick.call(price); + } + + public void bind(int price, boolean selected) { + this.price = price; + textView.setText(String.format("$%s", locale.formatNumber(price))); + textView.setTextColor( + selected + ? getData(context, R.attr.colorPrimary) + : ContextCompat.getColor(context, R.color.text_primary)); + } +} diff --git a/app/src/main/java/org/tasks/billing/SkusAdapter.java b/app/src/main/java/org/tasks/billing/SkusAdapter.java deleted file mode 100644 index 97828d940..000000000 --- a/app/src/main/java/org/tasks/billing/SkusAdapter.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.tasks.billing; - -import static com.google.common.collect.Lists.transform; -import static java.lang.annotation.RetentionPolicy.SOURCE; -import static java.util.Arrays.asList; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import java.lang.annotation.Retention; -import java.util.List; -import org.tasks.BuildConfig; -import org.tasks.R; -import org.tasks.billing.row.RowDataProvider; -import org.tasks.billing.row.RowViewHolder; -import org.tasks.billing.row.RowViewHolder.ButtonClick; -import org.tasks.billing.row.SkuRowData; - -public class SkusAdapter extends RecyclerView.Adapter - implements RowDataProvider, ButtonClick { - - public static final int TYPE_HEADER = 0; - public static final int TYPE_NORMAL = 1; - private final Context context; - private final Inventory inventory; - private final OnClickHandler onClickHandler; - private List data = ImmutableList.of(); - - SkusAdapter(Context context, Inventory inventory, OnClickHandler onClickHandler) { - this.context = context; - this.inventory = inventory; - this.onClickHandler = onClickHandler; - } - - public void setData(List data) { - this.data = data; - - notifyDataSetChanged(); - } - - @Override - public @RowTypeDef int getItemViewType(int position) { - return data.isEmpty() ? TYPE_HEADER : data.get(position).getRowType(); - } - - @Override - @NonNull - public RowViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @RowTypeDef int viewType) { - // Selecting a flat layout for header rows - if (viewType == SkusAdapter.TYPE_HEADER) { - View item = - LayoutInflater.from(parent.getContext()) - .inflate(R.layout.sku_details_row_header, parent, false); - return new RowViewHolder(item, null); - } else { - View item = - LayoutInflater.from(parent.getContext()).inflate(R.layout.sku_details_row, parent, false); - return new RowViewHolder(item, this); - } - } - - @Override - public void onBindViewHolder(@NonNull RowViewHolder holder, int position) { - SkuRowData data = getData(position); - if (data != null) { - holder.title.setText(data.getTitle()); - if (getItemViewType(position) != SkusAdapter.TYPE_HEADER) { - - String sku = data.getSku(); - if (SkuDetails.TYPE_SUBS.equals(data.getSkuType())) { - String[] rows = context.getResources().getStringArray(R.array.pro_description); - holder.description.setText( - Joiner.on('\n').join(transform(asList(rows), item -> "\u2022 " + item))); - holder.subscribeButton.setVisibility(View.VISIBLE); - holder.price.setVisibility(View.VISIBLE); - holder.price.setText(data.getPrice()); - if (inventory.purchased(sku)) { - holder.subscribeButton.setText(R.string.button_subscribed); - holder.auxiliaryButton.setVisibility(View.GONE); - } else { - holder.subscribeButton.setText(R.string.button_subscribe); - holder.auxiliaryButton.setVisibility(View.VISIBLE); - } - } else { - holder.description.setText(data.getDescription()); - holder.subscribeButton.setVisibility(View.GONE); - holder.price.setVisibility(View.GONE); - holder.auxiliaryButton.setVisibility(View.GONE); - if (BuildConfig.DEBUG) { - holder.subscribeButton.setVisibility(View.VISIBLE); - holder.subscribeButton.setText( - inventory.purchased(sku) ? R.string.debug_consume : R.string.debug_buy); - } - } - } - } - } - - @Override - public int getItemCount() { - return data.size(); - } - - @Override - public SkuRowData getData(int position) { - return data.isEmpty() ? null : data.get(position); - } - - @Override - public void onAuxiliaryClick(int row) { - onClickHandler.clickAux(getData(row)); - } - - @Override - public void onClick(int row) { - onClickHandler.click(getData(row)); - } - - public interface OnClickHandler { - void clickAux(SkuRowData skuRowData); - - void click(SkuRowData skuRowData); - } - - /** Types for adapter rows */ - @Retention(SOURCE) - @IntDef({TYPE_HEADER, TYPE_NORMAL}) - public @interface RowTypeDef {} -} diff --git a/app/src/main/java/org/tasks/billing/row/RowDataProvider.java b/app/src/main/java/org/tasks/billing/row/RowDataProvider.java deleted file mode 100644 index a61835e72..000000000 --- a/app/src/main/java/org/tasks/billing/row/RowDataProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.tasks.billing.row; - -/** Provider for data that corresponds to a particular row */ -public interface RowDataProvider { - SkuRowData getData(int position); -} diff --git a/app/src/main/java/org/tasks/billing/row/RowViewHolder.java b/app/src/main/java/org/tasks/billing/row/RowViewHolder.java deleted file mode 100644 index cb3f537a7..000000000 --- a/app/src/main/java/org/tasks/billing/row/RowViewHolder.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.tasks.billing.row; - -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import androidx.recyclerview.widget.RecyclerView; -import org.tasks.R; - -public final class RowViewHolder extends RecyclerView.ViewHolder { - public final TextView title; - public final TextView description; - public final TextView price; - public final Button subscribeButton; - public final Button auxiliaryButton; - - public RowViewHolder(final View itemView, final ButtonClick onClick) { - super(itemView); - title = itemView.findViewById(R.id.title); - price = itemView.findViewById(R.id.price); - description = itemView.findViewById(R.id.description); - subscribeButton = itemView.findViewById(R.id.buy_button); - auxiliaryButton = itemView.findViewById(R.id.aux_button); - if (auxiliaryButton != null) { - auxiliaryButton.setOnClickListener(view -> onClick.onAuxiliaryClick(getAdapterPosition())); - } - if (subscribeButton != null) { - subscribeButton.setOnClickListener(view -> onClick.onClick(getAdapterPosition())); - } - } - - public interface ButtonClick { - void onAuxiliaryClick(int row); - - void onClick(int row); - } -} diff --git a/app/src/main/java/org/tasks/billing/row/SkuRowData.java b/app/src/main/java/org/tasks/billing/row/SkuRowData.java deleted file mode 100644 index f9070d98d..000000000 --- a/app/src/main/java/org/tasks/billing/row/SkuRowData.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2017 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.tasks.billing.row; - -import org.tasks.billing.SkuDetails; -import org.tasks.billing.SkusAdapter; -import org.tasks.billing.SkusAdapter.RowTypeDef; - -/** A model for SkusAdapter's row */ -public class SkuRowData { - private String sku, title, price, description; - private @RowTypeDef int type; - private String billingType; - - public SkuRowData(SkuDetails details) { - sku = details.getSku(); - title = details.getTitle(); - price = details.getPrice(); - description = details.getDescription(); - type = SkusAdapter.TYPE_NORMAL; - billingType = details.getSkuType(); - } - - public SkuRowData(String title) { - this.title = title; - type = SkusAdapter.TYPE_HEADER; - } - - public String getSku() { - return sku; - } - - public String getTitle() { - return title; - } - - public String getPrice() { - return price; - } - - public String getDescription() { - return description; - } - - public @RowTypeDef int getRowType() { - return type; - } - - public String getSkuType() { - return billingType; - } -} diff --git a/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java b/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java index afb90af17..75cec5e11 100644 --- a/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java +++ b/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java @@ -70,6 +70,11 @@ public class AlertDialogBuilder { return this; } + public AlertDialogBuilder setView(int layoutResId) { + builder.setView(layoutResId); + return this; + } + public AlertDialogBuilder setOnCancelListener(DialogInterface.OnCancelListener onCancelListener) { builder.setOnCancelListener(onCancelListener); return this; diff --git a/app/src/main/java/org/tasks/dialogs/IconLayoutManager.java b/app/src/main/java/org/tasks/dialogs/IconLayoutManager.java index 9bb736ec0..4d3a1e33b 100644 --- a/app/src/main/java/org/tasks/dialogs/IconLayoutManager.java +++ b/app/src/main/java/org/tasks/dialogs/IconLayoutManager.java @@ -5,11 +5,11 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import org.tasks.R; -class IconLayoutManager extends GridLayoutManager { +public class IconLayoutManager extends GridLayoutManager { private int iconSize; - IconLayoutManager(Context context) { + public IconLayoutManager(Context context) { super(context, DEFAULT_SPAN_COUNT, RecyclerView.VERTICAL, false); this.iconSize = (int) context.getResources().getDimension(R.dimen.icon_picker_size); } @@ -17,7 +17,7 @@ class IconLayoutManager extends GridLayoutManager { @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { int width = getWidth(); - if (getSpanCount() == DEFAULT_SPAN_COUNT && iconSize > 0 && width > 0 && getHeight() > 0) { + if (getSpanCount() == DEFAULT_SPAN_COUNT && width > 0) { setSpanCount(Math.max(1, (width - getPaddingRight() - getPaddingLeft()) / iconSize)); } super.onLayoutChildren(recycler, state); diff --git a/app/src/main/java/org/tasks/dialogs/IconPickerDialog.java b/app/src/main/java/org/tasks/dialogs/IconPickerDialog.java index a787c6490..2619eaa12 100644 --- a/app/src/main/java/org/tasks/dialogs/IconPickerDialog.java +++ b/app/src/main/java/org/tasks/dialogs/IconPickerDialog.java @@ -1,10 +1,11 @@ package org.tasks.dialogs; +import static org.tasks.billing.PurchaseDialog.newPurchaseDialog; + import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -16,7 +17,6 @@ import butterknife.ButterKnife; import javax.inject.Inject; import org.tasks.R; import org.tasks.billing.Inventory; -import org.tasks.billing.PurchaseActivity; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.ForActivity; import org.tasks.injection.InjectingDialogFragment; @@ -24,6 +24,7 @@ import org.tasks.themes.CustomIcons; public class IconPickerDialog extends InjectingDialogFragment { + private static final String FRAG_TAG_PURCHASE = "frag_tag_purchase"; private static final String EXTRA_CURRENT = "extra_current"; @BindView(R.id.icons) @@ -65,7 +66,7 @@ public class IconPickerDialog extends InjectingDialogFragment { if (!inventory.hasPro()) { builder.setPositiveButton( R.string.button_subscribe, - (dialog, which) -> startActivity(new Intent(context, PurchaseActivity.class))); + (dialog, which) -> newPurchaseDialog().show(getFragmentManager(), FRAG_TAG_PURCHASE)); } return builder.show(); } diff --git a/app/src/main/java/org/tasks/filters/FilterProvider.java b/app/src/main/java/org/tasks/filters/FilterProvider.java index 69ca0a78c..ae7d7f6a9 100644 --- a/app/src/main/java/org/tasks/filters/FilterProvider.java +++ b/app/src/main/java/org/tasks/filters/FilterProvider.java @@ -4,13 +4,13 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Lists.newArrayList; import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread; -import static com.todoroo.astrid.adapter.NavigationDrawerAdapter.REQUEST_PURCHASE; -import static com.todoroo.astrid.adapter.NavigationDrawerAdapter.REQUEST_SETTINGS; import static org.tasks.caldav.CaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT; +import static org.tasks.ui.NavigationDrawerFragment.REQUEST_DONATE; +import static org.tasks.ui.NavigationDrawerFragment.REQUEST_PURCHASE; +import static org.tasks.ui.NavigationDrawerFragment.REQUEST_SETTINGS; import android.content.Context; import android.content.Intent; -import android.net.Uri; import com.google.common.collect.ImmutableList; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterListItem; @@ -31,7 +31,6 @@ import org.tasks.R; import org.tasks.activities.GoogleTaskListSettingsActivity; import org.tasks.activities.TagSettingsActivity; import org.tasks.billing.Inventory; -import org.tasks.billing.PurchaseActivity; import org.tasks.caldav.CaldavCalendarSettingsActivity; import org.tasks.caldav.CaldavFilterExposer; import org.tasks.data.CaldavAccount; @@ -175,14 +174,12 @@ public class FilterProvider { new NavigationDrawerAction( context.getString(R.string.TLA_menu_donate), R.drawable.ic_outline_attach_money_24px, - new Intent(Intent.ACTION_VIEW, Uri.parse("http://tasks.org/donate")), - REQUEST_PURCHASE)); + REQUEST_DONATE)); } else if (!inventory.hasPro()) { items.add( new NavigationDrawerAction( - context.getString(R.string.upgrade_to_pro), + context.getString(R.string.name_your_price), R.drawable.ic_outline_attach_money_24px, - new Intent(context, PurchaseActivity.class), REQUEST_PURCHASE)); } diff --git a/app/src/main/java/org/tasks/filters/NavigationDrawerAction.java b/app/src/main/java/org/tasks/filters/NavigationDrawerAction.java index 41a9fa621..50e93e0c5 100644 --- a/app/src/main/java/org/tasks/filters/NavigationDrawerAction.java +++ b/app/src/main/java/org/tasks/filters/NavigationDrawerAction.java @@ -32,6 +32,10 @@ public class NavigationDrawerAction extends FilterListItem { private NavigationDrawerAction() {} + public NavigationDrawerAction(String listingTitle, int icon, int requestCode) { + this(listingTitle, icon, null, requestCode); + } + public NavigationDrawerAction(String listingTitle, int icon, Intent intent, int requestCode) { this.listingTitle = listingTitle; this.icon = icon; diff --git a/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java b/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java index 8603ba59e..3a04873e4 100644 --- a/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java +++ b/app/src/main/java/org/tasks/injection/DialogFragmentComponent.java @@ -3,6 +3,8 @@ package org.tasks.injection; import dagger.Subcomponent; import org.tasks.activities.CalendarSelectionDialog; import org.tasks.activities.RemoteListSupportPicker; +import org.tasks.billing.NameYourPriceDialog; +import org.tasks.billing.PurchaseDialog; import org.tasks.dialogs.AddAttachmentDialog; import org.tasks.dialogs.ColorPickerDialog; import org.tasks.dialogs.GeofenceDialog; @@ -43,4 +45,8 @@ public interface DialogFragmentComponent { void inject(SeekBarDialog seekBarDialog); void inject(IconPickerDialog iconPickerDialog); + + void inject(PurchaseDialog purchaseDialog); + + void inject(NameYourPriceDialog nameYourPriceDialog); } diff --git a/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java b/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java index efb81cc4d..9f7ece6ed 100755 --- a/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java +++ b/app/src/main/java/org/tasks/locale/ui/activity/TaskerCreateTaskActivity.java @@ -1,5 +1,7 @@ package org.tasks.locale.ui.activity; +import static org.tasks.billing.PurchaseDialog.newPurchaseDialog; + import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -14,7 +16,6 @@ import net.dinglisch.android.tasker.TaskerPlugin; import org.tasks.LocalBroadcastManager; import org.tasks.R; import org.tasks.billing.Inventory; -import org.tasks.billing.PurchaseActivity; import org.tasks.injection.ActivityComponent; import org.tasks.locale.bundle.TaskCreationBundle; import org.tasks.preferences.Preferences; @@ -23,7 +24,7 @@ import org.tasks.ui.MenuColorizer; public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCompatActivity implements Toolbar.OnMenuItemClickListener { - private static final int REQUEST_SUBSCRIPTION = 10101; + private static final String FRAG_TAG_PURCHASE = "frag_tag_purchase"; @Inject Preferences preferences; @Inject Inventory inventory; @@ -84,10 +85,14 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom } if (!inventory.purchasedTasker()) { - startActivityForResult(new Intent(this, PurchaseActivity.class), REQUEST_SUBSCRIPTION); + showPurchaseDialog(); } } + private void showPurchaseDialog() { + newPurchaseDialog().show(getSupportFragmentManager(), FRAG_TAG_PURCHASE); + } + @Override public void onPostCreateWithPreviousResult( final Bundle previousBundle, final String previousBlurb) { @@ -144,7 +149,11 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom } private void save() { - finish(); + if (!inventory.purchasedTasker()) { + showPurchaseDialog(); + } else { + finish(); + } } private void discardButtonClick() { @@ -165,26 +174,11 @@ public final class TaskerCreateTaskActivity extends AbstractFragmentPluginAppCom @Override public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_save: - save(); - return true; - case R.id.menu_help: - startActivity( - new Intent(Intent.ACTION_VIEW).setData(Uri.parse("http://tasks.org/help/tasker"))); - return true; + if (item.getItemId() == R.id.menu_help) { + startActivity( + new Intent(Intent.ACTION_VIEW).setData(Uri.parse("http://tasks.org/help/tasker"))); + return true; } return onOptionsItemSelected(item); } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == REQUEST_SUBSCRIPTION) { - if (!inventory.purchasedTasker()) { - discardButtonClick(); - } - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } } diff --git a/app/src/main/java/org/tasks/preferences/BasicPreferences.java b/app/src/main/java/org/tasks/preferences/BasicPreferences.java index c68818d33..af7d5938d 100644 --- a/app/src/main/java/org/tasks/preferences/BasicPreferences.java +++ b/app/src/main/java/org/tasks/preferences/BasicPreferences.java @@ -34,6 +34,7 @@ import org.tasks.activities.ColorPickerActivity.ColorPalette; import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracking; import org.tasks.analytics.Tracking.Events; +import org.tasks.billing.BillingClient; import org.tasks.billing.Inventory; import org.tasks.billing.PurchaseActivity; import org.tasks.dialogs.DialogBuilder; @@ -81,6 +82,7 @@ public class BasicPreferences extends InjectingPreferenceActivity @Inject Device device; @Inject ActivityPermissionRequestor permissionRequestor; @Inject GoogleAccountManager googleAccountManager; + @Inject BillingClient billingClient; private Bundle result; @@ -221,23 +223,22 @@ public class BasicPreferences extends InjectingPreferenceActivity }); Preference upgradeToPro = findPreference(R.string.upgrade_to_pro); + upgradeToPro.setOnPreferenceClickListener( + p -> { + startActivity(new Intent(this, PurchaseActivity.class)); + return false; + }); if (inventory.hasPro()) { upgradeToPro.setTitle(R.string.manage_subscription); - upgradeToPro.setOnPreferenceClickListener( - p -> { - startActivity( - new Intent( - Intent.ACTION_VIEW, Uri.parse(getString(R.string.manage_subscription_url)))); - return false; - }); - } else { - upgradeToPro.setOnPreferenceClickListener( - p -> { - startActivity(new Intent(this, PurchaseActivity.class)); - return false; - }); + upgradeToPro.setSummary(R.string.manage_subscription_summary); } + findPreference(R.string.refresh_purchases).setOnPreferenceClickListener( + preference -> { + billingClient.queryPurchases(); + return false; + }); + findPreference(R.string.changelog) .setSummary(getString(R.string.version_string, BuildConfig.VERSION_NAME)); @@ -251,7 +252,12 @@ public class BasicPreferences extends InjectingPreferenceActivity //noinspection ConstantConditions if (BuildConfig.FLAVOR.equals("generic")) { - requires(R.string.about, false, R.string.rate_tasks, R.string.upgrade_to_pro); + requires( + R.string.about, + false, + R.string.rate_tasks, + R.string.upgrade_to_pro, + R.string.refresh_purchases); requires(R.string.privacy, false, R.string.p_collect_statistics); } diff --git a/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java b/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java index 161238804..0ccd566bd 100644 --- a/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java +++ b/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java @@ -6,11 +6,13 @@ import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import static org.tasks.LocalBroadcastManager.REFRESH; import static org.tasks.LocalBroadcastManager.REFRESH_LIST; +import static org.tasks.billing.PurchaseDialog.newPurchaseDialog; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -48,10 +50,15 @@ import org.tasks.preferences.AppearancePreferences; public class NavigationDrawerFragment extends InjectingFragment { public static final int FRAGMENT_NAVIGATION_DRAWER = R.id.navigation_drawer; - public static final int REQUEST_NEW_LIST = 4; - public static final int ACTIVITY_REQUEST_NEW_FILTER = 5; - public static final int REQUEST_NEW_GTASK_LIST = 6; - public static final int REQUEST_NEW_CALDAV_COLLECTION = 7; + public static final int REQUEST_NEW_LIST = 10100; + public static final int ACTIVITY_REQUEST_NEW_FILTER = 10101; + public static final int REQUEST_NEW_GTASK_LIST = 10102; + public static final int REQUEST_NEW_CALDAV_COLLECTION = 10103; + public static final int REQUEST_SETTINGS = 10104; + public static final int REQUEST_PURCHASE = 10105; + public static final int REQUEST_DONATE = 10106; + private static final String FRAG_TAG_PURCHASE_DIALOG = "frag_tag_purchase_dialog"; + private final RefreshReceiver refreshReceiver = new RefreshReceiver(); @Inject LocalBroadcastManager localBroadcastManager; @Inject NavigationDrawerAdapter adapter; @@ -84,16 +91,12 @@ public class NavigationDrawerFragment extends InjectingFragment { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == NavigationDrawerAdapter.REQUEST_SETTINGS) { - if (resultCode == Activity.RESULT_OK && data != null) { - if (data.getBooleanExtra(AppearancePreferences.EXTRA_RESTART, false)) { + if (requestCode == REQUEST_SETTINGS) { + if (resultCode == Activity.RESULT_OK) { + if (data != null && data.getBooleanExtra(AppearancePreferences.EXTRA_RESTART, false)) { getActivity().recreate(); } } - } else if (requestCode == NavigationDrawerAdapter.REQUEST_PURCHASE) { - if (resultCode == Activity.RESULT_OK) { - getActivity().recreate(); - } } else if (requestCode == REQUEST_NEW_LIST || requestCode == ACTIVITY_REQUEST_NEW_FILTER || requestCode == REQUEST_NEW_GTASK_LIST @@ -144,10 +147,12 @@ public class NavigationDrawerFragment extends InjectingFragment { openFilter((Filter) item); } else if (item instanceof NavigationDrawerAction) { NavigationDrawerAction action = (NavigationDrawerAction) item; - if (action.requestCode > 0) { - startActivityForResult(action.intent, action.requestCode); + if (action.requestCode == REQUEST_PURCHASE) { + newPurchaseDialog().show(getFragmentManager(), FRAG_TAG_PURCHASE_DIALOG); + } else if (action.requestCode == REQUEST_DONATE) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://tasks.org/donate"))); } else { - startActivity(action.intent); + startActivityForResult(action.intent, action.requestCode); } } } diff --git a/app/src/main/res/layout/activity_purchase.xml b/app/src/main/res/layout/activity_purchase.xml index c43034afa..97555190c 100644 --- a/app/src/main/res/layout/activity_purchase.xml +++ b/app/src/main/res/layout/activity_purchase.xml @@ -1,32 +1,83 @@ - - - - - + android:layout_height="wrap_content"> - + android:padding="@dimen/keyline_first" + android:orientation="vertical"> - + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_purchase.xml b/app/src/main/res/layout/dialog_purchase.xml new file mode 100644 index 000000000..e5c29aacd --- /dev/null +++ b/app/src/main/res/layout/dialog_purchase.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_purchase_cell.xml b/app/src/main/res/layout/dialog_purchase_cell.xml new file mode 100644 index 000000000..197f550c8 --- /dev/null +++ b/app/src/main/res/layout/dialog_purchase_cell.xml @@ -0,0 +1,15 @@ + + diff --git a/app/src/main/res/layout/loading_indicator.xml b/app/src/main/res/layout/loading_indicator.xml deleted file mode 100644 index 6e2823aa3..000000000 --- a/app/src/main/res/layout/loading_indicator.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/app/src/main/res/layout/sku_details_row.xml b/app/src/main/res/layout/sku_details_row.xml deleted file mode 100644 index 086e2b6d0..000000000 --- a/app/src/main/res/layout/sku_details_row.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - -