forked from geode-sdk/android-launcher
change package name
This commit is contained in:
13 files changed
+25
-25
No files matched your search
@@ -0,0 +1,311 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.FileUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.utils.Constants
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
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.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
|
||||
class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperListener {
|
||||
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 mReceiver: BroadcastReceiver? = null
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setupUIState()
|
||||
|
||||
// return back to main if Geometry Dash isn't found
|
||||
if (!LaunchUtils.isGeometryDashInstalled(packageManager)) {
|
||||
val launchIntent = Intent(this, MainActivity::class.java)
|
||||
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
||||
startActivity(launchIntent)
|
||||
}
|
||||
|
||||
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
|
||||
val gdNativeLibraryPath = "${gdPackageInfo.applicationInfo.nativeLibraryDir}/"
|
||||
|
||||
try {
|
||||
LaunchUtils.addAssetsFromPackage(assets, gdPackageInfo)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
try {
|
||||
// fixes bugs specific to the new app directory, such as package name
|
||||
LauncherFix.loadLibrary()
|
||||
LauncherFix.setOriginalDataPath(Constants.GJ_DATA_DIR)
|
||||
LauncherFix.setDataPath(filesDir.path)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
System.load("$gdNativeLibraryPath/lib${Constants.FMOD_LIB_NAME}.so")
|
||||
System.load("$gdNativeLibraryPath/lib${Constants.COCOS_LIB_NAME}.so")
|
||||
|
||||
if (getLoadTesting()) {
|
||||
loadTestingLibraries()
|
||||
}
|
||||
|
||||
FMOD.init(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(createView())
|
||||
|
||||
Cocos2dxHelper.init(this, this)
|
||||
|
||||
JniToCpp.setupHSSAssets(
|
||||
gdPackageInfo.applicationInfo.sourceDir,
|
||||
Environment.getExternalStorageDirectory().absolutePath
|
||||
)
|
||||
Cocos2dxHelper.nativeSetApkPath(gdPackageInfo.applicationInfo.sourceDir)
|
||||
|
||||
BaseRobTopActivity.setCurrentActivity(this)
|
||||
registerReceiver()
|
||||
}
|
||||
|
||||
private fun createView(): FrameLayout {
|
||||
val frameLayoutParams = ViewGroup.LayoutParams(-1, -1)
|
||||
val frameLayout = FrameLayout(this)
|
||||
frameLayout.layoutParams = frameLayoutParams
|
||||
val editTextLayoutParams = ViewGroup.LayoutParams(-1, -2)
|
||||
val editText = Cocos2dxEditText(this)
|
||||
editText.layoutParams = editTextLayoutParams
|
||||
frameLayout.addView(editText)
|
||||
|
||||
val glSurfaceView = Cocos2dxGLSurfaceView(this)
|
||||
|
||||
this.mGLSurfaceView = glSurfaceView
|
||||
frameLayout.addView(this.mGLSurfaceView)
|
||||
|
||||
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8)
|
||||
|
||||
if (isAndroidEmulator()) {
|
||||
glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0)
|
||||
}
|
||||
|
||||
glSurfaceView.initView()
|
||||
glSurfaceView.setCocos2dxRenderer(Cocos2dxRenderer())
|
||||
|
||||
editText.inputType = 145
|
||||
glSurfaceView.cocos2dxEditText = editText
|
||||
|
||||
return frameLayout
|
||||
}
|
||||
|
||||
private fun setupUIState() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||
|
||||
hideSystemUi()
|
||||
}
|
||||
|
||||
private fun hideSystemUi() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
// window compat is dumb!!
|
||||
setLegacyVisibility()
|
||||
window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
|
||||
if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
|
||||
setLegacyVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||
systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLegacyVisibility() {
|
||||
// setDecorFitsSystemWindows doesn't hide anything
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceivers()
|
||||
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 onPause() {
|
||||
super.onPause()
|
||||
mIsOnPause = true
|
||||
BaseRobTopActivity.isPaused = true
|
||||
if (mIsRunning) {
|
||||
pauseGame()
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerReceiver() {
|
||||
unregisterReceivers()
|
||||
try {
|
||||
val filter = IntentFilter(Intent.ACTION_SCREEN_ON)
|
||||
filter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
filter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
mReceiver = BaseRobTopActivity.ReceiverScreen()
|
||||
registerReceiver(mReceiver, filter)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterReceivers() {
|
||||
if (mReceiver != null) {
|
||||
unregisterReceiver(mReceiver)
|
||||
mReceiver = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasWindowFocus)
|
||||
mHasWindowFocus = hasWindowFocus
|
||||
if (hasWindowFocus && !mIsOnPause) {
|
||||
resumeGame()
|
||||
}
|
||||
}
|
||||
|
||||
override fun runOnGLThread(runnable: Runnable) {
|
||||
mGLSurfaceView?.queueEvent(runnable)
|
||||
}
|
||||
|
||||
override fun showDialog(title: String, message: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private fun getLoadTesting(): Boolean {
|
||||
val preferences = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE)
|
||||
|
||||
return preferences.getBoolean(getString(R.string.preference_load_testing), false)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
private fun loadTestingLibraries() {
|
||||
// clear data dir
|
||||
val testDirPath = File(filesDir.path + File.separator + "testlib" + File.separator)
|
||||
if (testDirPath.exists()) {
|
||||
testDirPath.deleteRecursively()
|
||||
}
|
||||
testDirPath.mkdir()
|
||||
|
||||
getExternalFilesDir(null)?.let { dir ->
|
||||
val testingPath = dir.path + File.separator + "test" + File.separator
|
||||
|
||||
File(testingPath).walk().forEach {
|
||||
if (it.isFile) {
|
||||
// welcome to the world of Android classloader permissions
|
||||
val outputFile = File(testDirPath.path + File.separator + it.name)
|
||||
copyFile(FileInputStream(it), FileOutputStream(outputFile))
|
||||
|
||||
try {
|
||||
println("Loading test library ${outputFile.name}")
|
||||
System.load(outputFile.path)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
|
||||
// gotta love copying
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
FileUtils.copy(inputStream, outputStream)
|
||||
} else {
|
||||
inputStream.use { input ->
|
||||
outputStream.use { output ->
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
while (true) {
|
||||
val byteCount = input.read(buffer)
|
||||
if (byteCount < 0) break
|
||||
output.write(buffer, 0, byteCount)
|
||||
}
|
||||
output.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import com.geode.launcher.utils.Constants
|
||||
|
||||
object LauncherFix {
|
||||
fun loadLibrary() {
|
||||
System.loadLibrary(Constants.LAUNCHER_FIX_LIB_NAME)
|
||||
}
|
||||
|
||||
external fun setDataPath(dataPath: String)
|
||||
|
||||
external fun setOriginalDataPath(dataPath: String)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.countdownTimerWatcher
|
||||
import com.geode.launcher.utils.preferenceWatcher
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager)
|
||||
|
||||
setContent {
|
||||
GeodeLauncherTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
MainScreen(gdInstalled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onLaunch(context: Context) {
|
||||
val launchIntent = Intent(context, GeometryDashActivity::class.java)
|
||||
launchIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
fun onSettings(context: Context) {
|
||||
val launchIntent = Intent(context, SettingsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(gdInstalled: Boolean = true) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val shouldAutomaticallyLaunch = preferenceWatcher(
|
||||
preferenceFileKey = R.string.preference_file_key,
|
||||
preferenceId = R.string.preference_load_automatically
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.geode_logo),
|
||||
contentDescription = context.getString(R.string.launcher_logo_alt),
|
||||
modifier = Modifier.size(136.dp, 136.dp)
|
||||
)
|
||||
Text(
|
||||
context.getString(R.string.launcher_title),
|
||||
fontSize = 32.sp,
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
if (gdInstalled) {
|
||||
if (shouldAutomaticallyLaunch.value) {
|
||||
val countdownTimer = countdownTimerWatcher(
|
||||
time = 3000,
|
||||
onCountdownFinish = {
|
||||
// just in case this changed async
|
||||
if (shouldAutomaticallyLaunch.value) {
|
||||
onLaunch(context)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (countdownTimer.value != 0) {
|
||||
Text(
|
||||
context.resources.getQuantityString(
|
||||
R.plurals.automatically_load_countdown, countdownTimer.value, countdownTimer.value
|
||||
),
|
||||
style = Typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Button(onClick = { onLaunch(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = context.getString(R.string.launcher_launch_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(context.getString(R.string.launcher_launch))
|
||||
}
|
||||
Spacer(Modifier.size(2.dp))
|
||||
IconButton(onClick = { onSettings(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
context.getString(R.string.game_not_found),
|
||||
modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
OutlinedButton(onClick = { onSettings(context) }) {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
|
||||
)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
Text(context.getString(R.string.launcher_settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun MainScreenLightPreview() {
|
||||
GeodeLauncherTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun MainScreenDarkPreview() {
|
||||
GeodeLauncherTheme(darkTheme = true) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
MainScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun MainScreenNoGeometryDashPreview() {
|
||||
GeodeLauncherTheme {
|
||||
MainScreen(gdInstalled = false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.geode.launcher
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
|
||||
class SettingsActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
GeodeLauncherTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
SettingsScreen(
|
||||
onBackPressedDispatcher = onBackPressedDispatcher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
fun onExportSaveData(context: Context) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"This function is not implemented yet!",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
*/
|
||||
|
||||
fun onOpenFolder(context: Context) {
|
||||
context.getExternalFilesDir(null)?.let { file ->
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.export_folder_tag),
|
||||
file.path
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.export_folder_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = context.getString(R.string.settings_back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
context.getString(R.string.title_activity_settings),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OptionsGroup(context.getString(R.string.preference_category_testing)) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_load_testing_name),
|
||||
preferenceId = R.string.preference_load_testing
|
||||
)
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_load_automatically_name),
|
||||
description = context.getString(R.string.preference_load_automatically_description),
|
||||
preferenceId = R.string.preference_load_automatically
|
||||
)
|
||||
OptionsButton(
|
||||
title = context.getString(R.string.preferences_copy_external_button),
|
||||
onClick = { onOpenFolder(context) }
|
||||
)
|
||||
}
|
||||
/*
|
||||
OptionsGroup("Data") {
|
||||
OptionsButton(
|
||||
title = "Export save data",
|
||||
onClick = { onExportSaveData(context) }
|
||||
)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun toggleSetting(context: Context, @StringRes preferenceId: Int): Boolean {
|
||||
val preferences = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key), Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
val current = preferences.getBoolean(
|
||||
context.getString(preferenceId), false
|
||||
)
|
||||
preferences.edit {
|
||||
putBoolean(context.getString(preferenceId), !current)
|
||||
commit()
|
||||
}
|
||||
|
||||
return preferences.getBoolean(context.getString(preferenceId), false)
|
||||
}
|
||||
|
||||
fun getSetting(context: Context, @StringRes preferenceId: Int): Boolean {
|
||||
val preferences = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key), Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
return preferences.getBoolean(context.getString(preferenceId), false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsButton(title: String, onClick: () -> Unit) {
|
||||
OptionsCard(
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick, role = Role.Button)
|
||||
) { }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsCard(title: String, description: String? = null, @StringRes preferenceId: Int) {
|
||||
val context = LocalContext.current
|
||||
val settingEnabled = remember {
|
||||
mutableStateOf(getSetting(context, preferenceId)) }
|
||||
|
||||
OptionsCard(
|
||||
title = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(0.75f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(title)
|
||||
if (!description.isNullOrEmpty()) {
|
||||
Text(
|
||||
description,
|
||||
style = Typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.toggleable(
|
||||
value = settingEnabled.value,
|
||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceId) },
|
||||
role = Role.Switch,
|
||||
)
|
||||
) {
|
||||
Switch(checked = settingEnabled.value, onCheckedChange = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
title()
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun OptionsCardPreview() {
|
||||
GeodeLauncherTheme {
|
||||
OptionsGroup(title = "Preview Group") {
|
||||
SettingsCard(
|
||||
title = "Load files from /test",
|
||||
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
|
||||
preferenceId = R.string.preference_load_testing
|
||||
)
|
||||
SettingsCard(
|
||||
title = "Testing option 2",
|
||||
preferenceId = R.string.preference_load_automatically
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
GeodeLauncherTheme {
|
||||
SettingsScreen(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.geode.launcher.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.geode.launcher.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GeodeLauncherTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
with ((view.context as Activity).window) {
|
||||
statusBarColor = colorScheme.background.toArgb()
|
||||
navigationBarColor = colorScheme.background.toArgb()
|
||||
}
|
||||
with (ViewCompat.getWindowInsetsController(view)) {
|
||||
this?.isAppearanceLightStatusBars = !darkTheme
|
||||
this?.isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.geode.launcher.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 18.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
||||
object Constants {
|
||||
const val PACKAGE_NAME = "com.robtopx.geometryjump"
|
||||
|
||||
const val COCOS_LIB_NAME = "cocos2dcpp"
|
||||
const val FMOD_LIB_NAME = "fmod"
|
||||
const val LAUNCHER_FIX_LIB_NAME = "launcherfix"
|
||||
|
||||
// this value is hardcoded into GD
|
||||
@SuppressLint("SdCardPath")
|
||||
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
const val MS_TO_SEC = 1000
|
||||
|
||||
@Composable
|
||||
fun countdownTimerWatcher(lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, time: Int, onCountdownFinish: () -> Unit): MutableState<Int> {
|
||||
val timeData = remember {
|
||||
mutableStateOf(time / MS_TO_SEC % MS_TO_SEC)
|
||||
}
|
||||
|
||||
val countdownTimer =
|
||||
object : CountDownTimer(time.toLong(), 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
timeData.value = (millisUntilFinished / MS_TO_SEC % MS_TO_SEC).toInt() + 1
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
onCountdownFinish()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_START) {
|
||||
countdownTimer.start()
|
||||
} else if (event == Lifecycle.Event.ON_STOP) {
|
||||
countdownTimer.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
return timeData
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.AssetManager
|
||||
|
||||
object LaunchUtils {
|
||||
fun isGeometryDashInstalled(packageManager: PackageManager): Boolean {
|
||||
return try {
|
||||
packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
|
||||
true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun addAssetsFromPackage(assetManager: AssetManager, packageInfo: PackageInfo) {
|
||||
// this method is officially marked as deprecated but it is the only method we are allowed to reflect
|
||||
// (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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.geode.launcher.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
|
||||
@Composable
|
||||
fun preferenceWatcher(@StringRes preferenceFileKey: Int, @StringRes preferenceId: Int, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
|
||||
val context = LocalContext.current
|
||||
val preferences = context.getSharedPreferences(
|
||||
context.getString(preferenceFileKey), Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
val preferenceValue = remember {
|
||||
mutableStateOf(
|
||||
preferences.getBoolean(
|
||||
context.getString(preferenceId),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
sharedPreferences, key -> preferenceValue.value = sharedPreferences.getBoolean(key, false)
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_START) {
|
||||
preferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
} else if (event == Lifecycle.Event.ON_DESTROY) {
|
||||
preferences.unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
return preferenceValue
|
||||
}
|
||||
Reference in new issue
Block a user