Compare commits

...
Author SHA1 Message Date
Chloe 8dd122631c run shared library initialization on separate context 2026-03-13 23:37:50 -07:00
km7dev 69188075a3 make open folder follow the directory devs specify (#65)
* this feels hacky but it works

* make the change chloe suggested
2026-03-12 18:50:05 -07:00
Chloe 187d3f9819 add game version to link failed log 2026-03-12 17:38:35 -07:00
Chloe 559cedb0f0 add symbols to blocklist 2026-03-12 17:35:18 -07:00
6 changed files with 195 additions and 120 deletions

No files matched your search

+4 -2
View File
@@ -15,7 +15,7 @@ public:
}; };
// this is every function that i thought would be relevant // this is every function that i thought would be relevant
constexpr std::array<const std::string_view, 10> remove_symbols{ constexpr std::array<const std::string_view, 12> remove_symbols{
"__gxx_personality_v0", "__gxx_personality_v0",
"__cxa_throw", "__cxa_throw",
"__cxa_rethrow", "__cxa_rethrow",
@@ -25,7 +25,9 @@ constexpr std::array<const std::string_view, 10> remove_symbols{
"__cxa_guard_abort", "__cxa_guard_abort",
"__cxa_guard_acquire", "__cxa_guard_acquire",
"__cxa_guard_release", "__cxa_guard_release",
"__cxa_free_exception" "__cxa_free_exception",
"__emutls_get_address", // added in 2.208
"__cxa_thread_atexit"
// android 10's libc doesn't have these symbols // android 10's libc doesn't have these symbols
// and the ndk is forbidden from exporting them!! so keep it for old mods... // and the ndk is forbidden from exporting them!! so keep it for old mods...
@@ -24,8 +24,10 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.customRobTop.BaseRobTopActivity import com.customRobTop.BaseRobTopActivity
import com.customRobTop.JniToCpp import com.customRobTop.JniToCpp
import com.geode.launcher.main.FullScreenLoadingIndicator
import com.geode.launcher.main.LaunchNotification import com.geode.launcher.main.LaunchNotification
import com.geode.launcher.main.determineDisplayedCards import com.geode.launcher.main.determineDisplayedCards
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
@@ -35,13 +37,14 @@ import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cocos2dx.lib.Cocos2dxGLSurfaceView import org.cocos2dx.lib.Cocos2dxGLSurfaceView
import org.cocos2dx.lib.Cocos2dxHelper import org.cocos2dx.lib.Cocos2dxHelper
import org.cocos2dx.lib.Cocos2dxRenderer import org.cocos2dx.lib.Cocos2dxRenderer
import org.fmod.FMOD import org.fmod.FMOD
import java.io.File import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import javax.microedition.khronos.egl.EGL10 import javax.microedition.khronos.egl.EGL10
import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.egl.EGLConfig
@@ -91,97 +94,88 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
return return
} }
try { val rootLayout = setupContext()
createVersionFile() val loadingView = ComposeView(this).apply {
tryLoadGame() setContent {
} catch (e: UnsatisfiedLinkError) { FullScreenLoadingIndicator()
Log.e("GeodeLauncher", "Library linkage failure", e) }
}
// generates helpful information for use in debugging library load failures if (rootLayout != null) {
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) rootLayout.addView(loadingView)
val abiMismatch = GamePackageUtils.detectAbiMismatch(this, gdPackageInfo, e) } else {
setContentView(loadingView)
}
val is64bit = LaunchUtils.is64bit lifecycleScope.launch {
val errorMessage = when { val error = preloadGame(rootLayout)
abiMismatch && is64bit -> LaunchUtils.LauncherError.LINKER_NEEDS_32BIT if (error != null) {
abiMismatch -> LaunchUtils.LauncherError.LINKER_NEEDS_64BIT returnToMain(error)
e.message?.contains("__gxx_personality_v0") == true ->
LaunchUtils.LauncherError.LINKER_FAILED_STL
else -> LaunchUtils.LauncherError.LINKER_FAILED
} }
returnToMain(errorMessage, e.message, e.stackTraceToString()) rootLayout?.removeView(loadingView)
return
} catch (e: Exception) {
Log.e("GeodeLauncher", "Uncaught error during game load", e)
returnToMain(
LaunchUtils.LauncherError.GENERIC,
e.message ?: "Unknown Exception",
e.stackTraceToString()
)
return
} }
}
suspend fun preloadGame(rootLayout: FrameLayout?): LaunchUtils.FullLauncherError? = try {
createVersionFile()
val loadFailed = tryLoadGame()
val baseView = createView()
if (rootLayout != null) {
rootLayout.addView(baseView)
} else {
setContentView(baseView)
}
mGLSurfaceView?.manualBackEvents = true
setupNotificationView(baseView, loadFailed)
null
} catch (e: UnsatisfiedLinkError) {
Log.e("GeodeLauncher", "Library linkage failure", e)
// generates helpful information for use in debugging library load failures
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val abiMismatch = GamePackageUtils.detectAbiMismatch(this, gdPackageInfo, e)
val is64bit = LaunchUtils.is64bit
val errorMessage = when {
abiMismatch && is64bit -> LaunchUtils.LauncherError.LINKER_NEEDS_32BIT
abiMismatch -> LaunchUtils.LauncherError.LINKER_NEEDS_64BIT
e.message?.contains("__gxx_personality_v0") == true ->
LaunchUtils.LauncherError.LINKER_FAILED_STL
else -> LaunchUtils.LauncherError.LINKER_FAILED
}
LaunchUtils.FullLauncherError(
errorMessage,
e.message ?: "Unknown exception",
e.stackTraceToString()
)
} catch (e: Exception) {
Log.e("GeodeLauncher", "Uncaught error during game load", e)
LaunchUtils.FullLauncherError(
LaunchUtils.LauncherError.GENERIC,
e.message ?: "Unknown Exception",
e.stackTraceToString()
)
}
private fun setupContext(): FrameLayout? {
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
mGLSurfaceView?.sendKeyBack() mGLSurfaceView?.sendKeyBack()
} }
}) })
mGLSurfaceView?.manualBackEvents = true
}
private fun createVersionFile() {
val versionPath = File(filesDir, "game_version.txt")
val gameVersion = GamePackageUtils.getGameVersionCode(packageManager)
versionPath.writeText("$gameVersion")
}
private fun returnToMain(
error: LaunchUtils.LauncherError? = null,
returnMessage: String? = null,
returnExtendedMessage: String? = null
) {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
if (error != null && !returnMessage.isNullOrEmpty()) {
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR, error)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE, returnMessage)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE, returnExtendedMessage)
}
}
startActivity(launchIntent)
}
private fun tryLoadGame() {
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
setupRedirection(gdPackageInfo)
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
GeodeUtils.setCapabilityListener(this)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
// this fix requires geode v3, which is 2.206+
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
LauncherFix.enableExceptionsRenaming()
}
LauncherFix.performPatches()
loadInternalMods()
val baseView = createView()
if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
val frameLayoutParams = ViewGroup.LayoutParams( val frameLayoutParams = ViewGroup.LayoutParams(
@@ -204,15 +198,61 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
WindowInsets.CONSUMED WindowInsets.CONSUMED
} }
frameLayout.addView(baseView)
setContentView(frameLayout) setContentView(frameLayout)
} else { return frameLayout
setContentView(baseView)
} }
return null
}
private suspend fun createVersionFile() = runCatching {
val gameVersion = GamePackageUtils.getGameVersionCode(packageManager)
val versionPath = File(filesDir, "game_version.txt")
withContext(Dispatchers.IO) {
if (versionPath.exists()) {
versionPath.delete()
}
versionPath.writeText("$gameVersion")
}
}
private fun returnToMain(message: LaunchUtils.FullLauncherError? = null, ) {
val launchIntent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
if (message != null) {
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR, message.error)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE, message.returnMessage)
putExtra(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE, message.extendedMessage)
}
}
startActivity(launchIntent)
}
private suspend fun tryLoadGame(): Boolean {
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
setupRedirection(gdPackageInfo)
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (GamePackageUtils.getGameVersionCode(packageManager) >= 39L) {
// this fix requires geode v3, which is 2.206+
// there is a short period in which 2.206 users will still have geode v2, but whatever. ig
LauncherFix.enableExceptionsRenaming()
}
LauncherFix.performPatches()
loadInternalMods()
setupPostLibraryLoad(gdPackageInfo) setupPostLibraryLoad(gdPackageInfo)
val geodeFailed = try { return try {
loadGeodeLibrary() loadGeodeLibrary()
false false
} catch (e: UnsatisfiedLinkError) { } catch (e: UnsatisfiedLinkError) {
@@ -222,8 +262,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
handleGeodeException(e) handleGeodeException(e)
true true
} }
setupNotificationView(baseView, geodeFailed)
} }
private fun setupNotificationView(baseView: FrameLayout, geodeFailed: Boolean) { private fun setupNotificationView(baseView: FrameLayout, geodeFailed: Boolean) {
@@ -283,7 +321,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) { private suspend fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) {
val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo!!) val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo!!)
val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so" val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so"
@@ -302,7 +340,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibraryCopy(libraryName: String, libraryPath: String) { private suspend fun loadLibraryCopy(libraryName: String, libraryPath: String) {
if (libraryPath.contains("!/")) { if (libraryPath.contains("!/")) {
// library in apk can't be loaded directly // library in apk can't be loaded directly
loadLibraryFromAssetsCopy(libraryName) loadLibraryFromAssetsCopy(libraryName)
@@ -312,11 +350,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val library = File(libraryPath) val library = File(libraryPath)
val libraryCopy = File(cacheDir, "lib$libraryName.so") val libraryCopy = File(cacheDir, "lib$libraryName.so")
libraryCopy.outputStream().use { libraryOutput -> DownloadUtils.copyFile(library, libraryCopy)
library.inputStream().use { inputStream ->
DownloadUtils.copyFile(inputStream, libraryOutput)
}
}
System.load(libraryCopy.path) System.load(libraryCopy.path)
@@ -324,7 +358,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadLibraryFromAssetsCopy(libraryName: String) { private suspend fun loadLibraryFromAssetsCopy(libraryName: String) {
// loads a library loaded in assets // loads a library loaded in assets
// this copies the library to a non-compressed directory // this copies the library to a non-compressed directory
@@ -339,9 +373,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
// there doesn't seem to be a way to load a library from a file descriptor // there doesn't seem to be a way to load a library from a file descriptor
val libraryCopy = File(cacheDir, "lib$libraryName.so") val libraryCopy = File(cacheDir, "lib$libraryName.so")
libraryCopy.outputStream().use { libraryOutput -> withContext(Dispatchers.IO) {
libraryFd.createInputStream().use { inputStream -> libraryCopy.outputStream().use { libraryOutput ->
DownloadUtils.copyFile(inputStream, libraryOutput) libraryFd.createInputStream().use { inputStream ->
DownloadUtils.copyFile(inputStream, libraryOutput)
}
} }
} }
@@ -376,7 +412,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
@SuppressLint("UnsafeDynamicallyLoadedCode") @SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary() { private suspend fun loadGeodeLibrary() {
// Load Geode if exists // Load Geode if exists
// bundling the object with the application allows for nicer backtraces // bundling the object with the application allows for nicer backtraces
try { try {
@@ -397,22 +433,23 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
// also i get 20 million permission denied errors // also i get 20 million permission denied errors
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!! val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
val copiedPath = File(filesDir.path, "copied") val copiedPath = File(filesDir, "copied")
if (copiedPath.exists()) { if (copiedPath.exists()) {
copiedPath.deleteRecursively() copiedPath.deleteRecursively()
} }
copiedPath.mkdir() copiedPath.mkdir()
val copiedGeodePath = File(copiedPath.path, "Geode.so") val copiedGeodePath = File(copiedPath, "Geode.so")
if (externalGeodePath.exists()) { if (externalGeodePath.exists()) {
DownloadUtils.copyFile( DownloadUtils.copyFile(
FileInputStream(externalGeodePath), externalGeodePath,
FileOutputStream(copiedGeodePath) copiedGeodePath
) )
if (copiedGeodePath.exists()) { if (copiedGeodePath.exists()) {
println("Loading Geode from ${externalGeodePath.name}") println("Loading Geode from ${externalGeodePath.name}")
System.load(copiedGeodePath.path) System.load(copiedGeodePath.path)
return return
} }
@@ -10,13 +10,16 @@ import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@@ -120,9 +123,10 @@ fun LaunchNotification(cards: List<LaunchNotificationType>) {
val theme = Theme.fromInt(themeOption) val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND) val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) { CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) { GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
// surface is not in use, so this is unfortunately not provided // surface is not in use, so this is unfortunately not provided
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
Column( Column(
@@ -279,4 +283,27 @@ fun ForwardCompatibilityContent(modifier: Modifier = Modifier) {
}, },
modifier = modifier.width(IntrinsicSize.Max) modifier = modifier.width(IntrinsicSize.Max)
) )
}
@Composable
fun FullScreenLoadingIndicator() {
val theme = Theme.DARK
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = true, dynamicColor = !dynamicColorOption) {
Surface(modifier = Modifier.fillMaxSize()) {
var showProgress by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(1000L)
showProgress = true
}
AnimatedVisibility(showProgress) {
CircularProgressIndicator()
}
}
}
}
} }
@@ -69,8 +69,15 @@ object GamePackageUtils {
packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS packageInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS
val isSplit = (packageInfo.applicationInfo?.splitSourceDirs?.size ?: 0) > 1 val isSplit = (packageInfo.applicationInfo?.splitSourceDirs?.size ?: 0) > 1
val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toLong()
}
val metadata = val metadata =
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi" "Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi\nGame version: $versionCode"
Log.i("GeodeLauncher", metadata) Log.i("GeodeLauncher", metadata)
// easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp // easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp
@@ -208,24 +208,20 @@ object GeodeUtils {
val context = activity.get()!! val context = activity.get()!!
val pathFile = File(path) val pathFile = File(path)
val baseDirectory = LaunchUtils.getBaseDirectory(context) val baseDirectory = LaunchUtils.getBaseDirectory(context, true)
val isInternalPath = pathFile.startsWith(baseDirectory) val isInternalPath = pathFile.startsWith(baseDirectory)
val intent = if (isInternalPath) { val intent = if (isInternalPath) {
// TODO: figure out how to get this to point to the path it should be pointing at val relativePath = pathFile.relativeTo(baseDirectory)
// (the best i got was pointing at a file) val uri = DocumentsContract.buildDocumentUri(
// val relativePath = pathFile.relativeTo(baseDirectory) "com.android.externalstorage.documents",
"primary:Android/media/${context.packageName}/$relativePath"
)
Intent(Intent.ACTION_VIEW).apply { Intent(Intent.ACTION_VIEW).apply {
data = DocumentsContract.buildRootUri( setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR)
"${context.packageName}.user", UserDirectoryProvider.ROOT
)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
)
} }
} else { } else {
Intent(Intent.ACTION_OPEN_DOCUMENT).apply { Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
@@ -119,6 +119,12 @@ object LaunchUtils {
} }
} }
data class FullLauncherError(
val error: LauncherError,
val returnMessage: String,
val extendedMessage: String? = null,
) : Serializable
const val LAUNCHER_KEY_RETURN_ERROR = "return_error" const val LAUNCHER_KEY_RETURN_ERROR = "return_error"
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message" const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
const val LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE = "return_extended_message" const val LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE = "return_extended_message"