AppDetailsFragment: Implement compatibility section using plexus

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-12-24 13:43:23 +07:00
parent 42ea3a8af9
commit 516af19172
11 changed files with 207 additions and 12 deletions

View File

@@ -0,0 +1,42 @@
package com.aurora.store.data.model
import androidx.annotation.StringRes
import com.aurora.store.R
import com.google.gson.annotations.SerializedName
data class PlexusReport(
@SerializedName("data")
val report: Data?
)
data class Data(
val name: String,
val scores: Scores,
@SerializedName("updated_at")
val updatedAt: String
)
data class Scores(
@SerializedName("micro_g")
val microG: Rating,
@SerializedName("native")
val aosp: Rating
)
data class Rating(
val denominator: Float,
val numerator: Float,
val rating_type: String,
val total_count: Long
) {
private val fraction get() = numerator / denominator
@get:StringRes
val status: Int
get() = when {
fraction == 0F -> R.string.details_compatibility_status_unknown
fraction >= 0.90 -> R.string.details_compatibility_status_compatible
fraction >= 0.50 -> R.string.details_compatibility_status_limited
else -> R.string.details_compatibility_status_unsupported
}
}

View File

@@ -90,6 +90,7 @@ object OkHttpClientModule {
.add("auroraoss.com", "sha256/mEflZT5enoR1FuXLgYYGqnVEoZvmf9c2bVBpiOjYQ0c=") // GTS Root R4
.add("*.exodus-privacy.eu.org", "sha256/C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=") // ISRG Root X1
.add("gitlab.com", "sha256/x4QzPSC810K5/cMjb05Qm4k3Bw5zBn4lTdO/nEW/Td4=") // USERTrust RSA Certification Authority
.add("plexus.techlore.tech", "sha256/C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M=") // ISRG Root X1
.build()
}

View File

@@ -50,7 +50,7 @@ object PackageUtil {
private const val TAG = "PackageUtil"
private const val PACKAGE_NAME_MICRO_G = "com.google.android.gms"
const val PACKAGE_NAME_GMS = "com.google.android.gms"
private const val VERSION_CODE_MICRO_G = 240913402
private const val VERSION_CODE_MICRO_G_HUAWEI = 240913007
@@ -66,15 +66,15 @@ object PackageUtil {
}
fun hasSupportedMicroG(context: Context): Boolean {
val isMicroG = CertUtil.isMicroGGMS(context, PACKAGE_NAME_MICRO_G)
val isMicroG = CertUtil.isMicroGGMS(context, PACKAGE_NAME_GMS)
// Do not proceed if MicroG variant is not installed
if (!isMicroG) return false
return if (isHuawei) {
isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G_HUAWEI)
isInstalled(context, PACKAGE_NAME_GMS, VERSION_CODE_MICRO_G_HUAWEI)
} else {
isInstalled(context, PACKAGE_NAME_MICRO_G, VERSION_CODE_MICRO_G)
isInstalled(context, PACKAGE_NAME_GMS, VERSION_CODE_MICRO_G)
}
}

View File

@@ -22,7 +22,9 @@ package com.aurora.store.view.custom.layouts
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import androidx.annotation.ColorRes
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.aurora.store.R
import com.aurora.store.databinding.ViewDevInfoBinding
@@ -87,4 +89,12 @@ class DevInfoLayout : RelativeLayout {
binding.txtSubtitle.isVisible = text != null
invalidate()
}
fun setTitleColor(@ColorRes color: Int) {
binding.txtTitle.setTextColor(ContextCompat.getColor(context, color))
}
fun setSubtitleColor(@ColorRes color: Int) {
binding.txtSubtitle.setTextColor(ContextCompat.getColor(context, color))
}
}

View File

@@ -205,6 +205,7 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
updateToolbar(app)
updateAppHeader(app) // Re-inflate the app details, as web data may vary.
updateExtraDetails(app)
updateCompatibilityInfo()
if (app.versionCode == 0) {
warnAppUnavailable(app)
@@ -331,6 +332,27 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
}
// Plexus Report
viewModel.plexusReport.onEach { plexusReport ->
if (plexusReport?.report?.scores == null) {
binding.layoutDetailsCompatibility.txtStatusMicroG.subTitle =
getString(R.string.details_compatibility_status_unknown)
binding.layoutDetailsCompatibility.txtStatusAosp.subTitle =
getString(R.string.details_compatibility_status_unknown)
} else {
binding.layoutDetailsCompatibility.headerCompatibility.addClickListener {
requireContext().browse("${Constants.PLEXUS_SEARCH_URL}${app.packageName}")
}
binding.layoutDetailsCompatibility.txtStatusMicroG.subTitle =
getString(plexusReport.report.scores.microG.status)
binding.layoutDetailsCompatibility.txtStatusAosp.subTitle =
getString(plexusReport.report.scores.microG.status)
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
// Beta program
viewLifecycleOwner.lifecycleScope.launch {
viewModel.testingProgramStatus.collect {
@@ -1013,6 +1035,26 @@ class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
}
}
private fun updateCompatibilityInfo() {
if (app.dependencies.dependentPackages.contains(PackageUtil.PACKAGE_NAME_GMS)) {
viewModel.fetchPlexusReport(app.packageName)
binding.layoutDetailsCompatibility.txtGmsDependency.apply {
title = getString(R.string.details_compatibility_gms_required_title)
subTitle = getString(R.string.details_compatibility_gms_required_subtitle)
setTitleColor(R.color.colorRed)
}
binding.layoutDetailsCompatibility.compatibilityStatusLayout.isVisible = true
} else {
binding.layoutDetailsCompatibility.txtGmsDependency.apply {
title = getString(R.string.details_compatibility_gms_not_required_title)
subTitle = getString(R.string.details_compatibility_gms_not_required_subtitle)
setTitleColor(R.color.colorGreen)
}
}
}
private fun warnAppUnavailable(app: App) {
AuroraApp.events.send(InstallerEvent.Failed(app.packageName).apply {
error = getString(R.string.status_unavailable)

View File

@@ -13,16 +13,16 @@ import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.ReviewsHelper
import com.aurora.gplayapi.helpers.web.WebDataSafetyHelper
import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.AuroraApp
import com.aurora.store.BuildConfig
import com.aurora.store.R
import com.aurora.store.data.helper.DownloadHelper
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.PlexusReport
import com.aurora.store.data.model.Report
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.util.PackageUtil
import com.google.gson.GsonBuilder
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.lang.reflect.Modifier
import javax.inject.Inject
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@@ -47,7 +46,8 @@ class AppDetailsViewModel @Inject constructor(
private val webDataSafetyHelper: WebDataSafetyHelper,
private val downloadHelper: DownloadHelper,
private val favouriteDao: FavouriteDao,
private val httpClient: IHttpClient
private val httpClient: IHttpClient,
private val gson: Gson
) : ViewModel() {
private val TAG = AppDetailsViewModel::class.java.simpleName
@@ -72,6 +72,10 @@ class AppDetailsViewModel @Inject constructor(
private val _exodusReport = MutableSharedFlow<Report?>()
val exodusReport = _exodusReport.asSharedFlow()
private val plexusReportStash = mutableMapOf<String, PlexusReport?>()
private val _plexusReport = MutableSharedFlow<PlexusReport?>()
val plexusReport = _plexusReport.asSharedFlow()
private val testProgramStatusStash = mutableMapOf<String, TestingProgramStatus?>()
private val _testingProgramStatus = MutableSharedFlow<TestingProgramStatus?>()
val testingProgramStatus = _testingProgramStatus.asSharedFlow()
@@ -147,6 +151,24 @@ class AppDetailsViewModel @Inject constructor(
}
}
fun fetchPlexusReport(packageName: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
val plexusReport = plexusReportStash.getOrPut(packageName) {
val url = "${Constants.PLEXUS_API_URL}/${packageName}/?scores=true"
val playResponse = httpClient.get(url, emptyMap())
gson.fromJson(String(playResponse.responseBytes), PlexusReport::class.java)
}
_plexusReport.emit(plexusReport)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch compatibility report", exception)
plexusReportStash[packageName] = null
_plexusReport.emit(null)
}
}
}
fun fetchTestingProgramStatus(packageName: String, subscribe: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
try {
@@ -258,10 +280,6 @@ class AppDetailsViewModel @Inject constructor(
private fun parseExodusResponse(response: String, packageName: String): List<Report> {
try {
val gson = GsonBuilder()
.excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC)
.create()
val jsonObject = JSONObject(response)
val exodusObject = jsonObject.getJSONObject(packageName)
val exodusReport: ExodusReport = gson.fromJson(