implement enough to launch 2.2 (#2)

* generic maintenance and allow armv8

* implement missing java functions

* Update build.yml

* add missing audiodevice code

* split apk support

* ignore (gitignore)

* replace library load with native code

* set supported abis again

---------

Co-authored-by: alk <45172705+altalk23@users.noreply.github.com>
This commit is contained in:
Chloeandalk authored and GitHub committed 2023-12-22 04:14:05 -07:00
1 parent c08ae0ec13
commit c85470013f
24 files changed
+522 -252

No files matched your search

+7 -8
View File
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId "com.geode.launcher"
minSdk 23
targetSdk 33
targetSdk 34
versionCode 1
versionName "0.6.0-alpha.1"
@@ -21,8 +21,8 @@ android {
arguments "-DDOBBY_DEBUG=OFF"
}
}
// support only armv7 as GD only supports armv7
ndkConfig.abiFilters "armeabi-v7a"
ndkConfig.abiFilters "arm64-v8a", "armeabi-v7a"
}
buildTypes {
@@ -62,14 +62,13 @@ android {
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'androidx.compose.material3:material3:1.1.1'
implementation 'androidx.compose.material3:material3:1.1.2'
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation files('../libs/fmod.jar')
implementation 'com.google.android.material:material:1.11.0'
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
}
+3 -1
View File
@@ -22,4 +22,6 @@ if(NOT dobby_POPULATED)
add_subdirectory("${dobby_SOURCE_DIR}" ${dobby_BINARY_DIR} EXCLUDE_FROM_ALL)
target_include_directories(launcherfix PRIVATE ${dobby_SOURCE_DIR}/include)
target_link_libraries(launcherfix dobby)
endif()
endif()
target_link_libraries(launcherfix log)
+44 -2
View File
@@ -1,7 +1,13 @@
#include <android/dlext.h>
#include <android/log.h>
#include <dlfcn.h>
#include <string>
#include <dobby.h>
#include <jni.h>
#include <string>
#include <unistd.h>
#ifndef DISABLE_LAUNCHER_FIX
#include <dobby.h>
#endif
class DataPaths {
public:
@@ -27,6 +33,8 @@ JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setDataPath(
auto data_path_str = env->GetStringUTFChars(data_path, &is_copy);
DataPaths::get_instance().data_path = std::string(data_path_str);
env->ReleaseStringUTFChars(data_path, data_path_str);
}
extern "C"
@@ -39,6 +47,38 @@ JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setOriginalDataPath(
auto data_path_str = env->GetStringUTFChars(data_path, &is_copy);
DataPaths::get_instance().original_data_path = std::string(data_path_str);
env->ReleaseStringUTFChars(data_path, data_path_str);
}
extern "C"
JNIEXPORT jboolean JNICALL Java_com_geode_launcher_LauncherFix_loadLibraryFromOffset(JNIEnv *env, jobject, jstring library_name, jint fd, jlong offset) {
// loads a given library at an offset and file descriptor
// assumes we have ownership of the file passed in fd
auto is_copy = jboolean();
auto library_cname = env->GetStringUTFChars(library_name, &is_copy);
android_dlextinfo ext_info{};
ext_info.flags = ANDROID_DLEXT_USE_LIBRARY_FD | ANDROID_DLEXT_USE_LIBRARY_FD_OFFSET;
ext_info.library_fd = fd;
ext_info.library_fd_offset = offset;
auto handle = android_dlopen_ext(library_cname, RTLD_NOW | RTLD_GLOBAL, &ext_info);
env->ReleaseStringUTFChars(library_name, library_cname);
close(fd);
if (handle == nullptr) {
auto error = dlerror();
__android_log_print(ANDROID_LOG_WARN, "GeodeLauncher-Fix", "dlopen_ext failed. given: %s\n", error);
return false;
}
// we don't need the library anymore
dlclose(handle);
return true;
}
FILE* (*fopen_original)(const char *pathname, const char *mode);
@@ -60,6 +100,7 @@ FILE* fopen_hook(const char* pathname, const char* mode) {
}
[[gnu::constructor]] [[gnu::used]] void setup_hooks() {
#ifndef DISABLE_LAUNCHER_FIX
auto fopen_addr = dlsym(RTLD_NEXT, "fopen");
DobbyHook(
@@ -67,4 +108,5 @@ FILE* fopen_hook(const char* pathname, const char* mode) {
reinterpret_cast<dobby_dummy_func_t>(&fopen_hook),
reinterpret_cast<dobby_dummy_func_t*>(&fopen_original)
);
#endif
}
@@ -9,6 +9,7 @@ import android.net.NetworkCapabilities
import android.net.Uri
import android.provider.Settings
import android.util.Log
import android.view.WindowManager
import android.widget.Toast
import com.customRobTop.JniToCpp.resumeSound
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
@@ -127,6 +128,11 @@ object BaseRobTopActivity {
isLoaded = true
}
@JvmStatic
fun getDeviceRefreshRate(): Float {
return (me.get()?.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.refreshRate
}
// Everyplay doesn't even exist anymore lol
@JvmStatic
fun setupEveryplay() {}
@@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.PackageInfo
import android.os.Build
import android.os.Bundle
import android.os.Environment
@@ -44,7 +45,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mHasWindowFocus = false
private var mReceiver: BroadcastReceiver? = null
@SuppressLint("UnsafeDynamicallyLoadedCode")
override fun onCreate(savedInstanceState: Bundle?) {
setupUIState()
@@ -57,7 +57,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val gdNativeLibraryPath = "${gdPackageInfo.applicationInfo.nativeLibraryDir}/"
try {
LaunchUtils.addAssetsFromPackage(assets, gdPackageInfo)
@@ -86,8 +85,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this)
System.load("$gdNativeLibraryPath/lib${Constants.FMOD_LIB_NAME}.so")
System.load("$gdNativeLibraryPath/lib${Constants.COCOS_LIB_NAME}.so")
tryLoadLibrary(gdPackageInfo, Constants.FMOD_LIB_NAME)
tryLoadLibrary(gdPackageInfo, Constants.COCOS_LIB_NAME)
if (getLoadTesting()) {
loadTestingLibraries()
@@ -114,6 +113,44 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
}
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun tryLoadLibrary(packageInfo: PackageInfo, libraryName: String) {
try {
val nativeDir = packageInfo.applicationInfo.nativeLibraryDir
System.load("$nativeDir/lib$libraryName.so")
} catch (ule: UnsatisfiedLinkError) {
loadLibraryFromAssets(libraryName)
}
}
private fun loadLibraryFromAssets(libraryName: String) {
// loads a library loaded in assets (which points to the apk + an offset)
// these libraries are available to the application after merging
// find the first instance of the library in preferred abi order
val libraryFd = Build.SUPPORTED_ABIS.asSequence()
.mapNotNull {
try {
assets.openNonAssetFd("lib/$it/lib$libraryName.so")
} catch (_: Exception) {
null
}
}
.firstOrNull() ?: throw UnsatisfiedLinkError("Could not find library lib$libraryName.so")
val fdOffset = libraryFd.startOffset
val fdDescriptor = libraryFd.parcelFileDescriptor.detachFd()
if (!LauncherFix.loadLibraryFromOffset("lib$libraryName.so", fdDescriptor, fdOffset)) {
throw UnsatisfiedLinkError("Failed to load asset lib$libraryName.so")
}
// after the library is opened in native code, it should be available for loading
// you can read more about the behavior:
// https://android.googlesource.com/platform/libcore/+/7f3eb2e0ac87bdea471bc577380cf50025aebde5/ojluni/src/main/java/java/lang/Runtime.java#1060
System.loadLibrary(libraryName)
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
private fun loadGeodeLibrary(): Boolean {
// Load Geode if exists
@@ -10,4 +10,6 @@ object LauncherFix {
external fun setDataPath(dataPath: String)
external fun setOriginalDataPath(dataPath: String)
external fun loadLibraryFromOffset(libraryName: String, fd: Int, offset: Long): Boolean
}
@@ -4,7 +4,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.content.Context
import com.geode.launcher.utils.Constants
import java.io.File
object LaunchUtils {
@@ -45,6 +44,10 @@ object LaunchUtils {
// (the source recommends replacing with AssetManager.setApkAssets(ApkAssets[], boolean) lol)
val clazz = assetManager.javaClass
val aspMethod = clazz.getDeclaredMethod("addAssetPath", String::class.java)
aspMethod.invoke(assetManager, packageInfo.applicationInfo.sourceDir)
packageInfo.applicationInfo.splitSourceDirs?.forEach {
aspMethod.invoke(assetManager, it)
}
}
}
+66
View File
@@ -0,0 +1,66 @@
package org.fmod
import android.media.AudioTrack
import android.util.Log
class AudioDevice {
private var mTrack: AudioTrack? = null
private fun fetchChannelConfigFromCount(i: Int): Int {
if (i == 1) {
return 2
}
if (i == 2) {
return 3
}
if (i == 6) {
return 252
}
return if (i == 8) 6396 else 0
}
fun init(i: Int, i2: Int, i3: Int, i4: Int): Boolean {
val fetchChannelConfigFromCount = fetchChannelConfigFromCount(i)
val minBufferSize = AudioTrack.getMinBufferSize(i2, fetchChannelConfigFromCount, 2)
if (minBufferSize < 0) {
Log.w(
"fmod",
"AudioDevice::init : Couldn't query minimum buffer size, possibly unsupported sample rate or channel count"
)
} else {
Log.i("fmod", "AudioDevice::init : Min buffer size: $minBufferSize bytes")
}
val i5 = i3 * i4 * i * 2
val i6 = if (i5 > minBufferSize) i5 else minBufferSize
Log.i("fmod", "AudioDevice::init : Actual buffer size: $i6 bytes")
return try {
val audioTrack = AudioTrack(3, i2, fetchChannelConfigFromCount, 2, i6, 1)
mTrack = audioTrack
try {
audioTrack.play()
true
} catch (unused: IllegalStateException) {
Log.e("fmod", "AudioDevice::init : AudioTrack play caused IllegalStateException")
mTrack?.release()
mTrack = null
false
}
} catch (unused2: IllegalArgumentException) {
Log.e("fmod", "AudioDevice::init : AudioTrack creation caused IllegalArgumentException")
false
}
}
fun close() {
try {
mTrack?.stop()
} catch (unused: IllegalStateException) {
Log.e("fmod", "AudioDevice::init : AudioTrack stop caused IllegalStateException")
}
mTrack?.release()
mTrack = null
}
fun write(sArr: ShortArray?, i: Int) {
mTrack!!.write(sArr!!, 0, i)
}
}
+142
View File
@@ -0,0 +1,142 @@
package org.fmod
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.AssetManager
import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.util.Log
import java.io.FileNotFoundException
// i never agreed to your licenses! take that
@SuppressLint("StaticFieldLeak")
object FMOD {
private var gContext: Context? = null
private val gPluginBroadcastReceiver = PluginBroadcastReceiver()
external fun OutputAAudioHeadphonesChanged()
@JvmStatic
fun init(context: Context?) {
gContext = context
if (context != null) {
gContext!!.registerReceiver(
gPluginBroadcastReceiver,
IntentFilter("android.intent.action.HEADSET_PLUG")
)
}
}
@JvmStatic
fun close() {
val context = gContext
context?.unregisterReceiver(gPluginBroadcastReceiver)
gContext = null
}
@JvmStatic
fun checkInit(): Boolean {
return gContext != null
}
@JvmStatic
fun getAssetManager(): AssetManager? {
val context = gContext
return context?.assets
}
@JvmStatic
fun supportsLowLatency(): Boolean {
val outputBlockSize = getOutputBlockSize()
val lowLatencyFlag = lowLatencyFlag()
val proAudioFlag = proAudioFlag()
val z = outputBlockSize in 1..1024
val isBluetoothOn = isBluetoothOn()
Log.i(
"fmod",
"FMOD::supportsLowLatency : Low latency = $lowLatencyFlag, Pro Audio = $proAudioFlag, Bluetooth On = $isBluetoothOn, Acceptable Block Size = $z ($outputBlockSize)"
)
return z && lowLatencyFlag && !isBluetoothOn
}
@JvmStatic
fun lowLatencyFlag(): Boolean {
if (Build.VERSION.SDK_INT < 5) {
return false
}
val context = gContext ?: return false
return context.packageManager.hasSystemFeature("android.hardware.audio.low_latency")
}
@JvmStatic
fun proAudioFlag(): Boolean {
if (Build.VERSION.SDK_INT < 5) {
return false
}
val context = gContext ?: return false
return context.packageManager.hasSystemFeature("android.hardware.audio.pro")
}
@JvmStatic
fun supportsAAudio(): Boolean {
return Build.VERSION.SDK_INT >= 27
}
@JvmStatic
fun getOutputSampleRate(): Int {
if (Build.VERSION.SDK_INT < 17) {
return 0
}
val context = gContext ?: return 0
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val property = audioService.getProperty("android.media.property.OUTPUT_SAMPLE_RATE")
return property?.toInt() ?: 0
}
@JvmStatic
fun getOutputBlockSize(): Int {
if (Build.VERSION.SDK_INT < 17) {
return 0
}
val context = gContext ?: return 0
val audioService = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val property = audioService.getProperty("android.media.property.OUTPUT_FRAMES_PER_BUFFER")
return property?.toInt() ?: 0
}
@JvmStatic
fun isBluetoothOn(): Boolean {
val context = gContext ?: return false
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return audioManager.isBluetoothA2dpOn || audioManager.isBluetoothScoOn
}
@JvmStatic
fun fileDescriptorFromUri(str: String?): Int {
gContext?.apply {
try {
contentResolver.openFileDescriptor(Uri.parse(str), "r")?.apply {
return detachFd()
}
} catch (_: FileNotFoundException) { }
}
return -1
}
internal class PluginBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
OutputAAudioHeadphonesChanged()
}
}
}
+200
View File
@@ -0,0 +1,200 @@
package org.fmod
import android.media.MediaCrypto
import android.media.MediaDataSource
import android.media.MediaExtractor
import android.media.MediaCodec.BufferInfo
import android.util.Log
import android.view.Surface
import java.io.IOException
import java.nio.ByteBuffer
class MediaCodec {
private var channelCount = 0
var mCodecPtr: Long = 0
private var mCurrentOutputBufferIndex = -1
private var mDataSourceProxy: Any? = null
private var mDecoder: android.media.MediaCodec? = null
private var mExtractor: MediaExtractor? = null
private var mInputBuffers: Array<ByteBuffer>? = null
private var mInputFinished = false
private var length: Long = 0
private var mOutputBuffers: Array<ByteBuffer>? = null
private var mOutputFinished = false
private var sampleRate = 0
fun init(j: Long): Boolean {
mCodecPtr = j
var i = 0
try {
val mediaExtractor = MediaExtractor()
mExtractor = mediaExtractor
mediaExtractor.setDataSource(object : MediaDataSource() {
override fun close() {}
override fun readAt(j: Long, bArr: ByteArray, i: Int, i2: Int): Int {
return fmodReadAt(mCodecPtr, j, bArr, i, i2)
}
override fun getSize(): Long {
return fmodGetSize(mCodecPtr)
}
})
} catch (e5: IOException) {
Log.w("fmod", "MediaCodec::init : $e5")
return false
}
val trackCount = mExtractor!!.trackCount
var i2 = 0
while (i2 < trackCount) {
val trackFormat = mExtractor!!.getTrackFormat(i2)
val string = trackFormat.getString("mime")
Log.d(
"fmod",
"MediaCodec::init : Format $i2 / $trackCount -- $trackFormat"
)
if (string == "audio/mp4a-latm") {
return try {
mDecoder = android.media.MediaCodec.createDecoderByType(string)
mExtractor!!.selectTrack(i2)
mDecoder!!.configure(trackFormat, null as Surface?, null as MediaCrypto?, 0)
mDecoder!!.start()
mInputBuffers = mDecoder!!.inputBuffers
mOutputBuffers = mDecoder!!.outputBuffers
val integer =
if (trackFormat.containsKey("encoder-delay")) trackFormat.getInteger("encoder-delay") else 0
if (trackFormat.containsKey("encoder-padding")) {
i = trackFormat.getInteger("encoder-padding")
}
val j2 = trackFormat.getLong("durationUs")
channelCount = trackFormat.getInteger("channel-count")
val integer2 = trackFormat.getInteger("sample-rate")
sampleRate = integer2
length =
(((j2 * integer2.toLong() + 999999) / 1000000).toInt() - integer - i).toLong()
true
} catch (e6: IOException) {
Log.e("fmod", "MediaCodec::init : $e6")
false
}
} else {
i2++
}
}
return false
}
fun release() {
val mediaCodec = mDecoder
if (mediaCodec != null) {
mediaCodec.stop()
mDecoder?.release()
mDecoder = null
}
val mediaExtractor = mExtractor
if (mediaExtractor != null) {
mediaExtractor.release()
mExtractor = null
}
}
fun read(bArr: ByteArray?, i: Int): Int {
var dequeueInputBuffer = 0
val i2 =
if (!mInputFinished || !mOutputFinished || mCurrentOutputBufferIndex != -1) 0 else -1
while (!mInputFinished && mDecoder!!.dequeueInputBuffer(0).also {
dequeueInputBuffer = it
} >= 0) {
val readSampleData = mExtractor!!.readSampleData(mInputBuffers!![dequeueInputBuffer], 0)
if (readSampleData >= 0) {
mDecoder!!.queueInputBuffer(
dequeueInputBuffer,
0,
readSampleData,
mExtractor!!.sampleTime,
0
)
mExtractor!!.advance()
} else {
mDecoder!!.queueInputBuffer(dequeueInputBuffer, 0, 0, 0, 4)
mInputFinished = true
}
}
if (!mOutputFinished && mCurrentOutputBufferIndex == -1) {
val bufferInfo = BufferInfo()
val dequeueOutputBuffer = mDecoder!!.dequeueOutputBuffer(bufferInfo, 10000)
if (dequeueOutputBuffer >= 0) {
mCurrentOutputBufferIndex = dequeueOutputBuffer
mOutputBuffers!![dequeueOutputBuffer].limit(bufferInfo.size)
mOutputBuffers!![dequeueOutputBuffer].position(bufferInfo.offset)
} else if (dequeueOutputBuffer == -3) {
mOutputBuffers = mDecoder!!.outputBuffers
} else if (dequeueOutputBuffer == -2) {
Log.d(
"fmod",
"MediaCodec::read : MediaCodec::dequeueOutputBuffer returned MediaCodec.INFO_OUTPUT_FORMAT_CHANGED " + mDecoder!!.outputFormat
)
} else if (dequeueOutputBuffer == -1) {
Log.d(
"fmod",
"MediaCodec::read : MediaCodec::dequeueOutputBuffer returned MediaCodec.INFO_TRY_AGAIN_LATER."
)
} else {
Log.w(
"fmod",
"MediaCodec::read : MediaCodec::dequeueOutputBuffer returned $dequeueOutputBuffer"
)
}
if (bufferInfo.flags and 4 !== 0) {
mOutputFinished = true
}
}
val i3 = mCurrentOutputBufferIndex
if (i3 == -1) {
return i2
}
val byteBuffer = mOutputBuffers!![i3]
val min = byteBuffer.remaining().coerceAtMost(i)
byteBuffer[bArr, 0, min]
if (!byteBuffer.hasRemaining()) {
byteBuffer.clear()
mDecoder!!.releaseOutputBuffer(mCurrentOutputBufferIndex, false)
mCurrentOutputBufferIndex = -1
}
return min
}
fun seek(i: Int) {
val i2 = mCurrentOutputBufferIndex
if (i2 != -1) {
mOutputBuffers!![i2].clear()
mCurrentOutputBufferIndex = -1
}
mInputFinished = false
mOutputFinished = false
mDecoder!!.flush()
val j = i.toLong()
mExtractor!!.seekTo(j * 1000000 / sampleRate.toLong(), MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
val sampleTime = (mExtractor!!.sampleTime * sampleRate.toLong() + 999999) / 1000000
var i3 = ((j - sampleTime) * channelCount.toLong() * 2).toInt()
if (i3 < 0) {
Log.w(
"fmod",
"MediaCodec::seek : Seek to $i resulted in position $sampleTime"
)
return
}
val bArr = ByteArray(1024)
while (i3 > 0) {
i3 -= read(bArr, Math.min(1024, i3))
}
}
companion object {
external fun fmodGetSize(j: Long): Long
external fun fmodReadAt(j: Long, j2: Long, bArr: ByteArray?, i: Int, i2: Int): Int
}
}