Compare commits

..
Author SHA1 Message Date
Chloe 7d2b5e0091 add multiline input util 2025-06-26 16:56:58 -07:00
71 changed files with 1587 additions and 3651 deletions

No files matched your search

-15
View File
@@ -1,18 +1,3 @@
# Geode Android Launcher
Launches a vanilla copy of Geometry Dash with the Geode loader added.
## Shortcut Links
The launcher supports a handful of links as an escape hatch when the main functionality is impaired in some way (such as needing to prevent an automatic launch due to a crash).
You can open these links by copying them into the URL bar in some browsers (like Chrome), or through ADB.
| Link | Action |
|---------------------------------------------|--------------------------------------------------------------------------|
| geode-launcher://main | Opens to the main activity, forces the user to explicitly confirm launch |
| geode-launcher://main/launch | Opens to the game after the launcher is finished checking for updates |
| geode-launcher://main/launch?safe-mode=true | Opens to the game like before, but with safe mode enabled |
| geode-launcher://settings | Opens the main settings |
| geode-launcher://developer-settings | Opens the developer settings |
| geode-launcher://logs | Opens the application logs |
| geode-launcher://crashes | Opens the crash dumps viewer |
+24 -22
View File
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
val composeBOM: String by rootProject.extra
kotlin {
@@ -10,6 +8,7 @@ kotlin {
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization")
id("org.jetbrains.kotlin.plugin.compose")
}
@@ -20,9 +19,9 @@ android {
defaultConfig {
applicationId = "com.geode.launcher"
minSdk = 23
targetSdk = 36
versionCode = 23
versionName = "1.7.0"
targetSdk = 35
versionCode = 22
versionName = "1.6.1"
vectorDrawables {
useSupportLibrary = true
@@ -31,7 +30,7 @@ android {
@Suppress("UnstableApiUsage")
externalNativeBuild {
cmake {
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared")
arguments("-DUSE_TULIPHOOK:BOOL=OFF", "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES:BOOL=OFF")
}
}
@@ -69,6 +68,9 @@ android {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
buildConfig = true
@@ -85,29 +87,29 @@ android {
}
}
namespace = "com.geode.launcher"
ndkVersion = "29.0.14206865"
ndkVersion = "28.1.13356709"
}
dependencies {
implementation (platform("androidx.compose:compose-bom:$composeBOM"))
implementation ("androidx.core:core-ktx:1.17.0")
implementation ("androidx.core:core-ktx:1.16.0")
implementation ("androidx.compose.ui:ui")
implementation ("androidx.compose.material3:material3")
implementation ("androidx.compose.ui:ui-tooling-preview")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation ("androidx.activity:activity-compose:1.12.3")
implementation ("androidx.activity:activity-ktx:1.12.3")
implementation ("androidx.appcompat:appcompat:1.7.1")
implementation ("androidx.documentfile:documentfile:1.1.0")
implementation ("com.squareup.okio:okio:3.16.4")
implementation ("com.squareup.okhttp3:okhttp:5.3.2")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.10.0")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1")
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.39.1")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.39.1")
implementation ("androidx.browser:browser:1.9.0")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation ("androidx.activity:activity-compose:1.10.1")
implementation ("androidx.activity:activity-ktx:1.10.1")
implementation ("androidx.appcompat:appcompat:1.7.0")
implementation ("androidx.documentfile:documentfile:1.0.1")
implementation ("com.squareup.okio:okio:3.11.0")
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
implementation ("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.8.1")
implementation ("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation ("com.mikepenz:multiplatform-markdown-renderer-android:0.33.0")
implementation ("com.mikepenz:multiplatform-markdown-renderer-m3:0.33.0")
implementation ("androidx.browser:browser:1.8.0")
debugImplementation ("androidx.compose.ui:ui-tooling")
coreLibraryDesugaring ("com.android.tools:desugar_jdk_libs:2.1.5")
}
+15 -126
View File
@@ -20,10 +20,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.LIGHTS" />
<uses-feature
android:name="android.hardware.wifi"
@@ -31,90 +29,49 @@
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<application
android:allowBackup="true"
android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hasFragileUserData="true"
android:requestLegacyExternalStorage="true"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.GeodeLauncher"
tools:targetApi="35">
<receiver
android:name=".InstallReceiver"
android:enabled="true"
android:exported="false" />
<activity
android:name=".AltMainActivity"
android:exported="false"
android:theme="@style/Theme.GeodeLauncher" />
<activity
android:name=".preferences.DeveloperSettingsActivity"
android:exported="true"
android:exported="false"
android:label="@string/title_activity_developer_settings"
android:theme="@style/Theme.GeodeLauncher">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="developer-settings" />
</intent-filter>
</activity>
android:theme="@style/Theme.GeodeLauncher" />
<activity
android:name=".preferences.ApplicationLogsActivity"
android:exported="true"
android:label="@string/title_activity_application_logs"
android:theme="@style/Theme.GeodeLauncher">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="logs" />
</intent-filter>
</activity>
<activity
android:name=".preferences.CrashLogsActivity"
android:exported="true"
android:label="@string/title_activity_crash_logs"
android:theme="@style/Theme.GeodeLauncher">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="crashes" />
</intent-filter>
</activity>
<activity
android:name=".preferences.ApplicationIconActivity"
android:exported="false"
android:label="@string/title_activity_application_icon"
android:label="@string/title_activity_application_logs"
android:theme="@style/Theme.GeodeLauncher" />
<activity
android:name=".preferences.SettingsActivity"
android:exported="true"
android:exported="false"
android:label="@string/title_activity_settings"
android:theme="@style/Theme.GeodeLauncher">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="settings" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".GeometryDashActivity"
android:configChanges="orientation|screenSize|smallestScreenSize"
android:exported="true"
android:hardwareAccelerated="true"
android:screenOrientation="landscape"
android:theme="@style/Theme.NoActionBar.Fullscreen"
android:configChanges="orientation|screenSize|smallestScreenSize"
android:windowSoftInputMode="stateAlwaysHidden">
<meta-data
android:name="android.app.lib_name"
@@ -129,76 +86,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="main" />
</intent-filter>
</activity>
<activity-alias
android:name=".MainActivityTrans"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_trans"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="main" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityPride"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_pride"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="main" />
</intent-filter>
</activity-alias>
<activity-alias
android:name=".MainActivityGeode"
android:enabled="false"
android:exported="true"
android:icon="@mipmap/ic_launcher_geode"
android:label="@string/app_name"
android:targetActivity=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geode-launcher" android:host="main" />
</intent-filter>
</activity-alias>
<provider
android:name=".UserDirectoryProvider"
android:authorities="com.geode.launcher.user"
+1 -1
View File
@@ -13,7 +13,7 @@ option(USE_TULIPHOOK "enables choice of hooking library" OFF)
if (USE_TULIPHOOK)
FetchContent_Declare(TulipHook
GIT_REPOSITORY https://github.com/geode-sdk/TulipHook.git
GIT_TAG v3.1.4
GIT_TAG 9aa1f95091ebac18c657d57b755f59d00fe6f1f8
)
set(TULIP_LINK_SOURCE ON CACHE INTERNAL "")
@@ -5,11 +5,11 @@ import android.app.Activity
import android.content.*
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.core.net.toUri
import com.geode.launcher.R
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.closeIMEKeyboard
import org.cocos2dx.lib.Cocos2dxGLSurfaceView.Companion.openIMEKeyboard
import java.lang.ref.WeakReference
@@ -61,40 +61,33 @@ object BaseRobTopActivity {
@JvmStatic
fun openURL(url: String) {
Log.d("MAIN", "Open URL")
me.get()?.runOnUiThread {
try {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
me.get()?.startActivity(intent)
me.get()?.startActivity(
Intent(
"android.intent.action.VIEW",
Uri.parse(url)
)
)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
me.get(),
R.string.no_activity_found,
"No activity found to open this URL.",
Toast.LENGTH_SHORT,
).show()
}
}
}
@JvmStatic
fun copyToClipboard(str: String) {
val context = me.get() ?: return
context.runOnUiThread {
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(
ClipData.newPlainText("text", str)
)
}
}
@JvmStatic
fun sendMail(subject: String, body: String, to: String) {
me.get()?.runOnUiThread {
val i = Intent(Intent.ACTION_SEND)
val i = Intent("android.intent.action.SEND")
i.type = "message/rfc822"
i.putExtra(Intent.EXTRA_EMAIL, arrayOf(to))
i.putExtra(Intent.EXTRA_SUBJECT, subject)
i.putExtra(Intent.EXTRA_TEXT, body)
i.putExtra("android.intent.extra.EMAIL", arrayOf(to))
i.putExtra("android.intent.extra.SUBJECT", subject)
i.putExtra("android.intent.extra.TEXT", body)
try {
me.get()?.startActivity(Intent.createChooser(i, "Send mail..."))
} catch (e: ActivityNotFoundException) {
@@ -0,0 +1,522 @@
package com.geode.launcher
import android.os.Bundle
import android.text.format.Formatter
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
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.Warning
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.main.*
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.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
class AltMainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
clearDownloadedApks(this)
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
@Suppress("DEPRECATION") // the new api is android 13 only (why)
val returnError = intent.extras?.getSerializable(LaunchUtils.LAUNCHER_KEY_RETURN_ERROR)
as? LaunchUtils.LauncherError
val loadFailureInfo = if (returnError != null && returnMessage != null) {
LoadFailureInfo(returnError, returnMessage, returnExtendedMessage)
} else if (LaunchUtils.lastSessionCrashed(this)) {
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
} else { null }
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
if (loadFailureInfo != null) {
launchViewModel.loadFailure = loadFailureInfo
}
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize()
) {
AltMainScreen(launchViewModel)
}
}
}
}
if (gdInstalled && geodeInstalled) {
intent.getBooleanExtra("restarted", false).let {
if (it) {
onLaunch(this)
}
}
}
}
}
data class LaunchStatusInfo(
val title: String?,
val details: String? = null,
val progress: (() -> Float)? = null
)
@Composable
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifier: Modifier = Modifier) {
when (cancelReason) {
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(Icons.Default.Warning, contentDescription = null, modifier)
else -> Icon(Icons.Default.Info, contentDescription = null, modifier)
}
}
@Composable
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
return when (cancelReason) {
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_crash)
)
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
title = stringResource(R.string.geode_download_title)
)
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_error),
details = stringResource(R.string.game_not_found)
)
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_error),
details = stringResource(R.string.launcher_cancelled_outdated)
)
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
title = null
)
else -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_manual)
)
}
}
@Composable
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
return when (state) {
is LaunchViewModel.LaunchUIState.Initial,
is LaunchViewModel.LaunchUIState.Working,
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
title = stringResource(R.string.launcher_starting_game),
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
)
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
title = stringResource(R.string.release_fetch_in_progress)
)
is LaunchViewModel.LaunchUIState.Updating -> {
val context = LocalContext.current
val downloaded = remember(state.downloaded) {
Formatter.formatShortFileSize(context, state.downloaded)
}
val outOf = remember(state.outOf) {
state.outOf?.run {
Formatter.formatShortFileSize(context, this)
}
}
LaunchStatusInfo(
title = stringResource(R.string.launcher_downloading_update),
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
progress = {
val progress = state.downloaded / state.outOf!!.toDouble()
progress.toFloat()
}.takeIf { state.outOf != null }
)
}
is LaunchViewModel.LaunchUIState.Cancelled -> {
return mapCancelReasonToInfo(state.reason)
}
}
}
@Composable
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
return
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.width(300.dp)
) {
if (statusInfo.title != null) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
icon()
Spacer(Modifier.size(8.dp))
Text(statusInfo.title)
}
}
}
if (inProgress) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
if (statusInfo.details != null) {
Text(
statusInfo.details,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
}
}
}
@Composable
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.Start,
modifier = modifier.width(300.dp)
) {
if (statusInfo.title != null) {
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
}
if (statusInfo.progress != null) {
LinearProgressIndicator(
statusInfo.progress,
modifier = Modifier.fillMaxWidth()
)
} else {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
if (statusInfo.details != null) {
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
}
}
}
@Composable
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
when (reason) {
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
Icon(
painterResource(R.drawable.icon_resume),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_resume))
}
LaunchViewModel.LaunchCancelReason.MANUAL,
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
Icon(
Icons.Filled.PlayArrow,
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_launch))
}
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
Icon(
Icons.Filled.Refresh,
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_retry))
}
// these shouldn't be allowing retries anyways
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LaunchProgressCard(
uiState: LaunchViewModel.LaunchUIState,
crashInfo: LoadFailureInfo?,
onCancel: () -> Unit,
onResume: (safeMode: Boolean) -> Unit,
onMore: () -> Unit,
extraOptions: @Composable () -> Unit,
safeModeEnabled: Boolean,
modifier: Modifier = Modifier
) {
var showSafeModeDialog by remember { mutableStateOf(false) }
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
Box(modifier = modifier) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
LaunchCancelledBody(
statusInfo = status,
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
inProgress = uiState.inProgress
)
if (uiState.reason.isGameInstallIssue()) {
val context = LocalContext.current
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
if (showDownload) {
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
}
}
if (uiState.reason.allowsRetry()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
LongPressButton(onClick = { onResume(false) }, onLongPress = {
showSafeModeDialog = true
}) {
RetryButtonContents(uiState.reason)
}
if (crashInfo != null) {
FilledTonalButton(onClick = onMore) {
Icon(
painterResource(R.drawable.icon_question_mark),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_help_alt))
}
}
extraOptions()
}
}
} else {
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
TextButton(onClick = onCancel) {
Text(stringResource(R.string.release_fetch_button_cancel))
}
Spacer(Modifier.weight(1.0f))
extraOptions()
}
}
}
}
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
showSafeModeDialog = false
},
onLaunch = {
onResume(true)
showSafeModeDialog = false
}
)
}
}
@Composable
fun ExtraOptions(onSettings: () -> Unit) {
IconButton(
onClick = onSettings,
) {
Icon(
Icons.Filled.Settings,
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
)
}
}
@Composable
fun GeodeLogo(modifier: Modifier = Modifier) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
Image(
painter = painterResource(id = R.drawable.geode_logo),
contentDescription = null,
modifier = Modifier.size(76.dp, 76.dp)
)
Text(
stringResource(R.string.launcher_title),
style = MaterialTheme.typography.displayMedium,
modifier = Modifier.padding(12.dp)
)
}
}
@Composable
fun AltMainScreen(
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
launchViewModel.beginLaunchFlow()
}
val launchUIState by launchViewModel.uiState.collectAsState()
val coroutineScope = rememberCoroutineScope()
var launchInSafeMode by remember { mutableStateOf(false) }
var showErrorInfo by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding(),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(8.dp)
.verticalScroll(rememberScrollState())
) {
GeodeLogo()
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
LaunchProgressCard(
launchUIState,
crashInfo = launchViewModel.currentCrashInfo(),
onCancel = {
launchInSafeMode = false
coroutineScope.launch {
launchViewModel.cancelLaunch()
}
},
onResume = { safeMode ->
launchInSafeMode = safeMode
launchViewModel.clearCrashInfo()
coroutineScope.launch {
launchViewModel.beginLaunchFlow(true)
}
},
onMore = {
showErrorInfo = true
},
extraOptions = {
ExtraOptions(
onSettings = {
coroutineScope.launch {
launchViewModel.cancelLaunch(true)
}
onSettings(context)
}
)
},
safeModeEnabled = launchInSafeMode
)
// only show launcher update in a case where a user won't see it ingame
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
UnsupportedVersionWarning()
}
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
if (nextLauncherUpdate != null) {
LauncherUpdateIndicator()
}
}
}
}
}
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
UpdateWarning(launchInSafeMode) {
launchInSafeMode = false
coroutineScope.launch {
launchViewModel.cancelLaunch()
}
}
}
val loadFailure = launchViewModel.currentCrashInfo()
if (showErrorInfo && loadFailure != null) {
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
}
}
@@ -12,7 +12,6 @@ import android.os.Environment
import android.text.InputType
import android.util.Log
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
@@ -23,7 +22,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updatePadding
import com.customRobTop.BaseRobTopActivity
import com.customRobTop.JniToCpp
import com.geode.launcher.main.LaunchNotification
@@ -187,34 +185,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
loadInternalMods()
val baseView = createView()
if (!GeodeUtils.handleSafeArea && Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
val frameLayoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
val frameLayout = FrameLayout(this)
frameLayout.layoutParams = frameLayoutParams
frameLayout.setOnApplyWindowInsetsListener { v, insets ->
val displayInsets = insets.displayCutout
if (displayInsets != null) {
v.updatePadding(
left = displayInsets.safeInsetLeft,
top = displayInsets.safeInsetTop,
right = displayInsets.safeInsetRight,
bottom = displayInsets.safeInsetBottom
)
}
WindowInsets.CONSUMED
}
frameLayout.addView(baseView)
setContentView(frameLayout)
} else {
setContentView(baseView)
}
setContentView(createView())
setupPostLibraryLoad(gdPackageInfo)
@@ -444,8 +415,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
this.mGLSurfaceView = glSurfaceView
frameLayout.addView(this.mGLSurfaceView)
val disableNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.DISABLE_REDESIGN)
if (!disableNotification) {
val showNotification = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
if (showNotification) {
val hasCards = determineDisplayedCards(this)
if (hasCards.isNotEmpty()) {
val notificationView = ComposeView(this)
@@ -494,11 +465,11 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
val limitedRefreshRate = preferenceUtils.getInt(PreferenceUtils.Key.LIMIT_FRAME_RATE).takeIf { it != 0 }
mLimitedRefreshRate = limitedRefreshRate
mForceRefreshRate = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
mForceRefreshRate = preferenceUtils.getBoolean(PreferenceUtils.Key.FORCE_HRR)
if (mForceRefreshRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val chosenDisplay = display.supportedModes?.maxByOrNull { it.refreshRate }
if (chosenDisplay != null) {
val chosenRefreshRate = chosenDisplay.refreshRate
var chosenRefreshRate = chosenDisplay.refreshRate
println("setupUIState: Forcing a refresh rate of $chosenRefreshRate (display ${chosenDisplay.modeId})")
window.attributes.preferredRefreshRate = chosenRefreshRate
@@ -617,8 +588,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL
mGLSurfaceView?.sendTimestampEvents = true
true
}
GeodeUtils.CAPABILITY_RICH_INPUT -> {
mGLSurfaceView?.enableControllerEvents()
GeodeUtils.CAPABILITY_INTERNAL_CALLBACKS -> {
mGLSurfaceView?.sendInternalTimestampEvents = true
true
}
else -> false
@@ -1,33 +0,0 @@
package com.geode.launcher
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.widget.Toast
import com.geode.launcher.main.clearDownloadedApks
class InstallReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val activityIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (activityIntent != null) {
context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
PackageInstaller.STATUS_SUCCESS -> {
clearDownloadedApks(context)
}
else -> {
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
println("failed to install update: $status, $msg")
val message = context.getString(R.string.launcher_self_update_failed, msg)
Toast.makeText(context, message, Toast.LENGTH_SHORT)
.show()
}
}
}
}
@@ -1,52 +1,17 @@
package com.geode.launcher
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import android.text.format.Formatter
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -54,58 +19,36 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.main.*
import com.geode.launcher.updater.ReleaseViewModel
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.Theme.DARK
import com.geode.launcher.ui.theme.Theme.LIGHT
import com.geode.launcher.ui.theme.launcherTitleStyle
import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
import java.net.ConnectException
import java.net.UnknownHostException
import com.geode.launcher.utils.GamePackageUtils
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
clearDownloadedApks(this)
super.onCreate(savedInstanceState)
val gdInstalled = GamePackageUtils.isGameInstalled(packageManager)
val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
if (gdInstalled && geodeInstalled && intent.getBooleanExtra("restarted", false)) {
onLaunch(this)
}
enableEdgeToEdge()
clearDownloadedApks(this)
val returnMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_MESSAGE)
val returnExtendedMessage = intent.extras?.getString(LaunchUtils.LAUNCHER_KEY_RETURN_EXTENDED_MESSAGE)
@@ -119,19 +62,13 @@ class MainActivity : ComponentActivity() {
LoadFailureInfo(LaunchUtils.LauncherError.CRASHED)
} else { null }
var launchArguments: LaunchArguments? = null
val intentData = intent.data
if (intent.action == Intent.ACTION_VIEW && intentData != null) {
val isLaunch = intentData.path == "/launch"
val safeMode = if (isLaunch) {
intentData.getBooleanQueryParameter("safe-mode", false)
} else false
val redirectToAlt = PreferenceUtils.get(this).getBoolean(PreferenceUtils.Key.ENABLE_REDESIGN)
if (redirectToAlt) {
val launchIntent = Intent(this, AltMainActivity::class.java)
launchIntent.putExtras(intent)
startActivity(launchIntent)
launchArguments = LaunchArguments(
autoSafeMode = safeMode,
forcePause = !isLaunch,
forceLaunch = isLaunch,
)
return
}
setContent {
@@ -139,630 +76,196 @@ class MainActivity : ComponentActivity() {
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
val launchViewModel = viewModel<LaunchViewModel>(factory = LaunchViewModel.Factory)
if (loadFailureInfo != null) {
launchViewModel.loadFailure = loadFailureInfo
}
if (launchArguments != null) {
launchViewModel.launchArguments = launchArguments
}
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AltMainScreen(launchViewModel)
MainScreen(gdInstalled, geodeInstalled, loadFailureInfo)
}
}
}
}
if (gdInstalled && geodeInstalled) {
intent.getBooleanExtra("restarted", false).let {
if (it) {
onLaunch(this)
}
}
}
}
}
data class LaunchStatusInfo(
val title: String?,
val details: String? = null,
val progress: (() -> Float)? = null
)
@Composable
fun LaunchCancelledIcon(cancelReason: LaunchViewModel.LaunchCancelReason, modifier: Modifier = Modifier) {
when (cancelReason) {
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED,
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> Icon(painterResource(R.drawable.icon_error), contentDescription = null, modifier)
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED,
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> Icon(painterResource(R.drawable.icon_warning), contentDescription = null, modifier)
else -> Icon(painterResource(R.drawable.icon_info), contentDescription = null, modifier)
}
}
@Composable
fun mapCancelReasonToInfo(cancelReason: LaunchViewModel.LaunchCancelReason): LaunchStatusInfo {
return when (cancelReason) {
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_crash)
)
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> LaunchStatusInfo(
title = stringResource(R.string.geode_download_title)
)
LaunchViewModel.LaunchCancelReason.GAME_MISSING -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_error),
details = stringResource(R.string.game_not_found)
)
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_error),
details = stringResource(R.string.launcher_cancelled_outdated)
)
LaunchViewModel.LaunchCancelReason.AUTOMATIC,
LaunchViewModel.LaunchCancelReason.MANUAL-> LaunchStatusInfo(
title = null
)
/*
else -> LaunchStatusInfo(
title = stringResource(R.string.launcher_cancelled_manual)
)
*/
}
}
@Composable
fun mapLaunchStatusToInfo(state: LaunchViewModel.LaunchUIState, inSafeMode: Boolean = false): LaunchStatusInfo {
return when (state) {
is LaunchViewModel.LaunchUIState.Initial,
is LaunchViewModel.LaunchUIState.Working,
is LaunchViewModel.LaunchUIState.Ready -> LaunchStatusInfo(
title = stringResource(R.string.launcher_starting_game),
details = if (inSafeMode) stringResource(R.string.launcher_in_safe_mode) else null
)
is LaunchViewModel.LaunchUIState.UpdateCheck -> LaunchStatusInfo(
title = stringResource(R.string.release_fetch_in_progress)
)
is LaunchViewModel.LaunchUIState.Updating -> {
val context = LocalContext.current
val downloaded = remember(state.downloaded) {
Formatter.formatShortFileSize(context, state.downloaded)
}
val outOf = remember(state.outOf) {
state.outOf?.run {
Formatter.formatShortFileSize(context, this)
}
}
LaunchStatusInfo(
title = stringResource(R.string.launcher_downloading_update),
details = stringResource(R.string.launcher_downloading_update_details, downloaded, outOf ?: "…"),
progress = {
val progress = state.downloaded / state.outOf!!.toDouble()
progress.toFloat()
}.takeIf { state.outOf != null }
)
}
is LaunchViewModel.LaunchUIState.Cancelled -> {
return mapCancelReasonToInfo(state.reason)
}
}
}
@Composable
fun LaunchCancelledBody(statusInfo: LaunchStatusInfo, icon: @Composable () -> Unit, inProgress: Boolean, modifier: Modifier = Modifier) {
if (statusInfo.title == null && statusInfo.details == null && !inProgress) {
return
}
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.width(300.dp)
) {
if (statusInfo.title != null) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.bodyLarge) {
icon()
Spacer(Modifier.size(8.dp))
Text(statusInfo.title)
}
}
}
if (inProgress) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
if (statusInfo.details != null) {
Text(
statusInfo.details,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center
)
}
}
}
@Composable
fun LaunchProgressBody(statusInfo: LaunchStatusInfo, modifier: Modifier = Modifier) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.Start,
modifier = modifier.width(300.dp)
) {
if (statusInfo.title != null) {
Text(statusInfo.title, style = MaterialTheme.typography.bodyLarge)
}
if (statusInfo.progress != null) {
LinearProgressIndicator(
statusInfo.progress,
modifier = Modifier.fillMaxWidth()
)
} else {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
if (statusInfo.details != null) {
Text(statusInfo.details, style = MaterialTheme.typography.bodyMedium)
}
}
}
@Composable
fun RetryButtonContents(reason: LaunchViewModel.LaunchCancelReason) {
when (reason) {
LaunchViewModel.LaunchCancelReason.LAST_LAUNCH_CRASHED -> {
Icon(
painterResource(R.drawable.icon_resume),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_resume))
}
LaunchViewModel.LaunchCancelReason.MANUAL,
LaunchViewModel.LaunchCancelReason.AUTOMATIC -> {
Icon(
painterResource(R.drawable.icon_play_arrow),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_launch))
}
LaunchViewModel.LaunchCancelReason.GEODE_NOT_FOUND -> {
Icon(
painterResource(R.drawable.icon_refresh),
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_retry))
}
// these shouldn't be allowing retries anyways
LaunchViewModel.LaunchCancelReason.GAME_MISSING,
LaunchViewModel.LaunchCancelReason.GAME_OUTDATED -> {}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LaunchProgressCard(
uiState: LaunchViewModel.LaunchUIState,
crashInfo: LoadFailureInfo?,
onCancel: () -> Unit,
onResume: (safeMode: Boolean) -> Unit,
onMore: () -> Unit,
extraOptions: @Composable () -> Unit,
safeModeEnabled: Boolean,
modifier: Modifier = Modifier
fun MainScreen(
gdInstalled: Boolean = true,
geodePreinstalled: Boolean = true,
loadFailureInfo: LoadFailureInfo? = null,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
var showSafeModeDialog by remember { mutableStateOf(false) }
val status = mapLaunchStatusToInfo(uiState, safeModeEnabled)
val context = LocalContext.current
Box(modifier = modifier) {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
if (uiState is LaunchViewModel.LaunchUIState.Cancelled) {
LaunchCancelledBody(
statusInfo = status,
icon = { LaunchCancelledIcon(uiState.reason, Modifier.size(32.dp)) },
inProgress = uiState.inProgress
val shouldUpdate by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
val autoUpdateState by releaseViewModel.uiState.collectAsState()
val geodeJustInstalled = (autoUpdateState as? ReleaseViewModel.ReleaseUIState.Finished)
?.hasUpdated == true
val geodeInstalled = geodePreinstalled || geodeJustInstalled
var beginLaunch by remember { mutableStateOf(false) }
var launchInSafeMode by remember { mutableStateOf(false) }
LaunchedEffect(shouldUpdate) {
if (shouldUpdate && !releaseViewModel.hasPerformedCheck && gdInstalled) {
releaseViewModel.runReleaseCheck()
} else {
releaseViewModel.useGlobalCheckState()
}
}
val snackbarHostState = remember { SnackbarHostState() }
// automatically show load failed dialog (but not crash)
var showErrorInfo by remember { mutableStateOf(
loadFailureInfo != null && loadFailureInfo.title != LaunchUtils.LauncherError.CRASHED
)}
val hasError = loadFailureInfo != null
LaunchedEffect(hasError, showErrorInfo) {
if (!showErrorInfo && loadFailureInfo != null) {
showFailureSnackbar(context, loadFailureInfo, snackbarHostState) {
showErrorInfo = true
}
}
}
if (showErrorInfo && loadFailureInfo != null) {
ErrorInfoSheet(loadFailureInfo, onDismiss = { showErrorInfo = false })
}
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
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(100.dp, 100.dp)
)
Text(
context.getString(R.string.launcher_title),
fontSize = 32.sp,
modifier = Modifier.padding(12.dp)
)
if (uiState.reason.isGameInstallIssue()) {
val context = LocalContext.current
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
if (showDownload) {
GooglePlayBadge(modifier = Modifier.align(Alignment.CenterHorizontally))
}
when {
gdInstalled && geodeInstalled -> {
val gdVersion = remember {
GamePackageUtils.getGameVersionCode(context.packageManager)
}
if (uiState.reason.allowsRetry()) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
modifier = Modifier.align(Alignment.CenterHorizontally)
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN) {
val versionName = remember {
GamePackageUtils.getUnifiedVersionName(context.packageManager)
}
LaunchBlockedLabel(stringResource(R.string.game_outdated, versionName))
} else {
val stopLaunch = releaseViewModel.isInUpdate || hasError
PlayButton(
stopAutomaticLaunch = stopLaunch,
blockLaunch = releaseViewModel.isInUpdate,
onPlayGame = { safeMode ->
launchInSafeMode = safeMode
beginLaunch = true
},
)
if (gdVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
UnsupportedVersionWarning()
}
}
}
gdInstalled -> {
Text(
context.getString(R.string.geode_download_title),
modifier = Modifier.padding(12.dp)
)
Row {
Button(
onClick = { releaseViewModel.runReleaseCheck(true) },
enabled = !releaseViewModel.isInUpdate
) {
LongPressButton(onClick = { onResume(false) }, onLongPress = {
showSafeModeDialog = true
}) {
RetryButtonContents(uiState.reason)
}
if (crashInfo != null) {
FilledTonalButton(onClick = onMore) {
Icon(
painterResource(R.drawable.icon_question_mark),
contentDescription = null
painterResource(R.drawable.icon_download),
contentDescription = context.getString(R.string.launcher_download_icon_alt)
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_cancelled_help_alt))
Text(context.getString(R.string.launcher_download))
}
}
extraOptions()
}
}
} else {
LaunchProgressBody(statusInfo = status, modifier = Modifier.padding(top = 16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.width(300.dp)) {
TextButton(onClick = onCancel) {
Text(stringResource(R.string.release_fetch_button_cancel))
}
Spacer(Modifier.weight(1.0f))
extraOptions()
}
}
}
}
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
showSafeModeDialog = false
},
onLaunch = {
onResume(true)
showSafeModeDialog = false
}
)
}
}
@Composable
fun ExtraOptions(onSettings: () -> Unit) {
IconButton(
onClick = onSettings,
) {
Spacer(Modifier.size(2.dp))
IconButton(onClick = { onSettings(context) }) {
Icon(
painterResource(R.drawable.icon_settings),
contentDescription = stringResource(R.string.launcher_settings_icon_alt)
)
}
}
val lightOuterColorGradient = arrayOf(
0.0f to Color(0xfff5b11d),
0.10f to Color(0xfff0834d),
0.24f to Color(0xffe85f6b),
0.49f to Color(0xffc9659e),
1.0f to Color(0xffb9588f)
)
val lightInnerColorGradient = arrayOf(
0.48f to Color(0xfff2ce00),
0.63f to Color(0xfff4b97e),
0.73f to Color(0xfff7b66a),
1.0f to Color(0xffeb8fac)
)
val darkOuterColorGradient = arrayOf(
0.0f to Color(0xfffffdff),
0.08f to Color(0xffF2EAF5),
0.12f to Color(0xffEDE5EF),
0.31f to Color(0xffCDB5CD),
0.49f to Color(0xffBA9BBC),
1.0f to Color(0xff8D7ACF),
)
val darkInnerColorGradient = arrayOf(
0.48f to Color(0xffffffff),
0.63f to Color(0xfff5e4c2),
0.73f to Color(0xffe5c7ad),
1.0f to Color(0xffb790a9),
)
@Preview(name = "non-animated, light")
@Preview(name = "non-animated, dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun GeodeLogoPreview() {
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface {
GeodeLogo()
}
}
}
}
@Preview(name = "animated, light")
@Preview(name = "animated, dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun GeodeLogoAnimatedPreview() {
val theme = if (isSystemInDarkTheme()) DARK else LIGHT
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface {
GeodeLogo(true)
}
}
}
}
@Composable
fun AnimatedLogo(modifier: Modifier = Modifier) {
val theme = LocalTheme.current
val innerColorPalette = remember(theme) {
if (theme == LIGHT) lightInnerColorGradient else darkInnerColorGradient
}
val outerColorPalette = remember(theme) {
if (theme == LIGHT) lightOuterColorGradient else darkOuterColorGradient
}
val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val offset by infiniteTransition.animateFloat(
initialValue = -1.0f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "gradient offset"
)
val innerBrush = Brush.linearGradient(
colorStops = innerColorPalette,
start = Offset(0.0f, 0.0f + (165.06f * offset)),
end = Offset(0.0f, 165.06f + (165.06f * offset)),
tileMode = TileMode.Mirror
)
val outerBrush = Brush.linearGradient(
colorStops = outerColorPalette,
start = Offset(0.0f + (84.69f * offset), 0.0f + (165.06f * offset)),
end = Offset(84.69f + (84.69f * offset), 169.61f + (165.06f * offset)),
tileMode = TileMode.Mirror
)
Box(modifier = modifier) {
Icon(
painter = painterResource(id = R.drawable.geode_monochrome_inner),
contentDescription = null,
modifier = Modifier
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
drawRect(
brush = innerBrush,
blendMode = BlendMode.SrcIn
)
}
)
Icon(
painter = painterResource(id = R.drawable.geode_monochrome_outer),
contentDescription = null,
modifier = Modifier
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
.drawWithContent {
drawContent()
drawRect(
brush = outerBrush,
blendMode = BlendMode.SrcIn
)
}
)
}
}
@Composable
fun GeodeLogo(shouldAnimate: Boolean = false, modifier: Modifier = Modifier) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
val theme = LocalTheme.current
Crossfade(
targetState = shouldAnimate,
animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing),
label="logo fade"
) { screen ->
when (screen) {
true -> AnimatedLogo(modifier = Modifier.size(64.dp, 64.dp))
false -> Image(
painterResource(if (theme == LIGHT)
R.drawable.geode_base_light else R.drawable.geode_base
),
contentDescription = null,
modifier = Modifier.size(64.dp, 64.dp)
Icons.Filled.Settings,
contentDescription = context.getString(R.string.launcher_settings_icon_alt)
)
}
}
}
else -> LaunchBlockedLabel(stringResource(R.string.game_not_found))
}
Text(
stringResource(R.string.launcher_title),
style = launcherTitleStyle,
fontSize = 64.sp,
UpdateCard(
releaseViewModel,
modifier = Modifier
.padding(12.dp)
)
}
}
@Composable
fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Unit) {
val context = LocalContext.current
val resources = LocalResources.current
val releaseState = ReleaseManager.get(context).uiState.collectAsState().value
if (releaseState !is ReleaseManager.ReleaseManagerState.Failure) {
return
}
val message = when (releaseState.exception) {
is UnknownHostException, is ConnectException ->
stringResource(R.string.release_fetch_no_internet_short)
is ReleaseManager.UpdateException -> {
when (releaseState.exception.reason) {
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
R.string.release_fetch_external_in_use_short
)
else -> stringResource(R.string.release_fetch_generic_short)
}
}
is CancellationException -> null
else -> stringResource(R.string.release_fetch_generic_short)
}
if (message == null) {
return
}
LaunchedEffect(message) {
val result = snackbarHostState.showSnackbar(
message = message,
actionLabel = resources.getString(R.string.release_fetch_button_retry),
duration = SnackbarDuration.Indefinite,
withDismissAction = true
)
when (result) {
SnackbarResult.ActionPerformed -> {
onRetry()
}
else -> {}
}
}
}
@Composable
fun AltMainScreen(
launchViewModel: LaunchViewModel = viewModel(factory = LaunchViewModel.Factory)
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
launchViewModel.beginLaunchFlow()
}
val launchUIState by launchViewModel.uiState.collectAsState()
val coroutineScope = rememberCoroutineScope()
var launchInSafeMode by remember {
mutableStateOf(launchViewModel.launchArguments?.autoSafeMode == true)
}
var showErrorInfo by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
modifier = Modifier.safeDrawingPadding()
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(8.dp)
.verticalScroll(rememberScrollState())
) {
GeodeLogo(shouldAnimate = launchUIState.isInProgress())
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
LaunchProgressCard(
launchUIState,
crashInfo = launchViewModel.currentCrashInfo(),
onCancel = {
launchInSafeMode = false
coroutineScope.launch {
launchViewModel.cancelLaunch()
}
},
onResume = { safeMode ->
launchInSafeMode = safeMode
launchViewModel.clearCrashInfo()
coroutineScope.launch {
launchViewModel.beginLaunchFlow(true)
}
},
onMore = {
showErrorInfo = true
},
extraOptions = {
ExtraOptions(
onSettings = {
coroutineScope.launch {
launchViewModel.cancelLaunch(true)
}
onSettings(context)
}
)
},
safeModeEnabled = launchInSafeMode
)
// only show launcher update in a case where a user won't see it ingame
if (launchUIState is LaunchViewModel.LaunchUIState.Cancelled) {
val gameVersion = remember { GamePackageUtils.getGameVersionCodeOrNull(context.packageManager) }
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE_MIN_WARNING) {
UnsupportedVersionWarning()
}
val nextLauncherUpdate by launchViewModel.nextLauncherUpdate.collectAsState()
if (nextLauncherUpdate != null) {
LauncherUpdateIndicator()
}
GeodeUpdateIndicator(snackbarHostState) {
launchViewModel.retryUpdate()
}
}
}
}
}
}
if (launchUIState is LaunchViewModel.LaunchUIState.Ready) {
if (beginLaunch) {
UpdateWarning(launchInSafeMode) {
beginLaunch = false
launchInSafeMode = false
coroutineScope.launch {
launchViewModel.cancelLaunch()
}
}
}
val loadFailure = launchViewModel.currentCrashInfo()
if (showErrorInfo && loadFailure != null) {
ErrorInfoSheet(loadFailure, onDismiss = { showErrorInfo = false })
}
}
@Preview(showSystemUi = true)
@Composable
fun MainScreenLightPreview() {
GeodeLauncherTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
MainScreen()
}
}
}
@Preview(showSystemUi = true)
@Composable
fun MainScreenDarkPreview() {
GeodeLauncherTheme(theme = Theme.DARK) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
MainScreen()
}
}
}
@Preview
@Composable
fun MainScreenNoGeometryDashPreview() {
GeodeLauncherTheme {
MainScreen(gdInstalled = false)
}
}
@@ -1,115 +0,0 @@
package com.geode.launcher.log
import android.app.Application
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.geode.launcher.utils.LaunchUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.io.File
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
data class CrashDump @OptIn(ExperimentalTime::class) constructor(
val filename: String,
val lastModified: Instant
)
class CrashViewModel(private val application: Application) : ViewModel() {
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
CrashViewModel(
application = application
)
}
}
}
private val _lineState = mutableStateListOf<CrashDump>()
val lineState = _lineState
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
private var failedToLoad = false
private var logJob: Job? = null
@OptIn(ExperimentalTime::class)
private fun getCrashList(): List<CrashDump> {
val crashDirectory = LaunchUtils.getCrashDirectory(application)
if (!crashDirectory.exists()) {
return emptyList()
}
val children = crashDirectory.listFiles {
// ignore indicator files (including old file)
_, name -> name != LaunchUtils.CRASH_INDICATOR_NAME && name != "last-pid"
} ?: return emptyList()
return children
.map {
CrashDump(
filename = it.name,
lastModified = Instant.fromEpochMilliseconds(it.lastModified())
)
}
.sortedByDescending { it.lastModified }
}
fun clearCrashes() {
logJob?.cancel()
viewModelScope.launch(Dispatchers.IO) {
val crashDirectory = LaunchUtils.getCrashDirectory(application)
if (crashDirectory.exists()) {
val files = crashDirectory.listFiles()
files?.forEach {
it.delete()
}
}
}
_lineState.clear()
}
private fun loadLogs() {
_isLoading.value = true
logJob?.cancel()
logJob = viewModelScope.launch(Dispatchers.IO) {
val lines = getCrashList()
_lineState.clear()
_lineState.addAll(lines)
_isLoading.value = false
logJob = null
}
}
fun removeCrash(filename: String) {
logJob?.cancel()
val crashPath = File(LaunchUtils.getCrashDirectory(application), filename)
if (crashPath.exists()) {
crashPath.delete()
loadLogs()
}
}
init {
loadLogs()
}
}
@@ -1,14 +1,13 @@
package com.geode.launcher.log
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toLocalDateTime
import okio.Buffer
import okio.BufferedSource
import java.io.IOException
import kotlin.time.ExperimentalTime
import kotlin.time.toJavaInstant
fun BufferedSource.readCChar(): Char {
return this.readUtf8CodePoint().toChar()
@@ -116,7 +115,7 @@ enum class LogPriority {
/**
* Represents a log entry from logcat.
*/
data class LogLine @OptIn(ExperimentalTime::class) constructor(
data class LogLine(
val process: ProcessInformation,
val time: Instant,
val logId: Int,
@@ -135,7 +134,6 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
else -> throw IOException("LogLine::fromInputStream: unknown format for (headerSize = $size)")
}
@OptIn(ExperimentalTime::class)
fun fromBufferedSource(source: BufferedSource): LogLine {
/*
// from android <liblog/include/log/log_read.h>
@@ -202,7 +200,6 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
)
}
@OptIn(ExperimentalTime::class)
fun showException(exception: Exception) = LogLine(
process = ProcessInformation(0, 0, 0),
time = Clock.System.now(),
@@ -213,14 +210,12 @@ data class LogLine @OptIn(ExperimentalTime::class) constructor(
)
}
@OptIn(ExperimentalTime::class)
val identifier = time.toJavaInstant()
val messageTrimmed by lazy {
this.message.trim { it <= ' ' }
}
@OptIn(ExperimentalTime::class)
val formattedTime by lazy { this.time.toLocalDateTime(TimeZone.currentSystemDefault()) }
val asSimpleString by lazy {
"$formattedTime [${this.priority.toChar()}/${this.tag}]: ${this.messageTrimmed}"
@@ -3,13 +3,16 @@ package com.geode.launcher.main
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
@@ -24,13 +27,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.geode.launcher.R
import com.geode.launcher.utils.Constants
import androidx.core.net.toUri
fun onDownloadGame(context: Context) {
val appUrl = "https://play.google.com/store/apps/details?id=${Constants.PACKAGE_NAME}"
try {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = appUrl.toUri()
data = Uri.parse(appUrl)
setPackage("com.android.vending")
}
context.startActivity(intent)
@@ -53,6 +55,22 @@ fun GooglePlayBadge(modifier: Modifier = Modifier) {
)
}
@Composable
fun StatusIndicator(
icon: @Composable () -> Unit,
text: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
ElevatedCard(modifier = modifier, onClick = onClick) {
Row(modifier = Modifier.padding(8.dp)) {
icon()
Spacer(Modifier.size(8.dp))
text()
}
}
}
@Composable
fun InlineDialog(headline: @Composable () -> Unit, body: @Composable () -> Unit, modifier: Modifier = Modifier) {
ElevatedCard(modifier = modifier) {
@@ -1,7 +1,6 @@
package com.geode.launcher.main
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.os.Build
@@ -19,6 +18,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -33,14 +34,10 @@ import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -60,7 +57,6 @@ import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
data class LoadFailureInfo(
val title: LaunchUtils.LauncherError,
@@ -213,12 +209,9 @@ fun ErrorInfoDescription(description: String, modifier: Modifier = Modifier) {
@Composable
@OptIn(ExperimentalLayoutApi::class)
fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val resources = LocalResources.current
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(
@@ -236,15 +229,7 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
}
FilledTonalButton(onClick = {
val clipboardLabel = resources.getString(R.string.application_log_copy_label)
coroutineScope.launch {
clipboard.setClipEntry(
ClipEntry(ClipData.newPlainText(
clipboardLabel, extraDetails
))
)
}
clipboardManager.setText(AnnotatedString(extraDetails))
}) {
Icon(
painterResource(R.drawable.icon_content_copy),
@@ -256,17 +241,13 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
} else {
Button(onClick = { onShareCrash(context) }) {
Icon(
painterResource(R.drawable.icon_share),
Icons.Filled.Share,
contentDescription = null
)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.launcher_error_export_crash))
}
val developerSettingsEnabled = remember {
PreferenceUtils.get(context).getBoolean(PreferenceUtils.Key.DEVELOPER_MODE)
}
if (developerSettingsEnabled) {
FilledTonalButton(onClick = { onShowLogs(context) }) {
Icon(
painterResource(R.drawable.icon_description),
@@ -277,7 +258,6 @@ fun ErrorInfoActions(extraDetails: String?, modifier: Modifier = Modifier) {
}
}
}
}
}
@@ -4,12 +4,21 @@ import android.content.Context
import android.content.Intent
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -19,21 +28,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.geode.launcher.GeometryDashActivity
import com.geode.launcher.R
import com.geode.launcher.preferences.SettingsActivity
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.PreferenceUtils
import com.geode.launcher.utils.useCountdownTimer
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
@@ -58,7 +71,7 @@ fun UpdateWarning(inSafeMode: Boolean = false, onDismiss: () -> Unit) {
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_warning),
Icons.Filled.Warning,
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
@@ -103,11 +116,11 @@ fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_shield_alert),
contentDescription = null
Icons.Filled.Warning,
contentDescription = stringResource(R.string.launcher_warning_icon_alt)
)
},
title = { Text(stringResource(R.string.preference_launch_safe_mode_confirm_title)) },
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
dismissButton = {
TextButton(onClick = onDismiss) {
@@ -184,12 +197,105 @@ fun LongPressButton(onClick: () -> Unit, onLongPress: () -> Unit, enabled: Boole
)
}
@Composable
fun PlayButton(
stopAutomaticLaunch: Boolean,
blockLaunch: Boolean,
onPlayGame: (Boolean) -> Unit
) {
val context = LocalContext.current
var showSafeModeDialog by remember { mutableStateOf(false) }
var launchInSafeMode by remember { mutableStateOf(false) }
val shouldAutomaticallyLaunch by PreferenceUtils.useBooleanPreference(
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
)
if (shouldAutomaticallyLaunch && !stopAutomaticLaunch && !showSafeModeDialog) {
val countdownTimer = useCountdownTimer(
time = 3000,
onCountdownFinish = { onPlayGame(launchInSafeMode) }
)
if (countdownTimer != 0L) {
Text(
pluralStringResource(
R.plurals.automatically_load_countdown,
countdownTimer.toInt(),
countdownTimer
),
style = Typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,
)
Spacer(Modifier.size(12.dp))
}
}
Row {
LongPressButton(
enabled = !blockLaunch,
onLongPress = {
showSafeModeDialog = true
},
onClick = {
onPlayGame(launchInSafeMode)
}
) {
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)
)
}
}
if (showSafeModeDialog) {
SafeModeDialog(
onDismiss = {
launchInSafeMode = false
showSafeModeDialog = false
},
onLaunch = {
launchInSafeMode = true
onPlayGame(true)
showSafeModeDialog = false
}
)
}
}
@Composable
fun LaunchBlockedLabel(text: String) {
val context = LocalContext.current
val showDownload = remember { GamePackageUtils.showDownloadBadge(context.packageManager) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text, textAlign = TextAlign.Center, modifier = Modifier.padding(12.dp))
if (showDownload) {
GooglePlayBadge()
}
}
}
@Composable
fun UnsupportedVersionWarning(modifier: Modifier = Modifier) {
InlineDialog(
headline = {
Icon(
painterResource(R.drawable.icon_warning),
Icons.Default.Warning,
contentDescription = null,
modifier = Modifier.size(28.dp)
)
@@ -13,6 +13,9 @@ import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
@@ -38,7 +41,6 @@ import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.GeodeUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.delay
@@ -125,9 +127,7 @@ fun LaunchNotification() {
// surface is not in use, so this is unfortunately not provided
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
Column(
modifier = if (GeodeUtils.handleSafeArea)
Modifier.displayCutoutPadding()
else Modifier
modifier = Modifier.displayCutoutPadding()
) {
cards.forEach {
NotificationCardFromType(it)
@@ -199,7 +199,7 @@ fun LauncherUpdateContent(modifier: Modifier = Modifier) {
},
leadingContent = {
Icon(
painterResource(R.drawable.icon_info),
Icons.Filled.Info,
contentDescription = null,
)
},
@@ -232,7 +232,7 @@ fun OutdatedVersionContent(modifier: Modifier = Modifier) {
},
leadingContent = {
Icon(
painterResource(R.drawable.icon_warning),
Icons.Filled.Warning,
contentDescription = null,
)
},
@@ -251,7 +251,7 @@ fun UpdateFailedContent(modifier: Modifier = Modifier) {
},
leadingContent = {
Icon(
painterResource(R.drawable.icon_warning),
Icons.Filled.Warning,
contentDescription = null,
)
},
@@ -18,11 +18,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
data class LaunchArguments(
val autoSafeMode: Boolean,
val forcePause: Boolean,
val forceLaunch: Boolean
)
private const val COOLDOWN_LENGTH_MS = 3000L
class LaunchViewModel(private val application: Application): ViewModel() {
companion object {
@@ -41,13 +37,7 @@ class LaunchViewModel(private val application: Application): ViewModel() {
private var readyTimer: CountDownTimer? = null
private fun initReadyTimer() {
val cooldownLength = PreferenceUtils.get(application).getInt(PreferenceUtils.Key.WAIT_PERIOD) * 1000L
if (cooldownLength == 0L) {
readyTimerPassed = true
return
}
readyTimer = object : CountDownTimer(cooldownLength, 1000) {
readyTimer = object : CountDownTimer(COOLDOWN_LENGTH_MS, 1000) {
override fun onTick(millisUntilFinished: Long) {
// no tick necessary
}
@@ -90,43 +80,18 @@ class LaunchViewModel(private val application: Application): ViewModel() {
data class Cancelled(val reason: LaunchCancelReason, val inProgress: Boolean = false) : LaunchUIState()
data object Working : LaunchUIState()
data object Ready : LaunchUIState()
fun isInProgress() = when (this) {
is Ready,
is Working,
is Updating,
is UpdateCheck -> true
else -> false
}
}
private val _uiState: MutableStateFlow<LaunchUIState>
init {
val preferenceUtils = PreferenceUtils.get(application)
val initialCancel = !preferenceUtils.getBoolean(PreferenceUtils.Key.UPDATE_AUTOMATICALLY)
&& !preferenceUtils.getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
// fixes a single frame of progress being displayed when we're not actually going to do work
_uiState = MutableStateFlow<LaunchUIState>(
if (initialCancel) LaunchUIState.Cancelled(LaunchCancelReason.AUTOMATIC)
else LaunchUIState.Initial
)
}
private val _uiState = MutableStateFlow<LaunchUIState>(LaunchUIState.Initial)
val uiState = _uiState.asStateFlow()
val nextLauncherUpdate = ReleaseManager.get(application).availableLauncherUpdate
var loadFailure: LoadFailureInfo? = null
var launchArguments: LaunchArguments? = null
private var hasCheckedForUpdates = false
private var isCancelling = false
private var hasManuallyStarted = false
private var restoreUpdateState: LaunchUIState? = null
private suspend fun determineUpdateStatus() {
ReleaseManager.get(application).uiState.takeWhile {
it is ReleaseManager.ReleaseManagerState.InUpdateCheck || it is ReleaseManager.ReleaseManagerState.InDownload
@@ -141,13 +106,6 @@ class LaunchViewModel(private val application: Application): ViewModel() {
// flow terminates once update check is finished
hasCheckedForUpdates = true
val currentState = restoreUpdateState
if (currentState != null) {
_uiState.emit(currentState)
return
}
preReadyCheck()
}
@@ -199,15 +157,8 @@ class LaunchViewModel(private val application: Application): ViewModel() {
return _uiState.emit(LaunchUIState.Cancelled(LaunchCancelReason.GEODE_NOT_FOUND))
}
// if forcing immediate launch, then act as if it's manually started (skips timers)
val forceImmediate = launchArguments?.forceLaunch == true
if (forceImmediate) {
hasManuallyStarted = true
}
val loadAutomatically = PreferenceUtils.get(application).getBoolean(PreferenceUtils.Key.LOAD_AUTOMATICALLY)
val forcePause = launchArguments?.forcePause == true
if (!hasManuallyStarted && (forcePause || !loadAutomatically)) {
if (!loadAutomatically && !hasManuallyStarted) {
return cancelLaunch(true)
}
@@ -250,17 +201,6 @@ class LaunchViewModel(private val application: Application): ViewModel() {
preReadyCheck()
}
fun retryUpdate() {
ReleaseManager.get(application).checkForUpdates(true)
if (ReleaseManager.get(application).isInUpdate) {
restoreUpdateState = _uiState.value
viewModelScope.launch {
determineUpdateStatus()
}
return
}
}
suspend fun cancelLaunch(isAutomatic: Boolean = false) {
// no need to double cancel
if (_uiState.value is LaunchUIState.Cancelled) {
@@ -270,10 +210,10 @@ class LaunchViewModel(private val application: Application): ViewModel() {
val reason = if (isAutomatic) LaunchCancelReason.AUTOMATIC else LaunchCancelReason.MANUAL
isCancelling = true
_uiState.emit(LaunchUIState.Cancelled(reason, true))
val releaseManager = ReleaseManager.get(application)
if (releaseManager.isInUpdate) {
_uiState.emit(LaunchUIState.Cancelled(reason, true))
releaseManager.cancelUpdate()
}
@@ -1,26 +1,26 @@
package com.geode.launcher.main
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Build
import android.net.Uri
import android.provider.DocumentsContract
import android.text.format.Formatter
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
@@ -35,12 +35,10 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
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.Alignment
import androidx.compose.ui.Modifier
@@ -48,30 +46,26 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.geode.launcher.InstallReceiver
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownTypography
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.toJavaInstant
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import java.io.File
import java.net.ConnectException
import java.net.UnknownHostException
import java.text.DateFormat
import java.util.Date
import kotlin.time.toJavaInstant
fun clearDownloadedApks(context: Context) {
// technically we should be using the activity results but it was too inconsistent for my liking
@@ -90,172 +84,57 @@ fun clearDownloadedApks(context: Context) {
it.delete()
}
context.filesDir.listFiles { _, name -> name.lowercase().endsWith(".apk") }?.forEach {
it.delete()
}
preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false)
}
// lazy singleton idc
object LauncherUpdater {
enum class LauncherUpdateState {
Began,
Active,
Inactive,
Finished;
}
private val _uiState = MutableStateFlow<LauncherUpdateState>(LauncherUpdateState.Inactive)
val uiState = _uiState.asStateFlow()
suspend fun resetState() {
_uiState.emit(LauncherUpdateState.Inactive)
}
suspend fun installPackage(context: Context, file: File) {
if (!file.exists()) {
_uiState.emit(LauncherUpdateState.Finished)
return
}
_uiState.emit(LauncherUpdateState.Began)
val installer = context.packageManager.packageInstaller
installer.registerSessionCallback(object : PackageInstaller.SessionCallback() {
override fun onActiveChanged(sessionId: Int, active: Boolean) {
if (active) {
_uiState.tryEmit(LauncherUpdateState.Active)
} else {
_uiState.tryEmit(LauncherUpdateState.Began)
}
}
override fun onBadgingChanged(sessionId: Int) {}
override fun onCreated(sessionId: Int) {
_uiState.tryEmit(LauncherUpdateState.Began)
}
override fun onFinished(sessionId: Int, success: Boolean) {
_uiState.tryEmit(LauncherUpdateState.Finished)
}
override fun onProgressChanged(sessionId: Int, progress: Float) {
_uiState.tryEmit(LauncherUpdateState.Active)
}
})
withContext(Dispatchers.IO) {
file.inputStream().use { apkStream ->
val length = file.length()
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
val sessionId = installer.createSession(params)
val session = installer.openSession(sessionId)
session.openWrite("package", 0, length).use { sessionStream ->
apkStream.copyTo(sessionStream)
session.fsync(sessionStream)
}
val intent = Intent(context, InstallReceiver::class.java)
val pi = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
session.commit(pi.intentSender)
session.close()
}
}
fun generateInstallIntent(uri: Uri): Intent {
// maybe one day i'll rewrite this to use packageinstaller. not today
return Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
setDataAndType(uri, "application/vnd.android.package-archive")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
suspend fun installLauncherUpdate(context: Context) {
fun installLauncherUpdate(context: Context) {
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
val launcherDownload = nextUpdate?.getDownload()
if (launcherDownload != null) {
val outputFile = launcherDownload.filename
val baseDirectory = context.filesDir
val baseDirectory = LaunchUtils.getBaseDirectory(context)
val outputPathFile = File(baseDirectory, outputFile)
if (!outputPathFile.exists()) {
return
}
val outputPath = "${UserDirectoryProvider.ROOT}${outputFile}"
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", outputPath)
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
try {
LauncherUpdater.installPackage(context, outputPathFile)
} catch (e: Exception) {
e.printStackTrace()
val msg = context.getString(R.string.launcher_self_update_failed, e.message)
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
val intent = generateInstallIntent(uri)
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
// if it fails to install, just open it in the browser
try {
val downloadUrl = launcherDownload.url
val downloadIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(downloadUrl)
}
LauncherUpdater.resetState()
context.startActivity(downloadIntent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
}
}
} else {
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
}
}
@Composable
fun LauncherUpdateDialog(onDismiss: () -> Unit) {
val updateState by LauncherUpdater.uiState.collectAsState()
val cancellable = updateState != LauncherUpdater.LauncherUpdateState.Active
LaunchedEffect(updateState) {
if (updateState == LauncherUpdater.LauncherUpdateState.Finished) {
onDismiss()
}
}
if (cancellable) {
return
}
Dialog(
onDismissRequest = {
if (cancellable) onDismiss()
},
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(18.dp),
modifier = Modifier.padding(24.dp)
) {
CircularProgressIndicator()
Text(
stringResource(R.string.launcher_self_update_progress),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
@@ -263,14 +142,6 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
val sheetState = rememberModalBottomSheetState()
val coroutineScope = rememberCoroutineScope()
var beginUpdate by remember { mutableStateOf(false) }
if (beginUpdate) {
LauncherUpdateDialog(onDismiss = {
beginUpdate = false
})
}
if (nextRelease != null) {
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
@@ -294,12 +165,7 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
Row(modifier = Modifier.padding(vertical = 12.dp)) {
Button(
onClick = {
coroutineScope.launch {
installLauncherUpdate(context)
beginUpdate = true
}
},
onClick = { installLauncherUpdate(context) },
) {
Icon(painterResource(R.drawable.icon_download), contentDescription = null)
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@@ -330,11 +196,9 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
Markdown(
content = nextRelease.release.body.replace("\r", ""),
typography = markdownTypography(
textLink = TextLinkStyles(
style = MaterialTheme.typography.bodyLarge.copy(
link = MaterialTheme.typography.bodyLarge.copy(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary,
).toSpanStyle()
),
),
flavour = GFMFlavourDescriptor(),
@@ -352,7 +216,6 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
@Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var showInfoDialog by remember { mutableStateOf(false) }
@@ -362,13 +225,6 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
}
}
var beginUpdate by remember { mutableStateOf(false) }
if (beginUpdate) {
LauncherUpdateDialog {
beginUpdate = false
}
}
ElevatedCard(modifier) {
Column(
// buttons add enough padding already, lower to compensate
@@ -393,12 +249,7 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
}
TextButton(
onClick = {
coroutineScope.launch {
installLauncherUpdate(context)
beginUpdate = true
}
},
onClick = { installLauncherUpdate(context) },
) {
Text(stringResource(R.string.launcher_install))
}
@@ -406,3 +257,174 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
}
}
}
@Composable
fun UpdateProgressIndicator(
message: String,
releaseViewModel: ReleaseViewModel,
modifier: Modifier = Modifier,
progress: (() -> Float)? = null
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.Start,
modifier = modifier
) {
Text(message, style = MaterialTheme.typography.labelMedium)
Spacer(modifier = Modifier.padding(4.dp))
if (progress == null) {
LinearProgressIndicator()
} else {
LinearProgressIndicator(progress = progress)
}
TextButton(
onClick = {
releaseViewModel.cancelUpdate()
},
modifier = Modifier.offset((-12).dp)
) {
Text(stringResource(R.string.release_fetch_button_cancel))
}
}
}
@Composable
fun UpdateMessageIndicator(
message: String,
releaseViewModel: ReleaseViewModel,
modifier: Modifier = Modifier,
allowRetry: Boolean = false
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Text(
message,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.padding(4.dp))
if (allowRetry) {
OutlinedButton(
onClick = {
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))
}
}
}
}
@Composable
fun UpdateCard(releaseViewModel: ReleaseViewModel, modifier: Modifier = Modifier) {
val context = LocalContext.current
val releaseState by releaseViewModel.uiState.collectAsState()
when (val state = releaseState) {
is ReleaseViewModel.ReleaseUIState.Failure -> {
val message = when (state.exception) {
is UnknownHostException, is ConnectException ->
stringResource(R.string.release_fetch_no_internet)
is ReleaseManager.UpdateException -> {
when (state.exception.reason) {
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
R.string.release_fetch_manual_check_required
)
else -> null
}
}
else -> null
}
UpdateMessageIndicator(
stringResource(
R.string.release_fetch_failed,
message ?: stringResource(R.string.release_fetch_try_later)
),
modifier = modifier,
allowRetry = true,
releaseViewModel = releaseViewModel
)
}
is ReleaseViewModel.ReleaseUIState.InDownload -> {
val downloaded = remember(state.downloaded) {
Formatter.formatShortFileSize(context, state.downloaded)
}
val outOf = remember(state.outOf) {
state.outOf?.run {
Formatter.formatShortFileSize(context, this)
}
}
UpdateProgressIndicator(
if (outOf != null) stringResource(
R.string.release_fetch_downloading,
downloaded,
outOf
) else stringResource(
R.string.release_fetch_downloading_indeterminate,
downloaded,
),
modifier = modifier,
releaseViewModel = releaseViewModel,
progress = {
val progress = state.downloaded / state.outOf!!.toDouble()
progress.toFloat()
}.takeIf { state.outOf != null }
)
}
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
UpdateProgressIndicator(
stringResource(R.string.release_fetch_in_progress),
modifier = modifier,
releaseViewModel = releaseViewModel
)
}
is ReleaseViewModel.ReleaseUIState.Finished -> {
if (state.hasUpdated) {
UpdateMessageIndicator(
stringResource(R.string.release_fetch_success),
modifier = modifier,
releaseViewModel = releaseViewModel
)
}
}
is ReleaseViewModel.ReleaseUIState.Cancelled -> {
if (state.isCancelling) {
UpdateProgressIndicator(
stringResource(R.string.release_fetch_cancelling),
modifier = modifier,
releaseViewModel = releaseViewModel
)
} else {
UpdateMessageIndicator(
stringResource(R.string.release_fetch_cancelled),
modifier = modifier,
allowRetry = true,
releaseViewModel = releaseViewModel
)
}
}
}
val nextUpdate by releaseViewModel.nextLauncherUpdate.collectAsState()
if (!releaseViewModel.isInUpdate && nextUpdate != null) {
LauncherUpdateIndicator(
modifier = modifier
)
}
}
@@ -1,249 +0,0 @@
package com.geode.launcher.preferences
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.geode.launcher.R
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.utils.PreferenceUtils
import androidx.compose.ui.semantics.Role
import com.geode.launcher.utils.ApplicationIcon
import com.geode.launcher.utils.ApplicationIconDetails
import com.geode.launcher.utils.IconUtils
import com.geode.launcher.utils.adaptiveIconPainterResource
class ApplicationIconActivity : 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)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ApplicationIconScreen(onBackPressedDispatcher)
}
}
}
}
}
}
@Composable
fun IconSelectConfirmDialog(selectedIcon: ApplicationIconDetails, onDismissRequest: () -> Unit) {
AlertDialog(
icon = {
Image(
adaptiveIconPainterResource(selectedIcon.iconId),
contentDescription = null
)
},
title = {
Text(stringResource(
R.string.application_icon_confirm,
stringResource(selectedIcon.nameId)
))
},
text = {
Text(stringResource(R.string.application_icon_confirm_description))
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
val context = LocalContext.current
TextButton(onClick = { onSelectIcon(context, selectedIcon) }) {
Text(stringResource(R.string.message_box_accept))
}
},
onDismissRequest = onDismissRequest
)
}
fun onSelectIcon(context: Context, selectedIcon: ApplicationIconDetails) {
val packageManager = context.packageManager
try {
ApplicationIcon.entries.forEach {
val iconData = IconUtils.getIconDetails(it)
packageManager.setComponentEnabledSetting(
ComponentName(context, "${context.packageName}.${iconData.component}"),
if (iconData.id == selectedIcon.id)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
}
} catch (_: IllegalArgumentException) {
Toast.makeText(context, R.string.application_icon_set_failed, Toast.LENGTH_SHORT).show()
}
PreferenceUtils.get(context).setString(PreferenceUtils.Key.SELECTED_ICON, selectedIcon.id)
}
@Composable
fun IconCard(iconData: ApplicationIconDetails, selected: Boolean, modifier: Modifier = Modifier) {
var showSelection by remember { mutableStateOf(false) }
if (showSelection) {
IconSelectConfirmDialog(iconData) {
showSelection = false
}
}
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 1.dp)
.clip(RoundedCornerShape(4.dp))
.then(modifier)
.selectable(
selected = selected,
onClick = {
if (!selected)
showSelection = true
},
role = Role.RadioButton
)
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 16.dp, vertical = 8.dp),
Arrangement.SpaceBetween,
Alignment.CenterVertically,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
adaptiveIconPainterResource(iconData.iconId),
contentDescription = null
)
Text(stringResource(iconData.nameId))
}
RadioButton(
selected = selected,
onClick = null
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ApplicationIconScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
title = {
Text(
stringResource(R.string.title_activity_application_icon),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
}
}
) { innerPadding ->
Column(
Modifier
.padding(innerPadding)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceEvenly,
) {
val selectedIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
Column(Modifier
.padding(horizontal = 12.dp)
.clip(RoundedCornerShape(16.dp))
) {
ApplicationIcon.entries.forEach {
val iconData = IconUtils.getIconDetails(it)
IconCard(iconData, selectedIcon == iconData.id)
}
}
Spacer(Modifier.height(4.dp))
}
}
}
@@ -1,6 +1,6 @@
package com.geode.launcher.preferences
import android.content.ClipData
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
@@ -18,6 +18,11 @@ 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.automirrored.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.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -45,13 +50,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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
@@ -65,7 +70,6 @@ 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.ui.theme.robotoMonoFamily
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
@@ -79,10 +83,9 @@ class ApplicationLogsActivity : ComponentActivity() {
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@@ -145,10 +148,7 @@ fun SelectLogLevelDialog(
@Composable
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
val haptics = LocalHapticFeedback.current
val clipboard = LocalClipboard.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
Column(
modifier = modifier
@@ -157,17 +157,7 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
detectTapGestures(
onLongPress = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
val clipboardLabel = resources.getString(R.string.application_log_copy_label)
val clipboardData = logLine.asSimpleString
coroutineScope.launch {
clipboard.setClipEntry(
ClipEntry(ClipData.newPlainText(
clipboardLabel, clipboardData
))
)
}
clipboardManager.setText(AnnotatedString(logLine.asSimpleString))
}
)
},
@@ -176,12 +166,12 @@ fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
style = Typography.labelSmall,
color = getLogColor(logLine.priority),
fontFamily = robotoMonoFamily
fontFamily = FontFamily.Monospace
)
Text(
"[${logLine.tag}]: ${logLine.messageTrimmed}",
fontFamily = robotoMonoFamily,
fontFamily = FontFamily.Monospace,
style = Typography.bodyMedium
)
}
@@ -197,7 +187,6 @@ fun ApplicationLogsScreen(
val isLoading by logViewModel.isLoading.collectAsState()
val context = LocalContext.current
val resources = LocalResources.current
val coroutineScope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
@@ -245,7 +234,7 @@ fun ApplicationLogsScreen(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -253,17 +242,26 @@ fun ApplicationLogsScreen(
actions = {
IconButton(onClick = {
coroutineScope.launch {
saveLauncher.launch(resources.getString(R.string.application_logs_default_filename))
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(
painterResource(R.drawable.icon_save),
contentDescription = stringResource(R.string.application_logs_export),
Icons.Filled.Share,
contentDescription = stringResource(R.string.application_logs_share)
)
}
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
painterResource(R.drawable.icon_more_vert),
Icons.Filled.MoreVert,
contentDescription = stringResource(R.string.application_logs_more)
)
}
@@ -282,7 +280,7 @@ fun ApplicationLogsScreen(
trailingIcon = {
if (logViewModel.filterCrashes) {
Icon(
painterResource(R.drawable.icon_check),
Icons.Filled.Check,
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
)
}
@@ -311,6 +309,23 @@ fun ApplicationLogsScreen(
}
)
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(
@@ -1,305 +0,0 @@
package com.geode.launcher.preferences
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.DocumentsContract
import android.widget.Toast
import java.text.DateFormat
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
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.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.log.CrashDump
import com.geode.launcher.log.CrashViewModel
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.ui.theme.robotoMonoFamily
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import java.io.File
import java.util.Date
import kotlin.time.ExperimentalTime
import kotlin.time.toJavaInstant
class CrashLogsActivity : 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)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
CrashLogsScreen(onBackPressedDispatcher)
}
}
}
}
}
}
fun shareCrash(context: Context, filename: String) {
val baseDirectory = LaunchUtils.getBaseDirectory(context)
val crashPath = File(LaunchUtils.getCrashDirectory(context), filename)
if (!crashPath.exists()) {
Toast.makeText(context, R.string.launcher_error_export_missing, Toast.LENGTH_SHORT).show()
return
}
val providerLocation = crashPath.toRelativeString(baseDirectory)
val documentsPath = "${UserDirectoryProvider.ROOT}${providerLocation}"
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", documentsPath)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "application/octet-stream"
}
try {
context.startActivity(shareIntent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, R.string.no_activity_found, Toast.LENGTH_SHORT).show()
}
}
@OptIn(ExperimentalTime::class)
@Composable
fun CrashCard(crashDump: CrashDump, crashViewModel: CrashViewModel, modifier: Modifier = Modifier) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
.fillMaxWidth()
.padding(vertical = 1.dp, horizontal = 8.dp)
.height(64.dp)
.then(modifier)
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.padding(vertical = 8.dp, horizontal = 8.dp)
) {
Column(
verticalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxHeight().offset(x = 12.dp).weight(0.7f)
) {
Text(
crashDump.filename,
fontFamily = robotoMonoFamily,
style = MaterialTheme.typography.titleSmall,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
val formattedLastModified = remember {
DateFormat.getDateTimeInstance()
.format(
Date.from(crashDump.lastModified.toJavaInstant())
)
}
Text(formattedLastModified, style = MaterialTheme.typography.labelMedium)
}
Row(modifier = Modifier.weight(0.3f), horizontalArrangement = Arrangement.End) {
val context = LocalContext.current
IconButton(onClick = {
shareCrash(context, crashDump.filename)
}) {
Icon(
painterResource(R.drawable.icon_share),
contentDescription = stringResource(R.string.application_logs_share)
)
}
IconButton(onClick = {
crashViewModel.removeCrash(crashDump.filename)
}) {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = stringResource(R.string.application_crashes_delete_one)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrashLogsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
crashViewModel: CrashViewModel = viewModel(factory = CrashViewModel.Factory)
) {
val logLines = crashViewModel.lineState
val isLoading by crashViewModel.isLoading.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
var showMoreOptions by remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
actions = {
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
Icon(
painterResource(R.drawable.icon_more_vert),
contentDescription = stringResource(R.string.application_logs_more)
)
}
DropdownMenu(
expanded = showMoreOptions,
onDismissRequest = { showMoreOptions = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(
painterResource(R.drawable.icon_delete),
contentDescription = null
)
},
text = {
Text(stringResource(R.string.application_logs_delete))
}, onClick = {
crashViewModel.clearCrashes()
showMoreOptions = false
}
)
}
},
title = {
Text(
stringResource(R.string.title_activity_crash_logs),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
if (isLoading) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
) { innerPadding ->
LazyColumn(
Modifier
.padding(innerPadding)
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceEvenly,
) {
if (!isLoading && logLines.isEmpty()) {
item {
Text(
stringResource(R.string.application_crashes_no_dumps),
textAlign = TextAlign.Center,
style = Typography.bodyLarge,
modifier = Modifier.fillMaxSize().padding(8.dp)
)
}
}
val itemsSize = logLines.size
itemsIndexed(
logLines,
key = { index, line -> line.filename }
) { index, line ->
val shape = RoundedCornerShape(
topStart = if (index == 0) 16.dp else 4.dp,
topEnd = if (index == 0) 16.dp else 4.dp,
bottomStart = if (index + 1 == itemsSize) 16.dp else 4.dp,
bottomEnd = if (index + 1 == itemsSize) 16.dp else 4.dp
)
CrashCard(
line,
crashViewModel,
modifier = Modifier.clip(shape)
)
}
item {
Spacer(Modifier.height(4.dp))
}
}
}
}
@Preview(showSystemUi = true)
@Composable
fun CrashLogsPreview() {
GeodeLauncherTheme {
CrashLogsScreen(null)
}
}
@@ -1,26 +1,24 @@
package com.geode.launcher.preferences
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.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -34,9 +32,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -44,7 +39,7 @@ import com.geode.launcher.R
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.utils.LaunchUtils
import com.geode.launcher.utils.LabelledText
import com.geode.launcher.utils.PreferenceUtils
class DeveloperSettingsActivity : ComponentActivity() {
@@ -57,10 +52,9 @@ class DeveloperSettingsActivity : ComponentActivity() {
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@@ -73,30 +67,6 @@ class DeveloperSettingsActivity : ComponentActivity() {
}
}
fun onOpenFolder(context: Context) {
val file = LaunchUtils.getBaseDirectory(context)
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.text_copied),
Toast.LENGTH_SHORT
).show()
}
}
fun onOpenLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
@@ -110,7 +80,7 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back_icon_alt)
)
}
@@ -123,7 +93,6 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
)
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
)
}
}
@@ -138,12 +107,16 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
val developerMode by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
OptionsGroup {
SettingsCard(
title = stringResource(R.string.preference_developer_mode),
preferenceKey = PreferenceUtils.Key.DEVELOPER_MODE,
)
}
InlineText(
stringResource(R.string.preference_developer_mode_about)
)
HorizontalDivider()
if (developerMode) {
OptionsGroup(title = stringResource(R.string.preference_category_gameplay)) {
@@ -157,14 +130,13 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
}}
)
/*
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
SettingsCard(
title = stringResource(R.string.preference_force_hrr),
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
)
}
/*
SettingsCard(
title = stringResource(R.string.preference_override_exceptions_name),
description = stringResource(R.string.preference_override_exceptions_description),
@@ -173,27 +145,12 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
*/
}
OptionsGroup(stringResource(R.string.preference_category_developer)) {
val context = LocalContext.current
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
icon = {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
},
onClick = { onOpenLogs(context) }
)
OptionsButton(
title = stringResource(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
)
}
OptionsGroup(title = stringResource(R.string.preference_category_updater)) {
SettingsCard(
title = stringResource(R.string.preference_use_index_update_api),
preferenceKey = PreferenceUtils.Key.USE_INDEX_API
)
SettingsSelectCard(
title = stringResource(R.string.preference_release_channel_tag_name),
dialogTitle = stringResource(R.string.preference_release_channel_select),
@@ -204,27 +161,23 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
2 to stringResource(R.string.preference_release_channel_nightly),
)
)
ElevatedCard(modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)) {
LabelledText(
label = stringResource(R.string.preference_developer_update_notice),
icon = {
Icon(Icons.Filled.Info, null)
},
modifier = Modifier.padding(8.dp)
)
}
}
OptionsGroup(title = stringResource(R.string.preference_category_testing)) {
SettingsSelectCard(
title = stringResource(R.string.preference_launch_delay),
dialogTitle = stringResource(R.string.preference_launch_delay_select),
preferenceKey = PreferenceUtils.Key.WAIT_PERIOD,
options = linkedMapOf(
0 to stringResource(R.string.preference_launch_delay_instant),
1 to pluralStringResource(R.plurals.preference_launch_delay_value, 1, 1),
3 to pluralStringResource(R.plurals.preference_launch_delay_value, 3, 3),
5 to pluralStringResource(R.plurals.preference_launch_delay_value, 5, 5),
)
)
/*
SettingsCard(
title = stringResource(R.string.preference_disable_redesign),
preferenceKey = PreferenceUtils.Key.DISABLE_REDESIGN,
title = stringResource(R.string.preference_enable_redesign),
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
)
*/
}
OptionsGroup(title = stringResource(R.string.preference_profiles)) {
@@ -232,8 +185,6 @@ fun DeveloperSettingsScreen(onBackPressedDispatcher: OnBackPressedDispatcher?) {
ProfileSelectCard()
}
}
Spacer(Modifier.height(4.dp))
}
}
}
@@ -2,12 +2,13 @@ package com.geode.launcher.preferences
import android.app.UiModeManager
import android.content.ActivityNotFoundException
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.provider.DocumentsContract
import android.text.format.DateUtils
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.ComponentActivity
@@ -22,46 +23,43 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
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.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.BuildConfig
import com.geode.launcher.MainActivity
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.updater.ReleaseViewModel
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.utils.ApplicationIcon
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.IconUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import java.net.ConnectException
import java.net.UnknownHostException
import kotlin.math.roundToInt
import kotlin.time.Clock
class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -73,10 +71,9 @@ class SettingsActivity : ComponentActivity() {
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
@@ -91,6 +88,25 @@ class SettingsActivity : ComponentActivity() {
}
}
fun onOpenFolder(context: Context) {
val file = LaunchUtils.getBaseDirectory(context)
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.text_copied),
Toast.LENGTH_SHORT
).show()
}
}
fun onOpenFileManager(context: Context) {
// future thoughts: Intent.ACTION_VIEW may not work on some devices
// the "correct" solution is to go through a list of possible intents/applications
@@ -111,7 +127,7 @@ fun onOpenFileManager(context: Context) {
try {
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
} catch (e: ActivityNotFoundException) {
Toast.makeText(
context,
context.getText(R.string.no_activity_found), Toast.LENGTH_SHORT
@@ -125,7 +141,6 @@ fun UpdateIndicator(
updateStatus: ReleaseViewModel.ReleaseUIState
) {
val context = LocalContext.current
val resources = LocalResources.current
var enablePopup by remember { mutableStateOf(false) }
@@ -161,8 +176,8 @@ fun UpdateIndicator(
is ReleaseViewModel.ReleaseUIState.Failure -> {
val message = when (updateStatus.exception) {
is UnknownHostException, is ConnectException ->
resources.getString(R.string.release_fetch_no_internet)
else -> resources.getString(R.string.preference_check_for_updates_failed)
context.getString(R.string.release_fetch_no_internet)
else -> context.getString(R.string.preference_check_for_updates_failed)
}
snackbarHostState.showSnackbar(message)
@@ -170,17 +185,17 @@ fun UpdateIndicator(
is ReleaseViewModel.ReleaseUIState.Finished -> {
if (updateStatus.hasUpdated) {
snackbarHostState.showSnackbar(
resources.getString(R.string.preference_check_for_updates_success)
context.getString(R.string.preference_check_for_updates_success)
)
} else {
val gameVersion = GamePackageUtils.getGameVersionCodeOrNull(context.packageManager)
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE) {
snackbarHostState.showSnackbar(
resources.getString(R.string.launcher_game_update_required_short)
context.getString(R.string.launcher_game_update_required_short)
)
} else {
snackbarHostState.showSnackbar(
resources.getString(R.string.preference_check_for_updates_none_found)
context.getString(R.string.preference_check_for_updates_none_found)
)
}
}
@@ -211,41 +226,82 @@ fun updateTheme(context: Context, theme: Int) {
}
}
fun onOpenLogs(context: Context) {
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
context.startActivity(launchIntent)
}
fun onOpenDeveloperOptions(context: Context) {
val launchIntent = Intent(context, DeveloperSettingsActivity::class.java)
context.startActivity(launchIntent)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_warning),
contentDescription = null,
)
},
title = { Text(stringResource(R.string.preference_developer_options_dialog_title)) },
text = { Text(stringResource(R.string.preference_developer_mode_about)) },
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = onEnable) {
Text(stringResource(R.string.message_box_accept))
}
},
onDismissRequest = onDismiss
)
}
@Composable
fun GeneralSettingsGroup() {
fun SettingsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
val context = LocalContext.current
OptionsGroup(stringResource(R.string.preference_category_testing)) {
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
val updateStatus by releaseViewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
var showUpdateInProgress by remember { mutableStateOf(false) }
if (showUpdateInProgress) {
LaunchedEffect(snackbarHostState) {
snackbarHostState.showSnackbar(
context.getString(R.string.preference_check_for_updates_already_updating)
)
showUpdateInProgress = false
}
}
// fix theme transition by giving it the exact same animation as the top bar
val containerColor by animateColorAsState(
targetValue = MaterialTheme.colorScheme.background,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "background color"
)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = containerColor,
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = context.getString(R.string.back_icon_alt)
)
}
},
title = {
Text(
context.getString(R.string.title_activity_settings),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
)
},
content = { innerPadding ->
Column(
Modifier
.padding(innerPadding)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OptionsGroup(context.getString(R.string.preference_category_testing)) {
SettingsSelectCard(
title = stringResource(R.string.preference_theme_name),
dialogTitle = stringResource(R.string.preference_theme_select),
@@ -261,54 +317,16 @@ fun GeneralSettingsGroup() {
title = stringResource(R.string.preference_black_background_name),
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SettingsCard(
title = stringResource(R.string.preference_disable_styles),
preferenceKey = PreferenceUtils.Key.DISABLE_USER_THEME
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val currentIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
val iconName = remember(currentIcon) {
IconUtils.getIconDetails(
ApplicationIcon.fromId(currentIcon ?: "default")
).nameId
}
OptionsButton(
title = stringResource(R.string.preference_set_application_icon),
onClick = {
val launchIntent = Intent(context, ApplicationIconActivity::class.java)
context.startActivity(launchIntent)
},
description = stringResource(iconName),
displayInline = true,
)
}
OptionsButton(
title = stringResource(R.string.preferences_open_file_manager),
icon = {
Icon(
painterResource(R.drawable.icon_folder),
contentDescription = null
)
},
onClick = { onOpenFileManager(context) }
)
}
}
@Composable
fun GameplaySettingsGroup() {
val context = LocalContext.current
OptionsGroup(stringResource(R.string.preference_category_gameplay)) {
SettingsCard(
title = stringResource(R.string.preference_load_automatically_name),
description = stringResource(R.string.preference_load_automatically_description),
title = context.getString(R.string.preference_load_automatically_name),
description = context.getString(R.string.preference_load_automatically_description),
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
)
SettingsSelectCard(
@@ -353,7 +371,7 @@ fun GameplaySettingsGroup() {
step = 5,
) {
SettingsCard(
title = stringResource(R.string.preference_screen_zoom_fit_name),
title = context.getString(R.string.preference_screen_zoom_fit_name),
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
asCard = false
)
@@ -375,51 +393,30 @@ fun GameplaySettingsGroup() {
maxFrameRate = maxFrameRate
)
}
}
@Composable
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
val updateStatus by releaseViewModel.uiState.collectAsState()
OptionsGroup(stringResource(R.string.preference_category_updater)) {
OptionsGroup(context.getString(R.string.preference_category_updater)) {
SettingsCard(
title = stringResource(R.string.preference_update_automatically_name),
title = context.getString(R.string.preference_update_automatically_name),
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
)
val lastUpdateCheck by PreferenceUtils.useLongPreference(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
val checkAgoString = remember(lastUpdateCheck) {
if (lastUpdateCheck != 0L) {
val now = Clock.System.now().toEpochMilliseconds()
DateUtils.getRelativeTimeSpanString(
lastUpdateCheck,
now,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
} else {
"never"
}
}
OptionsCard(
title = {
OptionsTitle(
title = stringResource(R.string.preference_check_for_updates_button),
icon = {
Icon(
painterResource(R.drawable.icon_update),
contentDescription = null
description = stringResource(
R.string.preference_check_for_updates_description,
currentRelease ?: "unknown"
)
},
description = stringResource(R.string.preference_check_for_updates_description, checkAgoString)
.takeIf { lastUpdateCheck != 0L }
)
},
modifier = Modifier
.clickable(
onClick = {
onUpdate()
if (releaseViewModel.isInUpdate) {
showUpdateInProgress = true
} else {
releaseViewModel.runReleaseCheck(true)
}
},
role = Role.Button
)
@@ -427,114 +424,53 @@ fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: S
UpdateIndicator(snackbarHostState, updateStatus)
}
}
}
@Composable
fun AdvancedSettingsGroup(developerModeEnabled: Boolean) {
val context = LocalContext.current
OptionsGroup(stringResource(R.string.preference_category_developer)) {
if (developerModeEnabled) {
OptionsButton(
title = stringResource(R.string.preferences_view_logs),
icon = {
Icon(painterResource(R.drawable.icon_description), contentDescription = null)
},
onClick = { onOpenLogs(context) }
)
OptionsButton(
title = stringResource(R.string.preference_open_developer_options),
icon = {
Icon(
painterResource(R.drawable.icon_data_object),
contentDescription = null
)
Icon(painterResource(R.drawable.icon_data_object), contentDescription = null)
},
onClick = { onOpenDeveloperOptions(context) }
)
}
OptionsButton(
title = stringResource(R.string.preferences_view_crashes),
icon = {
Icon(
painterResource(R.drawable.icon_bug_report),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, CrashLogsActivity::class.java)
context.startActivity(launchIntent)
}
)
var showSafeModeDialog by remember { mutableStateOf(false) }
OptionsButton(
title = stringResource(R.string.preference_launch_safe_mode),
icon = {
Icon(
painterResource(R.drawable.icon_shield),
contentDescription = null
title = context.getString(R.string.preferences_copy_external_button),
description = LaunchUtils.getBaseDirectory(context).path,
onClick = { onOpenFolder(context) }
)
}
) {
showSafeModeDialog = true
}
if (showSafeModeDialog) {
SafeModeDialog(onDismiss = {
showSafeModeDialog = false
}) {
val launchIntent = Intent(context, MainActivity::class.java).apply {
// hardcode it, why not
action = Intent.ACTION_VIEW
data = "geode-launcher://main/launch?safe-mode=true".toUri()
}
context.startActivity(launchIntent)
}
}
}
}
@Composable
fun AboutSettingsGroup(developerModeEnabled: Boolean) {
val context = LocalContext.current
val resources = LocalResources.current
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
OptionsGroup(stringResource(R.string.preference_category_about)) {
var launcherButtonTapped by remember { mutableIntStateOf(0) }
var showDeveloperDialog by remember { mutableStateOf(false) }
val clipboard = LocalClipboardManager.current
OptionsButton(
stringResource(R.string.preference_launcher_version_name),
stringResource(R.string.preference_launcher_version_description, BuildConfig.VERSION_NAME),
displayInline = true
) {
launcherButtonTapped += 1
if (launcherButtonTapped >= 7) {
if (developerModeEnabled) {
Toast.makeText(context, R.string.preference_enable_developer_again, Toast.LENGTH_SHORT)
.show()
} else {
showDeveloperDialog = true
}
launcherButtonTapped = 0
}
clipboard.setText(AnnotatedString(context.getString(
R.string.preference_launcher_version,
BuildConfig.VERSION_NAME
)))
}
if (showDeveloperDialog) {
DeveloperModeDialog(
onDismiss = {
showDeveloperDialog = false
},
onEnable = {
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.DEVELOPER_MODE, true)
showDeveloperDialog = false
}
)
}
OptionsLabel(
OptionsButton(
stringResource(R.string.preference_loader_version_name),
stringResource(R.string.preference_loader_version_description, currentRelease ?: "unknown"),
displayInline = true
)
) {
clipboard.setText(AnnotatedString(context.getString(
R.string.preference_loader_version,
currentRelease ?: "unknown"
)))
}
val gameInstalled = remember {
GamePackageUtils.isGameInstalled(context.packageManager)
@@ -546,108 +482,38 @@ fun AboutSettingsGroup(developerModeEnabled: Boolean) {
}
val gameSource = remember {
resources.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
context.getString(when (GamePackageUtils.identifyGameSource(context.packageManager)) {
GamePackageUtils.GameSource.GOOGLE -> R.string.preference_game_source_google
GamePackageUtils.GameSource.AMAZON -> R.string.preference_game_source_amazon
else -> R.string.preference_game_source_unknown
})
}
OptionsLabel(
OptionsButton(
stringResource(R.string.preference_game_version_name),
stringResource(R.string.preference_game_version_description, gameVersion, gameSource),
displayInline = true
)
) {
clipboard.setText(AnnotatedString(context.getString(
R.string.preference_game_version,
gameVersion,
gameSource
)))
}
OptionsLabel(
OptionsButton(
stringResource(R.string.preference_architecture_name),
stringResource(R.string.preference_architecture_description, Build.MODEL, LaunchUtils.applicationArchitecture),
displayInline = true
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
val resources = LocalResources.current
val snackbarHostState = remember { SnackbarHostState() }
var showUpdateInProgress by remember { mutableStateOf(false) }
if (showUpdateInProgress) {
LaunchedEffect(snackbarHostState) {
snackbarHostState.showSnackbar(
resources.getString(R.string.preference_check_for_updates_already_updating)
)
showUpdateInProgress = false
}
}
// fix theme transition by giving it the exact same animation as the top bar
val containerColor by animateColorAsState(
targetValue = MaterialTheme.colorScheme.background,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "background color"
)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = containerColor,
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
title = {
Text(
stringResource(R.string.title_activity_settings),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
)
},
content = { innerPadding ->
Column(
Modifier
.padding(innerPadding)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
GeneralSettingsGroup()
GameplaySettingsGroup()
UpdateSettingsGroup(releaseViewModel, snackbarHostState, onUpdate = {
if (releaseViewModel.isInUpdate) {
showUpdateInProgress = true
} else {
releaseViewModel.runReleaseCheck(true)
clipboard.setText(AnnotatedString(context.getString(
R.string.preference_architecture,
Build.MODEL,
LaunchUtils.applicationArchitecture
)))
}
}
}
})
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
AdvancedSettingsGroup(developerModeEnabled)
AboutSettingsGroup(developerModeEnabled)
Spacer(Modifier.height(4.dp))
}
}
)
@@ -3,12 +3,10 @@ package com.geode.launcher.preferences
import android.content.Context
import android.content.Intent
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
@@ -20,9 +18,12 @@ import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -45,8 +46,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -83,23 +82,15 @@ fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
}
@Composable
fun OptionsGroup(title: String? = null, content: @Composable () -> Unit) {
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
Column {
if (title != null) {
Text(
title,
style = Typography.titleMedium,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
Column(modifier = Modifier
.padding(horizontal = 12.dp)
.clip(RoundedCornerShape(16.dp))
) {
content()
}
}
}
@Composable
@@ -247,16 +238,12 @@ fun StringDialog(
) {
var enteredValue by remember { mutableStateOf(initialValue) }
BaseDialog(
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = title,
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
title = {
Text(title)
},
text = {
OutlinedTextField(
value = enteredValue,
onValueChange = {
@@ -271,15 +258,13 @@ fun StringDialog(
if (enteredValue.isNotEmpty()) {
IconButton(onClick = { enteredValue = "" }) {
Icon(
painterResource(R.drawable.icon_close_small),
Icons.Filled.Clear,
contentDescription = stringResource(R.string.preference_text_clear)
)
}
}
}
)
}
},
confirmButton = {
TextButton(onClick = { onSelect(enteredValue) }) {
@@ -310,14 +295,17 @@ fun RangeDialog(
mutableFloatStateOf(initialValue / scale.toFloat())
}
BaseDialog(
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = title,
content = {
title = {
Text(title)
},
text = {
Column {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
modifier = Modifier.weight(1.0f, false)
) {
val precision = log10(scale.toFloat()).roundToInt()
@@ -333,6 +321,8 @@ fun RangeDialog(
}
children()
}
},
confirmButton = {
TextButton(onClick = { onSelect((enteredValue * scale).roundToInt()) }) {
@@ -367,16 +357,16 @@ fun FrameRateDialog(
val maximumReached = currentValue != null && currentValue > maxFrameRate
val minimumReached = currentValue != null && currentValue < minFrameRate
BaseDialog(
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = title,
content = {
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
) {
title = {
Text(title)
},
text = {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1.0f, false)
) {
IconButton(onClick = {
val prevValue = max((currentValue ?: 0) - 5, minFrameRate)
@@ -416,7 +406,7 @@ fun FrameRateDialog(
enteredValue = nextValue.toString()
}, enabled = currentValue == null || currentValue < maxFrameRate) {
Icon(
painterResource(R.drawable.icon_add),
Icons.Filled.Add,
contentDescription = stringResource(R.string.preference_limit_framerate_add)
)
}
@@ -629,14 +619,16 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
val isDuplicate = currentProfiles.contains(filename)
BaseDialog(
AlertDialog(
onDismissRequest = { onDismissRequest() },
title = stringResource(R.string.preference_profiles_create),
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
title = {
Text(stringResource(R.string.preference_profiles_create))
},
text = {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1.0f, false)
) {
OutlinedTextField(
value = enteredValue,
@@ -650,9 +642,9 @@ fun ProfileCreateDialog(onDismissRequest: () -> Unit) {
label = {
Text(stringResource(R.string.preference_profiles_create_name))
},
isError = isDuplicate,
modifier = Modifier.align(Alignment.CenterHorizontally)
isError = isDuplicate
)
}
if (isDuplicate) {
Spacer(Modifier.size(8.dp))
@@ -757,8 +749,8 @@ fun ProfileSelectCard() {
.setCurrentProfile(selectedProfileId)
activity?.run {
packageManager.getLaunchIntentForPackage(packageName)?.also { intent ->
val mainIntent = Intent.makeRestartActivityTask(intent.component)
packageManager.getLaunchIntentForPackage(packageName)?.also {
val mainIntent = Intent.makeRestartActivityTask(it.component)
startActivity(mainIntent)
exitProcess(0)
}
@@ -791,7 +783,7 @@ fun ProfileSelectCard() {
)
} else {
Icon(
painterResource(R.drawable.icon_delete),
Icons.Filled.Delete,
contentDescription = stringResource(R.string.preference_profiles_delete_alt)
)
}
@@ -803,54 +795,6 @@ fun ProfileSelectCard() {
}
}
@Composable
fun BaseDialog(
title: String,
onDismissRequest: () -> Unit,
dismissButton: @Composable () -> Unit,
confirmButton: @Composable () -> Unit,
content: @Composable (ColumnScope.() -> Unit),
) {
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
content()
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
dismissButton()
confirmButton()
}
}
}
}
}
@Composable
fun <T> SelectDialog(
title: String,
@@ -861,20 +805,23 @@ fun <T> SelectDialog(
) {
val (selectedValue, setSelectedValue) = remember { mutableStateOf(initialValue) }
BaseDialog(
title = title,
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
CompositionLocalProvider(
LocalSelectValue provides (selectedValue as Any),
LocalSelectSetValue provides ({
@@ -884,6 +831,27 @@ fun <T> SelectDialog(
) {
options()
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
}
}
}
}
@@ -907,24 +875,6 @@ fun OptionsButton(title: String, description: String? = null, icon: (@Composable
}
}
@Composable
fun OptionsLabel(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, displayInline: Boolean = false) {
OptionsCard(
title = {
OptionsTitle(
title = title,
description = description.takeIf { !displayInline },
icon = icon
)
},
wrapContent = true
) {
if (displayInline && description != null) {
Text(description, textAlign = TextAlign.End)
}
}
}
@Composable
fun SettingsCard(title: String, description: String? = null, icon: (@Composable () -> Unit)? = null, asCard: Boolean = true, preferenceKey: PreferenceUtils.Key) {
val context = LocalContext.current
@@ -940,25 +890,24 @@ fun SettingsCard(title: String, description: String? = null, icon: (@Composable
icon = icon
)
},
modifier = Modifier.toggleable(
modifier = if (asCard) Modifier.toggleable(
value = settingEnabled.value,
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
role = Role.Switch,
),
backgroundColor = if (asCard)
MaterialTheme.colorScheme.surfaceContainer
else MaterialTheme.colorScheme.surfaceContainerHigh
) else Modifier
) {
Switch(
checked = settingEnabled.value,
onCheckedChange = null
onCheckedChange = if (!asCard)
{ _ -> settingEnabled.value = toggleSetting(context, preferenceKey) }
else null
)
}
}
@Composable
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null, icon: (@Composable () -> Unit)? = null) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (icon != null) {
icon()
}
@@ -971,7 +920,7 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
Text(
description,
style = Typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
@@ -979,20 +928,10 @@ fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: Stri
}
@Composable
fun OptionsCard(
modifier: Modifier = Modifier,
wrapContent: Boolean = false,
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer,
title: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
fun OptionsCard(modifier: Modifier = Modifier, wrapContent: Boolean = false, title: @Composable () -> Unit, content: @Composable () -> Unit) {
Row(
Modifier
modifier
.fillMaxWidth()
.padding(vertical = 1.dp)
.clip(RoundedCornerShape(4.dp))
.then(modifier)
.background(backgroundColor)
.defaultMinSize(minHeight = 64.dp)
.padding(horizontal = 16.dp),
Arrangement.SpaceBetween,
@@ -1011,31 +950,6 @@ fun OptionsCard(
}
}
@Composable
fun SafeModeDialog(onDismiss: () -> Unit, onLaunch: () -> Unit) {
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_shield_alert),
contentDescription = null
)
},
title = { Text(stringResource(R.string.safe_mode_enable_title)) },
text = { Text(stringResource(R.string.safe_mode_enable_description)) },
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = onLaunch) {
Text(stringResource(R.string.message_box_continue))
}
},
onDismissRequest = onDismiss
)
}
@Composable
fun InlineText(label: String, icon: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier) {
LabelledText(label = label, icon = icon, modifier = modifier.padding(horizontal = 16.dp, vertical = 4.dp))
@@ -1,24 +0,0 @@
package com.geode.launcher.ui.theme
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.geode.launcher.R
val robotoMonoFamily = FontFamily(
Font(R.font.robotomono_medium, FontWeight.Medium),
Font(R.font.robotomono_regular, FontWeight.Normal)
)
val quicksandFamily = FontFamily(
Font(R.font.quicksand_semibold, FontWeight.SemiBold)
)
val launcherTitleStyle = TextStyle(
fontFamily = quicksandFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 32.sp,
letterSpacing = 0.sp
)
@@ -1,5 +1,6 @@
package com.geode.launcher.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -8,9 +9,14 @@ 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.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
@@ -22,6 +28,16 @@ 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),
*/
)
enum class Theme {
@@ -1,12 +1,11 @@
package com.geode.launcher.updater
import com.geode.launcher.utils.LaunchUtils
import kotlin.time.Instant
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlin.time.ExperimentalTime
@Serializable
data class Asset @OptIn(ExperimentalTime::class) constructor(
data class Asset(
val url: String,
val id: Int,
val name: String,
@@ -17,7 +16,7 @@ data class Asset @OptIn(ExperimentalTime::class) constructor(
)
@Serializable
data class Release @OptIn(ExperimentalTime::class) constructor(
data class Release(
val url: String,
val id: Int,
val targetCommitish: String,
@@ -31,7 +30,7 @@ data class Release @OptIn(ExperimentalTime::class) constructor(
)
@Serializable
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
data class LoaderVersion(
val tag: String,
val version: String,
val createdAt: Instant,
@@ -67,7 +66,6 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
return release.tagName
}
@OptIn(ExperimentalTime::class)
override fun getDescriptor(): Long {
return release.createdAt.epochSeconds
}
@@ -97,7 +95,6 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
return release.tagName
}
@OptIn(ExperimentalTime::class)
override fun getDescriptor(): Long {
return release.createdAt.epochSeconds
}
@@ -135,7 +132,6 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
return version.tag
}
@OptIn(ExperimentalTime::class)
override fun getDescriptor(): Long {
return version.createdAt.epochSeconds
}
@@ -22,8 +22,6 @@ import okio.buffer
import okio.source
import java.io.File
import java.io.InterruptedIOException
import kotlin.time.Clock
import kotlin.time.Duration.Companion.minutes
private const val TAG_LATEST = "latest"
private const val TAG_BETA = "prerelease"
@@ -124,6 +122,7 @@ class ReleaseManager private constructor(
val gameVersion = GamePackageUtils.getGameVersionCode(applicationContext.packageManager)
val preferences = PreferenceUtils.get(applicationContext)
if (preferences.getBoolean(PreferenceUtils.Key.USE_INDEX_API)) {
val targetTag = preferences.getInt(PreferenceUtils.Key.RELEASE_CHANNEL_TAG)
return when (targetTag) {
@@ -131,12 +130,21 @@ class ReleaseManager private constructor(
1 -> releaseRepository.getLatestIndexRelease(gameVersion, true)
else -> releaseRepository.getLatestIndexRelease(gameVersion, false)
}
} else {
val targetTag = getBestReleaseForGameVersion(gameVersion) ?: return null
return when (targetTag) {
TAG_LATEST -> releaseRepository.getLatestGeodeRelease()
TAG_BETA -> releaseRepository.getLatestGeodePreRelease()
else -> releaseRepository.getReleaseByTag(targetTag)
}
}
}
private suspend fun downloadLauncherUpdate(release: Downloadable) {
val download = release.getDownload() ?: return
val outputDirectory = applicationContext.filesDir
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
val outputFile = File(outputDirectory, download.filename)
if (outputFile.exists()) {
@@ -247,16 +255,6 @@ class ReleaseManager private constructor(
}
private suspend fun checkForNewRelease(isManual: Boolean = false) {
val sharedPreferences = PreferenceUtils.get(applicationContext)
val lastCheckTime = sharedPreferences.getLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
// only check for updates if it's been over 15 minutes since last check
val checkMinTime = Clock.System.now().minus(15.minutes).toEpochMilliseconds()
if (!isManual && lastCheckTime > checkMinTime && !fileWasExternallyModified()) {
_uiState.value = ReleaseManagerState.Finished()
return
}
val release = try {
getLatestRelease()
} catch (e: Exception) {
@@ -275,9 +273,6 @@ class ReleaseManager private constructor(
checkLauncherUpdate(launcherRelease)
}
val currentTime = Clock.System.now().toEpochMilliseconds()
sharedPreferences.setLong(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME, currentTime)
if (release == null) {
downloadLauncherUpdateIfNecessary()
@@ -285,6 +280,8 @@ class ReleaseManager private constructor(
return
}
val sharedPreferences = PreferenceUtils.get(applicationContext)
val currentVersion = sharedPreferences.getLong(PreferenceUtils.Key.CURRENT_VERSION_TIMESTAMP)
val latestVersion = release.getDescriptor()
@@ -1,9 +1,9 @@
package com.geode.launcher.updater
import com.geode.launcher.utils.DownloadUtils.executeCoroutine
import kotlin.time.Clock
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlin.time.Instant
import kotlinx.datetime.Instant
import kotlinx.datetime.until
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
@@ -14,7 +14,6 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.net.URL
import kotlin.time.ExperimentalTime
class ReleaseRepository(private val httpClient: OkHttpClient) {
companion object {
@@ -50,7 +49,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
return getReleaseByUrl(url)?.let(::DownloadableGitHubLoaderRelease)
}
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
@OptIn(ExperimentalSerializationApi::class)
suspend fun getLatestGeodePreRelease(): DownloadableGitHubLoaderRelease? {
val releasesUrl = "$GITHUB_API_BASE/repos/geode-sdk/geode/releases?per_page=2"
val url = URL(releasesUrl)
@@ -105,7 +104,6 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
}
}
@OptIn(ExperimentalTime::class)
private fun generateRateLimitMessage(resetTime: Instant): String {
val currentTime = Clock.System.now()
val resetDelay = currentTime.until(resetTime, DateTimeUnit.SECOND)
@@ -115,7 +113,7 @@ class ReleaseRepository(private val httpClient: OkHttpClient) {
return "api ratelimit reached, try again in ${formattedWait}"
}
@OptIn(ExperimentalSerializationApi::class, ExperimentalTime::class)
@OptIn(ExperimentalSerializationApi::class)
private suspend fun getReleaseByUrl(url: URL): Release? {
val request = Request.Builder()
.url(url)
@@ -51,8 +51,7 @@ object GamePackageUtils {
37L to "2.200",
38L to "2.205",
39L to "2.206",
40L to "2.2074", // ok
41L to "2.2081",
40L to "2.2074" // ok
)
/**
@@ -7,9 +7,6 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.hardware.lights.Light
import android.hardware.lights.LightState
import android.hardware.lights.LightsRequest
import android.net.Uri
import android.os.Build
import android.os.Environment
@@ -19,7 +16,6 @@ import android.os.VibratorManager
import android.provider.DocumentsContract
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
import android.util.Log
import android.view.InputDevice
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
@@ -35,6 +31,7 @@ import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.activityresult.GeodeOpenFileActivityResult
import com.geode.launcher.activityresult.GeodeOpenFilesActivityResult
import com.geode.launcher.activityresult.GeodeSaveFileActivityResult
import org.cocos2dx.lib.Cocos2dxGLSurfaceView
import java.io.File
import java.lang.ref.WeakReference
import kotlin.system.exitProcess
@@ -480,10 +477,6 @@ object GeodeUtils {
*/
@JvmStatic
fun vibrate(ms: Long) {
if (ms == 0L) {
getVibrator()?.cancel()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getVibrator()?.vibrate(VibrationEffect.createOneShot(ms, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
@@ -550,7 +543,7 @@ object GeodeUtils {
const val CAPABILITY_EXTENDED_INPUT = "extended_input"
const val CAPABILITY_TIMESTAMP_INPUT = "timestamp_inputs"
const val CAPABILITY_RICH_INPUT = "internal_callbacks_v2"
const val CAPABILITY_INTERNAL_CALLBACKS = "internal_callbacks_v1"
private var capabilityListener: WeakReference<CapabilityListener?> = WeakReference(null)
@@ -580,186 +573,9 @@ object GeodeUtils {
.launchUrl(activity, url.toUri())
}
/**
* Determines if any gamepad or joystick input devices are connected.
* This is useful during start, but tracking inputDeviceAdded events is better during runtime.
*/
@JvmStatic
fun controllersConnected(): Int {
val deviceIds = InputDevice.getDeviceIds()
return deviceIds.count {
InputDevice.getDevice(it)?.run {
sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
} ?: false
}
}
@JvmStatic
fun getConnectedDevices(): IntArray = InputDevice.getDeviceIds()
@JvmStatic
fun getDevice(deviceId: Int): InputDevice? = InputDevice.getDevice(deviceId)
@JvmStatic
fun getDeviceBatteryCapacity(deviceId: Int): Float {
val device = InputDevice.getDevice(deviceId) ?: return 0.0f
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
device.batteryState.capacity
} else {
0.0f
}
}
@JvmStatic
fun deviceHasBattery(deviceId: Int): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return false
}
val device = InputDevice.getDevice(deviceId) ?: return false
return device.batteryState.isPresent
}
@JvmStatic
fun getDeviceBatteryStatus(deviceId: Int): Int {
val device = InputDevice.getDevice(deviceId) ?: return 0
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
device.batteryState.status
} else {
0
}
}
@JvmStatic
fun getDeviceLightsCount(deviceId: Int): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return 0
}
val device = InputDevice.getDevice(deviceId) ?: return 0
val lightsManager = device.lightsManager
return lightsManager.lights.size
}
// this is based on paddleboat's api
@JvmStatic
fun getLightType(deviceId: Int): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return 0
}
val device = InputDevice.getDevice(deviceId) ?: return 0
val lightsManager = device.lightsManager
var flags = 0
lightsManager.lights.forEach { light ->
if (light.type == Light.LIGHT_TYPE_PLAYER_ID) {
flags = flags or 1
} else if (light.hasRgbControl()) {
flags = flags or 2
}
}
return flags
}
/**
* Sets the color for a device.
* @return false if the given device does not exist or setting lights was otherwise unsuccessful
*/
@JvmStatic
fun setDeviceLightColor(deviceId: Int, color: Int, type: Int): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return false
}
val device = InputDevice.getDevice(deviceId) ?: return false
val lightsManager = device.lightsManager
val lights = lightsManager.lights
if (lights.isEmpty()) {
return false
}
val actedLights = lights.count { light ->
val state = LightState.Builder()
if (light.type == Light.LIGHT_TYPE_PLAYER_ID && (type and 1 == 1)) {
state.setPlayerId(color)
} else if (light.hasRgbControl() && (type and 2 == 2)) {
state.setColor(color)
} else {
return@count false
}
val request = LightsRequest.Builder()
.addLight(light, state.build())
.build()
lightsManager.openSession()
.requestLights(request)
true
}
return actedLights > 0
}
@JvmStatic
fun getDeviceHapticsCount(deviceId: Int): Int {
val device = InputDevice.getDevice(deviceId) ?: return 0
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return if (device.vibrator.hasVibrator()) 1 else 0
}
val vibratorManager = device.vibratorManager
val vibratorIds = vibratorManager.vibratorIds
return vibratorIds.size
}
private fun performVibration(vibrator: Vibrator?, durationMs: Long, intensity: Int) {
vibrator?.apply {
if (intensity == 0) {
cancel()
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createOneShot(durationMs, intensity)
vibrate(vibrationEffect)
} else {
vibrate(durationMs)
}
}
}
}
/**
* Vibrates a given device. Set motorIdx == -1 to vibrate all available motors on a device.
*/
@JvmStatic
fun vibrateDevice(deviceId: Int, durationMs: Long, intensity: Int, motorIdx: Int): Boolean {
val device = InputDevice.getDevice(deviceId) ?: return false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = device.vibratorManager
val vibratorIds = vibratorManager.vibratorIds
if (motorIdx == -1) {
vibratorIds.forEach {
val vibrator = vibratorManager.getVibrator(it)
performVibration(vibrator, durationMs, intensity)
}
} else {
val deviceId = vibratorIds.getOrNull(motorIdx) ?: return false
val vibrator = vibratorManager.getVibrator(deviceId)
performVibration(vibrator, durationMs, intensity)
}
} else {
val vibrator = device.vibrator
performVibration(vibrator, durationMs, intensity)
}
return true
fun openMultilineIMEKeyboard() {
Cocos2dxGLSurfaceView.openMultilineIMEKeyboard()
}
external fun nativeKeyUp(keyCode: Int, modifiers: Int)
@@ -773,18 +589,5 @@ object GeodeUtils {
* @see reportPlatformCapability
*/
external fun setNextInputTimestamp(timestamp: Long)
const val TOUCH_TYPE_BEGIN = 0
const val TOUCH_TYPE_MOVE = 1
const val TOUCH_TYPE_END = 2
const val TOUCH_TYPE_CANCEL = 3
external fun internalKeyEvent(timestamp: Long, deviceId: Int, eventSource: Int, keyCode: Int, modifiers: Int, isDown: Boolean, repeatCount: Int)
external fun internalTouchEvent(timestamp: Long, deviceId: Int, eventSource: Int, eventType: Int, ids: IntArray, xs: FloatArray, ys: FloatArray)
external fun internalScrollEvent(timestamp: Long, deviceId: Int, eventSource: Int, scrollX: Float, scrollY: Float)
external fun internalJoystickEvent(timestamp: Long, deviceId: Int, eventSource: Int, leftX: FloatArray, leftY: FloatArray, rightX: FloatArray, rightY: FloatArray, hatX: FloatArray, hatY: FloatArray, leftTrigger: FloatArray, rightTrigger: FloatArray)
external fun inputDeviceAdded(deviceId: Int)
external fun inputDeviceChanged(deviceId: Int)
external fun inputDeviceRemoved(deviceId: Int)
external fun setNextInputTimestampInternal(timestamp: Long)
}
@@ -1,100 +0,0 @@
package com.geode.launcher.utils
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Build
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.toBitmap
import com.geode.launcher.R
// https://gist.github.com/tkuenneth/ddf598663f041dc79960cda503d14448?permalink_comment_id=4660486#gistcomment-4660486
@Composable
fun adaptiveIconPainterResource(@DrawableRes id: Int): Painter {
val context = LocalContext.current
val res = LocalResources.current
val theme = context.theme
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val adaptiveIcon = remember(id) {
ResourcesCompat.getDrawable(res, id, theme) as? AdaptiveIconDrawable
}
if (adaptiveIcon != null) {
remember(id) {
BitmapPainter(adaptiveIcon.toBitmap().asImageBitmap())
}
} else {
painterResource(id)
}
} else {
painterResource(id)
}
}
enum class ApplicationIcon {
DEFAULT,
GEODE,
PRIDE,
TRANS;
companion object {
fun fromId(id: String) = when (id) {
"default" -> ApplicationIcon.DEFAULT
"geode" -> ApplicationIcon.GEODE
"pride" -> ApplicationIcon.PRIDE
"trans" -> ApplicationIcon.TRANS
else -> ApplicationIcon.DEFAULT
}
}
fun toId() = when (this) {
ApplicationIcon.PRIDE -> "pride"
ApplicationIcon.GEODE -> "geode"
ApplicationIcon.DEFAULT -> "default"
ApplicationIcon.TRANS -> "trans"
}
}
data class ApplicationIconDetails(
val id: String,
val component: String,
@param:DrawableRes val iconId: Int,
@param:StringRes val nameId: Int,
)
object IconUtils {
fun getIconDetails(id: ApplicationIcon) = when (id) {
ApplicationIcon.DEFAULT -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivity",
iconId = R.mipmap.ic_launcher,
nameId = R.string.application_icon_default
)
ApplicationIcon.GEODE -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivityGeode",
iconId = R.mipmap.ic_launcher_geode,
nameId = R.string.application_icon_geode
)
ApplicationIcon.PRIDE -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivityPride",
iconId = R.mipmap.ic_launcher_pride,
nameId = R.string.application_icon_pride
)
ApplicationIcon.TRANS -> ApplicationIconDetails(
id = id.toId(),
component = "MainActivityTrans",
iconId = R.mipmap.ic_launcher_trans,
nameId = R.string.application_icon_trans
)
}
}
@@ -73,9 +73,9 @@ object LaunchUtils {
return File(getBaseDirectory(context), "save")
}
const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
private const val CRASH_INDICATOR_NAME = "lastSessionDidCrash"
fun getCrashDirectory(context: Context): File {
private fun getCrashDirectory(context: Context): File {
val base = getBaseDirectory(context)
return File(base, "game/geode/crashlogs/")
}
@@ -139,6 +139,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
LIMIT_ASPECT_RATIO,
DISPLAY_MODE,
FORCE_HRR,
ENABLE_REDESIGN,
RELEASE_CHANNEL_TAG,
DEVELOPER_MODE,
CLEANUP_APKS,
@@ -147,17 +148,14 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
SCREEN_ZOOM,
SCREEN_ZOOM_FIT,
LIMIT_FRAME_RATE,
DISABLE_USER_THEME,
SELECTED_ICON,
DISABLE_REDESIGN,
WAIT_PERIOD,
LAST_UPDATE_CHECK_TIME
USE_INDEX_API,
}
private fun defaultValueForBooleanKey(key: Key): Boolean {
return when (key) {
Key.UPDATE_AUTOMATICALLY, Key.FORCE_HRR -> true
Key.DEVELOPER_MODE -> BuildConfig.DEBUG
Key.USE_INDEX_API -> true
else -> false
}
}
@@ -165,14 +163,12 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
private fun defaultValueForIntKey(key: Key) = when (key) {
Key.DISPLAY_MODE -> if (this.getBoolean(Key.LIMIT_ASPECT_RATIO)) 1 else 0
Key.SCREEN_ZOOM -> 100
Key.WAIT_PERIOD -> 3
// people wanted a reset on nightly anyways, so this is a good excuse to do so
else -> 0
}
private fun defaultValueForStringKey(key: Key) = when (key) {
Key.CUSTOM_ASPECT_RATIO -> "16_9"
Key.SELECTED_ICON -> "default"
else -> null
}
@@ -192,7 +188,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.LIMIT_ASPECT_RATIO -> "PreferenceLimitAspectRatio"
Key.DISPLAY_MODE -> "PreferenceDisplayMode"
Key.FORCE_HRR -> "PreferenceForceHighRefreshRate"
Key.DISABLE_REDESIGN -> "PreferenceDisableRedesign"
Key.ENABLE_REDESIGN -> "PreferenceEnableRedesign"
Key.RELEASE_CHANNEL_TAG -> "PreferenceReleaseChannelTag"
Key.DEVELOPER_MODE -> "PreferenceDeveloperMode"
Key.CLEANUP_APKS -> "PreferenceCleanupPackages"
@@ -201,10 +197,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.SCREEN_ZOOM -> "PreferenceScreenZoom"
Key.SCREEN_ZOOM_FIT -> "PreferenceScreenZoomFit"
Key.LIMIT_FRAME_RATE -> "PreferenceLimitFrameRate"
Key.DISABLE_USER_THEME -> "PreferenceDisableUserTheme"
Key.SELECTED_ICON -> "PreferenceSelectedIcon"
Key.WAIT_PERIOD -> "PreferenceWaitPeriod"
Key.LAST_UPDATE_CHECK_TIME -> "PreferenceLastUpdateCheckTime"
Key.USE_INDEX_API -> "PreferenceUseIndexApi"
}
}
@@ -2,14 +2,13 @@ package org.cocos2dx.lib
import android.app.Activity
import android.content.Context
import android.hardware.input.InputManager
import android.opengl.GLSurfaceView
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.text.InputType
import android.util.Log
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.Surface
@@ -18,12 +17,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.widget.AppCompatEditText
import com.customRobTop.BaseRobTopActivity
import com.geode.launcher.utils.GeodeUtils
import kotlin.math.abs
private const val HANDLER_OPEN_IME_KEYBOARD = 2
private const val HANDLER_CLOSE_IME_KEYBOARD = 3
private const val HANDLER_ARG_MULTILINE = 1
private const val HANDLER_ARG_SINGLELINE = 0
private const val MS_TO_NS = 1_000_000
private const val SEC_TO_NS = 1_000_000_000.0
class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
companion object {
@@ -31,11 +30,22 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
private lateinit var handler: Handler
private lateinit var cocos2dxTextInputWrapper: Cocos2dxTextInputWrapper
fun openMultilineIMEKeyboard() {
val msg = Message().apply {
what = HANDLER_OPEN_IME_KEYBOARD
obj = cocos2dxGLSurfaceView.getContentText()
arg1 = HANDLER_ARG_MULTILINE
}
handler.sendMessage(msg)
}
@JvmStatic
fun openIMEKeyboard() {
val msg = Message()
msg.what = HANDLER_OPEN_IME_KEYBOARD
msg.obj = cocos2dxGLSurfaceView.getContentText()
val msg = Message().apply {
what = HANDLER_OPEN_IME_KEYBOARD
obj = cocos2dxGLSurfaceView.getContentText()
arg1 = HANDLER_ARG_SINGLELINE
}
handler.sendMessage(msg)
}
@@ -60,9 +70,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
}
var sendTimestampEvents = false
var sendInternalTimestampEvents = false
var manualBackEvents = false
var controllerEventsEnabled = false
private set
var cocos2dxEditText: AppCompatEditText? = null
set(value) {
@@ -72,6 +81,18 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
requestFocus()
}
fun setKeyboardMultiline() {
val editText = cocos2dxEditText ?: return
editText.inputType = editText.inputType or InputType.TYPE_TEXT_FLAG_MULTI_LINE
editText.maxLines = Int.MAX_VALUE
}
fun setKeyboardSingleLine() {
val editText = cocos2dxEditText ?: return
editText.inputType = editText.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE.inv()
editText.maxLines = 1
}
fun initView() {
setEGLContextClientVersion(2)
isFocusableInTouchMode = true
@@ -83,6 +104,12 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
when (msg.what) {
HANDLER_OPEN_IME_KEYBOARD -> {
if (cocos2dxEditText?.requestFocus() == true) {
if (msg.arg1 == HANDLER_ARG_MULTILINE) {
setKeyboardMultiline()
} else {
setKeyboardSingleLine()
}
cocos2dxEditText?.apply {
removeTextChangedListener(
cocos2dxTextInputWrapper
@@ -159,6 +186,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
private fun sendNextEventTimestamp(timestamp: Long) {
if (sendTimestampEvents)
GeodeUtils.setNextInputTimestamp(timestamp)
if (sendInternalTimestampEvents)
GeodeUtils.setNextInputTimestampInternal(timestamp)
}
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
@@ -168,7 +198,6 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val ys = FloatArray(pointerNumber)
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
motionEvent.eventTimeNanos else motionEvent.eventTime * MS_TO_NS
val timestampSec = timestamp / SEC_TO_NS
for (i in 0 until pointerNumber) {
ids[i] = motionEvent.getPointerId(i)
@@ -182,15 +211,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val f = ys[0]
queueEvent {
sendNextEventTimestamp(timestamp)
if (controllerEventsEnabled)
GeodeUtils.internalTouchEvent(
timestamp, motionEvent.deviceId, motionEvent.source,
// slice here to convert to single element array
GeodeUtils.TOUCH_TYPE_BEGIN, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
)
else
cocos2dxRenderer.handleActionDown(idDown, xDown, f, timestampSec)
cocos2dxRenderer.handleActionDown(idDown, xDown, f)
}
true
}
@@ -200,42 +221,21 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val f3 = ys[0]
queueEvent {
sendNextEventTimestamp(timestamp)
if (controllerEventsEnabled)
GeodeUtils.internalTouchEvent(
timestamp, motionEvent.deviceId, motionEvent.source,
GeodeUtils.TOUCH_TYPE_END, ids.sliceArray(0..0), xs.sliceArray(0..0), ys.sliceArray(0..0)
)
else
cocos2dxRenderer.handleActionUp(idUp, f2, f3, timestampSec)
cocos2dxRenderer.handleActionUp(idUp, f2, f3)
}
true
}
MotionEvent.ACTION_MOVE -> {
queueEvent {
sendNextEventTimestamp(timestamp)
if (controllerEventsEnabled)
GeodeUtils.internalTouchEvent(
timestamp, motionEvent.deviceId, motionEvent.source,
GeodeUtils.TOUCH_TYPE_MOVE, ids, xs, ys
)
else
cocos2dxRenderer.handleActionMove(ids, xs, ys, timestampSec)
cocos2dxRenderer.handleActionMove(ids, xs, ys)
}
true
}
MotionEvent.ACTION_CANCEL -> {
queueEvent {
sendNextEventTimestamp(timestamp)
if (controllerEventsEnabled)
GeodeUtils.internalTouchEvent(
timestamp, motionEvent.deviceId, motionEvent.source,
GeodeUtils.TOUCH_TYPE_CANCEL, ids, xs, ys
)
else
cocos2dxRenderer.handleActionCancel(ids, xs, ys, timestampSec)
cocos2dxRenderer.handleActionCancel(ids, xs, ys)
}
true
}
@@ -246,14 +246,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val y = motionEvent.getY(indexPointerDown)
queueEvent {
sendNextEventTimestamp(timestamp)
if (controllerEventsEnabled)
GeodeUtils.internalTouchEvent(
timestamp, motionEvent.deviceId, motionEvent.source,
GeodeUtils.TOUCH_TYPE_BEGIN, intArrayOf(idPointerDown), floatArrayOf(xPointerDown), floatArrayOf(y)
)
else
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y, timestampSec)
cocos2dxRenderer.handleActionDown(idPointerDown, xPointerDown, y)
}
true
}
@@ -264,14 +257,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
val y2 = motionEvent.getY(indexPointUp)
queueEvent {
sendNextEventTimestamp(timestamp)
if (controllerEventsEnabled)
GeodeUtils.internalTouchEvent(
timestamp, motionEvent.deviceId, motionEvent.source,
GeodeUtils.TOUCH_TYPE_END, intArrayOf(idPointerUp), floatArrayOf(xPointerUp), floatArrayOf(y2)
)
else
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2, timestampSec)
cocos2dxRenderer.handleActionUp(idPointerUp, xPointerUp, y2)
}
true
}
@@ -312,42 +298,21 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
return
}
if (useKeyboardEvents) {
queueEvent {
cocos2dxRenderer.handleKeyDown(KeyEvent.KEYCODE_BACK)
}
} else {
val currentTime = System.nanoTime()
queueEvent {
sendNextEventTimestamp(currentTime)
if (controllerEventsEnabled) {
GeodeUtils.internalKeyEvent(
currentTime,
-1,
InputDevice.SOURCE_ANY,
KeyEvent.KEYCODE_BACK,
0,
true,
0
)
GeodeUtils.internalKeyEvent(
currentTime,
-1,
InputDevice.SOURCE_ANY,
KeyEvent.KEYCODE_BACK,
0,
false,
0
)
}
if (useKeyboardEvents) {
GeodeUtils.nativeKeyDown(KeyEvent.KEYCODE_BACK, 0, false)
GeodeUtils.nativeKeyUp(KeyEvent.KEYCODE_BACK, 0)
} else if (!controllerEventsEnabled) {
cocos2dxRenderer.handleKeyDown(KeyEvent.KEYCODE_BACK)
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
if (!useKeyboardEvents && !controllerEventsEnabled) {
if (!useKeyboardEvents) {
return legacyKeyDown(keyCode, event)
}
@@ -363,22 +328,15 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
queueEvent {
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
if (useKeyboardEvents) {
GeodeUtils.nativeKeyDown(keyCode, event.modifiers, event.repeatCount != 0)
}
if (controllerEventsEnabled) {
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, true, event.repeatCount)
}
}
true
}
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
if (!useKeyboardEvents && !controllerEventsEnabled) {
if (!useKeyboardEvents) {
return super.onKeyUp(keyCode, event)
}
@@ -393,82 +351,16 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
queueEvent {
sendNextEventTimestamp(event.eventTime * MS_TO_NS)
if (useKeyboardEvents) {
GeodeUtils.nativeKeyUp(keyCode, event.modifiers)
}
if (controllerEventsEnabled) {
GeodeUtils.internalKeyEvent(event.eventTime * MS_TO_NS, event.deviceId, event.source, event.keyCode, event.modifiers, false, event.repeatCount)
}
}
true
}
}
}
// i'm lazy.
// https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
private fun getCenteredAxis(
event: MotionEvent,
device: InputDevice,
axis: Int,
historyPos: Int
): Float {
device.getMotionRange(axis, event.source)?.apply {
val value: Float = if (historyPos < 0) {
event.getAxisValue(axis)
} else {
event.getHistoricalAxisValue(axis, historyPos)
}
if (abs(value) > flat) {
return value
}
}
return 0.0f
}
private fun getJoystickMotion(event: MotionEvent, axisHorizontal: Int, axisVertical: Int): Pair<FloatArray, FloatArray> {
val positionChangesX = ArrayList<Float>()
val positionChangesY = ArrayList<Float>()
val inputDevice = event.device
(0 until event.historySize).forEach { i ->
val x = getCenteredAxis(event, inputDevice, axisHorizontal, i)
val y = getCenteredAxis(event, inputDevice, axisVertical, i)
positionChangesX.add(x)
positionChangesY.add(y)
}
val x = getCenteredAxis(event, inputDevice, axisHorizontal, -1)
val y = getCenteredAxis(event, inputDevice, axisVertical, -1)
positionChangesX.add(x)
positionChangesY.add(y)
return Pair(positionChangesX.toFloatArray(), positionChangesY.toFloatArray())
}
private fun getJoystickMotion(event: MotionEvent, axis: Int): FloatArray {
val positionChanges = ArrayList<Float>()
val inputDevice = event.device
(0 until event.historySize).forEach { i ->
val x = getCenteredAxis(event, inputDevice, axis, i)
positionChanges.add(x)
}
val x = getCenteredAxis(event, inputDevice, axis, -1)
positionChanges.add(x)
return positionChanges.toFloatArray()
}
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
if (!useKeyboardEvents && !controllerEventsEnabled) {
if (!useKeyboardEvents) {
return super.onGenericMotionEvent(event)
}
@@ -480,41 +372,9 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
queueEvent {
sendNextEventTimestamp(timestamp)
if (useKeyboardEvents) {
GeodeUtils.nativeActionScroll(scrollX, scrollY)
}
if (controllerEventsEnabled) {
GeodeUtils.internalScrollEvent(timestamp, event.deviceId, event.source, scrollX, scrollY)
}
}
return true
}
if (controllerEventsEnabled && event?.action == MotionEvent.ACTION_MOVE && event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) {
val (motionLeftX, motionLeftY) = getJoystickMotion(event, MotionEvent.AXIS_X, MotionEvent.AXIS_Y)
val (motionRightX, motionRightY) = getJoystickMotion(event, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ)
val (motionDPadX, motionDPadY) = getJoystickMotion(event, MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y)
val motionLTrigger = getJoystickMotion(event, MotionEvent.AXIS_LTRIGGER)
val motionRTrigger = getJoystickMotion(event, MotionEvent.AXIS_RTRIGGER)
// i'm sure this is great for performance
val timestamp = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
event.eventTimeNanos else event.eventTime * MS_TO_NS
queueEvent {
GeodeUtils.internalJoystickEvent(
timestamp, event.deviceId, event.source,
motionLeftX, motionLeftY,
motionRightX, motionRightY,
motionDPadX, motionDPadY,
motionLTrigger, motionRTrigger
)
}
return true
}
@@ -537,40 +397,4 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) {
fun deleteBackward() {
queueEvent { cocos2dxRenderer.handleDeleteBackward() }
}
fun enableControllerEvents() {
if (controllerEventsEnabled) {
return
}
controllerEventsEnabled = true
val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
inputManager.registerInputDeviceListener(InputListener, null)
}
fun disableControllerEvents() {
if (!controllerEventsEnabled) {
return
}
controllerEventsEnabled = true
val inputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager
inputManager.unregisterInputDeviceListener(InputListener)
}
object InputListener : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) {
GeodeUtils.inputDeviceAdded(deviceId)
}
override fun onInputDeviceChanged(deviceId: Int) {
GeodeUtils.inputDeviceChanged(deviceId)
}
override fun onInputDeviceRemoved(deviceId: Int) {
GeodeUtils.inputDeviceRemoved(deviceId)
}
}
}
@@ -40,25 +40,21 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
@JvmStatic
private external fun nativeTextClosed()
// timestamp was added in 2.208, but the jni method has no required type signature.
// it doesn't hurt to unconditionally send one
@JvmStatic
private external fun nativeTouchesBegin(id: Int, x: Float, y: Float, timestamp: Double)
private external fun nativeTouchesBegin(id: Int, x: Float, y: Float)
@JvmStatic
private external fun nativeTouchesCancel(
ids: IntArray,
xs: FloatArray,
ys: FloatArray,
timestamp: Double,
ys: FloatArray
)
@JvmStatic
private external fun nativeTouchesEnd(id: Int, x: Float, y: Float, timestamp: Double)
private external fun nativeTouchesEnd(id: Int, x: Float, y: Float)
@JvmStatic
private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double)
private external fun nativeTouchesMove(ids: IntArray, xs: FloatArray, ys: FloatArray)
@JvmStatic
private external fun nativeInit(width: Int, height: Int)
@@ -113,20 +109,20 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi
}
}
fun handleActionDown(id: Int, x: Float, y: Float, timestamp: Double) {
nativeTouchesBegin(id, x, y, timestamp)
fun handleActionDown(id: Int, x: Float, y: Float) {
nativeTouchesBegin(id, x, y)
}
fun handleActionUp(id: Int, x: Float, y: Float, timestamp: Double) {
nativeTouchesEnd(id, x, y, timestamp)
fun handleActionUp(id: Int, x: Float, y: Float) {
nativeTouchesEnd(id, x, y)
}
fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double) {
nativeTouchesCancel(ids, xs, ys, timestamp)
fun handleActionCancel(ids: IntArray, xs: FloatArray, ys: FloatArray) {
nativeTouchesCancel(ids, xs, ys)
}
fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray, timestamp: Double) {
nativeTouchesMove(ids, xs, ys, timestamp)
fun handleActionMove(ids: IntArray, xs: FloatArray, ys: FloatArray) {
nativeTouchesMove(ids, xs, ys)
}
fun handleKeyDown(keyCode: Int) {
@@ -5,6 +5,7 @@ import android.text.TextWatcher
import android.widget.TextView.OnEditorActionListener
import android.text.Editable
import android.view.KeyEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
@@ -47,6 +48,10 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
override fun onTextChanged(pCharSequence: CharSequence, start: Int, before: Int, count: Int) {}
override fun onEditorAction(textView: TextView, actionID: Int, keyEvent: KeyEvent?): Boolean {
if (actionID != EditorInfo.IME_ACTION_DONE) {
return false
}
if (cocos2dxGLSurfaceView.cocos2dxEditText == textView && isFullScreenEdit) {
for (i in originText!!.length downTo 1) {
cocos2dxGLSurfaceView.deleteBackward()
@@ -58,9 +63,7 @@ class Cocos2dxTextInputWrapper(private val cocos2dxGLSurfaceView: Cocos2dxGLSurf
cocos2dxGLSurfaceView.insertText(text)
}
if (actionID != 6) {
return false
}
cocos2dxGLSurfaceView.requestFocus()
Cocos2dxGLSurfaceView.closeIMEKeyboard()
return false
+5 -108
View File
@@ -7,7 +7,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.media.AudioDeviceCallback
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.net.Uri
@@ -15,45 +14,27 @@ import android.os.Build
import android.util.Log
import java.io.FileNotFoundException
// i never agreed to your licenses! take that
@SuppressLint("StaticFieldLeak")
object FMOD {
private var gContext: Context? = null
private var gPluginAudioDeviceCallback: PluginAudioDeviceCallback? = null
private val gPluginBroadcastReceiver = PluginBroadcastReceiver()
external fun OutputAAudioHeadphonesChanged()
external fun SetInputEnumerationChanged()
external fun SetOutputEnumerationChanged()
@JvmStatic
fun init(context: Context?) {
gContext = context
if (context == null) {
return
}
val intentFilter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
if (Build.VERSION.SDK_INT >= 34) {
context.registerReceiver(gPluginBroadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(gPluginBroadcastReceiver, intentFilter)
}
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
val pluginAudioDeviceCallback = PluginAudioDeviceCallback(audioManager.getDevices(3))
gPluginAudioDeviceCallback = pluginAudioDeviceCallback
audioManager.registerAudioDeviceCallback(pluginAudioDeviceCallback, null)
gContext?.registerReceiver(
gPluginBroadcastReceiver,
IntentFilter(Intent.ACTION_HEADSET_PLUG)
)
}
@JvmStatic
fun close() {
val context = gContext
if (context != null) {
context.unregisterReceiver(gPluginBroadcastReceiver)
(context.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
.unregisterAudioDeviceCallback(gPluginAudioDeviceCallback)
}
context?.unregisterReceiver(gPluginBroadcastReceiver)
gContext = null
}
@@ -128,12 +109,6 @@ object FMOD {
}
}
@JvmStatic
fun getAudioDevices(i: Int): Array<AudioDeviceInfo?>? {
val context = gContext ?: return emptyArray()
return (context.getSystemService(Context.AUDIO_SERVICE) as AudioManager).getDevices(i)
}
@JvmStatic
fun fileDescriptorFromUri(str: String?): Int {
gContext?.apply {
@@ -152,82 +127,4 @@ object FMOD {
OutputAAudioHeadphonesChanged()
}
}
internal class PluginAudioDeviceCallback : AudioDeviceCallback {
val deviceSet: HashSet<Int> = HashSet()
constructor(audioDeviceInfoArr: Array<out AudioDeviceInfo?>?) {
if (audioDeviceInfoArr != null) {
deviceSet.addAll(audioDeviceInfoArr.mapNotNull { it?.id })
}
}
override fun onAudioDevicesAdded(audioDeviceInfoArr: Array<out AudioDeviceInfo?>?) {
var z: Boolean
var i = 0
if (audioDeviceInfoArr != null) {
var i2 = 0
z = false
while (i < audioDeviceInfoArr.size) {
if (!deviceSet.contains(audioDeviceInfoArr[i]!!.id)) {
if (audioDeviceInfoArr[i]!!.isSource) {
i2 = 1
}
if (audioDeviceInfoArr[i]!!.isSink) {
z = true
}
deviceSet.add(audioDeviceInfoArr[i]!!.id)
}
i++
}
i = i2
} else {
z = false
}
if (i != 0) {
try {
SetInputEnumerationChanged()
} catch (_: UnsatisfiedLinkError) { }
}
if (z) {
try {
SetOutputEnumerationChanged()
} catch (_: UnsatisfiedLinkError) { }
}
}
override fun onAudioDevicesRemoved(audioDeviceInfoArr: Array<out AudioDeviceInfo?>?) {
var z: Boolean
var i = 0
if (audioDeviceInfoArr != null) {
var i2 = 0
z = false
while (i < audioDeviceInfoArr.size) {
if (deviceSet.contains(audioDeviceInfoArr[i]!!.id)) {
if (audioDeviceInfoArr[i]!!.isSource) {
i2 = 1
}
if (audioDeviceInfoArr[i]!!.isSink) {
z = true
}
deviceSet.remove(audioDeviceInfoArr[i]!!.id)
}
i++
}
i = i2
} else {
z = false
}
if (i != 0) {
try {
SetInputEnumerationChanged()
} catch (_: UnsatisfiedLinkError) { }
}
if (z) {
try {
SetOutputEnumerationChanged()
} catch (_: UnsatisfiedLinkError) { }
}
}
}
}
@@ -1,29 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group android:scaleX="0.6689189"
android:scaleY="0.6689189"
android:translateX="169.51352"
android:translateY="182.1982">
<path
android:pathData="M553.7,189.9L536.9,177.1C522.2,165.8 501.8,165.8 487.1,177.1L207.6,390.5C194,400.9 188.3,418.8 193.3,435.2L300.9,783.2C306.2,800.3 322.1,812.1 340.1,812.1H683.9C701.9,812.1 717.8,800.3 723.1,783.2L830.7,435.2C835.7,418.8 830,400.9 816.4,390.5L792,371.9L762,466.3L680,733C676.5,744.4 666,752.1 654.2,752.1H368C356.1,752.1 345.7,744.4 342.2,733L252.8,442.3C249.5,431.5 253.2,419.8 262.1,413L494.6,234.5C495.6,233.7 496.6,233.1 497.6,232.5L553.7,189.9Z"
android:fillColor="#000000"
android:fillAlpha="0.25"
android:fillType="evenOdd"/>
<path
android:pathData="M500.6,320.6C508.5,314.5 519.5,314.5 527.4,320.6L686,442.2C693.3,447.8 696.3,457.3 693.6,466.1L632.6,664.4C629.7,673.6 621.2,679.9 611.6,679.9H416.4C406.8,679.9 398.3,673.6 395.4,664.4L385.4,632H439.4C440,632.1 440.7,632.2 441.4,632.2H583.2C588,632.2 592.3,629 593.7,624.4L638,477.5C639.3,473.2 637.8,468.5 634.3,465.7L519.1,375.6C515.1,372.5 509.5,372.5 505.5,375.6L399.1,458.8L344.5,499.1L334.4,466.1C331.7,457.3 334.7,447.8 342,442.2L500.6,320.6Z"
android:fillColor="#000000"
android:fillAlpha="0.25"
android:fillType="evenOdd"/>
<path
android:pathData="M557.1,165.4L536.9,150C522.2,138.8 501.8,138.8 487.1,150L180.9,383.8C167.3,394.2 161.6,412.1 166.7,428.5L284.4,809.4C289.7,826.6 305.6,838.3 323.6,838.3H700.4C718.4,838.3 734.3,826.6 739.6,809.4L857.3,428.5C862.4,412.1 856.7,394.2 843.1,383.8L814.8,362.3L782.4,464.4L693.2,754.4C689.7,765.7 679.2,773.5 667.4,773.5H354.6C342.8,773.5 332.3,765.7 328.8,754.4L231.2,437C227.9,426.2 231.6,414.5 240.6,407.6L494.6,212.6C495.6,211.8 496.6,211.1 497.7,210.5L557.1,165.4Z"
android:fillType="evenOdd"
android:fillColor="#ffffff" />
<path
android:pathData="M500.6,284.3C508.5,278.2 519.5,278.2 527.4,284.3L716.8,429.5C724.1,435.1 727.1,444.6 724.4,453.4L651.6,689.9C648.8,699.2 640.3,705.5 630.6,705.5H397.4C387.7,705.5 379.2,699.2 376.4,689.9L363.9,649.5H425.7C426.4,649.6 427.1,649.7 427.9,649.7H596.1C601,649.7 605.2,646.5 606.6,641.9L659.1,468C660.5,463.6 659,458.9 655.4,456.1L518.8,349.3C514.8,346.2 509.2,346.2 505.2,349.3L379.9,447.3L316.2,494.3L303.6,453.4C300.9,444.6 303.9,435.1 311.2,429.5L500.6,284.3Z"
android:fillType="evenOdd"
android:fillColor="#ffffff" />
</group>
</vector>
-43
View File
@@ -1,43 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="204dp"
android:height="205dp"
android:viewportWidth="204"
android:viewportHeight="205">
<path
android:pathData="M114.92,6.98L109,2.47C104.7,-0.82 98.72,-0.82 94.42,2.47L4.72,70.96C0.72,74.02 -0.95,79.25 0.54,84.06L35.03,195.65C36.59,200.69 41.24,204.12 46.51,204.12H156.91C162.18,204.12 166.83,200.69 168.38,195.65L202.88,84.06C204.37,79.25 202.7,74.02 198.7,70.96L190.43,64.65L180.92,94.56L154.79,179.53C153.77,182.85 150.71,185.12 147.23,185.12H55.6C52.13,185.12 49.06,182.85 48.04,179.53L19.44,86.54C18.47,83.38 19.57,79.95 22.19,77.94L96.6,20.81C96.9,20.58 97.21,20.38 97.53,20.19L114.92,6.98Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="65.53"
android:startY="18.28"
android:endX="150.22"
android:endY="187.89"
android:type="linear">
<item android:offset="0" android:color="#FFFFFDFF"/>
<item android:offset="0.08" android:color="#FFF2EAF5"/>
<item android:offset="0.12" android:color="#FFEDE5EF"/>
<item android:offset="0.31" android:color="#FFCDB5CD"/>
<item android:offset="0.49" android:color="#FFBA9BBC"/>
<item android:offset="1" android:color="#FF8D7ACF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M98.37,41.8C100.69,40.03 103.9,40.03 106.22,41.8L161.71,84.35C163.85,85.98 164.74,88.78 163.95,91.36L142.61,160.64C141.78,163.35 139.28,165.19 136.45,165.19H68.14C65.31,165.19 62.81,163.35 61.98,160.64L58.33,148.8H76.42C76.63,148.84 76.85,148.87 77.07,148.87H126.35C127.77,148.87 129.03,147.93 129.44,146.57L144.82,95.62C145.2,94.35 144.76,92.97 143.72,92.15L103.69,60.85C102.53,59.94 100.89,59.94 99.72,60.85L63.01,89.56L44.33,103.33L40.65,91.36C39.85,88.78 40.75,85.98 42.88,84.35L98.37,41.8Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="102.3"
android:startY="-83.96"
android:endX="102.3"
android:endY="165.06"
android:type="linear">
<item android:offset="0.48" android:color="#FFFFFFFF"/>
<item android:offset="0.63" android:color="#FFF5E4C2"/>
<item android:offset="0.73" android:color="#FFE5C7AD"/>
<item android:offset="1" android:color="#FFB790A9"/>
</gradient>
</aapt:attr>
</path>
</vector>
@@ -1,42 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="204dp"
android:height="205dp"
android:viewportWidth="204"
android:viewportHeight="205">
<path
android:pathData="M114.92,6.98L109,2.47C104.7,-0.82 98.72,-0.82 94.42,2.47L4.72,70.96C0.72,74.02 -0.95,79.25 0.54,84.06L35.03,195.65C36.59,200.69 41.24,204.12 46.51,204.12H156.91C162.18,204.12 166.83,200.69 168.38,195.65L202.88,84.06C204.37,79.25 202.7,74.02 198.7,70.96L190.43,64.65L180.92,94.56L154.79,179.53C153.77,182.85 150.71,185.12 147.23,185.12H55.6C52.13,185.12 49.06,182.85 48.04,179.53L19.44,86.54C18.47,83.38 19.57,79.95 22.19,77.94L96.6,20.81C96.9,20.58 97.21,20.38 97.53,20.19L114.92,6.98Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="65.53"
android:startY="18.28"
android:endX="150.22"
android:endY="187.89"
android:type="linear">
<item android:offset="0.0" android:color="#fff5b11d"/>
<item android:offset="0.10" android:color="#fff0834d"/>
<item android:offset="0.24" android:color="#ffe85f6b"/>
<item android:offset="0.49" android:color="#ffc9659e"/>
<item android:offset="1" android:color="#ffb9588f"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M98.37,41.8C100.69,40.03 103.9,40.03 106.22,41.8L161.71,84.35C163.85,85.98 164.74,88.78 163.95,91.36L142.61,160.64C141.78,163.35 139.28,165.19 136.45,165.19H68.14C65.31,165.19 62.81,163.35 61.98,160.64L58.33,148.8H76.42C76.63,148.84 76.85,148.87 77.07,148.87H126.35C127.77,148.87 129.03,147.93 129.44,146.57L144.82,95.62C145.2,94.35 144.76,92.97 143.72,92.15L103.69,60.85C102.53,59.94 100.89,59.94 99.72,60.85L63.01,89.56L44.33,103.33L40.65,91.36C39.85,88.78 40.75,85.98 42.88,84.35L98.37,41.8Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startX="102.3"
android:startY="-83.96"
android:endX="102.3"
android:endY="165.06"
android:type="linear">
<item android:offset="0.48" android:color="#fff2ce00"/>
<item android:offset="0.63" android:color="#fff4b97e"/>
<item android:offset="0.73" android:color="#fff7b66a"/>
<item android:offset="1" android:color="#ffeb8fac"/>
</gradient>
</aapt:attr>
</path>
</vector>
@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="96dp"
android:height="96dp"
android:viewportWidth="696"
android:viewportHeight="698"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M336.61,143.26C344.51,137.21 355.49,137.21 363.39,143.26L552.8,288.49C560.1,294.09 563.14,303.63 560.44,312.42L487.62,548.92C484.78,558.15 476.25,564.45 466.59,564.45H233.41C223.75,564.45 215.22,558.15 212.38,548.92L199.93,508.5H261.68C262.39,508.65 263.13,508.72 263.89,508.72H432.11C436.96,508.72 441.24,505.55 442.64,500.9L495.14,326.98C496.45,322.64 494.96,317.93 491.39,315.14L354.78,208.3C350.8,205.19 345.2,205.19 341.22,208.3L215.93,306.29L152.15,353.31L139.56,312.42C136.86,303.63 139.9,294.09 147.2,288.49L336.61,143.26Z"
android:fillType="evenOdd"
android:fillColor="@android:color/white">
</path>
</vector>
@@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="96dp"
android:height="96dp"
android:viewportWidth="696"
android:viewportHeight="698"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M393.08,24.42L372.88,9C358.19,-2.22 337.81,-2.22 323.12,9L16.94,242.81C3.28,253.24 -2.42,271.09 2.66,287.51L120.41,668.43C125.72,685.61 141.6,697.33 159.58,697.33H536.42C554.4,697.33 570.28,685.61 575.59,668.43L693.34,287.51C698.42,271.09 692.72,253.24 679.06,242.81L650.84,221.26L618.37,323.39L529.2,613.39C525.71,624.72 515.24,632.46 503.39,632.46H190.61C178.76,632.46 168.29,624.72 164.8,613.39L67.2,295.97C63.88,285.19 67.62,273.49 76.56,266.62L330.56,71.62C331.58,70.84 332.64,70.14 333.74,69.52L393.08,24.42Z"
android:fillType="evenOdd"
android:fillColor="@android:color/white">
</path>
</vector>
@@ -1,28 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
<clip-path
android:pathData="M0,0h1024v1024h-1024z"/>
<path
android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#F4D48E"/>
<path
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#F4D48E"/>
<path
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
android:fillColor="#F4A47B"/>
<path
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
android:fillColor="#E27880"/>
<path
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
android:fillColor="#B9588F"/>
<path
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
android:fillColor="#714A9A"/>
</group>
</vector>
@@ -1,28 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
<clip-path
android:pathData="M0,0h1024v1024h-1024z"/>
<path
android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#CC7C7C"/>
<path
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#CC7C7C"/>
<path
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
android:fillColor="#C6B471"/>
<path
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
android:fillColor="#59AF7F"/>
<path
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
android:fillColor="#5F8CAB"/>
<path
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
android:fillColor="#70558D"/>
</group>
</vector>
@@ -1,28 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<group android:scaleX="0.75" android:scaleY="0.75" android:translateX="128" android:translateY="128">
<clip-path
android:pathData="M0,0h1024v1024h-1024z"/>
<path
android:pathData="M0,0h1024v1024h-1024z"
android:fillColor="#65B8C2"/>
<path
android:pathData="M512,512m-512,0a512,512 0,1 1,1024 0a512,512 0,1 1,-1024 0"
android:fillColor="#65B8C2"/>
<path
android:pathData="M719.5,289C618,241.7 526,152.5 320.5,152.5C115,152.5 3.5,248 -8.5,293C-20.5,338 -8.2,391.5 -13.5,390.5L-33,706.5H1145L1137.5,460C1115.5,426.8 1043.5,399 1007,391.5C970.5,384 822.5,337 719.5,289Z"
android:fillColor="#DEA5CD"/>
<path
android:pathData="M561.5,514.5C488.5,476.5 341,402 214.5,402C88,402 30.3,440.7 -8.5,461.5C-47.3,482.3 -54,485.2 -59,486.5L-68.5,808L563.5,814.5L561.5,514.5Z"
android:fillColor="#DEE8EA"/>
<path
android:pathData="M1020.5,551C994,521.2 893.7,454.1 662.5,452.5C539.2,451.6 363,505.6 259,577.5C163.9,643.2 43,680 -13,686L-124,934.5H1187.5L1020.5,551Z"
android:fillColor="#C485BD"/>
<path
android:pathData="M819.5,856C638,856 518,772.5 359,772.5C200,772.5 100,860.5 65,869L-94,949.5V1041.5L1177.5,1025.5L1032.5,738C1028,758 967.4,856 819.5,856Z"
android:fillColor="#259CB4"/>
</group>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z"/>
</vector>
@@ -1,11 +0,0 @@
<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"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M382,720L154,492L211,435L382,606L749,239L806,296L382,720Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M336,680L280,624L424,480L280,337L336,281L480,425L623,281L679,337L535,480L679,624L623,680L480,536L336,680Z"/>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L400,160L480,240L800,240Q833,240 856.5,263.5Q880,287 880,320L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320Q800,320 800,320Q800,320 800,320L447,320L367,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240L160,320L160,320Q160,320 160,320Q160,320 160,320L160,720Q160,720 160,720Q160,720 160,720Z"/>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M320,760L320,200L760,480L320,760ZM400,480L400,480L400,480ZM400,614L610,480L400,346L400,614Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M480,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,160L800,160L800,440L520,440L520,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q557,720 619,676Q681,632 706,560L790,560Q762,666 676,733Q590,800 480,800Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M680,880Q630,880 595,845Q560,810 560,760Q560,754 563,732L282,568Q266,583 245,591.5Q224,600 200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360Q224,360 245,368.5Q266,377 282,392L563,228Q561,221 560.5,214.5Q560,208 560,200Q560,150 595,115Q630,80 680,80Q730,80 765,115Q800,150 800,200Q800,250 765,285Q730,320 680,320Q656,320 635,311.5Q614,303 598,288L317,452Q319,459 319.5,465.5Q320,472 320,480Q320,488 319.5,494.5Q319,501 317,508L598,672Q614,657 635,648.5Q656,640 680,640Q730,640 765,675Q800,710 800,760Q800,810 765,845Q730,880 680,880ZM680,800Q697,800 708.5,788.5Q720,777 720,760Q720,743 708.5,731.5Q697,720 680,720Q663,720 651.5,731.5Q640,743 640,760Q640,777 651.5,788.5Q663,800 680,800ZM200,520Q217,520 228.5,508.5Q240,497 240,480Q240,463 228.5,451.5Q217,440 200,440Q183,440 171.5,451.5Q160,463 160,480Q160,497 171.5,508.5Q183,520 200,520ZM680,240Q697,240 708.5,228.5Q720,217 720,200Q720,183 708.5,171.5Q697,160 680,160Q663,160 651.5,171.5Q640,183 640,200Q640,217 651.5,228.5Q663,240 680,240ZM680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760ZM200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480ZM680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Z"/>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M480,640Q497,640 508.5,628.5Q520,617 520,600Q520,583 508.5,571.5Q497,560 480,560Q463,560 451.5,571.5Q440,583 440,600Q440,617 451.5,628.5Q463,640 480,640ZM440,480L520,480L520,280L440,280L440,480ZM480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q584,763 652,664Q720,565 720,444L720,255L480,165L240,255L240,444Q240,565 308,664Q376,763 480,796ZM480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
-10
View File
@@ -1,10 +0,0 @@
<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="M480,840Q405,840 339.5,811.5Q274,783 225.5,734.5Q177,686 148.5,620.5Q120,555 120,480Q120,405 148.5,339.5Q177,274 225.5,225.5Q274,177 339.5,148.5Q405,120 480,120Q562,120 635.5,155Q709,190 760,254L760,160L840,160L840,400L600,400L600,320L710,320Q669,264 609,232Q549,200 480,200Q363,200 281.5,281.5Q200,363 200,480Q200,597 281.5,678.5Q363,760 480,760Q585,760 663.5,692Q742,624 756,520L838,520Q823,657 720.5,748.5Q618,840 480,840ZM592,648L440,496L440,280L520,280L520,464L648,592L592,648Z"/>
</vector>
@@ -1,10 +0,0 @@
<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="M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z"/>
</vector>
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_geode_background"/>
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_pride_background"/>
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_trans_background"/>
<foreground android:drawable="@drawable/ic_launcher_monochrome_shadow"/>
</adaptive-icon>
+13 -57
View File
@@ -12,7 +12,7 @@
<string name="launcher_logo_alt">geode logo</string>
<string name="launcher_launch_icon_alt">play icon</string>
<string name="launcher_download_icon_alt">download icon</string>
<string name="launcher_download_game">Download game</string>
<string name="launcher_download_game">Download Game</string>
<string name="launcher_download_32bit">Download the 32-bit version of launcher.</string>
<string name="launcher_download_universal">Download the non 32-bit version of launcher.</string>
<string name="launcher_settings_icon_alt">settings icon</string>
@@ -64,18 +64,13 @@
<string name="launcher_notification_compatibility_description">An update is required to receive Geode updates.</string>
<string name="launcher_notification_update_cta">Tap for more information.</string>
<string name="launcher_self_update_failed_title">Update failed</string>
<string name="launcher_self_update_failed">Failed to install launcher update: %1$s.</string>
<string name="launcher_self_update_progress">Updating Geode Launcher…</string>
<string name="launcher_starting_game">Starting Geometry Dash…</string>
<string name="launcher_downloading_update">Downloading update…</string>
<string name="launcher_downloading_update_details">(%1$s/%2$s)</string>
<string name="launcher_cancelled_manual">Launch paused.</string>
<string name="launcher_cancelled_navigated_away">Launch paused.</string>
<string name="launcher_cancelled_crash">Launch paused due to a crash.</string>
<string name="launcher_cancelled_error">Geode cannot be used.</string>
<string name="launcher_cancelled_error">Could not start Geode.</string>
<string name="launcher_cancelled_outdated">A Geometry Dash update is required.</string>
<string name="launcher_cancelled_restart">Restart</string>
<string name="launcher_cancelled_resume">Resume</string>
@@ -98,11 +93,6 @@
<string name="release_fetch_try_later">Please try again later.</string>
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
<string name="release_fetch_generic_short">Update failed. Please try again later.</string>
<!-- GeometryDashActivity strings -->
<string name="load_failed_link_error">Failed to link libraries</string>
<string name="load_failed_link_error_description">Game libraries could not be found. Try to:</string>
@@ -116,7 +106,7 @@
<string name="load_failed_recommendation_switch_64bit_abi">Install the non 32-bit version of the launcher.</string>
<string name="load_failed_recommendation_update">Verify that the latest versions of the launcher and Geometry Dash are installed.</string>
<string name="load_failed_recommendation_report">Report this issue to the developers.</string>
<string name="load_failed_recommendation_safe_mode">Enable the safe mode button in Settings.</string>
<string name="load_failed_recommendation_safe_mode">Hold the Launch button to launch in safe mode.</string>
<string name="load_failed_recommendation_dev_stl">Devs: If this is a locally built Geode version, please use the c++_shared ANDROID_STL!</string>
<string name="message_box_cancel">Cancel</string>
@@ -130,7 +120,7 @@
<string name="title_activity_settings">Settings</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="text_copied">Copied to clipboard.</string>
<string name="preference_category_testing">General</string>
@@ -139,7 +129,6 @@
<string name="preferences_copy_external_button">Copy external files directory</string>
<string name="preferences_open_file_manager">Open file manager</string>
<string name="preferences_view_logs">View application logs</string>
<string name="preferences_view_crashes">View crash dumps</string>
<string name="preference_theme_name">Theme</string>
<string name="preference_theme_select">Select theme</string>
@@ -147,19 +136,8 @@
<string name="preference_theme_light">Light</string>
<string name="preference_theme_dark">Dark</string>
<string name="preference_black_background_name">Black background in dark theme</string>
<string name="preference_disable_styles">Disable custom styling</string>
<string name="preference_set_application_icon">Application icon</string>
<string name="preference_force_hrr">Force maximum refresh rate</string>
<string name="preference_enable_redesign">✨ Enable UI refresh</string>
<string name="preference_disable_redesign">Disable UI refresh</string>
<string name="preference_launch_delay">Launch delay</string>
<string name="preference_launch_delay_select">Select launch delay</string>
<string name="preference_launch_delay_instant">None</string>
<plurals name="preference_launch_delay_value">
<item quantity="one">%1$d second</item>
<item quantity="other">%1$d seconds</item>
</plurals>
<string name="preference_display_mode_name">Display mode</string>
<string name="preference_display_mode_select">Select display mode</string>
@@ -210,7 +188,6 @@
<string name="preference_launcher_version_name">Android Launcher</string>
<string name="preference_launcher_version_description">v%1$s</string>
<string name="preference_launcher_version">Android Launcher v%1$s</string>
<string name="preference_enable_developer_again">You are already a developer!</string>
<string name="preference_loader_version_name">Geode</string>
<string name="preference_loader_version_description">%1$s</string>
@@ -228,7 +205,7 @@
<string name="preference_architecture">%1$s (%2$s)</string>
<string name="preference_check_for_updates_button">Check for updates</string>
<string name="preference_check_for_updates_description">Last checked %1$s</string>
<string name="preference_check_for_updates_description">Current loader version: %1$s</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_success">Geode has been updated!</string>
@@ -237,16 +214,11 @@
<string name="preference_check_for_updates_already_updating">An update is already in progress!</string>
<string name="preference_category_developer">Advanced</string>
<string name="preference_launch_safe_mode">Launch in safe mode</string>
<string name="preference_launch_safe_mode_confirm_title">Start in safe mode?</string>
<string name="preference_launch_arguments_name">Launch arguments</string>
<string name="preference_launch_arguments_set_title">Set launch arguments</string>
<string name="preference_text_clear">Clear</string>
<string name="preference_open_developer_options">Open developer options</string>
<string name="preference_developer_options_dialog_title">Enable developer options?</string>
<string name="preference_profiles">Profiles</string>
<string name="preference_profiles_create">Create profile</string>
@@ -262,42 +234,26 @@
<string name="preference_profiles_delete_undo_alt">Undo</string>
<!-- ApplicationLogsActivity strings -->
<string name="title_activity_application_logs">Application logs</string>
<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_delete">Clear All</string>
<string name="application_logs_share">Share</string>
<string name="application_log_copy_label">Log message</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_level">Set log level</string>
<string name="application_logs_export">Save To</string>
<string name="application_logs_level">Set Log Level</string>
<string name="application_logs_level_dialog_name">Set minimum log level</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">Show Crash Buffer</string>
<string name="application_logs_crash_only_enabled_alt">Enabled</string>
<!-- DeveloperSettingsActivity strings -->
<string name="title_activity_developer_settings">Developer options</string>
<string name="title_activity_developer_settings">Developer Settings</string>
<string name="preference_developer_mode">Enable developer options</string>
<string name="preference_developer_mode_about">Developer mode allows changing options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
<string name="preference_developer_mode_about">Developer mode enables some options that may make the game unstable. Please do not modify them unless you know what you\'re doing!\nThese options may change behavior, or be removed entirely in future updates.</string>
<string name="preference_developer_update_notice">Enabling developer mode automatically disallows the updater from overwriting custom loader builds.</string>
<string name="preference_override_exceptions_name">Custom exceptions fix symbols</string>
<string name="preference_override_exceptions_description">Uses exception_symbols.txt to patch</string>
<string name="preference_use_index_update_api">Use index release API</string>
<!-- CrashLogsActivity strings -->
<string name="title_activity_crash_logs">Crash dumps</string>
<string name="application_crashes_no_dumps">No crash dumps available.</string>
<string name="application_crashes_delete_one">Delete</string>
<!-- ApplicationIconActivity strings -->
<string name="title_activity_application_icon">Application icons</string>
<string name="application_icon_set_failed">Failed to change icon.</string>
<string name="application_icon_default">Default</string>
<string name="application_icon_geode">Geode</string>
<string name="application_icon_pride">Pride</string>
<string name="application_icon_trans">Transgender</string>
<string name="application_icon_confirm">Change icon to %1$s?</string>
<string name="application_icon_confirm_description">Geode Launcher will close itself to change the application icon.</string>
</resources>
+6 -5
View File
@@ -1,13 +1,14 @@
buildscript {
val composeBOM by extra("2026.01.01")
val composeBOM by extra("2025.04.01")
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "9.0.0" apply false
id("com.android.library") version "9.0.0" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.3.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
id("com.android.application") version "8.9.2" apply false
id("com.android.library") version "8.9.2" apply false
id("org.jetbrains.kotlin.android") version "2.1.20" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" apply false
}
tasks.register<Delete>("clean") {
Binary file not shown.
+2 -2
View File
@@ -1,7 +1,7 @@
#Wed Jan 28 04:12:33 MST 2026
#Thu Apr 24 10:54:01 MST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+3 -2
View File
@@ -86,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -205,7 +206,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.