replace updater with packageinstaller

This commit is contained in:
Chloe committed 2025-12-23 05:38:31 -07:00
1 parent 2b05f68af9
commit b69c915e51
5 files changed
+244 -47

No files matched your search

+19 -13
View File
@@ -20,7 +20,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <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.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.LIGHTS" /> <uses-permission android:name="android.permission.LIGHTS" />
@@ -38,14 +39,18 @@
android:allowBackup="true" android:allowBackup="true"
android:appCategory="game" android:appCategory="game"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:hasFragileUserData="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.GeodeLauncher" android:theme="@style/Theme.GeodeLauncher"
tools:targetApi="35"> tools:targetApi="35">
<receiver
android:name=".InstallReceiver"
android:enabled="true"
android:exported="false" />
<activity <activity
android:name=".LegacyMainActivity" android:name=".LegacyMainActivity"
android:exported="false" android:exported="false"
@@ -74,15 +79,14 @@
android:name=".preferences.SettingsActivity" android:name=".preferences.SettingsActivity"
android:exported="false" android:exported="false"
android:label="@string/title_activity_settings" android:label="@string/title_activity_settings"
android:theme="@style/Theme.GeodeLauncher"> android:theme="@style/Theme.GeodeLauncher" />
</activity>
<activity <activity
android:name=".GeometryDashActivity" android:name=".GeometryDashActivity"
android:configChanges="orientation|screenSize|smallestScreenSize"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:screenOrientation="landscape" android:screenOrientation="landscape"
android:theme="@style/Theme.NoActionBar.Fullscreen" android:theme="@style/Theme.NoActionBar.Fullscreen"
android:configChanges="orientation|screenSize|smallestScreenSize"
android:windowSoftInputMode="stateAlwaysHidden"> android:windowSoftInputMode="stateAlwaysHidden">
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
@@ -94,45 +98,47 @@
android:theme="@style/Theme.GeodeLauncher"> android:theme="@style/Theme.GeodeLauncher">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity-alias <activity-alias
android:name=".MainActivityGeode" android:name=".MainActivityTrans"
android:enabled="false" android:enabled="false"
android:exported="true" android:exported="true"
android:icon="@mipmap/ic_launcher_trans"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_geode"
android:targetActivity=".MainActivity"> android:targetActivity=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<activity-alias <activity-alias
android:name=".MainActivityPride" android:name=".MainActivityPride"
android:enabled="false" android:enabled="false"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_pride" android:icon="@mipmap/ic_launcher_pride"
android:label="@string/app_name"
android:targetActivity=".MainActivity"> android:targetActivity=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<activity-alias <activity-alias
android:name=".MainActivityTrans" android:name=".MainActivityGeode"
android:enabled="false" android:enabled="false"
android:exported="true" android:exported="true"
android:icon="@mipmap/ic_launcher_geode"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher_trans"
android:targetActivity=".MainActivity"> android:targetActivity=".MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
@@ -0,0 +1,33 @@
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,10 +1,10 @@
package com.geode.launcher.main package com.geode.launcher.main
import android.content.ActivityNotFoundException import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.content.pm.PackageInstaller
import android.provider.DocumentsContract import android.os.Build
import android.text.format.Formatter import android.text.format.Formatter
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -16,9 +16,13 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults 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.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@@ -33,10 +37,12 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -48,8 +54,10 @@ import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp 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.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.updater.ReleaseManager import com.geode.launcher.updater.ReleaseManager
import com.geode.launcher.updater.ReleaseViewModel import com.geode.launcher.updater.ReleaseViewModel
@@ -58,13 +66,17 @@ import com.geode.launcher.utils.PreferenceUtils
import com.mikepenz.markdown.compose.LocalBulletListHandler import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.m3.Markdown import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownTypography 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 org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import java.io.File import java.io.File
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
import kotlin.time.ExperimentalTime
import kotlin.time.toJavaInstant import kotlin.time.toJavaInstant
fun clearDownloadedApks(context: Context) { fun clearDownloadedApks(context: Context) {
@@ -79,69 +91,192 @@ fun clearDownloadedApks(context: Context) {
baseDirectory.listFiles { baseDirectory.listFiles {
// only select apk files // only select apk files
_, name -> name.lowercase().endsWith(".apk") _, name -> name.lowercase().endsWith(".apk")
}?.forEach { }?.forEach {
it.delete() it.delete()
} }
context.filesDir.listFiles { _, name -> name.lowercase().endsWith(".apk") }?.forEach {
it.delete()
}
preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false) preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false)
} }
fun generateInstallIntent(uri: Uri): Intent { // lazy singleton idc
// maybe one day i'll rewrite this to use packageinstaller. not today object LauncherUpdater {
return Intent(Intent.ACTION_INSTALL_PACKAGE).apply { enum class LauncherUpdateState {
setDataAndType(uri, "application/vnd.android.package-archive") Began,
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 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 installLauncherUpdate(context: Context) {
suspend fun installLauncherUpdate(context: Context) {
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
val launcherDownload = nextUpdate?.getDownload() val launcherDownload = nextUpdate?.getDownload()
if (launcherDownload != null) { if (launcherDownload != null) {
val outputFile = launcherDownload.filename val outputFile = launcherDownload.filename
val baseDirectory = LaunchUtils.getBaseDirectory(context) val baseDirectory = context.filesDir
val outputPathFile = File(baseDirectory, outputFile) val outputPathFile = File(baseDirectory, outputFile)
if (!outputPathFile.exists()) { if (!outputPathFile.exists()) {
return return
} }
val outputPath = "${UserDirectoryProvider.ROOT}${outputFile}"
val uri = DocumentsContract.buildDocumentUri("${context.packageName}.user", outputPath)
PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true) PreferenceUtils.get(context).setBoolean(PreferenceUtils.Key.CLEANUP_APKS, true)
try { try {
val intent = generateInstallIntent(uri) LauncherUpdater.installPackage(context, outputPathFile)
context.startActivity(intent) } catch (e: Exception) {
} catch (_: ActivityNotFoundException) { e.printStackTrace()
// if it fails to install, just open it in the browser val msg = context.getString(R.string.launcher_self_update_failed, e.message)
try { Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
val downloadUrl = launcherDownload.url
val downloadIntent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(downloadUrl)
}
context.startActivity(downloadIntent) LauncherUpdater.resetState()
} catch (_: ActivityNotFoundException) {
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
}
} }
} else { } else {
Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show() Toast.makeText(context, context.getString(R.string.release_fetch_no_releases), Toast.LENGTH_SHORT).show()
} }
} }
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class) @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 @Composable
fun LauncherUpdateInformation(onDismiss: () -> Unit) { fun LauncherUpdateInformation(onDismiss: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value } val nextRelease = remember { ReleaseManager.get(context).availableLauncherUpdate.value }
val sheetState = rememberModalBottomSheetState() val sheetState = rememberModalBottomSheetState()
val coroutineScope = rememberCoroutineScope()
var beginUpdate by remember { mutableStateOf(false) }
if (beginUpdate) {
LauncherUpdateDialog(onDismiss = {
beginUpdate = false
})
}
if (nextRelease != null) { if (nextRelease != null) {
ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) { ModalBottomSheet(onDismissRequest = { onDismiss() }, sheetState = sheetState) {
@@ -165,7 +300,12 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
Row(modifier = Modifier.padding(vertical = 12.dp)) { Row(modifier = Modifier.padding(vertical = 12.dp)) {
Button( Button(
onClick = { installLauncherUpdate(context) }, onClick = {
coroutineScope.launch {
installLauncherUpdate(context)
beginUpdate = true
}
},
) { ) {
Icon(painterResource(R.drawable.icon_download), contentDescription = null) Icon(painterResource(R.drawable.icon_download), contentDescription = null)
Spacer(Modifier.size(ButtonDefaults.IconSpacing)) Spacer(Modifier.size(ButtonDefaults.IconSpacing))
@@ -218,6 +358,7 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
@Composable @Composable
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) { fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
val context = LocalContext.current val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var showInfoDialog by remember { mutableStateOf(false) } var showInfoDialog by remember { mutableStateOf(false) }
@@ -227,6 +368,13 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
} }
} }
var beginUpdate by remember { mutableStateOf(false) }
if (beginUpdate) {
LauncherUpdateDialog {
beginUpdate = false
}
}
ElevatedCard(modifier) { ElevatedCard(modifier) {
Column( Column(
// buttons add enough padding already, lower to compensate // buttons add enough padding already, lower to compensate
@@ -251,7 +399,12 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
} }
TextButton( TextButton(
onClick = { installLauncherUpdate(context) }, onClick = {
coroutineScope.launch {
installLauncherUpdate(context)
beginUpdate = true
}
},
) { ) {
Text(stringResource(R.string.launcher_install)) Text(stringResource(R.string.launcher_install))
} }
@@ -134,7 +134,7 @@ class ReleaseManager private constructor(
private suspend fun downloadLauncherUpdate(release: Downloadable) { private suspend fun downloadLauncherUpdate(release: Downloadable) {
val download = release.getDownload() ?: return val download = release.getDownload() ?: return
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext) val outputDirectory = applicationContext.filesDir
val outputFile = File(outputDirectory, download.filename) val outputFile = File(outputDirectory, download.filename)
if (outputFile.exists()) { if (outputFile.exists()) {
+5
View File
@@ -64,6 +64,11 @@
<string name="launcher_notification_compatibility_description">An update is required to receive Geode updates.</string> <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_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_starting_game">Starting Geometry Dash…</string>
<string name="launcher_downloading_update">Downloading update…</string> <string name="launcher_downloading_update">Downloading update…</string>
<string name="launcher_downloading_update_details">(%1$s/%2$s)</string> <string name="launcher_downloading_update_details">(%1$s/%2$s)</string>