Allow huawei variants to install microG & support silent installs.

This commit is contained in:
Rahul Patel
2025-07-05 14:59:41 +05:30
parent cfe52d5a04
commit aea945491d
44 changed files with 1662 additions and 145 deletions

View File

@@ -62,4 +62,9 @@ object Constants {
const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY"
const val JSON_MIME_TYPE = "application/json"
// PACKAGE NAMES
const val PACKAGE_NAME_GMS = "com.google.android.gms"
const val PACKAGE_NAME_PLAY_STORE = "com.android.vending"
const val PACKAGE_NAME_APP_GALLERY = "com.huawei.appmarket"
}

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.gplayapi.data.models.App
import com.aurora.store.BuildConfig
import com.aurora.store.R
@@ -114,7 +115,7 @@ private fun buildExtras(app: App): List<String> {
add(stringResource(R.string.details_no_ads))
}
if (app.dependencies.dependentPackages.contains(PackageUtil.PACKAGE_NAME_GMS)) {
if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
add(stringResource(R.string.details_gsf_dependent))
}
}

View File

@@ -12,6 +12,7 @@ import com.aurora.store.AuroraApp
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.suite.ExternalApk
import com.aurora.store.data.room.update.Update
import com.aurora.store.data.work.DownloadWorker
import com.aurora.store.util.PathUtil
@@ -90,6 +91,14 @@ class DownloadHelper @Inject constructor(
downloadDao.insert(Download.fromUpdate(update))
}
/**
* Enqueues ExternalApk for download & install
* @param externalApk [ExternalApk] to download
*/
suspend fun enqueueStandalone(externalApk: ExternalApk) {
downloadDao.insert(Download.fromExternalApk(externalApk))
}
/**
* Cancels the download for the given package
* @param packageName Name of the package to cancel download

View File

@@ -25,6 +25,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.EXTRA_SESSION_ID
import android.content.pm.PackageInstaller.PACKAGE_SOURCE_STORE
import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager
@@ -240,24 +241,31 @@ class SessionInstaller @Inject constructor(
}
private fun commitInstall(sessionInfo: SessionInfo) {
Log.i(TAG, "Starting install session for ${sessionInfo.packageName}")
val sessionId = sessionInfo.sessionId
packageInstaller.getSessionInfo(sessionId) ?: run {
Log.e(TAG, "Session $sessionId is no longer valid, skipping commit.")
removeFromInstallQueue(sessionInfo.packageName)
return
}
try {
val session = packageInstaller.openSession(sessionId)
Log.i(TAG, "Starting install session for ${sessionInfo.packageName}")
val existingSessionInfo = packageInstaller.getSessionInfo(sessionInfo.sessionId)
if (existingSessionInfo == null) {
Log.e(TAG, "Session ${sessionInfo.sessionId} is no longer valid.")
return removeFromInstallQueue(sessionInfo.packageName)
}
commitSession(sessionInfo)
} catch (e: Exception) {
Log.e(TAG, "Error committing session: ${e.message}")
removeFromInstallQueue(sessionInfo.packageName)
postError(sessionInfo.packageName, e.localizedMessage, e.stackTraceToString())
}
}
private fun commitSession(sessionInfo: SessionInfo) {
try {
val session = packageInstaller.openSession(sessionInfo.sessionId)
session.commit(getCallBackIntent(sessionInfo)!!.intentSender)
session.close()
} catch (e: SecurityException) {
Log.e(TAG, "Failed to commit session $sessionId: ${e.message}")
removeFromInstallQueue(sessionInfo.packageName)
} catch (e: Exception) {
Log.e(TAG, "Unexpected error in commitInstall for session $sessionId", e)
Log.e(TAG, "Error committing session: ${e.message}")
} finally {
removeFromInstallQueue(sessionInfo.packageName)
}
}
@@ -266,6 +274,7 @@ class SessionInstaller @Inject constructor(
val callBackIntent = Intent(context, InstallerStatusReceiver::class.java).apply {
action = ACTION_INSTALL_STATUS
setPackage(context.packageName)
putExtra(EXTRA_SESSION_ID, sessionInfo.sessionId)
putExtra(EXTRA_PACKAGE_NAME, sessionInfo.packageName)
putExtra(EXTRA_VERSION_CODE, sessionInfo.versionCode)
putExtra(EXTRA_DISPLAY_NAME, sessionInfo.displayName)
@@ -280,4 +289,22 @@ class SessionInstaller @Inject constructor(
true
)
}
enum class ServiceResultCode(val code: Int, val reason: String) {
SUCCESS(0, "Request successful"),
SERVICE_VERSION_UPDATE_REQUIRED(2, "Interface depends on a higher version"),
SERVICE_INVALID(4, "Service is invalid"),
METHOD_UNSUPPORTED(5, "Interface is not supported"),
RESOLUTION_REQUIRED(6, "Needs to be resolved by opening PendingIntent"),
NETWORK_ERROR(7, "Network exception, unable to complete interface request"),
INTERNAL_ERROR(8, "Internal code error, incorrect parameter transmission in scenario"),
TIMEOUT(10, "Interface access timeout return"),
DEAD_CLIENT(12, "Current client is unavailable"),
RESPONSE_ERROR(13, "Server returns abnormal response"),
PROTOCOL_ERROR(15, "Not signed Huawei App Market agreement");
companion object {
fun fromCode(code: Int): ServiceResultCode? = entries.find { it.code == code }
}
}
}

View File

@@ -133,7 +133,10 @@ class ShizukuInstaller @Inject constructor(
sharedLibPkgName: String = "",
displayName: String = ""
) {
Log.i(TAG, "Received session install request for ${sharedLibPkgName.ifBlank { packageName }}")
Log.i(
TAG,
"Received session install request for ${sharedLibPkgName.ifBlank { packageName }}"
)
val (sessionId, session) = kotlin.runCatching {
val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
@@ -168,7 +171,11 @@ class ShizukuInstaller @Inject constructor(
Log.i(TAG, "Writing splits to session for ${sharedLibPkgName.ifBlank { packageName }}")
getFiles(packageName, versionCode, sharedLibPkgName).forEach {
it.inputStream().use { input ->
session.openWrite("${sharedLibPkgName.ifBlank { packageName }}_${System.currentTimeMillis()}", 0, -1).use { output ->
session.openWrite(
"${sharedLibPkgName.ifBlank { packageName }}_${System.currentTimeMillis()}",
0,
-1
).use { output ->
input.copyTo(output)
session.fsync(output)
}

View File

@@ -61,7 +61,7 @@ class AuthProvider @Inject constructor(
return if (rawAuth.isNotBlank()) {
json.decodeFromString<AuthData>(rawAuth)
} else {
null
AuthData("BOGUS")
}
}

View File

@@ -21,7 +21,6 @@ package com.aurora.store.data.providers
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.aurora.extensions.isNAndAbove
import com.aurora.store.util.Preferences
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -39,7 +38,11 @@ class BlacklistProvider @Inject constructor(
private val PREFERENCE_BLACKLIST = "PREFERENCE_BLACKLIST"
var blacklist: MutableSet<String>
set(value) = Preferences.putString(context, PREFERENCE_BLACKLIST, json.encodeToString(value))
set(value) = Preferences.putString(
context,
PREFERENCE_BLACKLIST,
json.encodeToString(value)
)
get() {
return try {
val rawBlacklist = if (isNAndAbove) {
@@ -54,7 +57,7 @@ class BlacklistProvider @Inject constructor(
Context.MODE_PRIVATE
) as SharedPreferences
PreferenceManager.getDefaultSharedPreferences(context)
Preferences.getPrefs(context)
.getString(
PREFERENCE_BLACKLIST,
refSharedPreferences.getString(PREFERENCE_BLACKLIST, "")

View File

@@ -23,6 +23,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.EXTRA_SESSION_ID
import android.util.Log
import androidx.core.content.IntentCompat
import androidx.core.content.getSystemService
@@ -35,28 +36,31 @@ import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_DISPLAY_NAME
import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_PACKAGE_NAME
import com.aurora.store.data.installer.AppInstaller.Companion.EXTRA_VERSION_CODE
import com.aurora.store.data.installer.base.InstallerBase
import com.aurora.store.util.CommonUtil.inForeground
import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PathUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class InstallerStatusReceiver : BroadcastReceiver() {
abstract class BaseInstallerStatusReceiver : BroadcastReceiver() {
private val TAG = InstallerStatusReceiver::class.java.simpleName
private val TAG = BaseInstallerStatusReceiver::class.java.simpleName
override fun onReceive(context: Context?, intent: Intent?) {
if (context != null && intent?.action == ACTION_INSTALL_STATUS) {
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!!
val displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME)!!
val versionCode = intent.getLongExtra(EXTRA_VERSION_CODE, -1)
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
val displayName = intent.getStringExtra(EXTRA_DISPLAY_NAME) ?: packageName
val versionCode = intent.getLongExtra(EXTRA_VERSION_CODE, -1)
val sessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1)
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
Log.i(
TAG,
"$packageName ($versionCode) sessionId=$sessionId, status=$status, extra=$extra"
)
// If package was successfully installed, exit after notifying user and doing cleanup
if (status == PackageInstaller.STATUS_SUCCESS) {
// No post-install steps for shared libraries
@@ -64,19 +68,22 @@ class InstallerStatusReceiver : BroadcastReceiver() {
AuroraApp.enqueuedInstalls.remove(packageName)
InstallerBase.notifyInstallation(context, displayName, packageName)
if (Preferences.getBoolean(context, PREFERENCE_AUTO_DELETE)) {
PathUtil.getAppDownloadDir(context, packageName, versionCode)
.deleteRecursively()
}
return
return postStatus(status, packageName, extra, context)
}
if (inForeground() && status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
promptUser(intent, context)
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
doAppropriatePrompt(context, intent, sessionId)
} else {
AuroraApp.enqueuedInstalls.remove(packageName)
postStatus(status, packageName, extra, context)
notifyUser(context, packageName, displayName, status)
postStatus(status, packageName, extra, context)
}
}
}
@@ -94,24 +101,38 @@ class InstallerStatusReceiver : BroadcastReceiver() {
displayName,
InstallerBase.getErrorString(context, status)
)
notificationManager!!.notify(packageName.hashCode(), notification)
notificationManager?.notify(packageName.hashCode(), notification)
}
private fun promptUser(intent: Intent, context: Context) {
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_INTENT, Intent::class.java)?.let {
it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.packageName)
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
internal fun promptUser(context: Context, intent: Intent) {
runOnUiThread {
val launchIntent = IntentCompat.getParcelableExtra(
intent,
Intent.EXTRA_INTENT,
Intent::class.java
)
try {
runOnUiThread { context.startActivity(it) }
} catch (exception: Exception) {
Log.e(TAG, "Failed to trigger installation!", exception)
if (launchIntent != null) {
launchIntent.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
launchIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.packageName)
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(launchIntent)
} catch (exception: Exception) {
Log.e(TAG, "Failed to launch intent!", exception)
}
} else {
Log.w(TAG, "No launch intent found in the installation request.")
}
}
}
private fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) {
open fun doAppropriatePrompt(context: Context, intent: Intent, sessionId: Int) {
promptUser(context, intent)
}
open fun postStatus(status: Int, packageName: String?, extra: String?, context: Context) {
val event = when (status) {
PackageInstaller.STATUS_SUCCESS -> {
InstallerEvent.Installed(packageName!!).apply {
@@ -132,6 +153,7 @@ class InstallerStatusReceiver : BroadcastReceiver() {
}
}
}
AuroraApp.events.send(event)
}
}

View File

@@ -6,6 +6,7 @@ import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.suite.ExternalApk
import com.aurora.store.data.room.update.Update
import kotlinx.parcelize.Parcelize
import java.util.Date
@@ -81,5 +82,26 @@ data class Download(
Date().time
)
}
fun fromExternalApk(externalApk: ExternalApk): Download {
return Download(
packageName = externalApk.packageName,
versionCode = externalApk.versionCode,
offerType = 0,
isInstalled = false,
displayName = externalApk.displayName,
iconURL = externalApk.iconURL,
size = 0,
id = 0,
downloadStatus = DownloadStatus.QUEUED,
progress = 0,
speed = 0L,
timeRemaining = 0L,
totalFiles = 1,
downloadedFiles = 0,
fileList = externalApk.fileList,
sharedLibs = emptyList(),
)
}
}
}

View File

@@ -0,0 +1,27 @@
package com.aurora.store.data.room.suite
import android.content.Context
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.util.PackageUtil
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "externalApk")
data class ExternalApk(
@PrimaryKey
val packageName: String,
val versionCode: Long,
val versionName: String,
val displayName: String,
val iconURL: String,
val developerName: String,
var fileList: List<PlayFile>
) : Parcelable {
fun isInstalled(context: Context): Boolean {
return PackageUtil.isInstalled(context, packageName)
}
}

View File

@@ -13,6 +13,7 @@ import androidx.core.os.bundleOf
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.data.model.AccountType
@@ -21,7 +22,6 @@ import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlin.coroutines.resume
@@ -140,7 +140,7 @@ open class AuthWorker @AssistedInject constructor(
Account(email, GOOGLE_ACCOUNT_TYPE),
GOOGLE_PLAY_AUTH_TOKEN_TYPE,
bundleOf(
"overridePackage" to GOOGLE_PLAY_PACKAGE_NAME,
"overridePackage" to PACKAGE_NAME_PLAY_STORE,
"overrideCertificate" to Base64.decode(
GOOGLE_PLAY_CERT,
Base64.DEFAULT

View File

@@ -24,6 +24,8 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.util.Base64
import android.util.Log
import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.extensions.generateX509Certificate
import com.aurora.extensions.getUpdateOwnerPackageNameCompat
import com.aurora.extensions.isPAndAbove
@@ -40,7 +42,6 @@ object CertUtil {
const val GOOGLE_ACCOUNT_TYPE = "com.google"
const val GOOGLE_PLAY_AUTH_TOKEN_TYPE = "oauth2:https://www.googleapis.com/auth/googleplay"
const val GOOGLE_PLAY_PACKAGE_NAME = "com.android.vending"
const val GOOGLE_PLAY_CERT =
"MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK"
@@ -49,7 +50,7 @@ object CertUtil {
}
fun isAppGalleryApp(context: Context, packageName: String): Boolean {
return context.packageManager.getUpdateOwnerPackageNameCompat(packageName) == "com.huawei.appmarket"
return context.packageManager.getUpdateOwnerPackageNameCompat(packageName) == PACKAGE_NAME_APP_GALLERY
}
fun isAuroraStoreApp(context: Context, packageName: String): Boolean {
@@ -91,16 +92,17 @@ object CertUtil {
}
}
fun isMicroGGMS(context: Context, packageName: String): Boolean {
fun isMicroGGms(context: Context): Boolean {
return try {
val packageInfo = getPackageInfo(context, packageName, PackageManager.GET_PERMISSIONS)
val packageInfo =
getPackageInfo(context, PACKAGE_NAME_GMS, PackageManager.GET_PERMISSIONS)
val hasFakePackageSignature = packageInfo.requestedPermissions?.any { permission ->
permission == "android.permission.FAKE_PACKAGE_SIGNATURE"
} == true
return hasFakePackageSignature
} catch (exception: Exception) {
Log.e(TAG, "Failed to check origin for $packageName")
Log.e(TAG, "Failed to check origin for $PACKAGE_NAME_GMS")
false
}
}

View File

@@ -36,6 +36,9 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import com.aurora.Constants.PACKAGE_NAME_APP_GALLERY
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
import com.aurora.extensions.isHuawei
import com.aurora.extensions.isOAndAbove
import com.aurora.extensions.isPAndAbove
@@ -50,7 +53,6 @@ object PackageUtil {
private const val TAG = "PackageUtil"
const val PACKAGE_NAME_GMS = "com.google.android.gms"
private const val VERSION_CODE_MICRO_G: Long = 240913402
private const val VERSION_CODE_MICRO_G_HUAWEI: Long = 240913007
@@ -63,8 +65,39 @@ object PackageUtil {
}
}
fun hasSupportedMicroG(context: Context): Boolean {
val isMicroG = CertUtil.isMicroGGMS(context, PACKAGE_NAME_GMS)
fun hasSupportedAppGallery(context: Context): Boolean {
return try {
val result = context.packageManager.checkPermission(
android.Manifest.permission.INSTALL_PACKAGES,
PACKAGE_NAME_APP_GALLERY
)
if (result != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "AppGallery does not have INSTALL_PACKAGES permission")
return false
}
val packageInfo = context.packageManager.getPackageInfo(
PACKAGE_NAME_APP_GALLERY,
PackageManager.GET_META_DATA
)
@Suppress("DEPRECATION")
val versionCode = if (Build.VERSION.SDK_INT >= 28)
packageInfo.longVersionCode
else
packageInfo.versionCode.toLong()
Log.i(TAG, "AppGallery - ${packageInfo.versionName} ($versionCode)")
versionCode >= 15010000L
} catch (_: Exception) {
false
}
}
fun hasSupportedMicroGVariant(context: Context): Boolean {
val isMicroG = CertUtil.isMicroGGms(context)
// Do not proceed if MicroG variant is not installed
if (!isMicroG) return false
@@ -76,20 +109,15 @@ object PackageUtil {
}
}
fun isInstalled(context: Context, packageName: String): Boolean {
fun isInstalled(context: Context, packageName: String, versionCode: Long? = null): Boolean {
return try {
getPackageInfo(context, packageName, PackageManager.GET_META_DATA)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun isInstalled(context: Context, packageName: String, versionCode: Long): Boolean {
return try {
val packageInfo = getPackageInfo(context, packageName)
return PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode.toLong()
} catch (e: PackageManager.NameNotFoundException) {
val packageInfo = getPackageInfo(context, packageName, PackageManager.GET_META_DATA)
if (versionCode != null) {
PackageInfoCompat.getLongVersionCode(packageInfo) >= versionCode
} else {
true
}
} catch (_: PackageManager.NameNotFoundException) {
false
}
}
@@ -110,7 +138,11 @@ object PackageUtil {
}
}
fun isSharedLibraryInstalled(context: Context, packageName: String, versionCode: Long): Boolean {
fun isSharedLibraryInstalled(
context: Context,
packageName: String,
versionCode: Long
): Boolean {
return if (isOAndAbove) {
val sharedLibraries = getAllSharedLibraries(context)
if (isPAndAbove) {
@@ -137,6 +169,16 @@ object PackageUtil {
}
}
fun isMicroGBundleInstalled(context: Context): Boolean {
/**
* Confirm if MicroG bundle is installed
* Considering the following:
* 1. GmsCore is installed and it is a microG huawei variant
* 2. Play Store is installed - (microG Companion)
*/
return hasSupportedMicroGVariant(context) && isInstalled(context, PACKAGE_NAME_PLAY_STORE)
}
fun getInstalledVersionName(context: Context, packageName: String): String {
return try {
getPackageInfo(context, packageName).versionName ?: ""

View File

@@ -0,0 +1,56 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.epoxy.views
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView
import com.aurora.store.R
import com.aurora.store.databinding.ViewTextBinding
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
baseModelClass = BaseModel::class
)
class EpoxyTextView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseView<ViewTextBinding>(context, attrs, defStyleAttr) {
@ModelProp
fun title(title: String) {
binding.txtView.text = title
}
@ModelProp
@JvmOverloads
fun size(int: Int = 16) {
binding.txtView.setTextSize(TypedValue.COMPLEX_UNIT_SP, int.toFloat())
}
@ModelProp
@JvmOverloads
fun style(resId: Int = R.style.AuroraTextStyle) {
binding.txtView.setTextAppearance(context, resId)
}
}

View File

@@ -34,6 +34,7 @@ import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.store.MobileNavigationDirections
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.data.providers.PermissionProvider
import com.aurora.store.view.ui.details.AppDetailsFragmentDirections
import java.lang.reflect.ParameterizedType
abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
@@ -131,6 +132,12 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
findNavController().navigate(MobileNavigationDirections.actionGlobalAppMenuSheet(app))
}
fun openGMSWarningFragment() {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToGmsWarnFragment()
)
}
private fun cleanupRecyclerViews(recyclerViews: List<EpoxyRecyclerView>) {
recyclerViews.forEach { recyclerView ->
runCatching {

View File

@@ -31,6 +31,7 @@ import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.compose.ui.util.fastAny
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
@@ -46,8 +47,10 @@ import coil3.transform.CircleCropTransformation
import coil3.transform.RoundedCornersTransformation
import com.aurora.Constants
import com.aurora.Constants.EXODUS_SUBMIT_PAGE
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.extensions.browse
import com.aurora.extensions.hide
import com.aurora.extensions.isHuawei
import com.aurora.extensions.px
import com.aurora.extensions.requiresObbDir
import com.aurora.extensions.runOnUiThread
@@ -397,6 +400,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.installerEvent.collect { onEvent(it) }
}
}
override fun onResume() {
@@ -525,6 +529,24 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
private fun purchase(app: App) {
/**
* MicroG Fragment Preconditions:
* 1. App being installed must have GSF dependency
* 2. It should be a Huawei device
* 3. Supported App Gallery should be available, i.e. v15.1.x or above
* 4. MicroG bundle should not be already installed
*
* TODO: Extract this trigger out of Vanilla & put in Huawei flavour
*/
if (
app.dependencies.dependentPackages.fastAny { it == PACKAGE_NAME_GMS } &&
isHuawei &&
PackageUtil.hasSupportedAppGallery(requireContext()) &&
!PackageUtil.isMicroGBundleInstalled(requireContext())
) {
return openGMSWarningFragment()
}
if (app.fileList.requiresObbDir()) {
if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) {
viewModel.download(app)
@@ -1036,7 +1058,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
private fun updateCompatibilityInfo() {
if (app.dependencies.dependentPackages.contains(PackageUtil.PACKAGE_NAME_GMS)) {
if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
viewModel.fetchPlexusReport(app.packageName)
binding.layoutDetailsCompatibility.txtGmsDependency.apply {

View File

@@ -0,0 +1,203 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.details
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.extensions.browse
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.Dash
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.databinding.FragmentDetailsMicrogBinding
import com.aurora.store.util.PackageUtil
import com.aurora.store.view.epoxy.views.EpoxyTextViewModel_
import com.aurora.store.view.epoxy.views.preference.DashViewModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.onboarding.MicroGViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@AndroidEntryPoint
class DetailsMicroGFragment : BaseFragment<FragmentDetailsMicrogBinding>() {
val microGViewModel: MicroGViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Toolbar
binding.toolbar.apply {
title = ""
setNavigationOnClickListener { findNavController().navigateUp() }
}
with(binding) {
// RecyclerView
epoxyRecycler.withModels {
setFilterDuplicates(true)
add(
EpoxyTextViewModel_()
.id("microg_title")
.title(getString(R.string.onboarding_title_gsf))
.size(32)
.style(R.style.AuroraTextStyle)
)
add(
EpoxyTextViewModel_()
.id("microg_desc")
.title(getString(R.string.onboarding_title_gsf_desc))
.size(18)
.style(R.style.AuroraTextStyle)
)
add(
EpoxyTextViewModel_()
.id("microg_desc")
.title(getString(R.string.onboarding_gms_missing))
.size(14)
.style(R.style.AuroraTextStyle)
)
add(
EpoxyTextViewModel_()
.id("microg_gms")
.title(getString(R.string.onboarding_gms_microg))
.size(14)
.style(R.style.AuroraTextStyle)
)
dashItems().forEach {
add(
DashViewModel_()
.id(it.id)
.dash(it)
.click { _ ->
requireContext().browse(it.url)
}
)
}
}
checkboxAgreement.setOnCheckedChangeListener { _, value ->
microGViewModel.markAgreement(value)
btnMicroG.isEnabled = value
}
btnMicroG.setOnClickListener { microGViewModel.downloadMicroG() }
btnSkip.setOnClickListener { findNavController().navigateUp() }
}
microGViewModel.download.filterNotNull().onEach {
when (it.downloadStatus) {
DownloadStatus.DOWNLOADING -> updateProgressBar(visible = true, it.progress)
DownloadStatus.FAILED -> updateProgressBar(visible = false, 0)
DownloadStatus.QUEUED -> updateProgressBar(visible = true, -1)
DownloadStatus.COMPLETED -> updateProgressBar(visible = true, -1)
else -> {}
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.installerEvent.collect { onEvent(it) }
}
}
private fun onEvent(event: Event) {
when (event) {
is InstallerEvent.Installed -> {
if (PackageUtil.isMicroGBundleInstalled(requireContext())) {
markInstallationComplete()
}
}
is InstallerEvent.Failed -> markInstallationFailed()
else -> {}
}
}
private fun updateProgressBar(visible: Boolean, downloadProgress: Int) {
with(binding.progressBar) {
if (visible) show() else hide()
isIndeterminate = downloadProgress == -1
progress = downloadProgress
}
}
private fun markInstallationComplete() {
with(binding) {
with(btnMicroG) {
isEnabled = true
text = getString(R.string.action_finish)
setOnClickListener { findNavController().navigateUp() }
}
checkboxAgreement.isEnabled = false
progressBar.hide()
}
}
private fun markInstallationFailed() {
with(binding) {
with(btnMicroG) {
isEnabled = false
text = getString(R.string.action_install)
}
checkboxAgreement.isChecked = false
progressBar.hide()
}
}
private fun dashItems(): List<Dash> {
return listOf(
Dash(
id = 2,
title = requireContext().getString(R.string.details_dev_website),
subtitle = requireContext().getString(R.string.microg_website),
icon = R.drawable.ic_network,
url = "https://microG.org"
),
Dash(
id = 4,
title = requireContext().getString(R.string.privacy_policy_title),
subtitle = requireContext().getString(R.string.microg_privacy_policy),
icon = R.drawable.ic_privacy,
url = "https://microg.org/privacy.html"
),
Dash(
id = 5,
title = requireContext().getString(R.string.menu_disclaimer),
subtitle = requireContext().getString(R.string.microg_license_agreement),
icon = R.drawable.ic_disclaimer,
url = "https://raw.githubusercontent.com/microg/GmsCore/refs/heads/master/LICENSE"
)
)
}
}

View File

@@ -4,17 +4,22 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.aurora.extensions.areNotificationsEnabled
import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.UpdateMode
import com.aurora.store.data.work.CacheWorker
import com.aurora.store.databinding.FragmentOnboardingBinding
@@ -25,15 +30,20 @@ import com.aurora.store.util.Preferences.PREFERENCE_INTRO
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO
import com.aurora.store.util.save
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.onboarding.MicroGViewModel
import com.aurora.store.viewmodel.onboarding.OnboardingPage
import com.aurora.store.viewmodel.onboarding.OnboardingViewModel
import com.google.android.material.tabs.TabLayoutMediator
import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
abstract class BaseFlavouredOnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
// Shared ViewModels
val microGViewModel: MicroGViewModel by activityViewModels()
val onboardingViewModel: OnboardingViewModel by activityViewModels()
val viewModel: OnboardingViewModel by viewModels()
var lastPosition = 0
var currentPage = 0
override fun onCreateView(
inflater: LayoutInflater,
@@ -64,52 +74,141 @@ abstract class BaseFlavouredOnboardingFragment : BaseFragment<FragmentOnboarding
if (PackageUtil.isTv(view.context)) finishOnboarding()
}
// ViewPager2
binding.viewpager2.apply {
adapter = PagerAdapter(
childFragmentManager,
viewLifecycleOwner.lifecycle,
onboardingPages()
)
isUserInputEnabled = false
setCurrentItem(0, true)
registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
activity?.runOnUiThread {
lastPosition = position
refreshButtonState()
val pages = onboardingPages()
with(binding) {
// ViewPager2
with(viewpager2) {
adapter = PagerAdapter(
childFragmentManager,
viewLifecycleOwner.lifecycle,
pages
)
isUserInputEnabled = false
setCurrentItem(0, true)
registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
onboardingViewModel.setCurrentPage(
when (position) {
0 -> OnboardingPage.WELCOME
1 -> OnboardingPage.PERMISSIONS
2 -> OnboardingPage.GSF
else -> OnboardingPage.WELCOME
}
)
currentPage = position
}
})
}
TabLayoutMediator(tabLayout, viewpager2, true) { tab, position ->
tab.text = (position + 1).toString()
}.attach()
}
updateBackwardButton(false)
updateForwardButton(true)
viewLifecycleOwner.lifecycleScope.launch {
// Combine both relevant flows
combine(
microGViewModel.checked,
onboardingViewModel.currentPage
) { isChecked, page -> isChecked to page }.collect { (isChecked, page) ->
when (page) {
OnboardingPage.WELCOME -> {
updateBackwardButton(enabled = false)
updateForwardButton(enabled = true)
}
OnboardingPage.PERMISSIONS -> {
updateBackwardButton(enabled = true)
val isLastPage = pages.size == 2
updateForwardButton(
enabled = true,
resId = if (isLastPage) R.string.action_finish else R.string.action_next,
if (isLastPage) {
{ finishOnboarding() }
} else {
null
}
)
}
OnboardingPage.GSF -> {
updateBackwardButton(enabled = true)
if (isChecked) {
val isInstalled = PackageUtil.isMicroGBundleInstalled(requireContext())
updateForwardButton(
enabled = isInstalled,
resId = R.string.action_finish,
action = if (isInstalled) {
{ finishOnboarding() }
} else {
null
}
)
} else {
updateForwardButton(
enabled = false,
resId = R.string.action_finish,
action = { finishOnboarding() }
)
}
}
}
})
}
}
TabLayoutMediator(binding.tabLayout, binding.viewpager2, true) { tab, position ->
tab.text = (position + 1).toString()
}.attach()
binding.btnForward.setOnClickListener {
binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem + 1, true)
}
binding.btnBackward.setOnClickListener {
binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem - 1, true)
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.installerEvent.collect { onEvent(it) }
}
}
fun refreshButtonState() {
binding.btnBackward.isEnabled = lastPosition != 0
binding.btnForward.isEnabled = lastPosition != 1
private fun updateBackwardButton(
enabled: Boolean = true
) {
with(binding.btnBackward) {
isEnabled = enabled
text = getString(R.string.action_back)
setOnClickListener({
binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem - 1, true)
})
}
}
private fun updateForwardButton(
enabled: Boolean = true,
@StringRes resId: Int = R.string.action_next,
action: ((View) -> Unit)? = null
) {
with(binding.btnForward) {
isEnabled = enabled
text = getString(resId)
setOnClickListener(action ?: {
binding.viewpager2.setCurrentItem(binding.viewpager2.currentItem + 1, true)
})
}
}
private fun onEvent(event: Event) {
when (event) {
is InstallerEvent.Installed -> {
if (PackageUtil.isMicroGBundleInstalled(requireContext())) {
with(binding.btnForward) {
isEnabled = true
text = getString(R.string.action_finish)
setOnClickListener {
finishOnboarding()
}
}
}
}
else -> {
if (lastPosition == 1) {
binding.btnForward.text = getString(R.string.action_finish)
binding.btnForward.isEnabled = true
binding.btnForward.setOnClickListener { finishOnboarding() }
} else {
binding.btnForward.text = getString(R.string.action_next)
binding.btnForward.setOnClickListener {
binding.viewpager2.setCurrentItem(
binding.viewpager2.currentItem + 1, true
)
}
}
}
@@ -136,7 +235,7 @@ abstract class BaseFlavouredOnboardingFragment : BaseFragment<FragmentOnboarding
save(PREFERENCE_UPDATES_AUTO, updateMode.ordinal)
viewModel.updateHelper.scheduleAutomatedCheck()
onboardingViewModel.updateHelper.scheduleAutomatedCheck()
}
internal class PagerAdapter(

View File

@@ -66,7 +66,7 @@ class NetworkPreference : BasePreferenceFragment() {
}
findPreference<SwitchPreferenceCompat>(PREFERENCE_MICROG_AUTH)?.isEnabled =
PackageUtil.hasSupportedMicroG(requireContext())
PackageUtil.hasSupportedMicroGVariant(requireContext())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -16,6 +16,7 @@ import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
import com.aurora.extensions.getPackageName
import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.navigate
@@ -27,7 +28,6 @@ import com.aurora.store.databinding.FragmentSplashBinding
import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
@@ -45,7 +45,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment<FragmentSplashBinding>
val viewModel: AuthViewModel by activityViewModels()
val canLoginWithMicroG: Boolean
get() = isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext()) &&
get() = isMAndAbove && PackageUtil.hasSupportedMicroGVariant(requireContext()) &&
Preferences.getBoolean(requireContext(), PREFERENCE_MICROG_AUTH, true)
val startForAccount =
@@ -215,7 +215,7 @@ abstract class BaseFlavouredSplashFragment : BaseFragment<FragmentSplashBinding>
Account(accountName, GOOGLE_ACCOUNT_TYPE),
GOOGLE_PLAY_AUTH_TOKEN_TYPE,
bundleOf(
"overridePackage" to GOOGLE_PLAY_PACKAGE_NAME,
"overridePackage" to PACKAGE_NAME_PLAY_STORE,
"overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT)
),
requireActivity(),

View File

@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.viewmodel.onboarding
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.Constants.PACKAGE_NAME_GMS
import com.aurora.Constants.PACKAGE_NAME_PLAY_STORE
import com.aurora.gplayapi.data.models.PlayFile
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.room.suite.ExternalApk
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MicroGViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val downloadHelper: DownloadHelper
) : ViewModel() {
private val _packageNames = MutableSharedFlow<List<String>>()
val packageNames = _packageNames.asSharedFlow()
private val _checked = MutableStateFlow(false)
val checked: StateFlow<Boolean> = _checked
val download = combine(packageNames, downloadHelper.downloadsList) { apps, list ->
list.find { d -> apps.any { it == d.packageName } }
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val externalApks = listOf<ExternalApk>(
ExternalApk(
packageName = "com.google.android.gms",
versionCode = 244735012,
versionName = "v0.3.6.244735",
displayName = "microG Services",
iconURL = "https://raw.githubusercontent.com/microg/GmsCore/refs/heads/master/play-services-core/src/main/res/mipmap-xxxhdpi/ic_app.png",
developerName = "microG Team",
fileList = listOf(
PlayFile(
url = "https://github.com/microg/GmsCore/releases/download/v0.3.6.244735/com.google.android.gms-244735012-hw.apk",
name = "com.google.android.gms-244735012-hw.apk",
size = 32509431,
sha256 = "2f14df2974811b576bfafa6167a97e3b3032f2bd6e6ec3887a833fd2fa350dda"
)
)
),
ExternalApk(
packageName = "com.android.vending",
versionCode = 84022612,
versionName = "v0.3.6.244735",
displayName = "microG Companion",
iconURL = "https://raw.githubusercontent.com/microg/FakeStore/refs/heads/main/fake-store/src/main/res/mipmap-xxxhdpi/ic_app.png",
developerName = "microG Team",
fileList = listOf(
PlayFile(
url = "https://github.com/microg/GmsCore/releases/download/v0.3.6.244735/com.android.vending-84022612-hw.apk",
name = "com.android.vending-84022612-hw.apk",
size = 3915551,
sha256 = "6835b09016cef0fc3469b4a36b1720427ad3f81161cf20b188f0dadb5f8594e1"
)
)
)
)
fun markAgreement(checked: Boolean) {
viewModelScope.launch {
_checked.emit(checked)
}
}
fun downloadMicroG() {
viewModelScope.launch {
try {
_packageNames.emit(
listOf(
PACKAGE_NAME_GMS,
PACKAGE_NAME_PLAY_STORE
)
)
externalApks.forEach {
// Enqueue download only if not already installed
if (!it.isInstalled(context)) {
downloadHelper.enqueueStandalone(it)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View File

@@ -0,0 +1,7 @@
package com.aurora.store.viewmodel.onboarding
enum class OnboardingPage {
WELCOME,
PERMISSIONS,
GSF,
}

View File

@@ -6,9 +6,23 @@
package com.aurora.store.viewmodel.onboarding
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.helper.UpdateHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class OnboardingViewModel @Inject constructor(val updateHelper: UpdateHelper) : ViewModel()
class OnboardingViewModel @Inject constructor(val updateHelper: UpdateHelper) : ViewModel() {
private val _page = MutableStateFlow<OnboardingPage>(OnboardingPage.WELCOME)
val currentPage: StateFlow<OnboardingPage> = _page
fun setCurrentPage(page: OnboardingPage) {
viewModelScope.launch {
_page.emit(page)
}
}
}