diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java
index 39a06b3ca2..e595fb28da 100644
--- a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java
+++ b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java
@@ -122,6 +122,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
private SwitchCompat swAuthNtlm;
private SwitchCompat swAuthSasl;
private SwitchCompat swExactAlarms;
+ private SwitchCompat swTestIab;
private TextView tvProcessors;
private TextView tvMemoryClass;
private TextView tvMemoryUsage;
@@ -147,7 +148,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
"protocol", "debug", "log_level",
"use_modseq", "perform_expunge",
"auth_plain", "auth_login", "auth_ntlm", "auth_sasl",
- "exact_alarms"
+ "exact_alarms", "test_iab"
};
private final static String[] RESET_QUESTIONS = new String[]{
@@ -229,6 +230,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
swAuthNtlm = view.findViewById(R.id.swAuthNtlm);
swAuthSasl = view.findViewById(R.id.swAuthSasl);
swExactAlarms = view.findViewById(R.id.swExactAlarms);
+ swTestIab = view.findViewById(R.id.swTestIab);
tvProcessors = view.findViewById(R.id.tvProcessors);
tvMemoryClass = view.findViewById(R.id.tvMemoryClass);
tvMemoryUsage = view.findViewById(R.id.tvMemoryUsage);
@@ -605,6 +607,13 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
}
});
+ swTestIab.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
+ prefs.edit().putBoolean("test_iab", checked).apply();
+ }
+ });
+
btnGC.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -821,6 +830,8 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
setLastCleanup(prefs.getLong("last_cleanup", -1));
+ swTestIab.setVisibility(BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
+
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
return view;
@@ -991,6 +1002,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
swAuthNtlm.setChecked(prefs.getBoolean("auth_ntlm", true));
swAuthSasl.setChecked(prefs.getBoolean("auth_sasl", true));
swExactAlarms.setChecked(prefs.getBoolean("exact_alarms", false));
+ swTestIab.setChecked(prefs.getBoolean("test_iab", false));
tvProcessors.setText(getString(R.string.title_advanced_processors, Runtime.getRuntime().availableProcessors()));
diff --git a/app/src/main/java/eu/faircode/email/FragmentPro.java b/app/src/main/java/eu/faircode/email/FragmentPro.java
index dcf0274063..0be85a230a 100644
--- a/app/src/main/java/eu/faircode/email/FragmentPro.java
+++ b/app/src/main/java/eu/faircode/email/FragmentPro.java
@@ -62,7 +62,7 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
private TextView tvPriceHint;
private TextView tvFamilyHint;
private TextView tvRestoreHint;
- private Button btnCheck;
+ private Button btnConsume;
private static final int HIDE_BANNER = 3; // weeks
@@ -90,12 +90,13 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
tvFamilyHint = view.findViewById(R.id.tvFamilyHint);
tvRestoreHint = view.findViewById(R.id.tvRestoreHint);
- btnCheck = view.findViewById(R.id.btnCheck);
+ btnConsume = view.findViewById(R.id.btnConsume);
btnBackup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- startActivity(new Intent(getContext(), ActivitySetup.class).putExtra("navigate", true));
+ startActivity(new Intent(getContext(), ActivitySetup.class)
+ .putExtra("navigate", true));
}
});
btnBackup.setVisibility(View.GONE);
@@ -129,8 +130,8 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
btnPurchase.setOnClickListener(new View.OnClickListener() {
@Override
- public void onClick(View view) {
- LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
+ public void onClick(View v) {
+ LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(v.getContext());
lbm.sendBroadcast(new Intent(ActivityBilling.ACTION_PURCHASE));
}
});
@@ -161,11 +162,11 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
}
});
- btnCheck.setOnClickListener(new View.OnClickListener() {
+ btnConsume.setOnClickListener(new View.OnClickListener() {
@Override
- public void onClick(View view) {
- LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext());
- lbm.sendBroadcast(new Intent(ActivityBilling.ACTION_PURCHASE_CHECK));
+ public void onClick(View v) {
+ LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(v.getContext());
+ lbm.sendBroadcast(new Intent(ActivityBilling.ACTION_PURCHASE_CONSUME));
}
});
@@ -175,8 +176,8 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
btnPurchase.setEnabled(!Helper.isPlayStoreInstall());
tvPrice.setText(null);
tvRestoreHint.setVisibility(Helper.isPlayStoreInstall() || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
- btnCheck.setEnabled(false);
- btnCheck.setVisibility(Helper.isPlayStoreInstall() && debug ? View.VISIBLE : View.GONE);
+ btnConsume.setEnabled(false);
+ btnConsume.setVisibility(ActivityBilling.isTesting(getContext()) ? View.VISIBLE : View.GONE);
return view;
}
@@ -188,12 +189,10 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
addBillingListener(new ActivityBilling.IBillingListener() {
@Override
public void onConnected() {
- btnCheck.setEnabled(true);
}
@Override
public void onDisconnected() {
- btnCheck.setEnabled(false);
}
@Override
@@ -213,10 +212,11 @@ public class FragmentPro extends FragmentBase implements SharedPreferences.OnSha
}
@Override
- public void onPurchased(String sku) {
+ public void onPurchased(String sku, boolean purchased) {
if (ActivityBilling.getSkuPro().equals(sku)) {
- btnPurchase.setEnabled(false);
+ btnPurchase.setEnabled(!purchased);
tvPending.setVisibility(View.GONE);
+ btnConsume.setEnabled(purchased);
}
}
diff --git a/app/src/main/res/layout/fragment_options_misc.xml b/app/src/main/res/layout/fragment_options_misc.xml
index 0084ffb50b..73d65c7cd7 100644
--- a/app/src/main/res/layout/fragment_options_misc.xml
+++ b/app/src/main/res/layout/fragment_options_misc.xml
@@ -678,6 +678,17 @@
app:layout_constraintTop_toBottomOf="@id/swAuthSasl"
app:switchPadding="12dp" />
+
+
+ app:layout_constraintTop_toBottomOf="@id/swTestIab" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ed94fdf112..b95f14daac 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -568,6 +568,7 @@
NTLM
SASL
Use exact timers
+ Test IAB
Processors: %1$d
Memory class: %1$s/%2$s Total: %3$s
Memory usage: %1$s/%2$s Native: %3$s
diff --git a/app/src/play/java/eu/faircode/email/ActivityBilling.java b/app/src/play/java/eu/faircode/email/ActivityBilling.java
index 32b5260745..8d91df04a5 100644
--- a/app/src/play/java/eu/faircode/email/ActivityBilling.java
+++ b/app/src/play/java/eu/faircode/email/ActivityBilling.java
@@ -31,7 +31,6 @@ import android.os.Handler;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Base64;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -53,8 +52,6 @@ import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
-import com.android.billingclient.api.PurchaseHistoryRecord;
-import com.android.billingclient.api.PurchaseHistoryResponseListener;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
@@ -75,9 +72,10 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
private List listeners = new ArrayList<>();
static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE";
- static final String ACTION_PURCHASE_CHECK = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_CHECK";
+ static final String ACTION_PURCHASE_CONSUME = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_CONSUME";
static final String ACTION_PURCHASE_ERROR = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_ERROR";
+ private static final String SKU_TEST = "android.test.purchased";
private final static long MAX_SKU_CACHE_DURATION = 24 * 3600 * 1000L; // milliseconds
private final static long MAX_SKU_NOACK_DURATION = 24 * 3600 * 1000L; // milliseconds
@@ -102,7 +100,7 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
getSupportFragmentManager().addOnBackStackChangedListener(this);
}
- if (Helper.isPlayStoreInstall()) {
+ if (Helper.isPlayStoreInstall() || isTesting(this)) {
Log.i("IAB start");
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases()
@@ -125,7 +123,7 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
IntentFilter iff = new IntentFilter();
iff.addAction(ACTION_PURCHASE);
- iff.addAction(ACTION_PURCHASE_CHECK);
+ iff.addAction(ACTION_PURCHASE_CONSUME);
iff.addAction(ACTION_PURCHASE_ERROR);
lbm.registerReceiver(receiver, iff);
@@ -152,11 +150,16 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
@NonNull
static String getSkuPro() {
if (BuildConfig.DEBUG)
- return "android.test.purchased";
+ return SKU_TEST;
else
return BuildConfig.APPLICATION_ID + ".pro";
}
+ static boolean isTesting(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ return (BuildConfig.DEBUG && prefs.getBoolean("test_iab", false));
+ }
+
private static String getChallenge(Context context) throws NoSuchAlgorithmException {
String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
return Helper.sha256(android_id);
@@ -206,8 +209,8 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
if (ACTION_PURCHASE.equals(intent.getAction()))
onPurchase(intent);
- else if (ACTION_PURCHASE_CHECK.equals(intent.getAction()))
- onPurchaseCheck(intent);
+ else if (ACTION_PURCHASE_CONSUME.equals(intent.getAction()))
+ onPurchaseConsume(intent);
else if (ACTION_PURCHASE_ERROR.equals(intent.getAction()))
onPurchaseError(intent);
}
@@ -215,7 +218,7 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
};
private void onPurchase(Intent intent) {
- if (Helper.isPlayStoreInstall()) {
+ if (Helper.isPlayStoreInstall() || isTesting(this)) {
String sku = getSkuPro();
Log.i("IAB purchase SKU=" + sku);
@@ -255,21 +258,13 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
}
}
- private void onPurchaseCheck(Intent intent) {
- billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() {
- @Override
- public void onPurchaseHistoryResponse(@NonNull BillingResult result, List records) {
- if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
- for (PurchaseHistoryRecord record : records)
- Log.i("IAB history=" + record.toString());
-
- queryPurchases();
-
- ToastEx.makeText(ActivityBilling.this, R.string.title_setup_done, Toast.LENGTH_LONG).show();
- } else
- reportError(result, "IAB history");
- }
- });
+ private void onPurchaseConsume(Intent intent) {
+ Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
+ if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ for (Purchase purchase : result.getPurchasesList())
+ consumePurchase(purchase);
+ } else
+ reportError(result.getBillingResult(), "IAB onPurchaseConsume");
}
private void onPurchaseError(Intent intent) {
@@ -342,7 +337,7 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
void onPurchasePending(String sku);
- void onPurchased(String sku);
+ void onPurchased(String sku, boolean purchased);
void onError(String message);
}
@@ -396,14 +391,9 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
" time=" + new Date(time));
Log.i("IAB json=" + purchase.getOriginalJson());
- //if (new Date().getTime() - purchase.getPurchaseTime() > 3 * 60 * 1000L) {
- // consumePurchase(purchase);
- // continue;
- //}
-
for (IBillingListener listener : listeners)
if (isPurchaseValid(purchase))
- listener.onPurchased(purchase.getSku());
+ listener.onPurchased(purchase.getSku(), true);
else
listener.onPurchasePending(purchase.getSku());
@@ -414,7 +404,8 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(publicKey);
sig.update(purchase.getOriginalJson().getBytes());
- if (sig.verify(Base64.decode(purchase.getSignature(), Base64.DEFAULT))) {
+ if (SKU_TEST.equals(purchase.getSku()) ||
+ sig.verify(Base64.decode(purchase.getSignature(), Base64.DEFAULT))) {
Log.i("IAB valid signature");
if (getSkuPro().equals(purchase.getSku())) {
if (isPurchaseValid(purchase)) {
@@ -466,14 +457,17 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
}
private void consumePurchase(final Purchase purchase) {
- Log.i("IAB SKU=" + purchase.getSku() + " consuming");
+ Log.i("IAB consuming SKU=" + purchase.getSku());
ConsumeParams params = ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.consumeAsync(params, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(@NonNull BillingResult result, @NonNull String purchaseToken) {
- if (result.getResponseCode() != BillingClient.BillingResponseCode.OK)
+ if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) {
+ for (IBillingListener listener : listeners)
+ listener.onPurchased(purchase.getSku(), false);
+ } else
reportError(result, "IAB consumed SKU=" + purchase.getSku());
}
});
@@ -496,7 +490,7 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
editor.apply();
for (IBillingListener listener : listeners)
- listener.onPurchased(purchase.getSku());
+ listener.onPurchased(purchase.getSku(), true);
WidgetUnified.updateData(ActivityBilling.this);
} else {
@@ -521,6 +515,7 @@ public class ActivityBilling extends ActivityBase implements PurchasesUpdatedLis
private boolean isPurchaseValid(Purchase purchase) {
return (isPurchased(purchase) &&
(purchase.isAcknowledged() ||
+ SKU_TEST.equals(purchase.getSku()) ||
purchase.getPurchaseTime() + MAX_SKU_NOACK_DURATION > new Date().getTime()));
}