view: details: Drop unused custom epoxy views

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-05-17 17:14:34 +08:00
parent 793db41bb7
commit e9f6bb42e9
8 changed files with 0 additions and 1423 deletions

View File

@@ -1,10 +0,0 @@
package com.aurora.store.data.model
import androidx.annotation.DrawableRes
import com.aurora.store.R
data class PermissionGroupInfo(
val name: String = "unknown",
@DrawableRes var icon: Int = R.drawable.ic_permission_unknown,
val label: String = "UNDEFINED"
)

View File

@@ -1,55 +0,0 @@
/*
* 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.custom
import android.content.Context
import android.util.AttributeSet
import android.widget.RelativeLayout
import com.aurora.store.R
import com.aurora.store.databinding.ViewRatingBinding
class RatingView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
private lateinit var binding: ViewRatingBinding
var number = 0
var max = 0
var rating = 0
constructor(context: Context, number: Int, max: Int, rating: Int) : this(context) {
this.number = number
this.max = max
this.rating = rating
init(context)
}
private fun init(context: Context) {
val view = inflate(context, R.layout.view_rating, this)
binding = ViewRatingBinding.bind(view)
binding.avgNum.text = number.toString()
binding.avgRating.max = max
binding.avgRating.progress = rating
}
}

View File

@@ -1,155 +0,0 @@
/*
* 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.custom.layouts
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PermissionInfo
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.aurora.extensions.showDialog
import com.aurora.store.R
import com.aurora.store.data.model.PermissionGroupInfo
import java.util.Locale
class PermissionGroup @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var permissionGroupInfo: PermissionGroupInfo
private lateinit var packageManager: PackageManager
private val permissionMap: MutableMap<String, String> = HashMap()
constructor(context: Context?, permissionGroupInfo: PermissionGroupInfo) : this(context) {
if (context != null) {
inflate(context, R.layout.layout_permission, this)
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
)
this.packageManager = context.packageManager
this.permissionGroupInfo = permissionGroupInfo
val imageView = findViewById<ImageView>(R.id.img)
imageView.setImageDrawable(getPermissionGroupIcon(permissionGroupInfo))
}
}
fun addPermission(permissionInfo: PermissionInfo, currentPerms: List<String> = emptyList()) {
val title = permissionInfo.loadLabel(packageManager)
val description = permissionInfo.loadDescription(packageManager)
permissionMap[getReadableLabel(title.toString(), permissionInfo.packageName)] =
if (description.isNullOrEmpty())
"No description"
else
description.toString()
val permissionLabels: List<String> = ArrayList(permissionMap.keys)
val permissionLabelsView = findViewById<LinearLayout>(R.id.permission_labels)
permissionLabelsView.removeAllViews()
permissionLabels
.filter { it.isNotEmpty() }
.sortedBy { it }
.forEach {
addPermissionLabel(
permissionLabelsView,
it,
permissionMap[it],
if (currentPerms.isNotEmpty()) permissionInfo.name !in currentPerms else false
)
}
}
private fun addPermissionLabel(
permissionLabelsView: LinearLayout,
label: String,
description: String?,
isNewPerm: Boolean = false
) {
val textView = TextView(context)
textView.text = label
if (isNewPerm) textView.setTextColor(ContextCompat.getColor(context, R.color.colorGreen))
textView.setOnClickListener {
var title: String = permissionGroupInfo.label
if (title.contains("UNDEFINED")) {
title = "Android"
}
title = title.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
context.showDialog(title, description)
}
permissionLabelsView.addView(textView)
}
private fun getPermissionGroupIcon(permissionGroupInfo: PermissionGroupInfo): Drawable? {
return ContextCompat.getDrawable(context, permissionGroupInfo.icon)
}
private fun getReadableLabel(label: String, packageName: String): String {
val prefixes: MutableList<String> = mutableListOf(
"android",
packageName
)
if (label.contains("UNDEFINED")) {
return "Android"
}
prefixes
.map { "$it.permission." }
.forEach {
if (label.startsWith(it)) {
return it.replace(it, "")
.replace("_", " ")
.lowercase(Locale.getDefault())
.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
}
}
return label.replaceFirstChar {
it.titlecase(Locale.getDefault())
}
}
}

View File

@@ -1,900 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
* Copyright (C) 2022, The Calyx Institute
*
* 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.animation.ObjectAnimator
import android.content.ActivityNotFoundException
import android.content.Intent
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.RelativeLayout
import androidx.compose.ui.util.fastAny
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import coil3.asDrawable
import coil3.load
import coil3.request.error
import coil3.request.transformations
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.px
import com.aurora.extensions.requiresObbDir
import com.aurora.extensions.share
import com.aurora.extensions.show
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Review
import com.aurora.gplayapi.data.models.StreamBundle
import com.aurora.gplayapi.data.models.StreamCluster
import com.aurora.gplayapi.data.models.datasafety.EntryType
import com.aurora.store.AppStreamStash
import com.aurora.store.AuroraApp
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.data.event.Event
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.model.PermissionType
import com.aurora.store.data.model.ViewState
import com.aurora.store.data.model.ViewState.Loading.getDataAs
import com.aurora.store.databinding.FragmentDetailsBinding
import com.aurora.store.util.CertUtil
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.FlavouredUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.ShortcutManagerUtil
import com.aurora.store.view.custom.RatingView
import com.aurora.store.view.epoxy.controller.DetailsCarouselController
import com.aurora.store.view.epoxy.controller.GenericCarouselController
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.details.AppDetailsViewModel
import com.aurora.store.viewmodel.details.DetailsClusterViewModel
import com.jakewharton.processphoenix.ProcessPhoenix
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.util.Locale
import com.aurora.gplayapi.data.models.datasafety.Report as DataSafetyReport
@AndroidEntryPoint
class AppDetailsFragment : BaseFragment<FragmentDetailsBinding>() {
private val viewModel: AppDetailsViewModel by activityViewModels()
private val detailsClusterViewModel: DetailsClusterViewModel by activityViewModels()
private val args: AppDetailsFragmentArgs by navArgs()
private lateinit var app: App
private lateinit var iconDrawable: Drawable
private var streamBundle: StreamBundle? = StreamBundle()
private val isExternal get() = activity?.intent?.action != Intent.ACTION_MAIN
private val isExtendedUpdateEnabled: Boolean
get() = Preferences.getBoolean(requireContext(), PREFERENCE_UPDATES_EXTENDED)
private fun onEvent(event: Event) {
when (event) {
is InstallerEvent.Installed -> {
if (app.packageName == event.packageName) {
checkAndSetupInstall()
}
}
is InstallerEvent.Uninstalled -> {
if (app.packageName == event.packageName) {
checkAndSetupInstall()
}
}
is InstallerEvent.Failed -> {
if (app.packageName == event.packageName) {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToInstallErrorDialogSheet(
app,
event.packageName,
event.error,
event.extra
)
)
}
}
is InstallerEvent.Installing -> {
if (event.packageName == app.packageName) {
checkAndSetupInstall()
}
}
else -> {
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
app = args.app ?: App(
packageName = args.packageName,
isInstalled = PackageUtil.isInstalled(requireContext(), args.packageName)
)
// Show the basic app details, while the rest of the data is being fetched
updateAppHeader(app, false)
// Toolbar
updateToolbar(app)
viewModel.download.filterNotNull().onEach {
when (it.downloadStatus) {
DownloadStatus.QUEUED,
DownloadStatus.DOWNLOADING -> {
updateProgress(it.progress)
binding.layoutDetailsApp.btnPrimaryAction.apply {
isEnabled = false
text = getString(R.string.action_open)
setOnClickListener(null)
}
binding.layoutDetailsApp.btnSecondaryAction.apply {
isEnabled = true
text = getString(R.string.action_cancel)
setOnClickListener { viewModel.cancelDownload(app) }
}
}
DownloadStatus.VERIFYING -> {
transformIcon(true)
binding.layoutDetailsApp.btnPrimaryAction.apply {
isEnabled = false
text = getString(R.string.action_open)
setOnClickListener(null)
}
binding.layoutDetailsApp.btnSecondaryAction.apply {
isEnabled = false
text = getString(R.string.action_cancel)
setOnClickListener(null)
}
}
else -> checkAndSetupInstall()
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
// Exodus Privacy Report
viewLifecycleOwner.lifecycleScope.launch {
viewModel.exodusReport.collect { report ->
if (report == null) {
binding.layoutDetailsPrivacy.txtStatus.text =
getString(R.string.failed_to_fetch_report)
return@collect
}
if (report.trackers.isNotEmpty()) {
binding.layoutDetailsPrivacy.txtStatus.apply {
setTextColor(
ContextCompat.getColor(
requireContext(),
if (report.trackers.size > 4)
R.color.colorRed
else
R.color.colorOrange
)
)
text =
"${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}"
}
} else {
binding.layoutDetailsPrivacy.txtStatus.apply {
setTextColor(ContextCompat.getColor(requireContext(), R.color.colorGreen))
text = getString(R.string.exodus_no_tracker)
}
}
}
}
// Beta program
viewLifecycleOwner.lifecycleScope.launch {
viewModel.testingProgramStatus.collect {
if (it != null) {
binding.layoutDetailsBeta.btnBetaAction.apply {
isEnabled = true
}
if (it.subscribed) {
updateBetaActions(true)
}
if (it.unsubscribed) {
updateBetaActions(false)
}
} else {
app.testingProgram?.let { testingProgram ->
updateBetaActions(testingProgram.isSubscribed)
toast(getString(R.string.details_beta_delay))
}
}
}
}
// Favorites
viewLifecycleOwner.lifecycleScope.launch {
viewModel.favourite.collect {
if (it) {
binding.toolbar.menu?.findItem(R.id.action_favourite)
?.setIcon(R.drawable.ic_favorite_checked)
} else {
binding.toolbar.menu?.findItem(R.id.action_favourite)
?.setIcon(R.drawable.ic_favorite_unchecked)
}
}
}
// Misc Bindings
binding.layoutDetailsPrivacy.btnRequestAnalysis.apply {
setOnClickListener {
it.context.browse("${EXODUS_SUBMIT_PAGE}${app.packageName}")
}
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.busEvent.collect { onEvent(it) }
}
viewLifecycleOwner.lifecycleScope.launch {
AuroraApp.events.installerEvent.collect { onEvent(it) }
}
}
override fun onResume() {
checkAndSetupInstall()
super.onResume()
}
private fun updateToolbar(app: App) {
binding.toolbar.apply {
elevation = 0f
navigationIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_arrow_back)
setNavigationOnClickListener {
if (isExternal) {
activity?.finish()
} else {
findNavController().navigateUp()
}
}
if (menu.size() == 0) {
// Inflate Menu only if it is not already inflated
inflateMenu(R.menu.menu_details)
}
// Adjust Menu Items
menu.let {
it.findItem(R.id.action_home_screen)?.isVisible =
app.isInstalled && ShortcutManagerUtil.canPinShortcut(
requireContext(),
app.packageName
)
it.findItem(R.id.menu_app_settings)?.isVisible = app.isInstalled
}
// Set Menu Item Clicks
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_home_screen -> {
ShortcutManagerUtil.requestPinShortcut(
requireContext(),
app.packageName
)
}
R.id.action_share -> {
requireContext().share(app)
}
R.id.action_favourite -> {
viewModel.toggleFavourite(app)
}
R.id.menu_download_manual -> {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToManualDownloadSheet(app)
)
}
R.id.menu_app_settings -> {
val intent = Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", app.packageName, null)
}
startActivity(intent)
}
R.id.action_playstore -> {
requireContext().browse("${Constants.SHARE_URL}${app.packageName}")
}
}
true
}
}
}
private fun updateAppHeader(app: App, isFullApp: Boolean = true) {
binding.layoutDetailsApp.apply {
val fallbackDrawable = if (app.iconArtwork.url.isNotBlank())
ContextCompat.getDrawable(requireContext(), R.drawable.bg_placeholder)
else
PackageUtil.getIconDrawableForPackage(requireContext(), app.packageName)
imgIcon.load(app.iconArtwork.url) {
error(fallbackDrawable)
transformations(RoundedCornersTransformation(32F))
listener { _, result ->
result.image.asDrawable(resources).let { iconDrawable = it }
}
}
packageName.text = app.packageName
txtLine1.text = app.displayName
txtLine2.text = app.developerName
txtLine3.text = getString(R.string.version, app.versionName, app.versionCode)
txtLine2.setOnClickListener {
findNavController().navigate(
AppDetailsFragmentDirections
.actionAppDetailsFragmentToDevAppsFragment(app.developerName)
)
}
// Do not show tags for web apps or unknown apps
if (isFullApp) {
val tags = mutableSetOf<String>().apply {
if (app.isFree) {
add(getString(R.string.details_free))
} else {
add(getString(R.string.details_paid))
}
if (app.containsAds) {
add(getString(R.string.details_contains_ads))
} else {
add(getString(R.string.details_no_ads))
}
}
txtLine4.text = tags.joinToString(separator = "")
}
}
}
private fun purchase(app: App) {
if (
app.dependencies.dependentPackages.fastAny { it == PACKAGE_NAME_GMS } &&
FlavouredUtil.promptMicroGInstall(requireContext())
) {
return openGMSWarningFragment()
}
if (app.fileList.requiresObbDir()) {
if (permissionProvider.isGranted(PermissionType.STORAGE_MANAGER)) {
viewModel.download(app)
} else {
permissionProvider.request(PermissionType.STORAGE_MANAGER) {
if (it) {
// Restart the app to ensure all permissions are granted
val intent = Intent(
requireContext(),
MainActivity::class.java
)
// Pass the packageName so we're back to same app
intent.putExtra("packageName", app.packageName)
ProcessPhoenix.triggerRebirth(requireContext(), intent)
}
}
}
} else {
viewModel.download(app)
}
}
private fun updateProgress(progress: Int) {
// No need to update progress if it is already 100% / completed
transformIcon(progress != 100)
if (progress == 100) return
binding.layoutDetailsApp.apply {
progressDownload.progress = progress
progressDownload.isIndeterminate = progress < 1
}
}
private fun transformIcon(ongoing: Boolean = false) {
if (::iconDrawable.isInitialized.not()) return
val imgIcon = binding.layoutDetailsApp.imgIcon
val progressDownload = binding.layoutDetailsApp.progressDownload
// Avoids flickering when the download is in progress
if (progressDownload.isShown && ongoing) return
if (!progressDownload.isShown && !ongoing) return
binding.layoutDetailsApp.progressDownload.isVisible = ongoing
val scaleFactor = if (ongoing) 0.75f else 1f
val scale = listOf(
ObjectAnimator.ofFloat(imgIcon, "scaleX", scaleFactor),
ObjectAnimator.ofFloat(imgIcon, "scaleY", scaleFactor)
)
scale.forEach { animation ->
animation.apply {
interpolator = AccelerateDecelerateInterpolator()
duration = 250
start()
}
}
imgIcon.load(iconDrawable) {
transformations(
if (ongoing) {
CircleCropTransformation()
} else {
RoundedCornersTransformation(8.px.toFloat())
}
)
}
}
private fun checkAndSetupInstall() {
app = app.copy(isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName))
// Setup primary and secondary action buttons
binding.layoutDetailsApp.btnPrimaryAction.isEnabled = true
binding.layoutDetailsApp.btnSecondaryAction.isEnabled = true
if (app.isInstalled) {
val isUpdatable =
PackageUtil.isUpdatable(requireContext(), app.packageName, app.versionCode.toLong())
val hasValidCert = app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
requireContext(),
app.packageName
)
}
if ((isUpdatable && hasValidCert) || (isUpdatable && isExtendedUpdateEnabled)) {
binding.layoutDetailsApp.txtLine3.text = getString(
R.string.version_update,
PackageUtil.getInstalledVersionName(requireContext(), app.packageName),
PackageUtil.getInstalledVersionCode(requireContext(), app.packageName),
app.versionName,
app.versionCode
)
binding.layoutDetailsApp.btnPrimaryAction.apply {
text = getString(R.string.action_update)
setOnClickListener {
if (app.versionCode == 0L) {
toast(R.string.toast_app_unavailable)
setText(R.string.status_unavailable)
} else {
purchase(app)
}
}
}
} else {
binding.layoutDetailsApp.apply {
txtLine3.text = getString(
R.string.version,
PackageUtil.getInstalledVersionName(requireContext(), app.packageName),
PackageUtil.getInstalledVersionCode(requireContext(), app.packageName)
)
btnPrimaryAction.apply {
val intent = PackageUtil.getLaunchIntent(requireContext(), app.packageName)
setText(R.string.action_open)
if (intent != null) {
isEnabled = true
setOnClickListener {
try {
startActivity(intent)
} catch (exception: ActivityNotFoundException) {
toast(getString(R.string.unable_to_open))
}
}
} else {
isEnabled = false
setOnClickListener(null)
}
}
}
}
binding.layoutDetailsApp.btnSecondaryAction.apply {
text = getString(R.string.action_uninstall)
setOnClickListener {
AppInstaller.uninstall(requireContext(), app.packageName)
}
}
} else {
if (PackageUtil.isArchived(requireContext(), app.packageName)) {
binding.layoutDetailsApp.btnPrimaryAction.text =
getString(R.string.action_unarchive)
} else {
if (app.isFree) {
binding.layoutDetailsApp.btnPrimaryAction.text =
getString(R.string.action_install)
} else {
binding.layoutDetailsApp.btnPrimaryAction.text = app.price
}
}
binding.layoutDetailsApp.btnPrimaryAction.setOnClickListener {
if (viewModel.authProvider.isAnonymous && !app.isFree) {
toast(R.string.toast_purchase_blocked)
return@setOnClickListener
} else if (app.versionCode == 0L) {
toast(R.string.toast_app_unavailable)
return@setOnClickListener
}
if (!permissionProvider.isGranted(PermissionType.INSTALL_UNKNOWN_APPS)) {
permissionProvider.request(PermissionType.INSTALL_UNKNOWN_APPS) {
if (it) {
purchase(app)
} else {
toast(R.string.permissions_denied)
}
}
} else {
purchase(app)
}
}
}
// Lay out the toolbar again
binding.toolbar.invalidateMenu()
if (app.isInstalled) {
binding.toolbar.menu.apply {
findItem(R.id.action_home_screen)?.isVisible =
ShortcutManagerUtil.canPinShortcut(requireContext(), app.packageName)
findItem(R.id.menu_app_settings)?.isVisible = true
}
} else {
binding.toolbar.menu.apply {
findItem(R.id.action_home_screen)?.isVisible = false
findItem(R.id.menu_app_settings)?.isVisible = false
}
}
// Restore icon and progress
updateProgress(100)
}
private fun updateExtraDetails(app: App) {
updateAppDescription(app)
updateAppRatingAndReviews(app)
updateUserReview()
updateAppDevInfo(app)
updateAppPermission(app)
// Allow users to handle beta subscriptions, if logged in by own account.
if (!viewModel.authProvider.isAnonymous) {
// Update app name to the testing program name, if subscribed
app.testingProgram?.let {
if (it.isAvailable && it.isSubscribed) {
binding.layoutDetailsApp.txtLine1.text = it.displayName
}
}
updateBetaSubscription(app)
}
checkAndSetupInstall()
}
private fun updateAppDescription(app: App) {
binding.layoutDetailDescription.apply {
val installs = CommonUtil.addDiPrefix(app.installs)
if (installs != null) {
txtInstalls.text = CommonUtil.addDiPrefix(app.installs)
} else {
txtInstalls.hide()
}
txtSize.text = CommonUtil.addSiPrefix(app.size)
txtRating.text = app.labeledRating
txtUpdated.text = app.updatedOn
txtDescription.text = HtmlCompat.fromHtml(
app.shortDescription,
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
)
app.changes.apply {
if (isEmpty()) {
txtChangelog.text = getString(R.string.details_changelog_unavailable)
} else {
txtChangelog.text = HtmlCompat.fromHtml(
this,
HtmlCompat.FROM_HTML_MODE_COMPACT
)
}
}
}
}
private fun updateAppRatingAndReviews(app: App) {
binding.layoutDetailsReview.apply {
var totalStars = 0L
totalStars += app.rating.oneStar
totalStars += app.rating.twoStar
totalStars += app.rating.threeStar
totalStars += app.rating.fourStar
totalStars += app.rating.fiveStar
avgRatingLayout.apply {
removeAllViews()
addView(addAvgReviews(5, totalStars, app.rating.fiveStar))
addView(addAvgReviews(4, totalStars, app.rating.fourStar))
addView(addAvgReviews(3, totalStars, app.rating.threeStar))
addView(addAvgReviews(2, totalStars, app.rating.twoStar))
addView(addAvgReviews(1, totalStars, app.rating.oneStar))
}
averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average)
txtReviewCount.text = app.rating.abbreviatedLabel
}
}
private fun updateUserReview(review: Review? = null) {
binding.layoutDetailsReview.apply {
layoutUserReview.isVisible = !viewModel.authProvider.isAnonymous
btnPostReview.setOnClickListener {
viewModel.postAppReview(
app.packageName,
Review(
title = inputTitle.text.toString(),
rating = userStars.rating.toInt(),
comment = inputReview.text.toString()
),
app.testingProgram?.isSubscribed ?: false
)
}
if (review != null) {
inputTitle.setText(review.title)
inputReview.setText(review.comment)
userStars.rating = review.rating.toFloat()
}
}
}
private fun updateAppDevInfo(app: App) {
binding.layoutDetailsDev.apply {
if (app.developerAddress.isNotEmpty()) {
devAddress.apply {
subTitle = HtmlCompat.fromHtml(
app.developerAddress,
HtmlCompat.FROM_HTML_MODE_LEGACY
).toString()
visibility = View.VISIBLE
}
}
if (app.developerWebsite.isNotEmpty()) {
devWeb.apply {
subTitle = app.developerWebsite
visibility = View.VISIBLE
}
}
if (app.developerEmail.isNotEmpty()) {
devMail.apply {
subTitle = app.developerEmail
visibility = View.VISIBLE
}
}
}
}
private fun updateBetaSubscription(app: App) {
binding.layoutDetailsBeta.apply {
app.testingProgram?.let { betaProgram ->
if (betaProgram.isAvailable) {
root.show()
updateBetaActions(betaProgram.isSubscribed)
if (betaProgram.isSubscribedAndInstalled) {
}
imgBeta.load(betaProgram.artwork.url) {
}
} else {
root.hide()
}
}
}
}
private fun updateAppStream(app: App) {
app.detailsStreamUrl?.let {
val carouselController =
DetailsCarouselController(object : GenericCarouselController.Callbacks {
override fun onHeaderClicked(streamCluster: StreamCluster) {
if (streamCluster.clusterBrowseUrl.isNotEmpty())
openStreamBrowseFragment(
streamCluster.clusterBrowseUrl,
streamCluster.clusterTitle
)
else
toast(getString(R.string.toast_page_unavailable))
}
override fun onClusterScrolled(streamCluster: StreamCluster) {
detailsClusterViewModel.observeCluster(
it,
streamCluster
)
}
override fun onAppClick(app: App) {
openDetailsFragment(app.packageName, app)
}
override fun onAppLongClick(app: App) {}
})
detailsClusterViewModel.liveData.observe(viewLifecycleOwner) { state ->
when (state) {
is ViewState.Success<*> -> {
val stash = state.getDataAs<AppStreamStash>()
streamBundle = stash[it]
carouselController.setData(streamBundle)
}
else -> {
}
}
}
binding.epoxyRecyclerStream.setController(carouselController)
detailsClusterViewModel.getStreamBundle(it)
}
}
private fun updateAppPermission(app: App) {
binding.layoutDetailsPermissions.apply {
headerPermission.addClickListener {
if (app.permissions.isNotEmpty()) {
findNavController().navigate(
AppDetailsFragmentDirections.actionAppDetailsFragmentToPermissionBottomSheet(
app
)
)
}
}
headerPermission.setSubTitle(
getString(
R.string.permissions_requested,
app.permissions.size
)
)
}
}
private fun updateBetaActions(isSubscribed: Boolean) {
binding.layoutDetailsBeta.apply {
if (isSubscribed) {
btnBetaAction.text = getString(R.string.action_leave)
headerRatingReviews.setSubTitle(getString(R.string.details_beta_subscribed))
} else {
btnBetaAction.text = getString(R.string.action_join)
headerRatingReviews.setSubTitle(getString(R.string.details_beta_available))
}
}
}
private fun updateDataSafetyViews(report: DataSafetyReport) {
report.entries.groupBy { it.type }.forEach { (type, entries) ->
when (type) {
EntryType.DATA_COLLECTED -> {
binding.layoutDetailsDataSafety.dataCollect.title = HtmlCompat.fromHtml(
entries.first().description,
HtmlCompat.FROM_HTML_MODE_COMPACT
).toString()
binding.layoutDetailsDataSafety.dataCollect.subTitle =
entries.first().subEntries.joinToString(", ") { it.name }.ifBlank { null }
}
EntryType.DATA_SHARED -> {
binding.layoutDetailsDataSafety.dataShare.title = HtmlCompat.fromHtml(
entries.first().description,
HtmlCompat.FROM_HTML_MODE_COMPACT
).toString()
binding.layoutDetailsDataSafety.dataShare.subTitle =
entries.first().subEntries.joinToString(", ") { it.name }.ifBlank { null }
}
else -> {}
}
}
}
private fun updateCompatibilityInfo() {
if (app.dependencies.dependentPackages.contains(PACKAGE_NAME_GMS)) {
binding.layoutDetailsCompatibility.txtGmsDependency.apply {
title = getString(R.string.details_compatibility_gms_required_title)
subTitle = getString(R.string.details_compatibility_gms_required_subtitle)
titleColor = ContextCompat.getColor(context, 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)
titleColor = ContextCompat.getColor(context, R.color.colorGreen)
}
}
}
private fun warnAppUnavailable(app: App) {
AuroraApp.events.send(InstallerEvent.Failed(app.packageName).apply {
error = getString(R.string.status_unavailable)
extra = getString(R.string.toast_app_unavailable)
})
}
/* App Review Helpers */
private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout {
return RatingView(requireContext(), number, max.toInt(), rating.toInt())
}
}

View File

@@ -1,128 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2019, 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.sheets
import android.content.pm.PackageManager
import android.content.pm.PermissionInfo
import android.os.Bundle
import android.view.View
import androidx.navigation.fragment.navArgs
import com.aurora.extensions.hide
import com.aurora.extensions.show
import com.aurora.store.R
import com.aurora.store.data.model.PermissionGroupInfo
import com.aurora.store.databinding.SheetPermissionsBinding
import com.aurora.store.view.custom.layouts.PermissionGroup
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PermissionBottomSheet : BaseDialogSheet<SheetPermissionsBinding>() {
private lateinit var packageManager: PackageManager
private lateinit var currentPerms: List<String>
private val args: PermissionBottomSheetArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
packageManager = requireContext().packageManager
currentPerms = try {
packageManager.getPackageInfo(args.app.packageName, PackageManager.GET_PERMISSIONS)
.requestedPermissions!!.toList()
} catch (_: Exception) {
emptyList()
}
val permissionGroupWidgets: MutableMap<String, PermissionGroup?> = mutableMapOf()
for (permissionName in args.app.permissions) {
val permissionInfo = getPermissionInfo(permissionName) ?: continue
val permissionGroupInfo = getPermissionGroupInfo(permissionInfo)
var permissionGroup: PermissionGroup?
if (permissionGroupWidgets.containsKey(permissionGroupInfo.name)) {
permissionGroup = permissionGroupWidgets[permissionGroupInfo.name]
} else {
permissionGroup = PermissionGroup(requireContext(), permissionGroupInfo)
permissionGroupWidgets[permissionGroupInfo.name] = permissionGroup
}
permissionGroup?.addPermission(permissionInfo, currentPerms)
}
binding.permissionsContainer.removeAllViews()
val permissionGroupLabels: List<String> = ArrayList(permissionGroupWidgets.keys)
permissionGroupLabels.sortedBy { it }.forEach {
binding.permissionsContainer.addView(permissionGroupWidgets[it])
}
if (permissionGroupLabels.isEmpty()) {
binding.permissionsNone.show()
} else {
binding.permissionsNone.hide()
}
}
private fun getPermissionInfo(permissionName: String): PermissionInfo? {
return try {
packageManager.getPermissionInfo(permissionName, 0)
} catch (_: PackageManager.NameNotFoundException) {
null
}
}
private fun getPermissionGroupInfo(permissionInfo: PermissionInfo): PermissionGroupInfo {
val permissionGroup = permissionInfo.group
val permissionGroupInfo: PermissionGroupInfo =
if (permissionGroup == null) {
getFakePermissionGroupInfo(permissionInfo.packageName)
} else {
try {
val platformGroup = packageManager.getPermissionGroupInfo(permissionGroup, 0)
PermissionGroupInfo(
platformGroup.name,
platformGroup.icon,
platformGroup.loadLabel(packageManager).toString()
)
} catch (_: PackageManager.NameNotFoundException) {
getFakePermissionGroupInfo(permissionInfo.packageName)
}
}
if (permissionGroupInfo.icon == 0) {
permissionGroupInfo.icon = R.drawable.ic_permission_android
}
return permissionGroupInfo
}
private fun getFakePermissionGroupInfo(packageName: String): PermissionGroupInfo {
return when (packageName) {
"android" -> PermissionGroupInfo("android", R.drawable.ic_permission_android)
"com.google.android.gsf",
"com.android.vending" -> PermissionGroupInfo("google", R.drawable.ic_permission_google)
else -> PermissionGroupInfo()
}
}
}

View File

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
~
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/permissions_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/margin_small"
android:paddingHorizontal="@dimen/margin_normal"
android:text="@string/details_permission"
android:textAppearance="@style/TextAppearance.Aurora.Title" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/padding_medium"
android:scrollbars="none">
<LinearLayout
android:id="@+id/permissions_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/permissions_container_widgets"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/permissions_none"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/details_no_permission"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
android:visibility="gone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</FrameLayout>

View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
~
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/margin_normal"
android:layout_marginEnd="@dimen/margin_small">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/avg_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:textAppearance="@style/TextAppearance.Aurora.Line1"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
tools:text="4.0" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/avg_rating"
style="@style/Widget.Material3.LinearProgressIndicator.Legacy"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/avg_num"
android:indeterminate="false"
app:indicatorColor="?colorPrimary"
app:trackColor="?colorControlNormal"
app:trackCornerRadius="10dp"
app:trackThickness="8dp"
tools:progress="40" />
</RelativeLayout>

View File

@@ -23,9 +23,6 @@
android:id="@+id/mobile_navigation"
app:startDestination="@id/splashFragment">
<action
android:id="@+id/action_global_appDetailsFragment"
app:destination="@id/appDetailsFragment" />
<action
android:id="@+id/action_global_categoryBrowseFragment"
app:destination="@id/categoryBrowseFragment" />
@@ -147,36 +144,6 @@
android:id="@+id/action_downloadFragment_to_downloadMenuSheet"
app:destination="@id/downloadMenuSheet" />
</fragment>
<fragment
android:id="@+id/appDetailsFragment"
android:name="com.aurora.store.view.ui.details.AppDetailsFragment"
tools:layout="@layout/fragment_details">
<argument
android:name="packageName"
app:argType="string" />
<argument
android:name="app"
android:defaultValue="@null"
app:argType="com.aurora.gplayapi.data.models.App"
app:nullable="true" />
<action
android:id="@+id/action_appDetailsFragment_to_devAppsFragment"
app:destination="@id/devAppsFragment" />
<argument
android:name="app"
android:defaultValue="@null"
app:argType="com.aurora.gplayapi.data.models.App"
app:nullable="true" />
<action
android:id="@+id/action_appDetailsFragment_to_permissionBottomSheet"
app:destination="@id/permissionBottomSheet" />
<action
android:id="@+id/action_appDetailsFragment_to_installErrorDialogSheet"
app:destination="@id/installErrorDialogSheet" />
<action
android:id="@+id/action_appDetailsFragment_to_gmsWarnFragment"
app:destination="@id/detailsMicroGFragment" />
</fragment>
<fragment
android:id="@+id/categoryBrowseFragment"
android:name="com.aurora.store.view.ui.commons.CategoryBrowseFragment"
@@ -275,12 +242,6 @@
app:launchSingleTop="true"
app:popUpTo="@id/mobile_navigation"
app:popUpToInclusive="true" />
<action
android:id="@+id/action_splashFragment_to_appDetailsFragment"
app:destination="@id/appDetailsFragment"
app:launchSingleTop="true"
app:popUpTo="@id/mobile_navigation"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/googleFragment"
@@ -346,14 +307,6 @@
android:name="com.aurora.store.view.ui.sheets.FilterSheet"
android:label="FilterSheet"
tools:layout="@layout/sheet_filter" />
<dialog
android:id="@+id/permissionBottomSheet"
android:name="com.aurora.store.view.ui.sheets.PermissionBottomSheet"
tools:layout="@layout/sheet_permissions">
<argument
android:name="app"
app:argType="com.aurora.gplayapi.data.models.App" />
</dialog>
<dialog
android:id="@+id/downloadMenuSheet"
android:name="com.aurora.store.view.ui.sheets.DownloadMenuSheet"