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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/sku_details_row_header.xml b/app/src/main/res/layout/sku_details_row_header.xml
deleted file mode 100644
index 4c3c938f4..000000000
--- a/app/src/main/res/layout/sku_details_row_header.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_purchase_activity.xml b/app/src/main/res/menu/menu_purchase_activity.xml
deleted file mode 100644
index 1c46ea920..000000000
--- a/app/src/main/res/menu/menu_purchase_activity.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml
index 1f0d26aa9..d65cc17f8 100644
--- a/app/src/main/res/values-bg-rBG/strings.xml
+++ b/app/src/main/res/values-bg-rBG/strings.xml
@@ -484,8 +484,7 @@
Home set не е намерен
Свързването е неуспешно
Само при неограничени връзки
- Актуализация
- Обнови към pro
+ Обнови към pro
Управление на абонамент
Обнови покупки
Подписан
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1b8339410..6bac2053e 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -465,8 +465,7 @@
CalDAV-Home-Set nicht gefunden
Verbindung fehlgeschlagen
Nur bei unbeschränkter Verbindung
- Verbessern
- Pro-Funktionen freischalten
+ Pro-Funktionen freischalten
Abonnements verwalten
Einkäufe aktualisieren
Abonniert
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index d9604e49b..4248bef20 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -471,8 +471,7 @@
Origen no encontrado
La conexión ha fallado
Sólo en conexiones sin cargos
- Actualizar
- Actualizar a profesional
+ Actualizar a profesional
Gestionar suscripción
Actualizar los acquisiciones
Suscribió
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index a41735704..17069eae1 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -486,8 +486,7 @@
Ez da jatorria aurkitu
Konexioak huts egin du
Mugagabeko konexioetan besterik ez
- Aldatu bertsioz
- Aldatu pro bertsiora
+ Aldatu pro bertsiora
Kudeatu harpidetza
Freskatu erosketak
Harpidetuta
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 1347af5c7..2818a5e70 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -456,8 +456,7 @@
Ensemble d\'accueil non trouvé
Connexion échouée
Uniquement sur les connexions illimitées
- Passer pro
- Passer en pro
+ Passer en pro
Gérer les abonnements
Rafraîchir les achats
Abonné
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 23ad64eff..0fc7d6bcc 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -531,5 +531,4 @@
%s m
CalDAV
", "
- Upgrade
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 8cbdf5bc4..b61b24b06 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -480,8 +480,7 @@
Home set non trovato
Connessione fallita
Solo su connessioni non a consumo
- Aggiorna
- Aggiorna a pro
+ Aggiorna a pro
Gestisci abbonamento
Aggiorna acquisti
Abbonato
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index d33ae7fa0..07f99283b 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -477,8 +477,7 @@
קבוצת הבית לא נמצאה
החיבור נכשל
רק בחיבורים ללא חיוב לפי נפח גלישה
- שידרוג
- שידרוג לגירסת pro
+ שידרוג לגירסת pro
נהל מנוי
עדכן רכישות
מנוי רשום
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 5db1ad84c..ba1c56b7e 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -481,8 +481,7 @@
ホームの設定が見つかりません
接続に失敗しました
定額の接続時のみ
- アップグレード
- プロ版にアップグレード
+ プロ版にアップグレード
サブスクリプションの管理
購入を更新
購入済
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index b7d498b67..d77be6c0e 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -484,8 +484,7 @@
CalDAV 홈 설정 없음
연결 실패
요금이 부과되지 않는 접속인 경우에만
- 업그레이드
- 프로 서비스로 업그레이드
+ 프로 서비스로 업그레이드
구매 새로고침
신청함
신청
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index a12cd2e4c..d3a6c251f 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -479,8 +479,7 @@
Kalendorių rinkinio direktorija nerasta
Sujungimas nepavyko
Naudoti tik \"unmetered\" ryšius
- Atnaujinti
- Atnaujinti į pro versiją
+ Atnaujinti į pro versiją
Redaguoti prenumeratą
Atnaujinti pirkimus
Prenumeruoti
diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml
index 6aa5494fd..8b3b03428 100644
--- a/app/src/main/res/values-nb/strings.xml
+++ b/app/src/main/res/values-nb/strings.xml
@@ -486,8 +486,7 @@
Fant ikke CalDAV-\"home set\"
Tilkoblingen gikk ned
Kun for ubegrensede tilkoblinger
- Oppgrader
- Oppgrader til pro
+ Oppgrader til pro
Håndter abonnement
Gjenoppfrisk kjøp
Abonnert
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 1a75c793c..267a1f8d6 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -471,8 +471,7 @@
Home set niet gevonden
Verbinding mislukt
Alleen bij onbeperkte verbindingen
- Opwaardeer
- Opwaarderen naar pro
+ Opwaarderen naar pro
Beheer aanmeldingen
Vernieuw aankopen
Aangemeld
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 18b80f3ee..2a93cf845 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -481,8 +481,7 @@
Conjunto inicial não encontrado
Conexão falhou
Apenas em conexões ilimitadas
- Atualizar
- Atualizar para Pro
+ Atualizar para Pro
Gerenciar subscrição
Atualizar compras
Subscrito
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 2e8068caa..f297db2a8 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -482,8 +482,7 @@
Домашний набор ненайден
Соединение не удалось
Только на метрических соединений
- Обновить
- Обновить в версию Про
+ Обновить в версию Про
Управление подпиской
Обновить покупки
Подписано
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 1b25af18d..15f119ab0 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -480,8 +480,7 @@
Nastavenie Domov nenájdené
Spojenie zlyhalo
Iba na bezplatných pripojeniach
- Nová verzia
- Rozšíriť na verziu pro
+ Rozšíriť na verziu pro
Platená verzia
Obnoviť nákupy
Prihlásený
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index bc6f8eebf..934b4b77f 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -485,8 +485,7 @@
Ev takımı (home set) bulunamadı
Bağlantı başarısız
Yalnızca ölçülmeyen bağlantılarda
- Yükselt
- Pro\'ya yükselt
+ Pro\'ya yükselt
Aboneliği yönet
Satın alımları yenile
Abone olundu
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 917296cd9..20710dcde 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -484,8 +484,7 @@
Домашній список не знайдено
З\'єднання не вдалося
Лише при з\'єднанні без обліку трафіка
- Оновити
- Покращити до Преміум
+ Покращити до Преміум
Управління підписками
Оновити покупки
Підписка
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 282e5a20d..65d5419d8 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -470,8 +470,7 @@
未设置CalDAVHome
连接失败
仅于不计费的连接
- 升级
- 升级至专业版
+ 升级至专业版
刷新购买
已订购
订购
diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml
index f538fa295..20aa7052f 100644
--- a/app/src/main/res/values/keys.xml
+++ b/app/src/main/res/values/keys.xml
@@ -248,7 +248,6 @@
Import JSON
Export
- Night Mismatch
Play Services Error
Upgrade
Task creation
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9cd7db67b..6aa76ec8c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -486,20 +486,28 @@ File %1$s contained %2$s.\n\n
Home set not found
Connection failed
Only on unmetered connections
- Upgrade
Upgrade to pro
+ Tasks needs your support!
Manage subscription
+ Upgrade, downgrade, or cancel your subscription
Refresh purchases
Subscribed
Subscribe
+ Current subscription
+ Restore subscription
+ Downgrade subscription
+ Upgrade subscription
+ Cancel subscription
More info
Owned
Billing unavailable. Make sure your Google Play app
is setup correctly
Billing unavailable. Please check your device.
About
- Tasks is free and open-source software, licensed under the GNU General Public License v3.0
- Additional themes
+ Tasks is libre open-source software, licensed under the GNU General Public License v3.0
+ Tasks is libre open-source software that does not display advertisements or sell your personal information
+ Subscribe now to support development and unlock additional features
+ Additional themes and icons
CalDAV synchronization
Multiple Google Task accounts
Google Places search
@@ -537,4 +545,7 @@ File %1$s contained %2$s.\n\n
Version %s
Invalid backup file
New tasks on top
+ Name your price
+ Monthly
+ Yearly
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 7453bdc2e..f61f1b140 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -164,6 +164,10 @@
android:key="@string/upgrade_to_pro"
android:title="@string/upgrade_to_pro"/>
+
+