mirror of
https://github.com/geode-sdk/android-launcher.git
synced 2026-10-21 16:32:39 +00:00
replace updater with packageinstaller
This commit is contained in:
5 files changed
+244
-47
No files matched your search
@@ -20,7 +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.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.LIGHTS" />
|
||||
|
||||
@@ -38,14 +39,18 @@
|
||||
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=".LegacyMainActivity"
|
||||
android:exported="false"
|
||||
@@ -74,15 +79,14 @@
|
||||
android:name=".preferences.SettingsActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
</activity>
|
||||
android:theme="@style/Theme.GeodeLauncher" />
|
||||
<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"
|
||||
@@ -94,45 +98,47 @@
|
||||
android:theme="@style/Theme.GeodeLauncher">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityGeode"
|
||||
android:name=".MainActivityTrans"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_trans"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_geode"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityPride"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
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>
|
||||
</activity-alias>
|
||||
|
||||
<activity-alias
|
||||
android:name=".MainActivityTrans"
|
||||
android:name=".MainActivityGeode"
|
||||
android:enabled="false"
|
||||
android:exported="true"
|
||||
android:icon="@mipmap/ic_launcher_geode"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_trans"
|
||||
android:targetActivity=".MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.text.format.Formatter
|
||||
import android.widget.Toast
|
||||
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.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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
|
||||
@@ -33,10 +37,12 @@ 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,8 +54,10 @@ 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
|
||||
@@ -58,13 +66,17 @@ 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 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.ExperimentalTime
|
||||
import kotlin.time.toJavaInstant
|
||||
|
||||
fun clearDownloadedApks(context: Context) {
|
||||
@@ -79,69 +91,192 @@ fun clearDownloadedApks(context: Context) {
|
||||
|
||||
baseDirectory.listFiles {
|
||||
// only select apk files
|
||||
_, name -> name.lowercase().endsWith(".apk")
|
||||
_, name -> name.lowercase().endsWith(".apk")
|
||||
}?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
context.filesDir.listFiles { _, name -> name.lowercase().endsWith(".apk") }?.forEach {
|
||||
it.delete()
|
||||
}
|
||||
|
||||
preferenceUtils.setBoolean(PreferenceUtils.Key.CLEANUP_APKS, false)
|
||||
}
|
||||
|
||||
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)
|
||||
// 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 installLauncherUpdate(context: Context) {
|
||||
|
||||
suspend fun installLauncherUpdate(context: Context) {
|
||||
val nextUpdate = ReleaseManager.get(context).availableLauncherUpdate.value
|
||||
val launcherDownload = nextUpdate?.getDownload()
|
||||
|
||||
if (launcherDownload != null) {
|
||||
val outputFile = launcherDownload.filename
|
||||
val baseDirectory = LaunchUtils.getBaseDirectory(context)
|
||||
val baseDirectory = context.filesDir
|
||||
|
||||
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 {
|
||||
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.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()
|
||||
|
||||
context.startActivity(downloadIntent)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
Toast.makeText(context, context.getString(R.string.no_activity_found), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
LauncherUpdater.resetState()
|
||||
}
|
||||
} else {
|
||||
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
|
||||
fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
|
||||
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) {
|
||||
@@ -165,7 +300,12 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
|
||||
Row(modifier = Modifier.padding(vertical = 12.dp)) {
|
||||
Button(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
installLauncherUpdate(context)
|
||||
beginUpdate = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(painterResource(R.drawable.icon_download), contentDescription = null)
|
||||
Spacer(Modifier.size(ButtonDefaults.IconSpacing))
|
||||
@@ -218,6 +358,7 @@ fun LauncherUpdateInformation(onDismiss: () -> Unit) {
|
||||
@Composable
|
||||
fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
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) {
|
||||
Column(
|
||||
// buttons add enough padding already, lower to compensate
|
||||
@@ -251,7 +399,12 @@ fun LauncherUpdateIndicator(modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { installLauncherUpdate(context) },
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
installLauncherUpdate(context)
|
||||
beginUpdate = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.launcher_install))
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class ReleaseManager private constructor(
|
||||
private suspend fun downloadLauncherUpdate(release: Downloadable) {
|
||||
val download = release.getDownload() ?: return
|
||||
|
||||
val outputDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||
val outputDirectory = applicationContext.filesDir
|
||||
val outputFile = File(outputDirectory, download.filename)
|
||||
|
||||
if (outputFile.exists()) {
|
||||
|
||||
@@ -64,6 +64,11 @@
|
||||
<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>
|
||||
|
||||
Reference in new issue
Block a user