forked from geode-sdk/android-launcher
587 lines
19 KiB
Kotlin
587 lines
19 KiB
Kotlin
package com.geode.launcher
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.BroadcastReceiver
|
|
import android.content.Intent
|
|
import android.content.pm.ActivityInfo
|
|
import android.content.pm.ApplicationInfo
|
|
import android.content.pm.PackageInfo
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Environment
|
|
import android.util.Log
|
|
import android.view.ViewGroup
|
|
import android.view.WindowManager
|
|
import android.widget.FrameLayout
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.compose.ui.platform.ComposeView
|
|
import androidx.core.view.WindowCompat
|
|
import androidx.core.view.WindowInsetsCompat
|
|
import androidx.core.view.WindowInsetsControllerCompat
|
|
import com.customRobTop.BaseRobTopActivity
|
|
import com.customRobTop.JniToCpp
|
|
import com.geode.launcher.main.LaunchNotification
|
|
import com.geode.launcher.utils.Constants
|
|
import com.geode.launcher.utils.ConstrainedFrameLayout
|
|
import com.geode.launcher.utils.DownloadUtils
|
|
import com.geode.launcher.utils.GamePackageUtils
|
|
import com.geode.launcher.utils.GeodeUtils
|
|
import com.geode.launcher.utils.LaunchUtils
|
|
import com.geode.launcher.utils.PreferenceUtils
|
|
import org.cocos2dx.lib.Cocos2dxEditText
|
|
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
|
|
import org.cocos2dx.lib.Cocos2dxHelper
|
|
import org.cocos2dx.lib.Cocos2dxRenderer
|
|
import org.fmod.FMOD
|
|
import java.io.File
|
|
import java.io.FileInputStream
|
|
import java.io.FileOutputStream
|
|
import java.io.IOException
|
|
|
|
enum class DisplayMode {
|
|
DEFAULT, LIMITED, FULLSCREEN;
|
|
|
|
companion object {
|
|
fun fromInt(i: Int) = when (i) {
|
|
1 -> LIMITED
|
|
2 -> FULLSCREEN
|
|
else -> DEFAULT
|
|
}
|
|
}
|
|
}
|
|
|
|
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener, GeodeUtils.CapabilityListener {
|
|
private var mGLSurfaceView: Cocos2dxGLSurfaceView? = null
|
|
private val sTag = GeometryDashActivity::class.simpleName
|
|
private var mIsRunning = false
|
|
private var mIsOnPause = false
|
|
private var mHasWindowFocus = false
|
|
|
|
private var displayMode = DisplayMode.DEFAULT
|
|
private var forceRefreshRate = false
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
setupUIState()
|
|
FMOD.init(this)
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
// return back to main if Geometry Dash isn't found
|
|
if (!GamePackageUtils.isGameInstalled(packageManager)) {
|
|
returnToMain()
|
|
return
|
|
}
|
|
|
|
try {
|
|
createVersionFile()
|
|
tryLoadGame()
|
|
} 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
|
|
else -> LaunchUtils.LauncherError.LINKER_FAILED
|
|
}
|
|
|
|
returnToMain(errorMessage, e.message, e.stackTraceToString())
|
|
|
|
return
|
|
} catch (e: Exception) {
|
|
Log.e("GeodeLauncher", "Uncaught error during game load", e)
|
|
|
|
returnToMain(
|
|
LaunchUtils.LauncherError.GENERIC,
|
|
e.message ?: "Unknown Exception",
|
|
e.stackTraceToString()
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
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) {
|
|
val customSymbols = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.CUSTOM_SYMBOL_LIST)
|
|
if (customSymbols) {
|
|
val symbolFile = File(LaunchUtils.getBaseDirectory(this), "exception_symbols.txt")
|
|
LauncherFix.enableCustomSymbolList(symbolFile.path)
|
|
}
|
|
|
|
// 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.performExceptionsRenaming()
|
|
}
|
|
|
|
loadInternalMods()
|
|
|
|
setContentView(createView())
|
|
|
|
setupPostLibraryLoad(gdPackageInfo)
|
|
|
|
try {
|
|
loadGeodeLibrary()
|
|
} catch (e: UnsatisfiedLinkError) {
|
|
handleGeodeException(e)
|
|
} catch (e: Exception) {
|
|
handleGeodeException(e)
|
|
}
|
|
}
|
|
|
|
private fun handleGeodeException(e: Throwable) {
|
|
e.printStackTrace()
|
|
|
|
// ignore load failures if the game is newer than what's supported
|
|
// so people in the future can use their save data
|
|
if (GamePackageUtils.getGameVersionCode(packageManager) <= Constants.SUPPORTED_VERSION_CODE) {
|
|
throw e
|
|
}
|
|
}
|
|
|
|
private fun setupRedirection(packageInfo: PackageInfo) {
|
|
try {
|
|
GamePackageUtils.addAssetsFromPackage(assets, packageInfo)
|
|
} catch (e: NoSuchMethodException) {
|
|
Log.e("GeodeLauncher", "Failed to add asset redirection", e)
|
|
}
|
|
|
|
try {
|
|
// fixes bugs specific to the new app directory, such as package name
|
|
val saveDir = LaunchUtils.getSaveDirectory(this)
|
|
saveDir.mkdir()
|
|
|
|
// make sure it's loaded, just in case
|
|
System.loadLibrary("c++_shared")
|
|
|
|
LauncherFix.loadLibrary()
|
|
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
|
|
LauncherFix.setDataPath(saveDir.path)
|
|
} catch (e: UnsatisfiedLinkError) {
|
|
Log.e("GeodeLauncher", "Failed to load LauncherFix", e)
|
|
}
|
|
}
|
|
|
|
private fun setupPostLibraryLoad(packageInfo: PackageInfo) {
|
|
// call native functions after native libraries init
|
|
JniToCpp.setupHSSAssets(
|
|
packageInfo.applicationInfo!!.sourceDir,
|
|
Environment.getExternalStorageDirectory().absolutePath
|
|
)
|
|
Cocos2dxHelper.nativeSetApkPath(packageInfo.applicationInfo!!.sourceDir)
|
|
|
|
BaseRobTopActivity.setCurrentActivity(this)
|
|
}
|
|
|
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
|
private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) {
|
|
val nativeDir = getNativeLibraryDirectory(packageInfo.applicationInfo!!)
|
|
val libraryPath = if (nativeDir.endsWith('/')) "${nativeDir}lib$libraryName.so" else "$nativeDir/lib$libraryName.so"
|
|
|
|
try {
|
|
System.load(libraryPath)
|
|
} catch (ule: UnsatisfiedLinkError) {
|
|
// some devices (samsung a series, cough) have an overly restrictive application classloader
|
|
// this is a workaround for that. hopefully
|
|
if (ule.message?.contains("not accessible for the namespace") != true) {
|
|
throw ule
|
|
}
|
|
|
|
println("Using copy for library $libraryName")
|
|
loadLibraryCopy(libraryName, libraryPath)
|
|
}
|
|
}
|
|
|
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
|
private fun loadLibraryCopy(libraryName: String, libraryPath: String) {
|
|
if (libraryPath.contains("!/")) {
|
|
// library in apk can't be loaded directly
|
|
loadLibraryFromAssetsCopy(libraryName)
|
|
return
|
|
}
|
|
|
|
val library = File(libraryPath)
|
|
val libraryCopy = File(cacheDir, "lib$libraryName.so")
|
|
|
|
libraryCopy.outputStream().use { libraryOutput ->
|
|
library.inputStream().use { inputStream ->
|
|
DownloadUtils.copyFile(inputStream, libraryOutput)
|
|
}
|
|
}
|
|
|
|
System.load(libraryCopy.path)
|
|
|
|
return
|
|
}
|
|
|
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
|
private fun loadLibraryFromAssetsCopy(libraryName: String) {
|
|
// loads a library loaded in assets
|
|
// this copies the library to a non-compressed directory
|
|
|
|
val arch = LaunchUtils.applicationArchitecture
|
|
val libraryFd = try {
|
|
assets.openNonAssetFd("lib/$arch/lib$libraryName.so")
|
|
} catch (_: Exception) {
|
|
throw UnsatisfiedLinkError("Could not find library lib$libraryName.so for abi $arch")
|
|
}
|
|
|
|
// copy the library to a path we can access
|
|
// there doesn't seem to be a way to load a library from a file descriptor
|
|
val libraryCopy = File(cacheDir, "lib$libraryName.so")
|
|
|
|
libraryCopy.outputStream().use { libraryOutput ->
|
|
libraryFd.createInputStream().use { inputStream ->
|
|
DownloadUtils.copyFile(inputStream, libraryOutput)
|
|
}
|
|
}
|
|
|
|
System.load(libraryCopy.path)
|
|
|
|
return
|
|
}
|
|
|
|
private fun getNativeLibraryDirectory(applicationInfo: ApplicationInfo): String {
|
|
// native libraries have been extracted, so the path is same as usual
|
|
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS == ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) {
|
|
return applicationInfo.nativeLibraryDir
|
|
}
|
|
|
|
// might be split apks, select the best path for the library
|
|
val sourceDirs = applicationInfo.splitSourceDirs
|
|
val architecture = LaunchUtils.applicationArchitecture
|
|
|
|
if (!sourceDirs.isNullOrEmpty()) {
|
|
val configAbi = architecture.replace("-", "_")
|
|
val abiDir = sourceDirs.find { it.contains(configAbi) }
|
|
|
|
if (abiDir != null) {
|
|
return "$abiDir!/lib/$architecture/"
|
|
}
|
|
}
|
|
|
|
// this configuration should never happen!
|
|
// non split apk, but libraries are stored in apk
|
|
val sourceDir = applicationInfo.sourceDir
|
|
return "$sourceDir!/lib/$architecture/"
|
|
}
|
|
|
|
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
|
private fun loadGeodeLibrary() {
|
|
// Load Geode if exists
|
|
// bundling the object with the application allows for nicer backtraces
|
|
try {
|
|
// put libgeode.so in jniLibs/armeabi-v7a to get this
|
|
System.loadLibrary("geode")
|
|
return
|
|
} catch (e: UnsatisfiedLinkError) {
|
|
// but users may prefer it stored with data
|
|
val geodeFilename = LaunchUtils.geodeFilename
|
|
val geodePath = File(filesDir.path, "launcher/$geodeFilename")
|
|
if (geodePath.exists()) {
|
|
System.load(geodePath.path)
|
|
return
|
|
}
|
|
|
|
// you know zmx i have 0 clue what this does so im
|
|
// just gonna like copy the binary from external
|
|
// also i get 20 million permission denied errors
|
|
val externalGeodePath = LaunchUtils.getInstalledGeodePath(this)!!
|
|
|
|
val copiedPath = File(filesDir.path, "copied")
|
|
if (copiedPath.exists()) {
|
|
copiedPath.deleteRecursively()
|
|
}
|
|
copiedPath.mkdir()
|
|
|
|
val copiedGeodePath = File(copiedPath.path, "Geode.so")
|
|
|
|
if (externalGeodePath.exists()) {
|
|
DownloadUtils.copyFile(
|
|
FileInputStream(externalGeodePath),
|
|
FileOutputStream(copiedGeodePath)
|
|
)
|
|
|
|
if (copiedGeodePath.exists()) {
|
|
println("Loading Geode from ${externalGeodePath.name}")
|
|
System.load(copiedGeodePath.path)
|
|
return
|
|
}
|
|
}
|
|
|
|
throw e
|
|
}
|
|
}
|
|
|
|
private fun createView(): FrameLayout {
|
|
val frameLayoutParams = ViewGroup.LayoutParams(
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
ViewGroup.LayoutParams.MATCH_PARENT
|
|
)
|
|
val frameLayout = ConstrainedFrameLayout(this)
|
|
frameLayout.layoutParams = frameLayoutParams
|
|
|
|
if (displayMode == DisplayMode.LIMITED) {
|
|
// despite not being perfectly 16:9, this is what Android calls "16:9"
|
|
frameLayout.aspectRatio = 1.86f
|
|
}
|
|
|
|
val editTextLayoutParams = ViewGroup.LayoutParams(
|
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
)
|
|
val editText = Cocos2dxEditText(this)
|
|
editText.layoutParams = editTextLayoutParams
|
|
frameLayout.addView(editText)
|
|
|
|
val glSurfaceView = Cocos2dxGLSurfaceView(this)
|
|
|
|
this.mGLSurfaceView = glSurfaceView
|
|
frameLayout.addView(this.mGLSurfaceView)
|
|
|
|
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
|
|
if (showNotification) {
|
|
val notificationView = ComposeView(this)
|
|
frameLayout.addView(notificationView)
|
|
|
|
notificationView.setContent {
|
|
LaunchNotification()
|
|
}
|
|
}
|
|
|
|
glSurfaceView.setEGLContextClientVersion(2)
|
|
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8)
|
|
|
|
if (isAndroidEmulator()) {
|
|
glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
|
|
}
|
|
|
|
glSurfaceView.initView()
|
|
|
|
val renderer = Cocos2dxRenderer(glSurfaceView)
|
|
glSurfaceView.setCocos2dxRenderer(renderer)
|
|
|
|
if (forceRefreshRate) {
|
|
renderer.setFrameRate = true
|
|
}
|
|
|
|
editText.inputType = 145
|
|
glSurfaceView.cocos2dxEditText = editText
|
|
|
|
return frameLayout
|
|
}
|
|
|
|
private fun setupUIState() {
|
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
|
|
|
displayMode = DisplayMode.fromInt(
|
|
PreferenceUtils.get(this).getInt(PreferenceUtils.Key.DISPLAY_MODE)
|
|
)
|
|
|
|
forceRefreshRate = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.FORCE_HRR)
|
|
|
|
if (forceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
val maxRefreshRate = display?.supportedModes?.maxBy { it.refreshRate }?.refreshRate
|
|
if (maxRefreshRate != null) {
|
|
window.attributes.preferredRefreshRate = maxRefreshRate
|
|
}
|
|
}
|
|
|
|
if (displayMode == DisplayMode.FULLSCREEN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
}
|
|
|
|
hideSystemUi()
|
|
}
|
|
|
|
private fun hideSystemUi() {
|
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
|
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
|
hide(WindowInsetsCompat.Type.systemBars())
|
|
systemBarsBehavior =
|
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
}
|
|
}
|
|
|
|
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
|
super.onWindowFocusChanged(hasWindowFocus)
|
|
mHasWindowFocus = hasWindowFocus
|
|
if (hasWindowFocus && !mIsOnPause) {
|
|
resumeGame()
|
|
}
|
|
|
|
hideSystemUi()
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
FMOD.close()
|
|
}
|
|
|
|
private fun resumeGame() {
|
|
mIsRunning = true
|
|
// Cocos2dxHelper.onResume()
|
|
mGLSurfaceView?.onResume()
|
|
}
|
|
|
|
private fun pauseGame() {
|
|
mIsRunning = false
|
|
// Cocos2dxHelper.onPause()
|
|
mGLSurfaceView?.onPause()
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
mIsOnPause = false
|
|
BaseRobTopActivity.isPaused = false
|
|
if (mHasWindowFocus && !this.mIsRunning) {
|
|
resumeGame()
|
|
}
|
|
}
|
|
|
|
override fun onRestart() {
|
|
super.onRestart()
|
|
mIsOnPause = false
|
|
BaseRobTopActivity.isPaused = false
|
|
if (!this.mIsRunning) {
|
|
resumeGame()
|
|
}
|
|
}
|
|
|
|
override fun onPause() {
|
|
super.onPause()
|
|
mIsOnPause = true
|
|
BaseRobTopActivity.isPaused = true
|
|
if (mIsRunning) {
|
|
pauseGame()
|
|
}
|
|
}
|
|
|
|
override fun runOnGLThread(runnable: Runnable) {
|
|
mGLSurfaceView?.queueEvent(runnable)
|
|
}
|
|
|
|
override fun showDialog(title: String, message: String) {
|
|
runOnUiThread {
|
|
AlertDialog.Builder(this)
|
|
.setTitle(title)
|
|
.setMessage(message)
|
|
// the button shouldn't do anything but close
|
|
.setPositiveButton(R.string.message_box_accept) { _, _ -> }
|
|
.show()
|
|
}
|
|
}
|
|
|
|
override fun showEditTextDialog(
|
|
title: String,
|
|
message: String,
|
|
inputMode: Int,
|
|
inputFlag: Int,
|
|
returnType: Int,
|
|
maxLength: Int
|
|
) {
|
|
TODO("Not yet implemented")
|
|
}
|
|
|
|
private fun isAndroidEmulator(): Boolean {
|
|
Log.d(sTag, "model=" + Build.MODEL)
|
|
val product = Build.PRODUCT
|
|
Log.d(sTag, "product=$product")
|
|
var isEmulator = false
|
|
if (product != null) {
|
|
isEmulator = product == "sdk" || product.contains("_sdk") || product.contains("sdk_")
|
|
}
|
|
Log.d(sTag, "isEmulator=$isEmulator")
|
|
return isEmulator
|
|
}
|
|
|
|
override fun onCapabilityAdded(capability: String): Boolean {
|
|
return when (capability) {
|
|
GeodeUtils.CAPABILITY_EXTENDED_INPUT -> {
|
|
mGLSurfaceView?.useKeyboardEvents = true
|
|
true
|
|
}
|
|
GeodeUtils.CAPABILITY_TIMESTAMP_INPUT -> {
|
|
mGLSurfaceView?.sendTimestampEvents = true
|
|
true
|
|
}
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies a mod from the launcher's assets to the Geode mods directory.
|
|
* This method is not recommended for casual use, the new mod will not be automatically removed.
|
|
*/
|
|
private fun loadInternalMods() {
|
|
val internalModBase = "mods"
|
|
|
|
val modListing = try {
|
|
assets.list(internalModBase)
|
|
} catch (ioe: IOException) {
|
|
emptyArray<String>()
|
|
}
|
|
|
|
val modDirectory = File(
|
|
LaunchUtils.getBaseDirectory(this),
|
|
"game/geode/mods"
|
|
)
|
|
|
|
modDirectory.mkdirs()
|
|
|
|
modListing?.forEach { fileName ->
|
|
if (fileName.endsWith(".geode")) {
|
|
val modOutput = File(modDirectory, fileName)
|
|
|
|
val mod = assets.open("$internalModBase/$fileName")
|
|
DownloadUtils.copyFile(mod, modOutput.outputStream())
|
|
|
|
println("Copied internal mod $fileName")
|
|
}
|
|
}
|
|
}
|
|
}
|