SubwayTooter-Android-App/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt

177 lines
5.5 KiB
Kotlin

/*
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
}