Merge pull request 'Make Xposed Recovery Menu look better :P' (#1) from kmmiio99o/KettuXposed:master into master

Reviewed-on: https://codeberg.org/cocobo1/KettuXposed/pulls/1
This commit is contained in:
cocobo1 2025-12-19 23:05:36 +01:00
commit 27e4917c34

View file

@ -1,8 +1,25 @@
package io.github.revenge.xposed.modules
import android.animation.AnimatorSet
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.AlertDialog
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.view.animation.DecelerateInterpolator
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.setPadding
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
@ -10,11 +27,12 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage
import io.github.revenge.xposed.Module
import io.github.revenge.xposed.Utils.Companion.reloadApp
import io.github.revenge.xposed.Utils.Log
import io.github.revenge.xposed.Constants
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import org.json.JSONObject
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
object LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
@ -45,7 +63,7 @@ object LogBoxModule : Module() {
try {
Log.e("onContext called with context: $context")
contextForMenu = context
val possibleClasses = listOf(
"com.facebook.react.devsupport.BridgeDevSupportManager",
"com.facebook.react.devsupport.BridgelessDevSupportManager",
@ -53,7 +71,7 @@ object LogBoxModule : Module() {
"com.facebook.react.devsupport.DevSupportManagerBase",
"com.facebook.react.devsupport.DefaultDevSupportManager"
)
var foundAny = false
possibleClasses.forEach { className ->
try {
@ -65,14 +83,14 @@ object LogBoxModule : Module() {
Log.e("Class not found: $className - ${e.message}")
}
}
if (!foundAny) {
tryFindDevSupportClasses(context)
}
} catch (e: Exception) {
}
}
private fun tryFindDevSupportClasses(context: Context) {
try {
val dexFile = packageParam.classLoader.javaClass.getDeclaredField("pathList")
@ -85,15 +103,14 @@ object LogBoxModule : Module() {
private fun hookDevSupportManager(clazz: Class<*>, context: Context) {
Log.e("Attempting to hook ${clazz.name}")
// List all methods to see what's available
Log.e("Available methods in ${clazz.simpleName}:")
clazz.methods.forEach { method ->
if (method.name.contains("Dev") || method.name.contains("Reload") || method.name.contains("Options")) {
Log.e(" - ${method.name}")
}
}
try {
try {
val handleReloadJSMethod = clazz.methods.firstOrNull { it.name == "handleReloadJS" }
@ -105,7 +122,6 @@ object LogBoxModule : Module() {
return null
}
})
} else {
}
} catch (e: Exception) {
Log.e("Failed to hook handleReloadJS: ${e.message}")
@ -126,10 +142,10 @@ object LogBoxModule : Module() {
} catch (e: Exception) {
Log.e("Failed to get context from DevSupport (non-fatal): ${e.message}")
}
val finalContext = activityContext ?: contextForMenu ?: context
Log.e("Using context: $finalContext (type: ${finalContext.javaClass.name})")
showRecoveryMenu(finalContext)
} catch (e: Exception) {
e.printStackTrace()
@ -137,7 +153,6 @@ object LogBoxModule : Module() {
return null
}
})
} else {
}
} catch (e: Exception) {
}
@ -150,9 +165,9 @@ object LogBoxModule : Module() {
Log.e("getContextFromDevSupport: instance is null")
return null
}
return try {
// what if we just did this and searched for literally everything
val helpers = listOf(
"mReactInstanceDevHelper",
@ -160,7 +175,7 @@ object LogBoxModule : Module() {
"mReactInstanceManager",
"mApplicationContext"
)
for (helperName in helpers) {
try {
Log.e("Trying field: $helperName")
@ -169,22 +184,22 @@ object LogBoxModule : Module() {
Log.e("Field $helperName not found, skipping")
continue
}
val helper = helperField.get(instance)
if (helper == null) {
Log.e("Field $helperName is null, skipping")
continue
}
if (helper is Context) {
Log.e("Field $helperName is a Context, returning it")
return helper
}
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
it.name == "getCurrentActivity"
val getCurrentActivityMethod = helper.javaClass.methods.firstOrNull {
it.name == "getCurrentActivity"
}
if (getCurrentActivityMethod != null) {
val ctx = getCurrentActivityMethod.invoke(helper) as? Context
if (ctx != null) {
@ -196,7 +211,7 @@ object LogBoxModule : Module() {
Log.e("Error trying $helperName: ${e.message}")
}
}
Log.e("Could not get context from DevSupport object using any method")
null
} catch (e: Exception) {
@ -205,22 +220,157 @@ object LogBoxModule : Module() {
}
}
// material design helper functions
private fun dpToPx(context: Context, dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
private fun getM3Colors(): M3Colors {
// color scheme
return M3Colors(
surface = Color.parseColor("#000000"),
surfaceVariant = Color.parseColor("#1A1A1A"),
onSurface = Color.parseColor("#FFFFFF"),
onSurfaceVariant = Color.parseColor("#CCCCCC"),
primary = Color.parseColor("#FFFFFF"),
onPrimary = Color.parseColor("#000000"),
primaryContainer = Color.parseColor("#2A2A2A"),
onPrimaryContainer = Color.parseColor("#FFFFFF"),
error = Color.parseColor("#FF6B6B")
)
}
private data class M3Colors(
val surface: Int,
val surfaceVariant: Int,
val onSurface: Int,
val onSurfaceVariant: Int,
val primary: Int,
val onPrimary: Int,
val primaryContainer: Int,
val onPrimaryContainer: Int,
val error: Int
)
private fun createM3Background(context: Context, color: Int, cornerRadius: Float): GradientDrawable {
return GradientDrawable().apply {
setColor(color)
setCornerRadius(dpToPx(context, cornerRadius.toInt()).toFloat())
}
}
private fun createButton(context: Context, text: String, colors: M3Colors, onClick: () -> Unit): View {
return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
background = createM3Background(context, colors.primaryContainer, 12f)
setPadding(
dpToPx(context, 32),
dpToPx(context, 12),
dpToPx(context, 32),
dpToPx(context, 12)
)
isClickable = true
isFocusable = true
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
dpToPx(context, 48)
).apply {
setMargins(0, dpToPx(context, 8), 0, 0)
}
layoutParams = params
addView(TextView(context).apply {
this.text = text
setTextColor(colors.onPrimaryContainer)
textSize = 15f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
gravity = Gravity.CENTER
})
// Ripple effect
val rippleDrawable = android.graphics.drawable.RippleDrawable(
ColorStateList.valueOf(Color.argb(40, 255, 255, 255)),
createM3Background(context, colors.primaryContainer, 12f),
null
)
background = rippleDrawable
setOnClickListener { onClick() }
}
}
private fun showRecoveryMenu(context: Context) {
Log.e("showRecoveryMenu called with context: $context")
try {
val options = arrayOf(
if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode",
"Reset Bundle",
"Reload App"
val colors = getM3Colors()
lateinit var dialog: AlertDialog
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24)
)
background = createM3Background(context, colors.surface, 24f)
}
val titleView = TextView(context).apply {
text = "KettuXposed Recovery"
textSize = 20f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, dpToPx(context, 24))
gravity = Gravity.CENTER
}
container.addView(titleView)
// safe Mode Button
val safeModeText = if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode"
container.addView(createButton(context, safeModeText, colors) {
dialog.dismiss()
toggleSafeMode(context)
})
// load Custom Bundle Button
container.addView(createButton(context, "Load Custom Bundle", colors) {
dialog.dismiss()
showCustomBundleDialog(context)
})
// refetch Bundle Button
container.addView(createButton(context, "Refetch Bundle", colors) {
dialog.dismiss()
showConfirmAction(
context, "Refetch Bundle",
"This will download the latest bundle from Github."
) { refetchBundle(context) }
})
// reload App Button
container.addView(createButton(context, "Reload App", colors) {
dialog.dismiss()
reloadApp()
})
dialog = AlertDialog.Builder(context)
.setView(container)
.create()
dialog.window?.setBackgroundDrawable(
createM3Background(context, Color.TRANSPARENT, 24f)
)
AlertDialog.Builder(context)
.setTitle("KettuXposed Recovery Menu")
.setItems(options) { _, which ->
handleMenuSelection(context, which)
}
.setNegativeButton("Cancel", null)
.show()
constrainContainerWidth(context, container)
dialog.show()
setDialogWindowWidth(dialog, context)
Log.e("Recovery menu shown successfully")
} catch (e: Exception) {
Log.e("Error showing recovery menu: ${e.message}", e)
@ -228,21 +378,390 @@ object LogBoxModule : Module() {
}
}
private fun handleMenuSelection(context: Context, index: Int) {
when (index) {
0 -> toggleSafeMode(context)
1 -> confirmAction(context, "reset bundle") { resetBundle(context) }
2 -> reloadApp()
private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) {
val colors = getM3Colors()
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24)
)
background = createM3Background(context, colors.surface, 24f)
}
val titleView = TextView(context).apply {
text = title
textSize = 18f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, dpToPx(context, 16))
}
container.addView(titleView)
val messageView = TextView(context).apply {
text = message
textSize = 14f
setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, dpToPx(context, 24))
}
container.addView(messageView)
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.END
}
val cancelButton = TextView(context).apply {
text = "Cancel"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
dpToPx(context, 20),
dpToPx(context, 12),
dpToPx(context, 20),
dpToPx(context, 12)
)
isClickable = true
}
val confirmButton = TextView(context).apply {
text = "Confirm"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
dpToPx(context, 20),
dpToPx(context, 12),
dpToPx(context, 20),
dpToPx(context, 12)
)
isClickable = true
}
val dialog = AlertDialog.Builder(context)
.setView(container)
.create()
cancelButton.setOnClickListener { dialog.dismiss() }
confirmButton.setOnClickListener {
dialog.dismiss()
action()
}
buttonContainer.addView(cancelButton)
buttonContainer.addView(confirmButton)
container.addView(buttonContainer)
dialog.window?.setBackgroundDrawable(
createM3Background(context, Color.TRANSPARENT, 24f)
)
constrainContainerWidth(context, container)
dialog.show()
setDialogWindowWidth(dialog, context)
}
private fun confirmAction(context: Context, actionText: String, action: () -> Unit) {
AlertDialog.Builder(context)
.setTitle("Confirm Action")
.setMessage("Are you sure you want to $actionText?")
.setPositiveButton("Confirm") { _, _ -> action() }
.setNegativeButton("Cancel", null)
.show()
private fun showCustomBundleDialog(context: Context) {
val colors = getM3Colors()
val filesDir = File(context.filesDir, "pyoncord")
val configFile = File(filesDir, "loader_config.json")
var currentUrl: String? = null
var isEnabled = false
if (configFile.exists()) {
try {
val json = JSONObject(configFile.readText())
val custom = json.optJSONObject("customLoadUrl")
if (custom != null) {
isEnabled = custom.optBoolean("enabled", false)
currentUrl = custom.optString("url", "")
} else {
isEnabled = false
currentUrl = ""
}
} catch (_: Exception) {
}
}
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24)
)
background = createM3Background(context, colors.surface, 24f)
}
val titleView = TextView(context).apply {
text = "Custom Bundle URL"
textSize = 18f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, dpToPx(context, 16))
}
container.addView(titleView)
val toggleContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, dpToPx(context, 16))
}
layoutParams = params
}
val toggleLabel = TextView(context).apply {
text = "Enable Custom URL"
textSize = 14f
setTextColor(colors.onSurface)
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
}
// switch container
val trackPadding = dpToPx(context, 4)
val switchTrackWidth = dpToPx(context, 52)
val switchTrackHeight = dpToPx(context, 32)
val trackCornerRadius = switchTrackHeight / 2f
val switchContainer = FrameLayout(context).apply {
background = createM3Background(context, colors.surfaceVariant, trackCornerRadius)
layoutParams = LinearLayout.LayoutParams(
switchTrackWidth,
switchTrackHeight
)
tag = isEnabled
isClickable = true
}
// create the thumb with proper sizing
val thumbSize = switchTrackHeight - (trackPadding * 2)
val thumbCornerRadius = thumbSize / 2f
val switchThumb = View(context).apply {
background = createM3Background(context, if (isEnabled) colors.onPrimary else colors.onSurfaceVariant, thumbCornerRadius)
val params = FrameLayout.LayoutParams(thumbSize, thumbSize)
val thumbLeft = if (isEnabled) {
switchTrackWidth - thumbSize - trackPadding
} else {
trackPadding
}
params.leftMargin = thumbLeft
params.topMargin = trackPadding
layoutParams = params
}
if (isEnabled) {
switchContainer.background = createM3Background(context, colors.primary, trackCornerRadius)
}
switchContainer.addView(switchThumb)
toggleContainer.addView(toggleLabel)
toggleContainer.addView(switchContainer)
container.addView(toggleContainer)
val urlInput = EditText(context).apply {
hint = "http://localhost:4040/bundle.js"
setTextColor(colors.onSurface)
setHintTextColor(colors.onSurfaceVariant)
background = createM3Background(context, colors.surfaceVariant, 12f)
inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI
setText(currentUrl ?: "")
textSize = 14f
setPadding(
dpToPx(context, 16),
dpToPx(context, 12),
dpToPx(context, 16),
dpToPx(context, 12)
)
this.isEnabled = isEnabled
alpha = if (isEnabled) 1f else 0.5f
val params = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, dpToPx(context, 24))
}
layoutParams = params
}
container.addView(urlInput)
switchContainer.setOnClickListener {
val currentState = switchContainer.tag as Boolean
val newState = !currentState
switchContainer.tag = newState
val thumbParams = switchThumb.layoutParams as FrameLayout.LayoutParams
val targetLeftMargin = if (newState) {
switchTrackWidth - thumbSize - trackPadding
} else {
trackPadding
}
// Animate the thumb movement
val animator = ValueAnimator.ofInt(thumbParams.leftMargin, targetLeftMargin).apply {
duration = 200
interpolator = DecelerateInterpolator()
addUpdateListener { valueAnimator ->
val animatedValue = valueAnimator.animatedValue as Int
thumbParams.leftMargin = animatedValue
switchThumb.layoutParams = thumbParams
}
}
animator.start()
// animate track color change
val trackAnimator = ValueAnimator.ofObject(
ArgbEvaluator(),
if (currentState) colors.primary else colors.surfaceVariant,
if (newState) colors.primary else colors.surfaceVariant
).apply {
duration = 200
addUpdateListener { animator ->
val color = animator.animatedValue as Int
switchContainer.background = createM3Background(context, color, trackCornerRadius)
}
}
trackAnimator.start()
// animate thumb color change
val thumbAnimator = ValueAnimator.ofObject(
ArgbEvaluator(),
if (currentState) colors.onPrimary else colors.onSurfaceVariant,
if (newState) colors.onPrimary else colors.onSurfaceVariant
).apply {
duration = 200
addUpdateListener { animator ->
val color = animator.animatedValue as Int
switchThumb.background = createM3Background(context, color, thumbCornerRadius)
}
}
thumbAnimator.start()
urlInput.animate()
.alpha(if (newState) 1f else 0.5f)
.setDuration(200)
.withEndAction {
urlInput.isEnabled = newState
}
.start()
}
val buttonContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.END
}
val cancelButton = TextView(context).apply {
text = "Cancel"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
dpToPx(context, 20),
dpToPx(context, 12),
dpToPx(context, 20),
dpToPx(context, 12)
)
isClickable = true
}
val saveButton = TextView(context).apply {
text = "Save"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
setPadding(
dpToPx(context, 20),
dpToPx(context, 12),
dpToPx(context, 20),
dpToPx(context, 12)
)
isClickable = true
}
val dialog = AlertDialog.Builder(context)
.setView(container)
.create()
cancelButton.setOnClickListener { dialog.dismiss() }
saveButton.setOnClickListener {
try {
val url = urlInput.text?.toString()?.trim() ?: ""
val enabled = switchContainer.tag as Boolean
if (enabled && url.isNotEmpty()) {
setCustomBundleURL(context, url, true)
dialog.dismiss()
} else if (!enabled) {
setCustomBundleURL(context, url.ifEmpty { "http://localhost:4040/bundle.js" }, false)
dialog.dismiss()
} else {
Toast.makeText(context, "Please enter a valid URL", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
val err = e.message ?: "unknown error"
Toast.makeText(context, "Failed to set custom bundle: $err", Toast.LENGTH_LONG).show()
}
}
buttonContainer.addView(cancelButton)
buttonContainer.addView(saveButton)
container.addView(buttonContainer)
dialog.window?.setBackgroundDrawable(
createM3Background(context, Color.TRANSPARENT, 24f)
)
constrainContainerWidth(context, container)
dialog.show()
setDialogWindowWidth(dialog, context)
}
private fun setCustomBundleURL(context: Context, url: String, enabled: Boolean) {
try {
val filesDir = File(context.dataDir, Constants.FILES_DIR)
filesDir.mkdirs()
val configFile = File(filesDir, "loader.json")
val cfg = io.github.revenge.xposed.modules.LoaderConfig(
io.github.revenge.xposed.modules.CustomLoadUrl(enabled, url)
)
val jsonText = io.github.revenge.xposed.Utils.JSON.encodeToString(cfg)
configFile.writeText(jsonText)
// remove cached bundle so UpdaterModule will fetch the custom one
val cacheBundle = File(context.dataDir, "${Constants.CACHE_DIR}/${Constants.MAIN_SCRIPT_FILE}")
if (enabled) {
if (cacheBundle.exists()) cacheBundle.delete()
Toast.makeText(context, "Custom bundle enabled", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show()
}
reloadApp()
} catch (e: Exception) {
Log.e("Error setting custom bundle URL: ${e.message}")
showError(context, "Failed to save configuration", e.message)
}
}
private fun isSafeModeEnabled(context: Context): Boolean {
@ -298,26 +817,23 @@ object LogBoxModule : Module() {
}
}
private fun resetBundle(context: Context) {
private fun refetchBundle(context: Context) {
try {
val pyoncordDir = getPyoncordDirectory(context)
val bundleFile = File(pyoncordDir, "bundle.js")
val backupFile = File(pyoncordDir, "bundle.js.backup")
val configFile = File(pyoncordDir, "loader_config.json")
bundleFile.delete()
backupFile.delete()
if (configFile.exists()) {
val config = JSONObject(configFile.readText())
config.put("customLoadUrlEnabled", false)
configFile.writeText(config.toString())
if (bundleFile.exists()) {
backupFile.delete()
bundleFile.renameTo(backupFile)
Log.e("Bundle moved to backup")
}
reloadApp()
} catch (e: Exception) {
Log.e("Error resetting bundle: ${e.message}")
showError(context, "Failed to reset bundle", e.message)
Log.e("Error refetching bundle: ${e.message}")
showError(context, "Failed to refetch bundle", e.message)
}
}
@ -329,11 +845,89 @@ object LogBoxModule : Module() {
return dir
}
private fun showError(context: Context, title: String, message: String?) {
AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message ?: "An unknown error occurred")
.setPositiveButton("OK", null)
.show()
private fun constrainContainerWidth(context: Context, container: LinearLayout) {
try {
val maxW = (context.resources.displayMetrics.widthPixels * 0.88).toInt()
val lp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
container.layoutParams = lp
try {
container.minimumWidth = maxW
} catch (_: Exception) {}
} catch (_: Exception) {
}
}
}
private fun setDialogWindowWidth(dialog: AlertDialog, context: Context) {
try {
val screenW = context.resources.displayMetrics.widthPixels
val maxByPercent = (screenW * 0.88).toInt()
val maxByDp = dpToPx(context, 380)
val targetW = if (maxByPercent < maxByDp) maxByPercent else maxByDp
dialog.window?.setLayout(targetW, LinearLayout.LayoutParams.WRAP_CONTENT)
} catch (_: Exception) {
}
}
private fun showError(context: Context, title: String, message: String?) {
val colors = getM3Colors()
val container = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24),
dpToPx(context, 24)
)
background = createM3Background(context, colors.surface, 24f)
}
val titleView = TextView(context).apply {
text = title
textSize = 18f
setTextColor(colors.error)
typeface = android.graphics.Typeface.create("sans-serif", android.graphics.Typeface.BOLD)
setPadding(0, 0, 0, dpToPx(context, 16))
}
container.addView(titleView)
val messageView = TextView(context).apply {
text = message ?: "An unknown error occurred"
textSize = 14f
setTextColor(colors.onSurfaceVariant)
setPadding(0, 0, 0, dpToPx(context, 24))
}
container.addView(messageView)
val okButton = TextView(context).apply {
text = "OK"
setTextColor(colors.primary)
textSize = 14f
typeface = android.graphics.Typeface.create("sans-serif-medium", android.graphics.Typeface.NORMAL)
gravity = Gravity.END
setPadding(
dpToPx(context, 20),
dpToPx(context, 12),
dpToPx(context, 20),
dpToPx(context, 12)
)
isClickable = true
}
val dialog = AlertDialog.Builder(context)
.setView(container)
.create()
okButton.setOnClickListener { dialog.dismiss() }
container.addView(okButton)
dialog.window?.setBackgroundDrawable(
createM3Background(context, Color.TRANSPARENT, 24f)
)
constrainContainerWidth(context, container)
dialog.show()
setDialogWindowWidth(dialog, context)
}
}