diff --git a/app/build.gradle b/app/build.gradle
index 60fc72c089..0184a48ba1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -132,6 +132,7 @@ android {
dimension "all"
buildConfigField "boolean", "BETA_RELEASE", "true"
buildConfigField "boolean", "PLAY_STORE_RELEASE", "false"
+ buildConfigField "boolean", "AMAZON_RELEASE", "false"
buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/donate/\""
buildConfigField "String", "CHANGELOG", "\"https://github.com/M66B/FairEmail/releases/\""
buildConfigField "String", "GITHUB_LATEST_API", "\"https://api.github.com/repos/M66B/FairEmail/releases/latest\""
@@ -142,6 +143,7 @@ android {
dimension "all"
buildConfigField "boolean", "BETA_RELEASE", "true"
buildConfigField "boolean", "PLAY_STORE_RELEASE", "false"
+ buildConfigField "boolean", "AMAZON_RELEASE", "false"
buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/donate/\""
buildConfigField "String", "CHANGELOG", "\"https://github.com/M66B/FairEmail/releases/\""
buildConfigField "String", "GITHUB_LATEST_API", "\"https://api.github.com/repos/M66B/FairEmail/releases/latest\""
@@ -153,6 +155,19 @@ android {
//minSdkVersion 23
buildConfigField "boolean", "BETA_RELEASE", "true"
buildConfigField "boolean", "PLAY_STORE_RELEASE", "true"
+ buildConfigField "boolean", "AMAZON_RELEASE", "false"
+ buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/#pro\""
+ buildConfigField "String", "CHANGELOG", "\"\""
+ buildConfigField "String", "GITHUB_LATEST_API", "\"\""
+ buildConfigField "String", "GITHUB_LATEST_URI", "\"\""
+ buildConfigField "String", "GRAVATAR_URI", "\"\""
+ }
+ amazon {
+ dimension "all"
+ //minSdkVersion 23
+ buildConfigField "boolean", "BETA_RELEASE", "true"
+ buildConfigField "boolean", "PLAY_STORE_RELEASE", "false"
+ buildConfigField "boolean", "AMAZON_RELEASE", "true"
buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/#pro\""
buildConfigField "String", "CHANGELOG", "\"\""
buildConfigField "String", "GITHUB_LATEST_API", "\"\""
@@ -166,7 +181,7 @@ android {
// Builds: release, debug
// Flavors: github, fdroid, play
if (variant.buildType.name == "debug" &&
- (flavors.contains("fdroid") || flavors.contains("play"))) {
+ (flavors.contains("fdroid") || flavors.contains("play") || flavors.contains("_amazon"))) {
setIgnore(true)
}
}
@@ -236,7 +251,7 @@ configurations.all {
}
dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
+ //implementation fileTree(dir: 'libs', include: ['*.jar'])
def startup_version = "1.1.0-beta01"
def annotation_version = "1.1.0"
@@ -380,6 +395,9 @@ dependencies {
githubImplementation "com.android.billingclient:billing:$billingclient_version"
playImplementation "com.android.billingclient:billing:$billingclient_version"
+ // https://developer.amazon.com/docs/in-app-purchasing/iap-get-started.html
+ amazonImplementation files('lib/in-app-purchasing-2.0.76.jar')
+
// https://javaee.github.io/javamail/
// https://projects.eclipse.org/projects/ee4j.javamail
// https://mvnrepository.com/artifact/com.sun.mail
diff --git a/app/lib/in-app-purchasing-2.0.76.jar b/app/lib/in-app-purchasing-2.0.76.jar
new file mode 100644
index 0000000000..cd590eb9b9
Binary files /dev/null and b/app/lib/in-app-purchasing-2.0.76.jar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 1923c0aa3b..9b83c3d21e 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -132,3 +132,8 @@
-keep class org.commonmark.** {*;}
-keepnames class io.noties.markwon.** {*;}
-keepnames class org.commonmark.** {*;}
+
+#Amazon IAP
+-dontwarn com.amazon.**
+-keep class com.amazon.** {*;}
+-keepattributes *Annotation*
diff --git a/app/src/amazon/AndroidManifest.xml b/app/src/amazon/AndroidManifest.xml
new file mode 100644
index 0000000000..58bc635abf
--- /dev/null
+++ b/app/src/amazon/AndroidManifest.xml
@@ -0,0 +1,479 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/amazon/java/eu/faircode/email/ActivityBilling.java b/app/src/amazon/java/eu/faircode/email/ActivityBilling.java
new file mode 100644
index 0000000000..773ed9c6c8
--- /dev/null
+++ b/app/src/amazon/java/eu/faircode/email/ActivityBilling.java
@@ -0,0 +1,368 @@
+package eu.faircode.email;
+
+/*
+ This file is part of FairEmail.
+
+ FairEmail is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ FairEmail is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FairEmail. If not, see .
+
+ Copyright 2018-2021 by Marcel Bokhorst (M66B)
+*/
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.preference.PreferenceManager;
+
+import com.amazon.device.iap.PurchasingListener;
+import com.amazon.device.iap.PurchasingService;
+import com.amazon.device.iap.model.FulfillmentResult;
+import com.amazon.device.iap.model.Product;
+import com.amazon.device.iap.model.ProductDataResponse;
+import com.amazon.device.iap.model.PurchaseResponse;
+import com.amazon.device.iap.model.PurchaseUpdatesResponse;
+import com.amazon.device.iap.model.Receipt;
+import com.amazon.device.iap.model.RequestId;
+import com.amazon.device.iap.model.UserDataResponse;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ActivityBilling extends ActivityBase implements PurchasingListener, FragmentManager.OnBackStackChangedListener {
+ private boolean standalone = false;
+ private String currentUserId;
+ private String currentMarketplace;
+ private List listeners = new ArrayList<>();
+
+ static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE";
+ static final String ACTION_PURCHASE_CONSUME = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_CONSUME";
+ static final String ACTION_PURCHASE_ERROR = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_ERROR";
+
+ @Override
+ @SuppressLint("MissingSuperCall")
+ protected void onCreate(Bundle savedInstanceState) {
+ onCreate(savedInstanceState, true);
+ }
+
+ protected void onCreate(Bundle savedInstanceState, boolean standalone) {
+ super.onCreate(savedInstanceState);
+
+ this.standalone = standalone;
+
+ if (standalone) {
+ setContentView(R.layout.activity_billing);
+
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro");
+ fragmentTransaction.commit();
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ getSupportFragmentManager().addOnBackStackChangedListener(this);
+ }
+
+ if (Helper.isAmazonInstall() || isTesting(this)) {
+ Log.i("IAB start sandbox=" + PurchasingService.IS_SANDBOX_MODE);
+ PurchasingService.registerListener(this.getApplicationContext(), this);
+ }
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ if (getSupportFragmentManager().getBackStackEntryCount() == 0)
+ finish();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (standalone) {
+ LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
+ IntentFilter iff = new IntentFilter();
+ iff.addAction(ACTION_PURCHASE);
+ iff.addAction(ACTION_PURCHASE_CONSUME);
+ iff.addAction(ACTION_PURCHASE_ERROR);
+ lbm.registerReceiver(receiver, iff);
+ }
+
+ update();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ if (standalone) {
+ LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this);
+ lbm.unregisterReceiver(receiver);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @NonNull
+ static String getSkuPro() {
+ return BuildConfig.APPLICATION_ID.replace(".debug", "") + ".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);
+ }
+
+ private static String getResponse(Context context) throws NoSuchAlgorithmException {
+ return Helper.sha256(BuildConfig.APPLICATION_ID.replace(".debug", "") + getChallenge(context));
+ }
+
+ static boolean activatePro(Context context, Uri data) throws NoSuchAlgorithmException {
+ String response = data.getQueryParameter("response");
+ return activatePro(context, response);
+ }
+
+ static boolean activatePro(Context context, String response) throws NoSuchAlgorithmException {
+ String challenge = getChallenge(context);
+ Log.i("IAB challenge=" + challenge);
+ Log.i("IAB response=" + response);
+ String expected = getResponse(context);
+ if (expected.equals(response)) {
+ Log.i("IAB response valid");
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.edit()
+ .putBoolean("pro", true)
+ .putBoolean("play_store", false)
+ .apply();
+
+ WidgetUnified.updateData(context);
+ return true;
+ } else {
+ Log.i("IAB response invalid");
+ return false;
+ }
+ }
+
+ static boolean isPro(Context context) {
+ if (BuildConfig.DEBUG && false)
+ return true;
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean("pro", false);
+ }
+
+ private BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
+ if (ACTION_PURCHASE.equals(intent.getAction()))
+ onPurchase(intent);
+ else if (ACTION_PURCHASE_CONSUME.equals(intent.getAction()))
+ onPurchaseConsume(intent);
+ else if (ACTION_PURCHASE_ERROR.equals(intent.getAction()))
+ onPurchaseError(intent);
+ }
+ }
+ };
+
+ private void onPurchase(Intent intent) {
+ if (Helper.isAmazonInstall() || isTesting(this)) {
+ String skuPro = getSkuPro();
+ Log.i("IAB purchase SKU=" + skuPro);
+ RequestId requestId = PurchasingService.purchase(skuPro);
+ Log.i("IAB request=" + requestId);
+ } else
+ try {
+ Uri uri = Uri.parse(BuildConfig.PRO_FEATURES_URI +
+ "?challenge=" + getChallenge(this) +
+ "&version=" + BuildConfig.VERSION_CODE);
+ Helper.view(this, uri, true);
+ } catch (NoSuchAlgorithmException ex) {
+ Log.unexpectedError(getSupportFragmentManager(), ex);
+ }
+ }
+
+ private void onPurchaseConsume(Intent intent) {
+ PurchasingService.getPurchaseUpdates(true);
+ }
+
+ private void onPurchaseError(Intent intent) {
+ String message = intent.getStringExtra("message");
+ Uri uri = Uri.parse(Helper.SUPPORT_URI);
+ if (!TextUtils.isEmpty(message))
+ uri = uri.buildUpon().appendQueryParameter("message", "IAB: " + message).build();
+ Helper.view(this, uri, true);
+ }
+
+ private void update() {
+ if (Helper.isAmazonInstall() || isTesting(this)) {
+ Log.i("IAB update");
+ PurchasingService.getUserData();
+ PurchasingService.getPurchaseUpdates(true); // TODO: reset?
+ Set skus = new HashSet<>();
+ skus.add(getSkuPro());
+ PurchasingService.getProductData(skus);
+ }
+ }
+
+ @Override
+ public void onUserDataResponse(UserDataResponse response) {
+ Log.i("IAB user data status=" + response.getRequestStatus());
+
+ switch (response.getRequestStatus()) {
+ case SUCCESSFUL:
+ Log.i("IAB user=" + response.getUserData().toString().replace('\n', '|'));
+ currentUserId = response.getUserData().getUserId();
+ currentMarketplace = response.getUserData().getMarketplace();
+ for (IBillingListener listener : listeners)
+ listener.onConnected();
+ break;
+
+ case FAILED:
+ case NOT_SUPPORTED:
+ break;
+ }
+ }
+
+ @Override
+ public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse response) {
+ Log.i("IAB purchase updates status=" + response.getRequestStatus());
+
+ switch (response.getRequestStatus()) {
+ case SUCCESSFUL:
+ for (Receipt receipt : response.getReceipts())
+ handle(receipt);
+
+ if (response.hasMore())
+ PurchasingService.getPurchaseUpdates(false);
+ break;
+
+ case FAILED:
+ break;
+ }
+ }
+
+ @Override
+ public void onProductDataResponse(ProductDataResponse response) {
+ Log.i("IAB product data status=" + response.getRequestStatus());
+
+ switch (response.getRequestStatus()) {
+ case SUCCESSFUL:
+ for (final String sku : response.getUnavailableSkus())
+ Log.i("IAB unavailable sku=" + sku);
+
+ Map products = response.getProductData();
+ for (final String key : products.keySet()) {
+ Product product = products.get(key);
+ Log.i("IAB product=" + product.toString().replace('\n', '|'));
+ if (getSkuPro().equals(product.getSku()))
+ for (IBillingListener listener : listeners)
+ listener.onSkuDetails(product.getSku(), product.getPrice());
+ }
+ break;
+
+ case FAILED:
+ break;
+ }
+ }
+
+ @Override
+ public void onPurchaseResponse(PurchaseResponse response) {
+ Log.i("IAB purchase response status=" + response.getRequestStatus());
+
+ switch (response.getRequestStatus()) {
+ case SUCCESSFUL:
+ handle(response.getReceipt());
+ break;
+
+ case FAILED:
+ break;
+ }
+ }
+
+ private void handle(Receipt receipt) {
+ Log.i("IAB receipt=" + receipt.toString().replace('\n', '|') +
+ " canceled=" + receipt.isCanceled() + "/" + receipt.getCancelDate());
+
+ if (getSkuPro().equals(receipt.getSku())) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ if (receipt.isCanceled()) {
+ prefs.edit().remove("pro").apply();
+ PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.UNAVAILABLE);
+ } else {
+ prefs.edit().putBoolean("pro", true).apply();
+ PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED);
+ }
+
+ WidgetUnified.updateData(this);
+
+ for (IBillingListener listener : listeners)
+ listener.onPurchased(receipt.getSku(), !receipt.isCanceled());
+ }
+ }
+
+ interface IBillingListener {
+ void onConnected();
+
+ void onDisconnected();
+
+ void onSkuDetails(String sku, String price);
+
+ void onPurchasePending(String sku);
+
+ void onPurchased(String sku, boolean purchased);
+
+ void onError(String message);
+ }
+
+ void addBillingListener(final IBillingListener listener, LifecycleOwner owner) {
+ Log.i("IAB adding billing listener=" + listener);
+ listeners.add(listener);
+
+ update();
+
+ owner.getLifecycle().addObserver(new LifecycleObserver() {
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ public void onDestroyed() {
+ Log.i("IAB removing billing listener=" + listener);
+ listeners.remove(listener);
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java
index 0584285c45..4e1a78d038 100644
--- a/app/src/main/java/eu/faircode/email/Helper.java
+++ b/app/src/main/java/eu/faircode/email/Helper.java
@@ -471,6 +471,10 @@ public class Helper {
return BuildConfig.PLAY_STORE_RELEASE;
}
+ static boolean isAmazonInstall() {
+ return BuildConfig.AMAZON_RELEASE;
+ }
+
static boolean hasPlayStore(Context context) {
if (hasPlayStore == null)
try {