Compare commits

...
30 Commits
Author SHA1 Message Date
qimiko 498bbf4528 accidentally broke a dependency 2024-01-23 05:14:36 -07:00
qimiko dc72608622 properly set value in set 2024-01-23 05:11:56 -07:00
qimiko a50e0a0830 remove extra println 2024-01-23 05:05:06 -07:00
qimiko db707dfce1 only show unsupported version once 2024-01-23 05:03:37 -07:00
qimiko 929cc7615e api to set preference through compose 2024-01-23 05:03:18 -07:00
qimiko bb2e3fbb1b version strings are meaningless 2024-01-23 04:48:55 -07:00
qimiko 7d548aef6c remove unused import 2024-01-23 03:18:54 -07:00
qimiko 7f64236e89 add launcher update check 2024-01-23 03:15:39 -07:00
qimiko 199b6f142b use nightly by default in debug mode 2024-01-23 03:12:44 -07:00
qimiko af87320f11 clarify unsupported version warning 2024-01-23 02:18:48 -07:00
qimiko 665756d85b automatically trim log messages 2024-01-23 01:33:54 -07:00
qimiko 590006413c filter by crash buffer option 2024-01-23 01:31:38 -07:00
qimiko 1b079fa3ff add ability to export logs to file 2024-01-23 01:01:14 -07:00
qimiko cb16db3bf9 use specific message for abi differences 2024-01-23 00:19:55 -07:00
qimiko de6fad41f6 fix nightly download url 2024-01-22 11:38:11 -07:00
qimiko d1a43230a9 gd may have updated 2024-01-22 08:16:42 -07:00
qimiko 442ef83339 show launcher version in settings 2024-01-22 00:28:29 -07:00
qimiko 1eab544692 reorganize release api 2024-01-22 00:16:01 -07:00
qimiko af0ebeab84 save game during onstop 2024-01-21 16:15:22 -07:00
qimiko 0844dc97d2 use kotlin stream copyto for file copies 2024-01-21 16:15:11 -07:00
qimiko 42c10c2729 navigation bar visibility changes 2024-01-21 03:40:26 -07:00
qimiko cf568bcbe2 use release hash instead of modified time 2024-01-20 20:28:08 -07:00
qimiko 36152d8773 add icon to retry button 2024-01-20 20:26:00 -07:00
qimiko 3f371be097 pretend log messages are utf8 2024-01-20 20:17:39 -07:00
qimiko 5eba097e38 only request permissions when needed 2024-01-20 18:37:59 -07:00
qimiko 7e868d8d0a log optimizations 2024-01-20 18:09:53 -07:00
qimiko 97b851cbc1 add to the version string 2024-01-20 06:28:29 -07:00
qimiko 397cb9b9bd add in app log viewer 2024-01-20 06:20:50 -07:00
qimiko 05eb3af243 add internal directory method 2024-01-20 06:01:06 -07:00
qimiko e7a3b72fad disable automatic launch on error 2024-01-19 23:25:58 -07:00
22 changed files with 1047 additions and 153 deletions

No files matched your search

+9 -7
View File
@@ -11,8 +11,8 @@ android {
applicationId "com.geode.launcher" applicationId "com.geode.launcher"
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode 5 versionCode 7
versionName "1.0.0-beta.2" versionName "1.0.0"
vectorDrawables { vectorDrawables {
useSupportLibrary true useSupportLibrary true
@@ -56,6 +56,7 @@ android {
} }
buildFeatures { buildFeatures {
compose true compose true
buildConfig true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion compose_compiler kotlinCompilerExtensionVersion compose_compiler
@@ -75,10 +76,11 @@ android {
} }
dependencies { dependencies {
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui"
implementation 'androidx.compose.material3:material3:1.1.2' implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
implementation 'androidx.activity:activity-compose:1.8.2' implementation 'androidx.activity:activity-compose:1.8.2'
@@ -90,7 +92,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.0"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
} }
+9 -3
View File
@@ -11,7 +11,6 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-feature <uses-feature
android:name="android.hardware.wifi" android:name="android.hardware.wifi"
@@ -28,6 +27,11 @@
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.GeodeLauncher" android:theme="@style/Theme.GeodeLauncher"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".ApplicationLogsActivity"
android:exported="false"
android:label="@string/title_activity_application_logs"
android:theme="@style/Theme.GeodeLauncher" />
<activity <activity
android:name=".SettingsActivity" android:name=".SettingsActivity"
android:exported="false" android:exported="false"
@@ -58,11 +62,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider <provider
android:authorities="com.geode.launcher.user"
android:name=".UserDirectoryProvider" android:name=".UserDirectoryProvider"
android:grantUriPermissions="true" android:authorities="com.geode.launcher.user"
android:exported="true" android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS"> android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter> <intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" /> <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
@@ -78,4 +83,5 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
</application> </application>
</manifest> </manifest>
@@ -0,0 +1,360 @@
package com.geode.launcher
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Divider
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.log.LogLine
import com.geode.launcher.log.LogPriority
import com.geode.launcher.log.LogViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
class ApplicationLogsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ApplicationLogsScreen(onBackPressedDispatcher)
}
}
}
}
}
}
@Composable
fun getLogColor(priority: LogPriority): Color {
val isDark = LocalTheme.current != Theme.LIGHT
// i just copied these from my ide tbh
return if (isDark) {
when (priority) {
LogPriority.ERROR -> Color(0xffdf5151)
LogPriority.WARN -> Color(0xffbcb500)
LogPriority.FATAL -> Color(0xffb8b6fc)
else -> Color.Unspecified
}
} else {
when (priority) {
LogPriority.ERROR -> Color(0xffdf5151)
LogPriority.WARN -> Color(0xffeab700)
LogPriority.FATAL -> Color(0xffa142f4)
else -> Color.Unspecified
}
}
}
fun copyLogText(context: Context, logMessage: String) {
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(
ClipData.newPlainText(
context.getString(R.string.application_log_copy_label),
logMessage
)
)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText(
context,
context.getString(R.string.text_copied),
Toast.LENGTH_SHORT
).show()
}
}
@Composable
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val context = LocalContext.current
Column(
modifier = modifier
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = {
copyLogText(context, logLine.asSimpleString)
}
)
},
) {
Text(
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
style = Typography.labelSmall,
color = getLogColor(logLine.priority),
fontFamily = FontFamily.Monospace
)
Text(
"[${logLine.tag}]: ${logLine.messageTrimmed}",
fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ApplicationLogsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
logViewModel: LogViewModel = viewModel()
) {
val logLines = logViewModel.lineState
val isLoading by logViewModel.isLoading.collectAsState()
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val listState = rememberLazyListState()
var showMoreOptions by remember { mutableStateOf(false) }
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
if (uri != null) {
coroutineScope.launch {
val logs = logViewModel.getLogData()
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.bufferedWriter().use { writer ->
writer.write(logs)
}
}
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
.show()
}
}
}
LaunchedEffect(logLines.size) {
listState.scrollToItem(logLines.size)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
actions = {
IconButton(onClick = {
coroutineScope.launch {
val data = logViewModel.getLogData()
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, data)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
}, enabled = !logLines.isEmpty()) {
Icon(
Icons.Filled.Share,
contentDescription = stringResource(R.string.application_logs_share)
)
}
IconButton(onClick = { showMoreOptions = !showMoreOptions },) {
Icon(
Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.application_logs_more)
)
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_filter_list),
contentDescription = null,
)
},
trailingIcon = {
if (logViewModel.filterCrashes) {
Icon(
Icons.Filled.Check,
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
)
}
},
text = {
Text(stringResource(R.string.application_logs_crash_only))
}, onClick = {
logViewModel.toggleCrashBuffer()
showMoreOptions = false
}
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_save),
contentDescription = null,
)
},
text = {
Text(stringResource(R.string.application_logs_export))
},
onClick = {
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
showMoreOptions = false
},
enabled = !logLines.isEmpty()
)
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
logViewModel.clearLogs()
showMoreOptions = false
}
)
}
},
title = {
Text(
stringResource(R.string.title_activity_application_logs),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
) { innerPadding ->
LazyColumn(
Modifier
.padding(innerPadding)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceEvenly,
state = listState
) {
if (!isLoading && logLines.isEmpty()) {
item {
Text(
stringResource(R.string.application_logs_no_logs),
textAlign = TextAlign.Center,
style = Typography.bodyLarge,
modifier = Modifier.fillMaxSize()
)
}
}
items(
logLines
) { line ->
LogCard(line)
Divider()
}
}
}
}
@Preview(showSystemUi = true)
@Composable
fun LogsPreview() {
GeodeLauncherTheme {
ApplicationLogsScreen(null)
}
}
@@ -1,5 +1,6 @@
package com.geode.launcher package com.geode.launcher
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Intent import android.content.Intent
@@ -7,6 +8,7 @@ import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
@@ -22,8 +24,8 @@ import com.customRobTop.BaseRobTopActivity
import com.customRobTop.JniToCpp import com.customRobTop.JniToCpp
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.DownloadUtils import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.GeodeUtils import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import org.cocos2dx.lib.Cocos2dxEditText import org.cocos2dx.lib.Cocos2dxEditText
import org.cocos2dx.lib.Cocos2dxGLSurfaceView import org.cocos2dx.lib.Cocos2dxGLSurfaceView
@@ -44,10 +46,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private var mReceiver: BroadcastReceiver? = null private var mReceiver: BroadcastReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupUIState() setupUIState()
super.onCreate(savedInstanceState)
// return back to main if Geometry Dash isn't found // return back to main if Geometry Dash isn't found
if (!LaunchUtils.isGeometryDashInstalled(packageManager)) { if (!LaunchUtils.isGeometryDashInstalled(packageManager)) {
returnToMain() returnToMain()
@@ -61,11 +63,14 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
// generates helpful information for use in debugging library load failures // generates helpful information for use in debugging library load failures
val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) val gdPackageInfo = packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
val failureReason = LaunchUtils.diagnoseLoadErrors(this, gdPackageInfo) val abiMismatch = LaunchUtils.detectAbiMismatch(this, gdPackageInfo, e)
val errorMessage = if (abiMismatch) getString(R.string.load_failed_abi_error_description) else
getString(R.string.load_failed_link_error_description)
returnToMain( returnToMain(
getString(R.string.load_failed_link_error), getString(R.string.load_failed_link_error),
getString(R.string.load_failed_link_error_description, failureReason.name) errorMessage
) )
return return
@@ -100,13 +105,18 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
setupRedirection(gdPackageInfo) setupRedirection(gdPackageInfo)
// request read and write permissions // request read and write permissions
val hasStoragePerms = (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) and
(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
if (!hasStoragePerms) {
requestPermissions( requestPermissions(
arrayOf( arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
), ),
0 0
) )
}
Cocos2dxHelper.init(this, this) Cocos2dxHelper.init(this, this)
GeodeUtils.setContext(this) GeodeUtils.setContext(this)
@@ -281,13 +291,24 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
private fun hideSystemUi() { private fun hideSystemUi() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
WindowCompat.getInsetsController(window, window.decorView).apply { WindowCompat.getInsetsController(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(WindowInsetsCompat.Type.systemBars())
} }
} }
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
mHasWindowFocus = hasWindowFocus
if (hasWindowFocus && !mIsOnPause) {
resumeGame()
}
hideSystemUi()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceivers() unregisterReceivers()
@@ -319,6 +340,10 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
super.onPause() super.onPause()
mIsOnPause = true mIsOnPause = true
BaseRobTopActivity.isPaused = true BaseRobTopActivity.isPaused = true
}
override fun onStop() {
super.onStop()
if (mIsRunning) { if (mIsRunning) {
pauseGame() pauseGame()
} }
@@ -343,14 +368,6 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
} }
} }
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
super.onWindowFocusChanged(hasWindowFocus)
mHasWindowFocus = hasWindowFocus
if (hasWindowFocus && !mIsOnPause) {
resumeGame()
}
}
override fun runOnGLThread(runnable: Runnable) { override fun runOnGLThread(runnable: Runnable) {
mGLSurfaceView?.queueEvent(runnable) mGLSurfaceView?.queueEvent(runnable)
} }
@@ -6,12 +6,14 @@ import android.os.Bundle
import android.text.format.Formatter.formatShortFileSize import android.text.format.Formatter.formatShortFileSize
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -26,6 +28,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -34,7 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme import com.geode.launcher.ui.theme.Theme
@@ -42,7 +45,7 @@ import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.Constants import com.geode.launcher.utils.Constants
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 com.geode.launcher.utils.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.useCountdownTimer import com.geode.launcher.utils.useCountdownTimer
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
@@ -50,6 +53,8 @@ import java.net.UnknownHostException
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager) val gdInstalled = LaunchUtils.isGeometryDashInstalled(packageManager)
@@ -70,7 +75,8 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
MainScreen(gdInstalled, geodeInstalled) val loadFailed = !returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()
MainScreen(gdInstalled, geodeInstalled, loadFailed)
if (!returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()) { if (!returnMessage.isNullOrEmpty() && !returnTitle.isNullOrEmpty()) {
LoadFailedDialog(returnTitle, returnMessage) LoadFailedDialog(returnTitle, returnMessage)
@@ -89,6 +95,43 @@ class MainActivity : ComponentActivity() {
} }
} }
@Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier, openTo: String, onDismiss: () -> Unit) {
val uriHandler = LocalUriHandler.current
ElevatedCard(modifier) {
Column(
// buttons add enough padding already, lower to compensate
modifier = Modifier.padding(
top = 20.dp,
start = 12.dp,
end = 12.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
stringResource(R.string.launcher_update_available),
modifier = Modifier.padding(horizontal = 10.dp)
)
Row(modifier = Modifier.align(Alignment.End)) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.launcher_update_dismiss))
}
Spacer(Modifier.size(4.dp))
TextButton(onClick = { uriHandler.openUri(openTo) },) {
Text(stringResource(R.string.launcher_download))
}
}
}
}
}
@Composable @Composable
fun UpdateProgressIndicator( fun UpdateProgressIndicator(
message: String, message: String,
@@ -146,6 +189,11 @@ fun UpdateMessageIndicator(
releaseViewModel.runReleaseCheck(true) releaseViewModel.runReleaseCheck(true)
}, },
) { ) {
Icon(
Icons.Filled.Refresh,
contentDescription = stringResource(R.string.launcher_retry_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.release_fetch_button_retry)) Text(stringResource(R.string.release_fetch_button_retry))
} }
} }
@@ -187,7 +235,11 @@ fun UpdateWarning() {
var showUpdateWarning by remember { mutableStateOf(true) } var showUpdateWarning by remember { mutableStateOf(true) }
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE) { var lastDismissedVersion by PreferenceUtils.useLongPreference(
preferenceKey = PreferenceUtils.Key.DISMISSED_GJ_UPDATE
)
if (gdVersionCode != Constants.SUPPORTED_VERSION_CODE && gdVersionCode != lastDismissedVersion) {
if (showUpdateWarning) { if (showUpdateWarning) {
AlertDialog( AlertDialog(
icon = { icon = {
@@ -208,6 +260,8 @@ fun UpdateWarning() {
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
showUpdateWarning = false showUpdateWarning = false
lastDismissedVersion = gdVersionCode
onLaunch(context) onLaunch(context)
}) { }) {
Text(stringResource(R.string.message_box_accept)) Text(stringResource(R.string.message_box_accept))
@@ -312,6 +366,22 @@ fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier
} }
} }
} }
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
val nextUpdateValue = nextUpdate
if (!releaseViewModel.isInUpdate && nextUpdateValue != null) {
val updateUrl = nextUpdateValue.getLauncherDownload()?.browserDownloadUrl
?: nextUpdateValue.htmlUrl
LauncherUpdateIndicator(
modifier = modifier,
openTo = updateUrl,
onDismiss = {
releaseViewModel.dismissLauncherUpdate()
}
)
}
} }
fun onLaunch(context: Context) { fun onLaunch(context: Context) {
@@ -330,6 +400,7 @@ fun onSettings(context: Context) {
fun MainScreen( fun MainScreen(
gdInstalled: Boolean = true, gdInstalled: Boolean = true,
geodePreinstalled: Boolean = true, geodePreinstalled: Boolean = true,
disableAutomaticLaunch: Boolean = false,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory) releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -375,7 +446,7 @@ fun MainScreen(
) )
if (gdInstalled && geodeInstalled) { if (gdInstalled && geodeInstalled) {
if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate) { if (shouldAutomaticallyLaunch.value && !releaseViewModel.isInUpdate && !disableAutomaticLaunch) {
val countdownTimer = useCountdownTimer( val countdownTimer = useCountdownTimer(
time = 3000, time = 3000,
onCountdownFinish = { onCountdownFinish = {
@@ -463,7 +534,8 @@ fun MainScreen(
UpdateCard( UpdateCard(
releaseViewModel, releaseViewModel,
modifier = Modifier.padding(12.dp) modifier = Modifier
.padding(12.dp)
) )
} }
@@ -13,6 +13,7 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
@@ -46,7 +47,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme import com.geode.launcher.ui.theme.Theme
@@ -58,7 +59,9 @@ import java.net.UnknownHostException
class SettingsActivity : ComponentActivity() { class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME) val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption) val theme = Theme.fromInt(themeOption)
@@ -104,7 +107,7 @@ fun onOpenFolder(context: Context) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.export_folder_copied), context.getString(R.string.text_copied),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -224,6 +227,11 @@ fun updateTheme(context: Context, theme: Int) {
} }
} }
fun onOpenLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent)
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -267,7 +275,7 @@ fun SettingsScreen(
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon( Icon(
imageVector = Icons.Filled.ArrowBack, imageVector = Icons.Filled.ArrowBack,
contentDescription = context.getString(R.string.settings_back_icon_alt) contentDescription = context.getString(R.string.back_icon_alt)
) )
} }
}, },
@@ -302,10 +310,6 @@ fun SettingsScreen(
title = stringResource(R.string.preference_black_background_name), title = stringResource(R.string.preference_black_background_name),
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
) )
SettingsCard(
title = context.getString(R.string.preference_load_testing_name),
preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
)
SettingsCard( SettingsCard(
title = context.getString(R.string.preference_load_automatically_name), title = context.getString(R.string.preference_load_automatically_name),
description = context.getString(R.string.preference_load_automatically_description), description = context.getString(R.string.preference_load_automatically_description),
@@ -320,6 +324,10 @@ fun SettingsScreen(
title = stringResource(R.string.preferences_open_file_manager), title = stringResource(R.string.preferences_open_file_manager),
onClick = { onOpenFileManager(context) } onClick = { onOpenFileManager(context) }
) )
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
onClick = { onOpenLogs(context) }
)
} }
OptionsGroup(context.getString(R.string.preference_category_updater)) { OptionsGroup(context.getString(R.string.preference_category_updater)) {
@@ -356,15 +364,13 @@ fun SettingsScreen(
UpdateIndicator(snackbarHostState, updateStatus) UpdateIndicator(snackbarHostState, updateStatus)
} }
} }
/*
OptionsGroup("Data") { Text(
OptionsButton( stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
title = "Export save data", style = Typography.labelSmall,
onClick = { onExportSaveData(context) } modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
) )
} }
*/
}
} }
) )
} }
@@ -0,0 +1,162 @@
package com.geode.launcher.log
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toLocalDateTime
import okio.BufferedSource
import java.io.IOException
fun BufferedSource.readCChar(): Char {
return this.readUtf8CodePoint().toChar()
}
fun BufferedSource.readCString(): String {
val buffer = StringBuilder()
var lastByte = this.readCChar()
while (lastByte != '\u0000') {
buffer.append(lastByte)
lastByte = this.readCChar()
}
return buffer.toString()
}
data class ProcessInformation(val processId: Int, val threadId: Int, val processUid: Int)
enum class LogPriority {
UNKNOWN, DEFAULT, VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT;
companion object {
fun fromByte(byte: Byte): LogPriority {
return when (byte.toInt()) {
0x1 -> DEFAULT
0x2 -> VERBOSE
0x3 -> DEBUG
0x4 -> INFO
0x5 -> WARN
0x6 -> ERROR
0x7 -> FATAL
0x8 -> SILENT
else -> UNKNOWN
}
}
}
fun toChar(): Char {
return when (this) {
VERBOSE -> 'V'
DEBUG -> 'D'
INFO -> 'I'
WARN -> 'W'
ERROR -> 'E'
FATAL -> 'F'
SILENT -> 'S'
else -> '?'
}
}
}
/**
* Represents a log entry from logcat.
*/
data class LogLine(
val process: ProcessInformation,
val time: Instant,
val logId: Int,
val priority: LogPriority,
val tag: String,
val message: String
) {
companion object {
private enum class EntryVersion {
V3, V4
}
private fun headerSizeToVersion(size: UShort) = when (size.toInt()) {
0x18 -> EntryVersion.V3
0x1c -> EntryVersion.V4
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
}
fun fromBufferedSource(source: BufferedSource): LogLine {
/*
// from android <liblog/include/log/log_read.h>
// there are multiple logger entry formats
// use the header_size to determine the one you have
struct logger_entry_v3 {
uint16_t len; // length of the payload
uint16_t hdr_size; // sizeof(struct logger_entry_v3)
int32_t pid; // generating process's pid
int32_t tid; // generating process's tid
int32_t sec; // seconds since Epoch
int32_t nsec; // nanoseconds
uint32_t lid; // log id of the payload
}
struct logger_entry_v4 {
uint16_t len; // length of the payload
uint16_t hdr_size; // sizeof(struct logger_entry_v4 = 28)
int32_t pid; // generating process's pid
uint32_t tid; // generating process's tid
uint32_t sec; // seconds since Epoch
uint32_t nsec; // nanoseconds
uint32_t lid; // log id of the payload, bottom 4 bits currently
uint32_t uid; // generating process's uid
};
*/
/* val payloadLength = */ source.readShortLe()
val headerSize = source.readShortLe().toUShort()
val entryVersion = headerSizeToVersion(headerSize)
val pid = source.readIntLe()
val tid = source.readIntLe()
val sec = source.readIntLe().toLong()
val nSec = source.readIntLe()
val lid = source.readIntLe()
val uid = if (entryVersion == EntryVersion.V4)
source.readIntLe() else 0
val processInformation = ProcessInformation(pid, tid, uid)
val time = Instant.fromEpochSeconds(sec, nSec)
val priorityByte = source.readByte()
val priority = LogPriority.fromByte(priorityByte)
val tag = source.readCString()
val message = source.readCString()
return LogLine(
process = processInformation,
priority = priority,
time = time,
logId = lid,
tag = tag,
message = message
)
}
fun showException(exception: Exception) = LogLine(
process = ProcessInformation(0, 0, 0),
time = Clock.System.now(),
logId = 0,
priority = LogPriority.FATAL,
tag = "GeodeLauncher",
message = "Failed to parse log entry with ${exception.stackTraceToString()}"
)
}
val identifier = time.toJavaInstant()
val messageTrimmed by lazy { this.message.trim() }
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
val asSimpleString by lazy {
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
}
}
@@ -0,0 +1,111 @@
package com.geode.launcher.log
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import okio.buffer
import okio.source
import java.io.EOFException
import java.util.LinkedList
/**
* ViewModel to manage log output from logcat.
* Currently, it loads the list of logs at init with no ability to read additional logs.
*/
class LogViewModel: ViewModel() {
// the original version of this code did support streaming btw
// the states were way too annoying to manage across multiple threads
// yay!
private val _lineState = mutableStateListOf<LogLine>()
val lineState = _lineState
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
private var logJob: Job? = null
var filterCrashes = false
private set
fun toggleCrashBuffer() {
filterCrashes = !filterCrashes
_lineState.clear()
loadLogs()
}
private suspend fun logOutput(): List<LogLine> {
val logLines = LinkedList<LogLine>()
val logCommand = if (filterCrashes) "logcat -b crash -B -d"
else "logcat -B -d"
// -B = binary format, -d = dump logs
val logSource = Runtime.getRuntime().exec(logCommand)
.inputStream.source().buffer()
try {
coroutineScope {
// this runs until the stream is exhausted
while (true) {
val line = LogLine.fromBufferedSource(logSource)
// skip debug/verbose lines
if (line.priority >= LogPriority.INFO) {
logLines += line
}
yield()
}
}
} catch (_: EOFException) {
// ignore, end of file reached
} catch (e: Exception) {
e.printStackTrace()
logLines += LogLine.showException(e)
}
return logLines
}
fun clearLogs() {
logJob?.cancel()
viewModelScope.launch(Dispatchers.IO) {
// -c = clear
Runtime.getRuntime().exec("logcat -c")
.waitFor()
}
_lineState.clear()
}
fun getLogData(): String {
return _lineState.joinToString("\n") { it.asSimpleString }
}
private fun loadLogs() {
_isLoading.value = true
logJob?.cancel()
logJob = viewModelScope.launch(Dispatchers.IO) {
val lines = logOutput()
_lineState.addAll(lines)
_isLoading.value = false
logJob = null
}
}
init {
loadLogs()
}
}
@@ -79,24 +79,6 @@ fun GeodeLauncherTheme(
else -> LightColorScheme else -> LightColorScheme
} }
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
with ((view.context as Activity).window) {
WindowCompat.setDecorFitsSystemWindows(this, false)
statusBarColor = Color.Transparent.toArgb()
navigationBarColor = Color.Transparent.toArgb()
WindowCompat.getInsetsController(this, view).apply {
isAppearanceLightStatusBars = theme == Theme.LIGHT
isAppearanceLightNavigationBars = theme == Theme.LIGHT
}
}
}
}
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
@@ -1,4 +1,4 @@
package com.geode.launcher.api package com.geode.launcher.updater
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
@@ -23,7 +23,8 @@ class Release(
val tagName: String, val tagName: String,
val createdAt: Instant, val createdAt: Instant,
val publishedAt: Instant, val publishedAt: Instant,
val assets: List<Asset> val assets: List<Asset>,
val htmlUrl: String
) { ) {
fun getDescription(): String { fun getDescription(): String {
if (tagName == "nightly") { if (tagName == "nightly") {
@@ -42,7 +43,7 @@ class Release(
return createdAt.epochSeconds return createdAt.epochSeconds
} }
fun getAndroidDownload(): Asset? { fun getGeodeDownload(): Asset? {
// try to find an asset that matches the architecture first // try to find an asset that matches the architecture first
val platform = LaunchUtils.platformName val platform = LaunchUtils.platformName
@@ -51,4 +52,22 @@ class Release(
it.name.endsWith(releaseSuffix) it.name.endsWith(releaseSuffix)
} }
} }
fun getLauncherDownload(): Asset? {
val platform = LaunchUtils.platformName
val use32BitPlatform = platform == "android32"
return assets.find { asset ->
/* you know it's good when you pull out the truth table
* u32bp | contains | found
* 1 | 1 | 1
* 0 | 1 | 0
* 1 | 0 | 0
* 0 | 0 | 1
* surprise! it's an xnor
*/
(asset.name.contains("android32")) == use32BitPlatform
}
}
} }
@@ -1,9 +1,11 @@
package com.geode.launcher.utils package com.geode.launcher.updater
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.geode.launcher.api.Release import com.geode.launcher.BuildConfig
import com.geode.launcher.api.ReleaseRepository import com.geode.launcher.utils.DownloadUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -15,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okio.buffer
import okio.source
import java.io.File import java.io.File
import java.io.InterruptedIOException import java.io.InterruptedIOException
@@ -30,7 +34,7 @@ class ReleaseManager private constructor(
private lateinit var managerInstance: ReleaseManager private lateinit var managerInstance: ReleaseManager
fun get(context: Context): ReleaseManager { fun get(context: Context): ReleaseManager {
if (!::managerInstance.isInitialized) { if (!Companion::managerInstance.isInitialized) {
val applicationContext = context.applicationContext val applicationContext = context.applicationContext
val httpClient = OkHttpClient.Builder() val httpClient = OkHttpClient.Builder()
@@ -74,6 +78,9 @@ class ReleaseManager private constructor(
val isInUpdate: Boolean val isInUpdate: Boolean
get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished get() = _uiState.value !is ReleaseManagerState.Failure && _uiState.value !is ReleaseManagerState.Finished
private val _availableLauncherUpdate = MutableStateFlow<Release?>(null)
val availableLauncherUpdate = _availableLauncherUpdate.asStateFlow()
private fun sendError(e: Exception) { private fun sendError(e: Exception) {
_uiState.value = ReleaseManagerState.Failure(e) _uiState.value = ReleaseManagerState.Failure(e)
@@ -84,21 +91,24 @@ class ReleaseManager private constructor(
} }
} }
fun dismissLauncherUpdate() {
val update = availableLauncherUpdate.value ?: return
val sharedPreferences = PreferenceUtils.get(applicationContext)
sharedPreferences.setString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE, update.tagName)
_availableLauncherUpdate.value = null
}
private suspend fun getLatestRelease(): Release? { private suspend fun getLatestRelease(): Release? {
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL) val useNightly = sharedPreferences.getBoolean(PreferenceUtils.Key.RELEASE_CHANNEL)
val latestRelease = if (useNightly) { return releaseRepository.getLatestGeodeRelease(useNightly)
releaseRepository.getLatestNightlyRelease()
} else {
releaseRepository.getLatestRelease()
}
return latestRelease
} }
private suspend fun performUpdate(release: Release) { private suspend fun performUpdate(release: Release) {
val releaseAsset = release.getAndroidDownload() val releaseAsset = release.getGeodeDownload()
if (releaseAsset == null) { if (releaseAsset == null) {
val noAssetException = Exception("missing Android download") val noAssetException = Exception("missing Android download")
_uiState.value = ReleaseManagerState.Failure(noAssetException) _uiState.value = ReleaseManagerState.Failure(noAssetException)
@@ -110,7 +120,10 @@ class ReleaseManager private constructor(
_uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong()) _uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size.toLong())
try { try {
val fileStream = DownloadUtils.downloadStream(httpClient, releaseAsset.browserDownloadUrl) { progress, outOf -> val fileStream = DownloadUtils.downloadStream(
httpClient,
releaseAsset.browserDownloadUrl
) { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf) _uiState.value = ReleaseManagerState.InDownload(progress, outOf)
} }
@@ -145,13 +158,30 @@ class ReleaseManager private constructor(
} }
val sharedPreferences = PreferenceUtils.get(applicationContext) val sharedPreferences = PreferenceUtils.get(applicationContext)
val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED)
?: return false
val fileLastModified = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED) val currentFileHash = computeFileHash(geodeFile)
if (fileLastModified == 0L) {
return false return originalFileHash != currentFileHash
} }
return fileLastModified != geodeFile.lastModified() private fun checkLauncherUpdate(launcherUpdate: Release) {
if (BuildConfig.DEBUG) {
println("ignoring launcher update check - in debug mode!")
return
}
val sharedPreferences = PreferenceUtils.get(applicationContext)
val lastDismissedVersion = sharedPreferences.getString(PreferenceUtils.Key.LAST_DISMISSED_UPDATE)
if (!lastDismissedVersion.isNullOrEmpty() && launcherUpdate.tagName == lastDismissedVersion) {
return
}
if (launcherUpdate.tagName != BuildConfig.VERSION_NAME) {
_availableLauncherUpdate.value = launcherUpdate
}
} }
private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) { private suspend fun checkForNewRelease(allowOverwriting: Boolean = false) {
@@ -162,6 +192,17 @@ class ReleaseManager private constructor(
return return
} }
val launcherRelease = try {
releaseRepository.getLatestLauncherRelease()
} catch (e: Exception) {
sendError(e)
return
}
if (launcherRelease != null) {
checkLauncherUpdate(launcherRelease)
}
if (release == null) { if (release == null) {
_uiState.value = ReleaseManagerState.Finished() _uiState.value = ReleaseManagerState.Finished()
return return
@@ -203,13 +244,16 @@ class ReleaseManager private constructor(
release.getDescriptor() release.getDescriptor()
) )
// store hash as modified time is unreliable (don't know why)
val outputFile = getGeodeOutputPath() val outputFile = getGeodeOutputPath()
sharedPreferences.setLong( val fileHash = computeFileHash(outputFile)
PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED,
outputFile.lastModified() sharedPreferences.setString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED, fileHash)
)
} }
private fun computeFileHash(file: File): String =
file.source().buffer().readByteString().md5().hex()
private fun getGeodeOutputPath(): File { private fun getGeodeOutputPath(): File {
val geodeName = LaunchUtils.geodeFilename val geodeName = LaunchUtils.geodeFilename
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext) val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
@@ -1,4 +1,4 @@
package com.geode.launcher.api package com.geode.launcher.updater
import com.geode.launcher.utils.DownloadUtils.executeCoroutine import com.geode.launcher.utils.DownloadUtils.executeCoroutine
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
@@ -12,20 +12,24 @@ import java.net.URL
class ReleaseRepository(private val httpClient: OkHttpClient) { class ReleaseRepository(private val httpClient: OkHttpClient) {
companion object { companion object {
private const val GITHUB_API_BASE = "https://api.github.com/repos/geode-sdk/geode" private const val GITHUB_API_BASE = "https://api.github.com"
private const val GITHUB_API_HEADER = "X-GitHub-Api-Version" private const val GITHUB_API_HEADER = "X-GitHub-Api-Version"
private const val GITHUB_API_VERSION = "2022-11-28" private const val GITHUB_API_VERSION = "2022-11-28"
} }
suspend fun getLatestNightlyRelease(): Release? { suspend fun getLatestLauncherRelease(): Release? {
val nightlyPath = "$GITHUB_API_BASE/releases/tags/nightly" val releasePath = "$GITHUB_API_BASE/repos/geode-sdk/android-launcher/releases/latest"
val url = URL(nightlyPath)
val url = URL(releasePath)
return getReleaseByUrl(url) return getReleaseByUrl(url)
} }
suspend fun getLatestRelease(): Release? { suspend fun getLatestGeodeRelease(isNightly: Boolean = false): Release? {
val releasePath = "$GITHUB_API_BASE/releases/latest" val geodeBaseUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases"
val releasePath = if (isNightly) "$geodeBaseUrl/tags/nightly"
else "$geodeBaseUrl/latest"
val url = URL(releasePath) val url = URL(releasePath)
return getReleaseByUrl(url) return getReleaseByUrl(url)
@@ -1,4 +1,4 @@
package com.geode.launcher.api package com.geode.launcher.updater
import android.app.Application import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.AP
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.geode.launcher.utils.ReleaseManager import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -53,6 +53,8 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
val isInUpdate val isInUpdate
get() = ReleaseManager.get(application).isInUpdate get() = ReleaseManager.get(application).isInUpdate
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
var hasPerformedCheck = false var hasPerformedCheck = false
private set private set
@@ -78,7 +80,7 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
fun useGlobalCheckState() { fun useGlobalCheckState() {
hasPerformedCheck = true hasPerformedCheck = true
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val releaseFlow = ReleaseManager.get(application) val releaseFlow = ReleaseManager.get(application)
.uiState .uiState
@@ -89,11 +91,17 @@ class ReleaseViewModel(private val application: Application): ViewModel() {
fun runReleaseCheck(isManual: Boolean = false) { fun runReleaseCheck(isManual: Boolean = false) {
hasPerformedCheck = true hasPerformedCheck = true
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
val releaseFlow = ReleaseManager.get(application) val releaseFlow = ReleaseManager.get(application)
.checkForUpdates(isManual) .checkForUpdates(isManual)
syncUiState(releaseFlow) syncUiState(releaseFlow)
} }
} }
fun dismissLauncherUpdate() {
viewModelScope.launch(Dispatchers.IO) {
ReleaseManager.get(application).dismissLauncherUpdate()
}
}
} }
@@ -13,8 +13,8 @@ object Constants {
@SuppressLint("SdCardPath") @SuppressLint("SdCardPath")
const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}" const val GJ_DATA_DIR = "/data/data/${PACKAGE_NAME}"
const val SUPPORTED_VERSION_CODE = 37L const val SUPPORTED_VERSION_CODE = 38L
const val SUPPORTED_VERSION_STRING = "2.2.13" const val SUPPORTED_VERSION_STRING = "2.205"
const val LAUNCHER_KEY_RETURN_TITLE = "return_title" const val LAUNCHER_KEY_RETURN_TITLE = "return_title"
const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message" const val LAUNCHER_KEY_RETURN_MESSAGE = "return_message"
@@ -116,19 +116,13 @@ object DownloadUtils {
fun copyFile(inputStream: InputStream, outputStream: OutputStream) { fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
// gotta love copying // gotta love copying
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileUtils.copy(inputStream, outputStream) inputStream.use { input -> outputStream.use { output ->
FileUtils.copy(input, output)
}}
} else { } else {
inputStream.use { input -> inputStream.use { input -> outputStream.use { output ->
outputStream.use { output -> input.copyTo(output)
val buffer = ByteArray(4 * 1024) }}
while (true) {
val byteCount = input.read(buffer)
if (byteCount < 0) break
output.write(buffer, 0, byteCount)
}
output.flush()
}
}
} }
} }
} }
@@ -225,11 +225,18 @@ object GeodeUtils {
@JvmStatic @JvmStatic
fun getBaseDirectory(): String { fun getBaseDirectory(): String {
val activity = activity.get()!! val activity = activity.get()!!
return LaunchUtils.getBaseDirectory(activity).path return LaunchUtils.getBaseDirectory(activity).canonicalPath
}
@JvmStatic
fun getInternalDirectory(): String {
val activity = activity.get()!!
return activity.filesDir.canonicalPath
} }
private val gameVersionMap = mapOf( private val gameVersionMap = mapOf(
37L to "2.200" 37L to "2.200",
38L to "2.205"
) )
@JvmStatic @JvmStatic
@@ -11,10 +11,6 @@ import android.util.Log
import java.io.File import java.io.File
object LaunchUtils { object LaunchUtils {
enum class FailureReason {
NOT_FOUND, ABI_MISMATCH, UNKNOWN
}
fun isGeometryDashInstalled(packageManager: PackageManager): Boolean { fun isGeometryDashInstalled(packageManager: PackageManager): Boolean {
return try { return try {
packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0) packageManager.getPackageInfo(Constants.PACKAGE_NAME, 0)
@@ -40,7 +36,7 @@ object LaunchUtils {
return game.versionName return game.versionName
} }
fun diagnoseLoadErrors(context: Context, packageInfo: PackageInfo): FailureReason { fun detectAbiMismatch(context: Context, packageInfo: PackageInfo, loadException: Error): Boolean {
val abi = applicationArchitecture val abi = applicationArchitecture
val isExtracted = val isExtracted =
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
@@ -50,33 +46,42 @@ object LaunchUtils {
"Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi" "Geometry Dash metadata:\nSplit sources: $isSplit\nExtracted libraries: $isExtracted\nLauncher architecture: $abi"
Log.i("GeodeLauncher", metadata) Log.i("GeodeLauncher", metadata)
// easiest check! these messages are hardcoded in bionic/linker/linker_phdr.cpp
if (loadException.message?.contains("32-bit instead of 64-bit") == true) {
return true
}
if (loadException.message?.contains("64-bit instead of 32-bit") == true) {
return true
}
// determine if it can find gd libraries for the opposite architecture // determine if it can find gd libraries for the opposite architecture
val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so" val gdBinaryName = "lib${Constants.COCOS_LIB_NAME}.so"
val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a" val oppositeArchitecture = if (abi == "arm64-v8a") "armeabi-v7a" else "arm64-v8a"
if (packageInfo.applicationInfo.nativeLibraryDir.contains(oppositeArchitecture)) { // detect issues using native library directory (only works on extracted libraries)
return FailureReason.ABI_MISMATCH val nativeLibraryDir = packageInfo.applicationInfo.nativeLibraryDir
if (nativeLibraryDir.contains(oppositeArchitecture)) {
return true
}
// android has some odd behavior where 32bit libraries get installed to the arm directory instead of armeabi-v7a
// detect it, ig
if (oppositeArchitecture == "armeabi-v7a" && nativeLibraryDir.contains("/lib/arm/")) {
return true
} }
if (!isExtracted) { if (!isExtracted) {
try { try {
context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName") context.assets.openNonAssetFd("lib/$oppositeArchitecture/$gdBinaryName")
return FailureReason.ABI_MISMATCH return true
} catch (_: Exception) { } catch (_: Exception) {
// this is good, actually! // this is good, actually!
} }
} }
// try fetching for its own libraries (only for uncompressed libraries) return false
if (!isExtracted) {
try {
context.assets.openNonAssetFd("lib/$abi/$gdBinaryName")
} catch (_: Exception) {
return FailureReason.NOT_FOUND
}
}
return FailureReason.UNKNOWN
} }
// supposedly CPU_ABI returns the current arch for the running application // supposedly CPU_ABI returns the current arch for the running application
@@ -13,6 +13,7 @@ import androidx.core.content.edit
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.geode.launcher.BuildConfig
/** /**
* Extension object for SharedPreferences to add better key safety and default values. * Extension object for SharedPreferences to add better key safety and default values.
@@ -23,36 +24,69 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
@Composable @Composable
fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> { fun useBooleanPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Boolean> {
return usePreference(preferenceKey, lifecycleOwner) { p, k -> return usePreference(
p.getBoolean(k) preferenceKey,
} lifecycleOwner,
preferenceGet = { p, k -> p.getBoolean(k) },
preferenceSet = { p, k, v -> p.setBoolean(k, v) }
)
} }
@Composable @Composable
fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> { fun useStringPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<String?> {
return usePreference(preferenceKey, lifecycleOwner) { p, k -> return usePreference(
p.getString(k) preferenceKey,
} lifecycleOwner,
preferenceGet = { p, k -> p.getString(k) },
preferenceSet = { p, k, v -> p.setString(k, v) }
)
} }
@Composable @Composable
fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> { fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> {
return usePreference(preferenceKey, lifecycleOwner) { p, k -> return usePreference(
p.getInt(k) preferenceKey,
} lifecycleOwner,
preferenceGet = { p, k -> p.getInt(k) },
preferenceSet = { p, k, v -> p.setInt(k, v) }
)
} }
@Composable @Composable
private fun <T> usePreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, preferenceGet: (PreferenceUtils, Key) -> T): MutableState<T> { fun useLongPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Long> {
return usePreference(
preferenceKey,
lifecycleOwner,
preferenceGet = { p, k -> p.getLong(k) },
preferenceSet = { p, k, v -> p.setLong(k, v) }
)
}
@Composable
private fun <T> usePreference(
preferenceKey: Key,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
preferenceGet: (PreferenceUtils, Key) -> T,
preferenceSet: (PreferenceUtils, Key, T) -> Unit
): MutableState<T> {
val context = LocalContext.current val context = LocalContext.current
val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE) val sharedPreferences = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE)
val preferences = get(sharedPreferences) val preferences = get(sharedPreferences)
val preferenceValue = remember { val preferenceValue = remember {
mutableStateOf( val state = mutableStateOf(preferenceGet(preferences, preferenceKey))
preferenceGet(preferences, preferenceKey) object : MutableState<T> by state {
) override var value: T
get() = state.value
set(value) {
// ignore setting the same thing (messes up default preferences)
if (state.value == value) { return }
state.value = value
preferenceSet(preferences, preferenceKey, value)
}
}
} }
val listener = SharedPreferences.OnSharedPreferenceChangeListener { val listener = SharedPreferences.OnSharedPreferenceChangeListener {
@@ -98,11 +132,14 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
THEME, THEME,
BLACK_BACKGROUND, BLACK_BACKGROUND,
CURRENT_RELEASE_MODIFIED, CURRENT_RELEASE_MODIFIED,
LAST_DISMISSED_UPDATE,
DISMISSED_GJ_UPDATE
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
return when (key) { return when (key) {
Key.UPDATE_AUTOMATICALLY -> true Key.UPDATE_AUTOMATICALLY -> true
Key.RELEASE_CHANNEL -> BuildConfig.DEBUG
else -> false else -> false
} }
} }
@@ -117,7 +154,9 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor" Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
Key.THEME -> "PreferenceTheme" Key.THEME -> "PreferenceTheme"
Key.BLACK_BACKGROUND -> "PreferenceBlackBackground" Key.BLACK_BACKGROUND -> "PreferenceBlackBackground"
Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModified" Key.CURRENT_RELEASE_MODIFIED -> "PreferenceReleaseModifiedHash"
Key.LAST_DISMISSED_UPDATE -> "PreferenceLastDismissedUpdate"
Key.DISMISSED_GJ_UPDATE -> "PreferenceDismissedGJUpdate"
} }
} }
@@ -128,6 +167,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
return sharedPreferences.getBoolean(keyName, defaultValue) return sharedPreferences.getBoolean(keyName, defaultValue)
} }
fun setBoolean(key: Key, value: Boolean) {
val keyName = keyToName(key)
sharedPreferences.edit {
putBoolean(keyName, value)
}
}
fun toggleBoolean(key: Key): Boolean { fun toggleBoolean(key: Key): Boolean {
val currentValue = getBoolean(key) val currentValue = getBoolean(key)
val keyName = keyToName(key) val keyName = keyToName(key)
@@ -144,7 +190,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
return sharedPreferences.getString(keyName, null) return sharedPreferences.getString(keyName, null)
} }
fun setString(key: Key, value: String) { fun setString(key: Key, value: String?) {
val keyName = keyToName(key) val keyName = keyToName(key)
sharedPreferences.edit { sharedPreferences.edit {
putString(keyName, value) putString(keyName, value)
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M400,720L400,640L560,640L560,720L400,720ZM240,520L240,440L720,440L720,520L240,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M840,280L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L680,120L840,280ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
</vector>
+24 -5
View File
@@ -12,6 +12,7 @@
<string name="launcher_launch_icon_alt">play icon</string> <string name="launcher_launch_icon_alt">play icon</string>
<string name="launcher_download_icon_alt">download icon</string> <string name="launcher_download_icon_alt">download icon</string>
<string name="launcher_settings_icon_alt">settings icon</string> <string name="launcher_settings_icon_alt">settings icon</string>
<string name="launcher_retry_icon_alt">retry icon</string>
<string name="geode_download_title">Geode is not installed.</string> <string name="geode_download_title">Geode is not installed.</string>
<string name="geode_download_description">Geode is downloading. Please wait.</string> <string name="geode_download_description">Geode is downloading. Please wait.</string>
<plurals name="automatically_load_countdown"> <plurals name="automatically_load_countdown">
@@ -22,8 +23,10 @@
<string name="launcher_error_icon_alt">error</string> <string name="launcher_error_icon_alt">error</string>
<string name="launcher_unsupported_version_title">Unsupported version</string> <string name="launcher_unsupported_version_title">Unsupported version</string>
<string name="launcher_unsupported_version_description"> <string name="launcher_unsupported_version_description">
The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s).\nLaunch will continue, but crashes may occur! The current version of Geometry Dash is not compatible with the version supported by the launcher (%2$s). A launcher update is required to fix this.\nLaunch will continue, but crashes may occur!
</string> </string>
<string name="launcher_update_available">A new launcher update is available!</string>
<string name="launcher_update_dismiss">Dismiss</string>
<!-- Updater strings --> <!-- Updater strings -->
<string name="release_fetch_failed">Update failed.\n%1$s</string> <string name="release_fetch_failed">Update failed.\n%1$s</string>
@@ -40,7 +43,8 @@
<!-- GeometryDashActivity strings --> <!-- GeometryDashActivity strings -->
<string name="load_failed_link_error">Failed to link libraries</string> <string name="load_failed_link_error">Failed to link libraries</string>
<string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nReported issue: %1$s.\nCheck application logs for more details.</string> <string name="load_failed_link_error_description">Game libraries could not be found. This may occur if the architecture of the launcher and Geometry Dash do not match, or from a system bug.\nCheck application logs for more details.</string>
<string name="load_failed_abi_error_description">The architecture of the launcher and Geometry Dash do not match. Please reinstall Geometry Dash from the Play Store or use the \"android32\" version of the launcher to resolve this issue.\nCheck application logs for more details.</string>
<string name="load_failed_generic_error">Load failed</string> <string name="load_failed_generic_error">Load failed</string>
<string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string> <string name="load_failed_generic_error_description">Uncaught error during game load: `%1$s`.\nCheck application logs for more details. Please report this issue!</string>
<string name="message_box_cancel">Cancel</string> <string name="message_box_cancel">Cancel</string>
@@ -48,10 +52,10 @@
<!-- SettingsActivity strings --> <!-- SettingsActivity strings -->
<string name="title_activity_settings">Settings</string> <string name="title_activity_settings">Settings</string>
<string name="settings_back_icon_alt">back arrow icon</string> <string name="back_icon_alt">back</string>
<string name="export_folder_tag">Launcher External Folder</string> <string name="export_folder_tag">Launcher External Folder</string>
<string name="export_folder_copied">Copied!</string> <string name="text_copied">Copied to clipboard.</string>
<string name="preference_category_testing">General</string> <string name="preference_category_testing">General</string>
<string name="preference_load_testing_name">Load files from /test</string> <string name="preference_load_testing_name">Load files from /test</string>
@@ -59,6 +63,7 @@
<string name="preference_load_automatically_description">Launches the game after a short delay.</string> <string name="preference_load_automatically_description">Launches the game after a short delay.</string>
<string name="preferences_copy_external_button">Copy external files directory</string> <string name="preferences_copy_external_button">Copy external files directory</string>
<string name="preferences_open_file_manager">Open file manager</string> <string name="preferences_open_file_manager">Open file manager</string>
<string name="preferences_view_logs">View application logs</string>
<string name="preference_theme_name">Theme</string> <string name="preference_theme_name">Theme</string>
<string name="preference_theme_select">Select theme</string> <string name="preference_theme_select">Select theme</string>
@@ -72,11 +77,25 @@
<string name="preference_release_channel_name">Use nightly release channel</string> <string name="preference_release_channel_name">Use nightly release channel</string>
<string name="preference_check_for_updates_button">Check for updates</string> <string name="preference_check_for_updates_button">Check for updates</string>
<string name="preference_check_for_updates_description">Current version: %1$s</string> <string name="preference_check_for_updates_description">Current loader version: %1$s</string>
<string name="preference_launcher_version">Geode Launcher v%1$s</string>
<string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string> <string name="preference_check_for_updates_failed">Update failed. See logs for more details.</string>
<string name="preference_check_for_updates_none_found">No updates found.</string> <string name="preference_check_for_updates_none_found">No updates found.</string>
<string name="preference_check_for_updates_success">Geode has been updated!</string> <string name="preference_check_for_updates_success">Geode has been updated!</string>
<string name="preference_check_for_updates_downloading">Downloading update…</string> <string name="preference_check_for_updates_downloading">Downloading update…</string>
<string name="preference_check_for_updates_no_internet">No internet found.</string> <string name="preference_check_for_updates_no_internet">No internet found.</string>
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string> <string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
<!-- ApplicationLogsActivity strings -->
<string name="title_activity_application_logs">Application Logs</string>
<string name="application_logs_no_logs">No logs available.</string>
<string name="application_logs_delete">Clear All</string>
<string name="application_logs_share">Share</string>
<string name="application_log_copy_label">Log Message</string>
<string name="application_logs_more">More</string>
<string name="application_logs_export">Save To</string>
<string name="application_logs_export_completed">Log export complete.</string>
<string name="application_logs_default_filename" translatable="false">launcher_logcat.txt</string>
<string name="application_logs_crash_only">Show Crash Buffer</string>
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
</resources> </resources>