Allow huawei variants to install microG & support silent installs.
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class AuthProvider @Inject constructor(
|
||||
return if (rawAuth.isNotBlank()) {
|
||||
json.decodeFromString<AuthData>(rawAuth)
|
||||
} else {
|
||||
null
|
||||
AuthData("BOGUS")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?: ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.aurora.store.viewmodel.onboarding
|
||||
|
||||
enum class OnboardingPage {
|
||||
WELCOME,
|
||||
PERMISSIONS,
|
||||
GSF,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user