/* original implementation is https://github.com/PureWriter/ToastCompat modification: - convert from Java to Kotlin - because Android 11's Toast.getView() returns null, we need to support view==null case. - only in case of API 25 device, we have to create custom context and set it to view. */ package me.drakeet.support.toast import android.annotation.SuppressLint import android.content.Context import android.content.ContextWrapper import android.content.res.Resources import android.os.Build import android.util.Log import android.view.Display import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.WindowManager.BadTokenException import android.widget.Toast import androidx.annotation.StringRes fun interface BadTokenListener { fun onBadTokenCaught(toast : Toast) } internal class SafeToastContext(base : Context, private val toast : Toast) : ContextWrapper(base) { companion object { private const val TAG = "WindowManagerWrapper" } private var badTokenListener : BadTokenListener? = null fun setBadTokenListener(badTokenListener : BadTokenListener?) { this.badTokenListener = badTokenListener } override fun getApplicationContext() : Context = ApplicationContextWrapper(baseContext.applicationContext) private inner class ApplicationContextWrapper(base : Context) : ContextWrapper(base) { override fun getSystemService(name : String) : Any? = if(WINDOW_SERVICE == name) { // noinspection ConstantConditions WindowManagerWrapper(baseContext.getSystemService(name) as WindowManager) } else { super.getSystemService(name) } } private inner class WindowManagerWrapper(private val base : WindowManager) : WindowManager { @Suppress("DEPRECATION") override fun getDefaultDisplay() : Display? = base.defaultDisplay override fun removeViewImmediate(view : View) = base.removeViewImmediate(view) override fun updateViewLayout(view : View, params : ViewGroup.LayoutParams) = base.updateViewLayout(view, params) override fun removeView(view : View) = base.removeView(view) override fun addView(view : View, params : ViewGroup.LayoutParams) { try { Log.d(TAG, "WindowManager's addView(view, params) has been hooked.") base.addView(view, params) } catch(e : BadTokenException) { e.message?.let { Log.i(TAG, it) } badTokenListener?.onBadTokenCaught(toast) } catch(throwable : Throwable) { Log.e(TAG, "[addView]", throwable) } } } } class ToastCompat private constructor( context : Context, private val base : Toast ) : Toast(context) { companion object { private fun setContextCompat(view : View?, contextCreator : () -> Context) { if(view != null && Build.VERSION.SDK_INT == 25) { try { val field = View::class.java.getDeclaredField("mContext") field.isAccessible = true field[view] = contextCreator() } catch(throwable : Throwable) { throwable.printStackTrace() } } } /** * Make a standard toast that just contains a text view with the text from a resource. * * @param context The context to use. Usually your [android.app.Application] * or [android.app.Activity] object. * @param resId The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message. Either [.LENGTH_SHORT] or * [.LENGTH_LONG] * @throws Resources.NotFoundException if the resource can't be found. */ @Suppress("unused") fun makeText(context : Context, @StringRes resId : Int, duration : Int) = makeText(context, context.resources.getText(resId), duration) /** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your [android.app.Application] * or [android.app.Activity] object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either [.LENGTH_SHORT] or * [.LENGTH_LONG] */ @SuppressLint("ShowToast") @Suppress("DEPRECATION") fun makeText(context : Context, text : CharSequence?, duration : Int) : ToastCompat { // We cannot pass the SafeToastContext to Toast.makeText() because // the View will unwrap the base context and we are in vain. val base = Toast.makeText(context, text, duration) setContextCompat(base.view) { SafeToastContext(context, base) } return ToastCompat(context, base) } } @Suppress("DEPRECATION") fun setBadTokenListener(listener : BadTokenListener?) : ToastCompat { (base.view?.context as? SafeToastContext) ?.setBadTokenListener(listener) return this } @Suppress("DEPRECATION") override fun setView(view : View) { base.view = view setContextCompat(base.view) { SafeToastContext(view.context, base) } } @Suppress("DEPRECATION") override fun getView() : View? = base.view override fun show() = base.show() override fun setDuration(duration : Int) { base.duration = duration } override fun setGravity(gravity : Int, xOffset : Int, yOffset : Int) = base.setGravity(gravity, xOffset, yOffset) override fun setMargin(horizontalMargin : Float, verticalMargin : Float) = base.setMargin(horizontalMargin, verticalMargin) override fun setText(resId : Int) = base.setText(resId) override fun setText(s : CharSequence) = base.setText(s) override fun getHorizontalMargin() = base.horizontalMargin override fun getVerticalMargin() = base.verticalMargin override fun getDuration() = base.duration override fun getGravity() = base.gravity override fun getXOffset() = base.xOffset override fun getYOffset() = base.yOffset }