From 3af6c67615b5699a252323f3098f7ed7b854c250 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sat, 6 Nov 2021 22:45:54 +0800 Subject: [PATCH 1/2] Update billing library to 4.0.0 Fixes #2985. Also added convenience method to run a Callable in main thread. --- .../filemanager/application/AppConfig.java | 19 +++++++--- .../com/amaze/filemanager/utils/Billing.java | 36 ++++++++++++------- build.gradle | 2 +- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java index 9491204bab..6f7f63399b 100644 --- a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java +++ b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java @@ -21,6 +21,7 @@ package com.amaze.filemanager.application; import java.lang.ref.WeakReference; +import java.util.concurrent.Callable; import org.acra.ACRA; import org.acra.annotation.AcraCore; @@ -65,7 +66,7 @@ reportSenderFactoryClasses = AcraReportSenderFactory.class) public class AppConfig extends GlideApplication { - public static final String TAG = AppConfig.class.getSimpleName(); + private static final String TAG = AppConfig.class.getSimpleName(); private UtilitiesProvider utilsProvider; private RequestQueue requestQueue; @@ -175,14 +176,24 @@ public static void toast(Context context, String message) { } /** - * Run a runnable in the main application thread + * Run a {@link Runnable} in the main application thread * - * @param r Runnable to run + * @param r {@link Runnable} to run */ - public void runInApplicationThread(Runnable r) { + public void runInApplicationThread(@NonNull Runnable r) { Completable.fromRunnable(r).subscribeOn(AndroidSchedulers.mainThread()).subscribe(); } + /** + * Convenience method to run a {@link Callable} in the main application thread. Use when the + * callable's return value is not processed. + * + * @param c {@link Callable} to run + */ + public void runInApplicationThread(@NonNull Callable c) { + Completable.fromCallable(c).subscribeOn(AndroidSchedulers.mainThread()).subscribe(); + } + public static synchronized AppConfig getInstance() { return instance; } diff --git a/app/src/play/java/com/amaze/filemanager/utils/Billing.java b/app/src/play/java/com/amaze/filemanager/utils/Billing.java index d7e910a026..c7cb31e53a 100644 --- a/app/src/play/java/com/amaze/filemanager/utils/Billing.java +++ b/app/src/play/java/com/amaze/filemanager/utils/Billing.java @@ -26,6 +26,8 @@ import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.holders.DonationViewHolder; +import com.amaze.filemanager.application.AppConfig; +import com.amaze.filemanager.databinding.AdapterDonationBinding; import com.amaze.filemanager.ui.activities.superclasses.BasicActivity; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClientStateListener; @@ -38,7 +40,6 @@ import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsParams; -import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -54,7 +55,6 @@ public class Billing extends RecyclerView.Adapter private BasicActivity activity; private List skuList; - private LayoutInflater layoutInflater; private List skuDetails; // create new donations client @@ -62,7 +62,7 @@ public class Billing extends RecyclerView.Adapter /** True if billing service is connected now. */ private boolean isServiceConnected; - public Billing(BasicActivity activity) { + public Billing(@NonNull BasicActivity activity) { this.activity = activity; skuList = new ArrayList<>(); @@ -71,8 +71,6 @@ public Billing(BasicActivity activity) { skuList.add("donations_3"); skuList.add("donations_4"); - layoutInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - billingClient = BillingClient.newBuilder(activity).setListener(this).enablePendingPurchases().build(); initiatePurchaseFlow(); @@ -128,7 +126,8 @@ private void popProductsList(BillingResult response, List skuDetails @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View rootView = layoutInflater.inflate(R.layout.adapter_donation, parent, false); + View rootView = + AdapterDonationBinding.inflate(LayoutInflater.from(this.activity), parent, false).getRoot(); return new DonationViewHolder(rootView); } @@ -201,7 +200,7 @@ private void startServiceConnection(final Runnable executeOnSuccess) { public void onBillingSetupFinished(BillingResult billingResponse) { Log.d( Billing.this.getClass().getSimpleName(), - "Setup finished. Response code: " + billingResponse); + "Setup finished. Response code: " + billingResponse.getResponseCode()); if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) { isServiceConnected = true; @@ -226,11 +225,22 @@ public void destroyBillingInstance() { } private void showPaymentsDialog(final BasicActivity context) { - final MaterialDialog.Builder builder = new MaterialDialog.Builder(context); - builder.title(R.string.donate); - builder.adapter(this, null); - builder.theme(context.getAppTheme().getMaterialDialogTheme()); - builder.cancelListener(dialog -> purchaseProduct.purchaseCancel()); - builder.show(); + /* + * As of Billing library 4.0, all callbacks are running on background thread. + * Need to use AppConfig.runInApplicationThread() for UI interactions + * + * + */ + AppConfig.getInstance() + .runInApplicationThread( + () -> { + final MaterialDialog.Builder builder = new MaterialDialog.Builder(context); + builder.title(R.string.donate); + builder.adapter(this, null); + builder.theme(context.getAppTheme().getMaterialDialogTheme()); + builder.cancelListener(dialog -> purchaseProduct.purchaseCancel()); + builder.show(); + return null; + }); } } diff --git a/build.gradle b/build.gradle index e1b1ff85fd..38eb493f01 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { slf4jVersion = "1.7.25" mockitoVersion = "3.9.0" mockitoKotlinVersion = "3.2.0" - androidBillingVersion = "2.1.0" + androidBillingVersion = "4.0.0" junrarVersion = "7.4.0" zip4jVersion = "2.6.4" espressoVersion = "3.3.0" From e3d47c81f76bff8d071ad9124b185895ecd5e0d2 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 7 Nov 2021 22:53:50 +0800 Subject: [PATCH 2/2] Changes per PR feedback - protection against crash for running debug build - Add emptiness check of skuDetailsList - Toast user of empty product list, and indicate in log if build is DEBUG variants --- app/src/main/res/values/strings.xml | 1 + .../com/amaze/filemanager/utils/Billing.java | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98e1a29b4d..0d2c281a1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -728,5 +728,6 @@ You only need to do this once, until the next time you select a new location for Select by type Select by date Select similar + Error fetching product list from Google Play. diff --git a/app/src/play/java/com/amaze/filemanager/utils/Billing.java b/app/src/play/java/com/amaze/filemanager/utils/Billing.java index c7cb31e53a..bf15f8702c 100644 --- a/app/src/play/java/com/amaze/filemanager/utils/Billing.java +++ b/app/src/play/java/com/amaze/filemanager/utils/Billing.java @@ -24,6 +24,7 @@ import java.util.List; import com.afollestad.materialdialogs.MaterialDialog; +import com.amaze.filemanager.BuildConfig; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.holders.DonationViewHolder; import com.amaze.filemanager.application.AppConfig; @@ -53,6 +54,8 @@ public class Billing extends RecyclerView.Adapter implements PurchasesUpdatedListener { + private static final String TAG = Billing.class.getSimpleName(); + private BasicActivity activity; private List skuList; private List skuDetails; @@ -101,9 +104,18 @@ private void initiatePurchaseFlow() { billingClient.querySkuDetailsAsync( params.build(), (responseCode, skuDetailsList) -> { - // Successfully fetched product details - skuDetails = skuDetailsList; - popProductsList(responseCode, skuDetailsList); + if (skuDetailsList != null && skuDetailsList.size() > 0) { + // Successfully fetched product details + skuDetails = skuDetailsList; + popProductsList(responseCode, skuDetailsList); + } else { + AppConfig.toast(activity, R.string.error_fetching_google_play_product_list); + if (BuildConfig.DEBUG) { + Log.w( + TAG, + "Error fetching product list - looks like you are running a DEBUG build."); + } + } }); }; @@ -133,7 +145,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - if (holder instanceof DonationViewHolder) { + if (holder instanceof DonationViewHolder && skuDetails.size() > 0) { String titleRaw = skuDetails.get(position).getTitle(); ((DonationViewHolder) holder) .TITLE.setText(titleRaw.subSequence(0, titleRaw.lastIndexOf("("))); @@ -198,10 +210,7 @@ private void startServiceConnection(final Runnable executeOnSuccess) { new BillingClientStateListener() { @Override public void onBillingSetupFinished(BillingResult billingResponse) { - Log.d( - Billing.this.getClass().getSimpleName(), - "Setup finished. Response code: " + billingResponse.getResponseCode()); - + Log.d(TAG, "Setup finished. Response code: " + billingResponse.getResponseCode()); if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) { isServiceConnected = true; if (executeOnSuccess != null) {