Will Toast.makeText cause app crash when I invoke it in asynchronous thread in Android Studio?

Viewed 773

The following code is about Google in-app payment based office sample code.

I have to test it in a real mobile phone because it' hard to test Google in-app payment in Android Studio emulator.

Unfortunately, the app crashed in real mobile phone when I open the FragmentBuy, I hardly get the error information about the crash.

I checked my code again and agin, I guest there are two place which cause crash, one is private fun processPurchases(purchasesResult: Set<Purchase>) , another is private fun acknowledgeNonConsumablePurchasesAsync(nonConsumables: List<Purchase>).

1: They lanuch Toast.makeTex in in asynchronous thread, is it reason of crash?

2: Which one is correct between Billing.getInstance(requireActivity(), getString(R.string.skuRegisterApp)) and Billing.getInstance(requireContext), getString(R.string.skuRegisterApp)) ?

3: Is there other problems with my code ?

FragmentBuy.kt

class FragmentBuy : Fragment() {

    private lateinit var binding: LayoutBuyBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = DataBindingUtil.inflate(
            inflater, R.layout.layout_buy, container, false
        )
   
        val mBilling=Billing.getInstance(requireActivity(), getString(R.string.skuRegisterApp)) // or Billing.getInstance(requireContext), getString(R.string.skuRegisterApp))

        mBilling.initBillingClient()

        binding.btnPurchase.setOnClickListener {
           mBilling.purchaseProduct(requireActivity())
        }

        return binding.root
    }

}

fun Context.toast(msg: String){
    Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
}


fun Fragment.toast(@StringRes resId: Int) {
    toast(requireContext().getString(resId))
}

Billing.kt

class Billing private constructor (private val mContext: Context, private val purchaseItem :String)
        :PurchasesUpdatedListener, BillingClientStateListener   {

     private lateinit var playStoreBillingClient: BillingClient
     private val mapSkuDetails = mutableMapOf<String,SkuDetails>()

     fun initBillingClient() {
         playStoreBillingClient = BillingClient
             .newBuilder(mContext)
             .enablePendingPurchases() // required or app will crash
             .setListener(this)
             .build()

         if (!playStoreBillingClient.isReady) {
             playStoreBillingClient.startConnection(this)
         }
     }

    override fun onBillingSetupFinished(billingResult: BillingResult) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                querySkuDetailsAsync(BillingClient.SkuType.INAPP, listOf(purchaseItem) )
                queryAndProcessPurchasesAsync()
                mContext.toast(R.string.msgBillINIOK)
            }
            ...
        }
    }

    private fun querySkuDetailsAsync(@BillingClient.SkuType skuType: String, skuList: List<String>) {
        val params = SkuDetailsParams.newBuilder().setSkusList(skuList).setType(skuType).build()
        playStoreBillingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
            when (billingResult.responseCode) {
                BillingClient.BillingResponseCode.OK -> {
                    if (skuDetailsList.orEmpty().isNotEmpty()) {
                        skuDetailsList?.forEach {
                            mapSkuDetails.put(it.sku,it)
                        }
                    }
                }
                '''
            }
        }
    }

    private fun queryAndProcessPurchasesAsync() {
        val purchasesResult = HashSet<Purchase>()
        var result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
        result?.purchasesList?.apply { purchasesResult.addAll(this) }

        processPurchases(purchasesResult)
    }


    private fun processPurchases(purchasesResult: Set<Purchase>): Job {
        return CoroutineScope(Job() + Dispatchers.IO).launch {
            val validPurchases = HashSet<Purchase>(purchasesResult.size)
            purchasesResult.forEach { purchase ->
                if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                    if (purchase.sku.equals(purchaseItem)) {
                        if (isSignatureValid(purchase)) {
                            validPurchases.add(purchase)
                            setIsRegisteredAppAsTrue(mContext)
                            mContext.toast(R.string.msgOrderOK)  //Will it be crash?
                        }
                    }
                }
                ...

            }
            acknowledgeNonConsumablePurchasesAsync(validPurchases.toList())
        }
    }


    private fun acknowledgeNonConsumablePurchasesAsync(nonConsumables: List<Purchase>) {
        nonConsumables.forEach { purchase ->
            val params = AcknowledgePurchaseParams.newBuilder()
                           .setPurchaseToken(purchase
                           .purchaseToken).build()

            playStoreBillingClient.acknowledgePurchase(params) { billingResult ->
                when (billingResult.responseCode) {
                    BillingClient.BillingResponseCode.OK -> {
                         mContext.toast(R.string.msgOrderAcknowledgeOK)     //Will it be crash?
                    }
                    else -> mContext.toast(R.string.msgOrderAcknowledgeError, billingResult.debugMessage)
                }
            }
        }
    }



    //执行playStoreBillingClient.launchBillingFlow(...)后 launch
    override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
        when (billingResult.responseCode) {
            BillingClient.BillingResponseCode.OK -> {
                purchases?.apply { processPurchases(this.toSet()) }
            }
            ...
        }
    }


    fun purchaseProduct(activity: Activity) {
        val skuDetails = mapSkuDetails[purchaseItem]
        skuDetails?.let{
            val purchaseParams = BillingFlowParams.newBuilder().setSkuDetails(skuDetails).build()
            playStoreBillingClient.launchBillingFlow(activity, purchaseParams)
        }
    }


    companion object {
        private const val LOG_TAG = "BillingRepository"

        private var INSTANCE: Billing? = null
        fun getInstance(mContext: Context, purchaseItem: String): Billing =
                     INSTANCE ?: synchronized(this) {
                         INSTANCE ?: Billing(mContext, purchaseItem).also { INSTANCE = it }
                     }
    }


}
4 Answers
  1. Yes. You can only show toast messages from the main UI thread. To launch a toask message from any other thread, you can use a Hander like so:
 new Handler(Looper.getMainLooper()).post(() -> {
        Toast.makeText(someContext, "some toast", Toast.LENGTH_SHORT).show();
 });

Also, you have to use activity context, rather than fragment context. To get the activity context from within a fragment, you can use:

getActivity()

So you can do something like this inside your fragment:

 new Handler(Looper.getMainLooper()).post(() -> {
            Toast.makeText(getActivity(), "some toast", Toast.LENGTH_SHORT).show();
 });

Since you are using an extension function, you need to change it to use activity context:

fun Fragment.toast(@StringRes resId: Int) {
    getActivity().toast(requireContext().getString(resId))
}

And then you can use toast() inside your Handler (to show it from a different Thread):

 new Handler(Looper.getMainLooper()).post(() -> {
        toast("some toast")
 });
  1. The first one is more correct, but you should use getString() with your activity's context:

    Billing.getInstance(requireActivity(), getActivity().getString(R.string.skuRegisterApp))

  2. everything else seems fine

to show Toast your Thread must be has MessageQueue, In this case I recommend you using Activity instead of Context and using method runOnUiThread

mActivity.runOnUiThread { 
    // show your Toast
}

For any interface changes call activity.runOnUiThread(runnable).

And with Kotlin call

activity.runOnUiThread{
   toast("Your text") //since you are using extension function.
}

Because the Toast is used by the Handler in Source code。

private static class TN extends ITransientNotification.Stub {
    TN(String packageName, @Nullable Looper looper) {
        //There need the Looper first
        if (looper == null) {
            // Use Looper.myLooper() if looper is not specified.
            looper = Looper.myLooper();
            if (looper == null) {
                throw new RuntimeException(
                        "Can't toast on a thread that has not called Looper.prepare()");
            }
        }
        mHandler = new Handler(looper, null) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case SHOW: {
                        IBinder token = (IBinder) msg.obj;
                        handleShow(token);
                        break;
                    }
                }
            }
        };
    }

    /**
     * schedule handleShow into the right thread
     */
    @Override
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public void show(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        //the show is used Handler
        mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }
}

This is the TN in Toast source code,the reason of showing the Toast is used by Handler, so it needs to used in Main Thread.If we need usd Toast in asynchronous thread,we need to create a Looper.

 new Thread(){
        @Override
        public void run() {
            super.run();
            Looper.prepare();
            Toast.makeText(MainActivity.this,"",Toast.LENGTH_SHORT).show();
            Looper.loop();
        }
    }.start();
Related