Name your own subscription price

pull/848/head
Alex Baker 5 years ago
parent bad32d7455
commit 1c94179adf

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

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

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

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

@ -4,6 +4,7 @@
<string name="play_services_available">play_services_available</string>
<string name="market_url">market://details?id=org.tasks</string>
<string name="p_purchases">purchases</string>
<string name="manage_subscription_url">https://play.google.com/store/account/subscriptions?sku=annual_499&amp;package=org.tasks</string>
<string name="manage_subscription_url">https://play.google.com/store/account/subscriptions?sku=%s&amp;package=org.tasks</string>
<string name="subscription_help_url">https://tasks.org/subscribe</string>
<string name="support_email">play-support@tasks.org</string>
</resources>

@ -505,7 +505,7 @@
<activity
android:name=".billing.PurchaseActivity"
android:theme="@style/Tasks"/>
android:theme="@style/TranslucentDialog"/>
<activity-alias
android:enabled="true"

@ -45,7 +45,7 @@ import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.activities.TagSettingsActivity;
import org.tasks.analytics.Tracker;
import org.tasks.analytics.Tracking;
import org.tasks.billing.Inventory;
import org.tasks.dialogs.SortDialog;
import org.tasks.fragments.CommentBarFragment;
import org.tasks.gtasks.PlayServices;
@ -94,6 +94,7 @@ public class MainActivity extends InjectingAppCompatActivity
@Inject TaskCreator taskCreator;
@Inject PlayServices playServices;
@Inject Toaster toaster;
@Inject Inventory inventory;
@BindView(R.id.drawer_layout)
DrawerLayout drawerLayout;
@ -107,6 +108,7 @@ public class MainActivity extends InjectingAppCompatActivity
private CompositeDisposable disposables;
private NavigationDrawerFragment navigationDrawer;
private int currentNightMode;
private boolean currentPro;
private Filter filter;
private ActionMode actionMode = null;
@ -120,6 +122,7 @@ public class MainActivity extends InjectingAppCompatActivity
getComponent().inject(viewModel);
currentNightMode = getNightMode();
currentPro = inventory.hasPro();
setContentView(R.layout.task_list_activity);
@ -303,8 +306,7 @@ public class MainActivity extends InjectingAppCompatActivity
protected void onResume() {
super.onResume();
if (currentNightMode != getNightMode()) {
tracker.reportEvent(Tracking.Events.NIGHT_MODE_MISMATCH);
if (currentNightMode != getNightMode() || currentPro != inventory.hasPro()) {
recreate();
return;
}

@ -34,9 +34,6 @@ import org.tasks.themes.ThemeCache;
public class NavigationDrawerAdapter extends ListAdapter<FilterListItem, FilterViewHolder> {
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;

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

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

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

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

@ -27,6 +27,7 @@ public class Inventory {
private final LocalBroadcastManager localBroadcastManager;
private Map<String, Purchase> 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));

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

@ -0,0 +1,5 @@
package org.tasks.billing;
public interface OnPurchasesUpdated {
void onPurchasesUpdated();
}

@ -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<SkuDetails> iaps = Collections.emptyList();
private List<SkuDetails> 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<SkuDetails> iaps) {
this.iaps = iaps;
updateSkuDetails();
}
private void onSubscriptionsUpdated(List<SkuDetails> subscriptions) {
this.subscriptions = subscriptions;
updateSkuDetails();
}
private void updateSkuDetails() {
List<SkuRowData> 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")));
}
}

@ -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<Integer, PurchaseHolder> {
private final Activity activity;
private final Locale locale;
private final Callback<Integer> onPriceChanged;
private int selected;
PurchaseAdapter(Activity activity, Locale locale, Callback<Integer> 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<Integer> {
@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;
}
}
}

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

@ -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<Integer> onClick;
private final Locale locale;
@BindView(R.id.price)
TextView textView;
private int price;
PurchaseHolder(Context context, @NonNull View view, Callback<Integer> 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));
}
}

@ -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<RowViewHolder>
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<SkuRowData> data = ImmutableList.of();
SkusAdapter(Context context, Inventory inventory, OnClickHandler onClickHandler) {
this.context = context;
this.inventory = inventory;
this.onClickHandler = onClickHandler;
}
public void setData(List<SkuRowData> data) {
this.data = data;
notifyDataSetChanged();
}
@Override
public @RowTypeDef int getItemViewType(int position) {
return data.isEmpty() ? TYPE_HEADER : data.get(position).getRowType();
}
@Override
@NonNull
public RowViewHolder onCreateViewHolder(@NonNull ViewGroup parent, @RowTypeDef int viewType) {
// Selecting a flat layout for header rows
if (viewType == SkusAdapter.TYPE_HEADER) {
View item =
LayoutInflater.from(parent.getContext())
.inflate(R.layout.sku_details_row_header, parent, false);
return new RowViewHolder(item, null);
} else {
View item =
LayoutInflater.from(parent.getContext()).inflate(R.layout.sku_details_row, parent, false);
return new RowViewHolder(item, 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 {}
}

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,32 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background">
<include layout="@layout/loading_indicator"/>
<include
layout="@layout/toolbar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_gravity="top" />
android:layout_height="wrap_content">
<TextView
android:id="@+id/error_textview"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/card_view_margin"
android:gravity="center"
android:visibility="gone"/>
android:padding="@dimen/keyline_first"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:gravity="center"/>
<ProgressBar
android:id="@+id/screen_wait"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"/>
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="gone"
app:checkedButton="@id/button_monthly"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_monthly"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/monthly"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_annually"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/annually"/>
</com.google.android.material.button.MaterialButtonToggleGroup>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/subscribe"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/button_subscribe"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/unsubscribe"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/button_unsubscribe"
android:visibility="gone"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_more_info"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/button_more_info"/>
</LinearLayout>
</FrameLayout>
</ScrollView>

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/keyline_first"
android:orientation="vertical">
<TextView
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pro_description"/>
<TextView
style="@style/TextAppearance.AppCompat.Body1"
android:paddingTop="@dimen/keyline_first"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pro_subscribe_now"/>
<TextView
android:id="@+id/feature_list"
style="@style/TextAppearance.AppCompat.Body1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</ScrollView>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/price"
android:layout_width="@dimen/icon_picker_size"
android:layout_height="@dimen/icon_picker_size"
android:padding="8dp"
android:textSize="18sp"
android:textAlignment="center"
android:textColor="@color/text_primary"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
tools:ignore="ContentDescription"/>

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

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

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

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

@ -484,8 +484,7 @@
<string name="caldav_home_set_not_found">Home set не е намерен</string>
<string name="network_error">Свързването е неуспешно</string>
<string name="background_sync_unmetered_only">Само при неограничени връзки</string>
<string name="upgrade">Актуализация</string>
<string name="upgrade_to_pro">Обнови към pro</string>
<string name="upgrade_to_pro">Обнови към pro</string>
<string name="manage_subscription">Управление на абонамент</string>
<string name="refresh_purchases">Обнови покупки</string>
<string name="button_subscribed">Подписан</string>

@ -465,8 +465,7 @@
<string name="caldav_home_set_not_found">CalDAV-Home-Set nicht gefunden</string>
<string name="network_error">Verbindung fehlgeschlagen</string>
<string name="background_sync_unmetered_only">Nur bei unbeschränkter Verbindung</string>
<string name="upgrade">Verbessern</string>
<string name="upgrade_to_pro">Pro-Funktionen freischalten</string>
<string name="upgrade_to_pro">Pro-Funktionen freischalten</string>
<string name="manage_subscription">Abonnements verwalten</string>
<string name="refresh_purchases">Einkäufe aktualisieren</string>
<string name="button_subscribed">Abonniert</string>

@ -471,8 +471,7 @@
<string name="caldav_home_set_not_found">Origen no encontrado</string>
<string name="network_error">La conexión ha fallado</string>
<string name="background_sync_unmetered_only">Sólo en conexiones sin cargos</string>
<string name="upgrade">Actualizar</string>
<string name="upgrade_to_pro">Actualizar a profesional</string>
<string name="upgrade_to_pro">Actualizar a profesional</string>
<string name="manage_subscription">Gestionar suscripción</string>
<string name="refresh_purchases">Actualizar los acquisiciones</string>
<string name="button_subscribed">Suscribió</string>

@ -486,8 +486,7 @@
<string name="caldav_home_set_not_found">Ez da jatorria aurkitu</string>
<string name="network_error">Konexioak huts egin du</string>
<string name="background_sync_unmetered_only">Mugagabeko konexioetan besterik ez</string>
<string name="upgrade">Aldatu bertsioz</string>
<string name="upgrade_to_pro">Aldatu pro bertsiora</string>
<string name="upgrade_to_pro">Aldatu pro bertsiora</string>
<string name="manage_subscription">Kudeatu harpidetza</string>
<string name="refresh_purchases">Freskatu erosketak</string>
<string name="button_subscribed">Harpidetuta</string>

@ -456,8 +456,7 @@
<string name="caldav_home_set_not_found">Ensemble d\'accueil non trouvé</string>
<string name="network_error">Connexion échouée</string>
<string name="background_sync_unmetered_only">Uniquement sur les connexions illimitées</string>
<string name="upgrade">Passer pro</string>
<string name="upgrade_to_pro">Passer en pro</string>
<string name="upgrade_to_pro">Passer en pro</string>
<string name="manage_subscription">Gérer les abonnements</string>
<string name="refresh_purchases">Rafraîchir les achats</string>
<string name="button_subscribed">Abonné</string>

@ -531,5 +531,4 @@
<string name="location_radius_meters">%s m</string>
<string name="CalDAV">CalDAV</string>
<string name="list_separator_with_space">", "</string>
<string name="upgrade">Upgrade</string>
</resources>

@ -480,8 +480,7 @@
<string name="caldav_home_set_not_found">Home set non trovato</string>
<string name="network_error">Connessione fallita</string>
<string name="background_sync_unmetered_only">Solo su connessioni non a consumo</string>
<string name="upgrade">Aggiorna</string>
<string name="upgrade_to_pro">Aggiorna a pro</string>
<string name="upgrade_to_pro">Aggiorna a pro</string>
<string name="manage_subscription">Gestisci abbonamento</string>
<string name="refresh_purchases">Aggiorna acquisti</string>
<string name="button_subscribed">Abbonato</string>

@ -477,8 +477,7 @@
<string name="caldav_home_set_not_found">קבוצת הבית לא נמצאה</string>
<string name="network_error">החיבור נכשל</string>
<string name="background_sync_unmetered_only">רק בחיבורים ללא חיוב לפי נפח גלישה</string>
<string name="upgrade">שידרוג</string>
<string name="upgrade_to_pro">שידרוג לגירסת pro</string>
<string name="upgrade_to_pro">שידרוג לגירסת pro</string>
<string name="manage_subscription"> נהל מנוי</string>
<string name="refresh_purchases">עדכן רכישות</string>
<string name="button_subscribed">מנוי רשום</string>

@ -481,8 +481,7 @@
<string name="caldav_home_set_not_found">ホームの設定が見つかりません</string>
<string name="network_error">接続に失敗しました</string>
<string name="background_sync_unmetered_only">定額の接続時のみ</string>
<string name="upgrade">アップグレード</string>
<string name="upgrade_to_pro">プロ版にアップグレード</string>
<string name="upgrade_to_pro">プロ版にアップグレード</string>
<string name="manage_subscription">サブスクリプションの管理</string>
<string name="refresh_purchases">購入を更新</string>
<string name="button_subscribed">購入済</string>

@ -484,8 +484,7 @@
<string name="caldav_home_set_not_found">CalDAV 홈 설정 없음</string>
<string name="network_error">연결 실패</string>
<string name="background_sync_unmetered_only">요금이 부과되지 않는 접속인 경우에만</string>
<string name="upgrade">업그레이드</string>
<string name="upgrade_to_pro">프로 서비스로 업그레이드</string>
<string name="upgrade_to_pro">프로 서비스로 업그레이드</string>
<string name="refresh_purchases">구매 새로고침</string>
<string name="button_subscribed">신청함</string>
<string name="button_subscribe">신청</string>

@ -479,8 +479,7 @@
<string name="caldav_home_set_not_found">Kalendorių rinkinio direktorija nerasta</string>
<string name="network_error">Sujungimas nepavyko</string>
<string name="background_sync_unmetered_only">Naudoti tik \"unmetered\" ryšius</string>
<string name="upgrade">Atnaujinti</string>
<string name="upgrade_to_pro">Atnaujinti į pro versiją</string>
<string name="upgrade_to_pro">Atnaujinti į pro versiją</string>
<string name="manage_subscription">Redaguoti prenumeratą</string>
<string name="refresh_purchases">Atnaujinti pirkimus</string>
<string name="button_subscribed">Prenumeruoti</string>

@ -486,8 +486,7 @@
<string name="caldav_home_set_not_found">Fant ikke CalDAV-\"home set\"</string>
<string name="network_error">Tilkoblingen gikk ned</string>
<string name="background_sync_unmetered_only">Kun for ubegrensede tilkoblinger</string>
<string name="upgrade">Oppgrader</string>
<string name="upgrade_to_pro">Oppgrader til pro</string>
<string name="upgrade_to_pro">Oppgrader til pro</string>
<string name="manage_subscription">Håndter abonnement</string>
<string name="refresh_purchases">Gjenoppfrisk kjøp</string>
<string name="button_subscribed">Abonnert</string>

@ -471,8 +471,7 @@
<string name="caldav_home_set_not_found">Home set niet gevonden</string>
<string name="network_error">Verbinding mislukt</string>
<string name="background_sync_unmetered_only">Alleen bij onbeperkte verbindingen</string>
<string name="upgrade">Opwaardeer</string>
<string name="upgrade_to_pro">Opwaarderen naar pro</string>
<string name="upgrade_to_pro">Opwaarderen naar pro</string>
<string name="manage_subscription">Beheer aanmeldingen</string>
<string name="refresh_purchases">Vernieuw aankopen</string>
<string name="button_subscribed">Aangemeld</string>

@ -481,8 +481,7 @@
<string name="caldav_home_set_not_found">Conjunto inicial não encontrado</string>
<string name="network_error">Conexão falhou</string>
<string name="background_sync_unmetered_only">Apenas em conexões ilimitadas</string>
<string name="upgrade">Atualizar</string>
<string name="upgrade_to_pro">Atualizar para Pro</string>
<string name="upgrade_to_pro">Atualizar para Pro</string>
<string name="manage_subscription">Gerenciar subscrição</string>
<string name="refresh_purchases">Atualizar compras</string>
<string name="button_subscribed">Subscrito</string>

@ -482,8 +482,7 @@
<string name="caldav_home_set_not_found">Домашний набор ненайден</string>
<string name="network_error">Соединение не удалось</string>
<string name="background_sync_unmetered_only">Только на метрических соединений</string>
<string name="upgrade">Обновить</string>
<string name="upgrade_to_pro">Обновить в версию Про</string>
<string name="upgrade_to_pro">Обновить в версию Про</string>
<string name="manage_subscription">Управление подпиской</string>
<string name="refresh_purchases">Обновить покупки</string>
<string name="button_subscribed">Подписано</string>

@ -480,8 +480,7 @@
<string name="caldav_home_set_not_found">Nastavenie Domov nenájdené</string>
<string name="network_error">Spojenie zlyhalo</string>
<string name="background_sync_unmetered_only">Iba na bezplatných pripojeniach</string>
<string name="upgrade">Nová verzia</string>
<string name="upgrade_to_pro">Rozšíriť na verziu pro</string>
<string name="upgrade_to_pro">Rozšíriť na verziu pro</string>
<string name="manage_subscription">Platená verzia</string>
<string name="refresh_purchases">Obnoviť nákupy</string>
<string name="button_subscribed">Prihlásený</string>

@ -485,8 +485,7 @@
<string name="caldav_home_set_not_found">Ev takımı (home set) bulunamadı</string>
<string name="network_error">Bağlantı başarısız</string>
<string name="background_sync_unmetered_only">Yalnızca ölçülmeyen bağlantılarda</string>
<string name="upgrade">Yükselt</string>
<string name="upgrade_to_pro">Pro\'ya yükselt</string>
<string name="upgrade_to_pro">Pro\'ya yükselt</string>
<string name="manage_subscription">Aboneliği yönet</string>
<string name="refresh_purchases">Satın alımları yenile</string>
<string name="button_subscribed">Abone olundu</string>

@ -484,8 +484,7 @@
<string name="caldav_home_set_not_found">Домашній список не знайдено</string>
<string name="network_error">З\'єднання не вдалося</string>
<string name="background_sync_unmetered_only">Лише при з\'єднанні без обліку трафіка</string>
<string name="upgrade">Оновити</string>
<string name="upgrade_to_pro">Покращити до Преміум</string>
<string name="upgrade_to_pro">Покращити до Преміум</string>
<string name="manage_subscription">Управління підписками</string>
<string name="refresh_purchases">Оновити покупки</string>
<string name="button_subscribed">Підписка</string>

@ -470,8 +470,7 @@
<string name="caldav_home_set_not_found">未设置CalDAVHome</string>
<string name="network_error">连接失败</string>
<string name="background_sync_unmetered_only">仅于不计费的连接</string>
<string name="upgrade">升级</string>
<string name="upgrade_to_pro">升级至专业版</string>
<string name="upgrade_to_pro">升级至专业版</string>
<string name="refresh_purchases">刷新购买</string>
<string name="button_subscribed">已订购</string>
<string name="button_subscribe">订购</string>

@ -248,7 +248,6 @@
<string name="tracking_action_import_json">Import JSON</string>
<string name="tracking_action_export">Export</string>
<string name="tracking_event_night_mode_mismatch">Night Mismatch</string>
<string name="tracking_event_play_services_error">Play Services Error</string>
<string name="tracking_event_upgrade">Upgrade</string>
<string name="tracking_event_task_creation">Task creation</string>

@ -486,20 +486,28 @@ File %1$s contained %2$s.\n\n
<string name="caldav_home_set_not_found">Home set not found</string>
<string name="network_error">Connection failed</string>
<string name="background_sync_unmetered_only">Only on unmetered connections</string>
<string name="upgrade">Upgrade</string>
<string name="upgrade_to_pro">Upgrade to pro</string>
<string name="pro_support_development">Tasks needs your support!</string>
<string name="manage_subscription">Manage subscription</string>
<string name="manage_subscription_summary">Upgrade, downgrade, or cancel your subscription</string>
<string name="refresh_purchases">Refresh purchases</string>
<string name="button_subscribed">Subscribed</string>
<string name="button_subscribe">Subscribe</string>
<string name="button_current_subscription">Current subscription</string>
<string name="button_restore_subscription">Restore subscription</string>
<string name="button_downgrade">Downgrade subscription</string>
<string name="button_upgrade">Upgrade subscription</string>
<string name="button_unsubscribe">Cancel subscription</string>
<string name="button_more_info">More info</string>
<string name="owned">Owned</string>
<string name="error_billing_unavailable">Billing unavailable. Make sure your Google Play app
is setup correctly</string>
<string name="error_billing_default">Billing unavailable. Please check your device.</string>
<string name="about">About</string>
<string name="license_summary">Tasks is free and open-source software, licensed under the GNU General Public License v3.0</string>
<string name="themes">Additional themes</string>
<string name="license_summary">Tasks is libre open-source software, licensed under the GNU General Public License v3.0</string>
<string name="pro_description">Tasks is libre open-source software that does not display advertisements or sell your personal information</string>
<string name="pro_subscribe_now">Subscribe now to support development and unlock additional features</string>
<string name="themes">Additional themes and icons</string>
<string name="pro_caldav_sync">CalDAV synchronization</string>
<string name="pro_multiple_google_task_accounts">Multiple Google Task accounts</string>
<string name="pro_google_places_search">Google Places search</string>
@ -537,4 +545,7 @@ File %1$s contained %2$s.\n\n
<string name="version_string">Version %s</string>
<string name="invalid_backup_file">Invalid backup file</string>
<string name="google_tasks_add_to_top">New tasks on top</string>
<string name="name_your_price">Name your price</string>
<string name="monthly">Monthly</string>
<string name="annually">Yearly</string>
</resources>

@ -164,6 +164,10 @@
android:key="@string/upgrade_to_pro"
android:title="@string/upgrade_to_pro"/>
<Preference
android:key="@string/refresh_purchases"
android:title="@string/refresh_purchases" />
</PreferenceCategory>
<PreferenceCategory

Loading…
Cancel
Save