Merge branch 'dev'

This commit is contained in:
Aayush Gupta
2025-05-28 20:52:48 +08:00
33 changed files with 654 additions and 448 deletions

5
.gitignore vendored
View File

@@ -36,9 +36,10 @@ gradle-app.setting
*.zip *.zip
app/release/* app/release/*
app/beta/*
app/alpha/*
app/nightly/* app/nightly/*
app/vanilla/*
app/huawei/*
app/preload/*
#Exclude signing configurations & keystore #Exclude signing configurations & keystore

View File

@@ -58,6 +58,8 @@ android {
testInstrumentationRunnerArguments["disableAnalytics"] = "true" testInstrumentationRunnerArguments["disableAnalytics"] = "true"
buildConfigField("String", "EXODUS_API_KEY", "\"bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae\"") buildConfigField("String", "EXODUS_API_KEY", "\"bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae\"")
missingDimensionStrategy("device", "vanilla")
} }
signingConfigs { signingConfigs {
@@ -108,12 +110,32 @@ android {
} }
} }
flavorDimensions += "device"
productFlavors {
create("vanilla") {
dimension = "device"
}
create("huawei") {
dimension = "device"
versionNameSuffix = "-hw"
}
// This flavor is only for preloaded devices / users who push the app to system
create("preload") {
dimension = "device"
versionNameSuffix = "-preload"
}
}
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
viewBinding = true viewBinding = true
aidl = true aidl = true
compose = true compose = true
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_21.toString() jvmTarget = JavaVersion.VERSION_21.toString()
} }
@@ -137,6 +159,15 @@ android {
} }
} }
androidComponents {
beforeVariants(selector().all()) { variant ->
val flavour = variant.flavorName
if ((flavour == "huawei" || flavour == "preload") && variant.buildType == "nightly") {
variant.enable = false
}
}
}
ksp { ksp {
arg("room.schemaLocation", "$projectDir/schemas") arg("room.schemaLocation", "$projectDir/schemas")
} }

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<lint> <lint>
<issue id="MissingTranslation" severity="ignore" /> <issue id="MissingTranslation" severity="ignore" />
<issue id="ProtectedPermissions" severity="ignore" />
<issue id="QueryAllPackagesPermission" severity="ignore" /> <issue id="QueryAllPackagesPermission" severity="ignore" />
<issue id="ScopedStorage" severity="ignore" /> <issue id="ScopedStorage" severity="ignore" />
</lint> </lint>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DELETE_PACKAGES" />
</manifest>

View File

@@ -0,0 +1,83 @@
/*
* 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.onboarding
import androidx.fragment.app.Fragment
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.aurora.store.util.save
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class OnboardingFragment : BaseFlavouredOnboardingFragment() {
override fun loadDefaultPreferences() {
/*Filters*/
save(PREFERENCE_FILTER_AURORA_ONLY, false)
save(PREFERENCE_FILTER_FDROID, true)
/*Network*/
save(PREFERENCE_DISPENSER_URLS, setOf())
save(PREFERENCE_VENDING_VERSION, 0)
/*Customization*/
save(PREFERENCE_THEME_STYLE, 0)
save(PREFERENCE_DEFAULT_SELECTED_TAB, 0)
save(PREFERENCE_FOR_YOU, true)
save(PREFERENCE_SIMILAR, true)
/*Installer*/
save(PREFERENCE_AUTO_DELETE, true)
save(PREFERENCE_INSTALLER_ID, 0)
/*Updates*/
save(PREFERENCE_UPDATES_EXTENDED, false)
save(PREFERENCE_UPDATES_CHECK_INTERVAL, 3)
}
override fun onboardingPages(): List<Fragment> {
return listOf(
WelcomeFragment(),
PermissionsFragment.newInstance()
)
}
override fun setupAutoUpdates() {
super.setupAutoUpdates()
// Remove super & implement variant logic here
}
override fun finishOnboarding() {
super.finishOnboarding()
// Remove super & implement variant logic here
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.splash
import com.aurora.extensions.hide
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SplashFragment : BaseFlavouredSplashFragment() {
override fun attachActions() {
super.attachActions()
binding.btnAnonymous.hide()
}
override fun resetActions() {
super.resetActions()
binding.btnAnonymous.hide()
}
}

View File

@@ -21,6 +21,7 @@ package com.aurora.extensions
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.UrlQuerySanitizer
import android.os.Bundle import android.os.Bundle
inline fun <reified T : Context> Context.newIntent(): Intent = inline fun <reified T : Context> Context.newIntent(): Intent =
@@ -40,3 +41,22 @@ inline fun <reified T : Context> Context.newIntent(flags: Int, extras: Bundle):
intent.putExtras(extras) intent.putExtras(extras)
return intent return intent
} }
fun Intent.getPackageName(fallbackBundle: Bundle? = null): String? {
return when (action) {
Intent.ACTION_VIEW -> {
data?.getQueryParameter("id")
}
Intent.ACTION_SEND -> {
val clipData = getStringExtra(Intent.EXTRA_TEXT).orEmpty()
UrlQuerySanitizer(clipData).getValue("id")
}
Intent.ACTION_SHOW_APP_INFO -> {
extras?.getString(Intent.EXTRA_PACKAGE_NAME)
}
else -> {
extras?.getString("packageName") ?: fallbackBundle?.getString("packageName")
}
}
}

View File

@@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.serializers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
import java.util.Locale
/**
* Serializer for [Locale] for working with Kotlin's Serialization library
*/
object LocaleSerializer : KSerializer<Locale> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Locale") {
element<String>("language")
element<String>("region")
}
override fun serialize(encoder: Encoder, value: Locale) {
return encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.language)
encodeStringElement(descriptor, 1, value.country)
}
}
override fun deserialize(decoder: Decoder): Locale {
return decoder.decodeStructure(descriptor) {
var language: String? = null
var region: String? = null
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> language = decodeStringElement(descriptor, 0)
1 -> region = decodeStringElement(descriptor, 1)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
require(!language.isNullOrBlank() && !region.isNullOrBlank())
Locale.Builder()
.setLanguage(language)
.setRegion(region)
.build()
}
}
}

View File

@@ -1,32 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.serializers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.Properties
/**
* Serializer for [Properties] for working with Kotlin's Serialization library
*/
object PropertiesSerializer : KSerializer<Properties> {
override val descriptor: SerialDescriptor =
MapSerializer(String.serializer(), String.serializer()).descriptor
override fun serialize(encoder: Encoder, value: Properties) {
val map = value.stringPropertyNames().associateWith { value.getProperty(it) }
MapSerializer(String.serializer(), String.serializer()).serialize(encoder, map)
}
override fun deserialize(decoder: Decoder): Properties {
val map = MapSerializer(String.serializer(), String.serializer()).deserialize(decoder)
return Properties().apply { putAll(map) }
}
}

View File

@@ -1,7 +1,7 @@
package com.aurora.store.module package com.aurora.store.module
import com.aurora.store.data.serializers.LocaleSerializer import com.aurora.gplayapi.data.serializers.LocaleSerializer
import com.aurora.store.data.serializers.PropertiesSerializer import com.aurora.gplayapi.data.serializers.PropertiesSerializer
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn

View File

@@ -24,6 +24,7 @@ import android.content.SharedPreferences
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.aurora.store.BuildConfig
object Preferences { object Preferences {
@@ -65,10 +66,17 @@ object Preferences {
private var prefs: SharedPreferences? = null private var prefs: SharedPreferences? = null
fun getPrefs(context: Context): SharedPreferences { fun getPrefs(context: Context): SharedPreferences {
if (prefs == null) { return when (BuildConfig.FLAVOR) {
prefs = PreferenceManager.getDefaultSharedPreferences(context) "vanilla" -> {
prefs ?: PreferenceManager.getDefaultSharedPreferences(context).also { prefs = it }
}
else -> {
val prefName = "${context.packageName}_${BuildConfig.FLAVOR}_preferences"
prefs ?: context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
.also { prefs = it }
}
} }
return prefs!!
} }
fun remove(context: Context, key: String) { fun remove(context: Context, key: String) {

View File

@@ -21,13 +21,14 @@ package com.aurora.store.view.epoxy.views.details
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.content.ContextCompat
import com.airbnb.epoxy.ModelProp import com.airbnb.epoxy.ModelProp
import com.airbnb.epoxy.ModelView import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled import com.airbnb.epoxy.OnViewRecycled
import com.aurora.store.R
import com.aurora.store.databinding.ViewInfoBinding import com.aurora.store.databinding.ViewInfoBinding
import com.aurora.store.view.epoxy.views.BaseModel import com.aurora.store.view.epoxy.views.BaseModel
import com.aurora.store.view.epoxy.views.BaseView import com.aurora.store.view.epoxy.views.BaseView
import java.util.Locale
@ModelView( @ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
@@ -41,16 +42,14 @@ class InfoView @JvmOverloads constructor(
@ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode]) @ModelProp(options = [ModelProp.Option.IgnoreRequireHashCode])
fun badge(info: Map.Entry<String, String>) { fun badge(info: Map.Entry<String, String>) {
binding.txtTitle.text = info.key binding.txtTitle.text = when (info.key) {
.replace("_", " ") "DOWNLOAD" -> ContextCompat.getString(context, R.string.app_info_downloads)
.lowercase(Locale.getDefault()) "UPDATED_ON" -> ContextCompat.getString(context, R.string.app_info_updated_on)
.replaceFirstChar { "REQUIRES" -> ContextCompat.getString(context, R.string.app_info_min_android)
if (it.isLowerCase()) { "TARGET" -> ContextCompat.getString(context, R.string.app_info_target_android)
it.titlecase(Locale.getDefault()) else -> info.key
} else { }
it.toString()
}
}
binding.txtSubtitle.text = info.value binding.txtSubtitle.text = info.value
} }

View File

@@ -42,7 +42,7 @@ abstract class BaseFragment<ViewBindingType : ViewBinding> : Fragment() {
lateinit var permissionProvider: PermissionProvider lateinit var permissionProvider: PermissionProvider
private var _binding: ViewBindingType? = null protected open var _binding: ViewBindingType? = null
protected val binding get() = _binding!! protected val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -58,9 +58,9 @@ class StreamBrowseFragment : BaseFragment<FragmentGenericWithToolbarBinding>() {
} }
}) })
viewModel.initCluster(streamCluster) viewModel.seedCluster(streamCluster)
viewModel.liveData.observe(viewLifecycleOwner) { viewModel.liveData.observe(viewLifecycleOwner) {
updateController(streamCluster) updateController(it)
} }
} }

View File

@@ -164,7 +164,7 @@ class DetailsMoreFragment : BaseFragment<FragmentDetailsMoreBinding>() {
add( add(
InfoViewModel_() InfoViewModel_()
.id(UUID.randomUUID().toString()) .id(UUID.randomUUID().toString())
.badge(mapOf("targets" to "API ${app.targetSdk}").entries.first()) .badge(mapOf("TARGET" to "${app.targetSdk}").entries.first())
) )
} }
} }

View File

@@ -1,15 +0,0 @@
package com.aurora.store.view.ui.onboarding
import android.os.Bundle
import android.view.View
import com.aurora.store.databinding.FragmentAppLinksBinding
import com.aurora.store.view.ui.commons.BaseFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class AppLinksFragment : BaseFragment<FragmentAppLinksBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}

View File

@@ -1,26 +1,9 @@
/*
* 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.onboarding package com.aurora.store.view.ui.onboarding
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -29,59 +12,36 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.aurora.Constants
import com.aurora.extensions.areNotificationsEnabled import com.aurora.extensions.areNotificationsEnabled
import com.aurora.extensions.isIgnoringBatteryOptimizations import com.aurora.extensions.isIgnoringBatteryOptimizations
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.model.UpdateMode import com.aurora.store.data.model.UpdateMode
import com.aurora.store.data.work.CacheWorker import com.aurora.store.data.work.CacheWorker
import com.aurora.store.databinding.FragmentOnboardingBinding import com.aurora.store.databinding.FragmentOnboardingBinding
import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
import com.aurora.store.util.Preferences.PREFERENCE_INTRO import com.aurora.store.util.Preferences.PREFERENCE_INTRO
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_AUTO
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.aurora.store.util.save import com.aurora.store.util.save
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.onboarding.OnboardingViewModel import com.aurora.store.viewmodel.onboarding.OnboardingViewModel
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint abstract class BaseFlavouredOnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
private val viewModel: OnboardingViewModel by viewModels() val viewModel: OnboardingViewModel by viewModels()
private var lastPosition = 0 var lastPosition = 0
internal class PagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : override fun onCreateView(
FragmentStateAdapter(fragmentManager, lifecycle) { inflater: LayoutInflater,
override fun createFragment(position: Int): Fragment { container: ViewGroup?,
when (position) { savedInstanceState: Bundle?
0 -> return WelcomeFragment() ): View {
1 -> return PermissionsFragment.newInstance() _binding = FragmentOnboardingBinding.inflate(inflater, container, false)
2 -> return AppLinksFragment() return binding.root
}
return Fragment()
}
override fun getItemCount(): Int {
return 2
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -95,6 +55,7 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
} }
val isDefaultPrefLoaded = Preferences.getBoolean(requireContext(), PREFERENCE_DEFAULT) val isDefaultPrefLoaded = Preferences.getBoolean(requireContext(), PREFERENCE_DEFAULT)
if (!isDefaultPrefLoaded) { if (!isDefaultPrefLoaded) {
save(PREFERENCE_DEFAULT, true) save(PREFERENCE_DEFAULT, true)
loadDefaultPreferences() loadDefaultPreferences()
@@ -105,7 +66,11 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
// ViewPager2 // ViewPager2
binding.viewpager2.apply { binding.viewpager2.apply {
adapter = PagerAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) adapter = PagerAdapter(
childFragmentManager,
viewLifecycleOwner.lifecycle,
onboardingPages()
)
isUserInputEnabled = false isUserInputEnabled = false
setCurrentItem(0, true) setCurrentItem(0, true)
registerOnPageChangeCallback(object : OnPageChangeCallback() { registerOnPageChangeCallback(object : OnPageChangeCallback() {
@@ -149,7 +114,11 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
} }
} }
private fun finishOnboarding() { abstract fun loadDefaultPreferences()
abstract fun onboardingPages(): List<Fragment>
open fun finishOnboarding() {
setupAutoUpdates() setupAutoUpdates()
CacheWorker.scheduleAutomatedCacheCleanup(requireContext()) CacheWorker.scheduleAutomatedCacheCleanup(requireContext())
Preferences.putBooleanNow(requireContext(), PREFERENCE_INTRO, true) Preferences.putBooleanNow(requireContext(), PREFERENCE_INTRO, true)
@@ -158,40 +127,29 @@ class OnboardingFragment : BaseFragment<FragmentOnboardingBinding>() {
ProcessPhoenix.triggerRebirth(context) ProcessPhoenix.triggerRebirth(context)
} }
private fun loadDefaultPreferences() { open fun setupAutoUpdates() {
/*Filters*/
save(PREFERENCE_FILTER_AURORA_ONLY, false)
save(PREFERENCE_FILTER_FDROID, true)
/*Network*/
// TODO: Gather feedback and drop setting default dispenser for all builds
if (!CertUtil.isAppGalleryApp(requireContext(), requireContext().packageName)) {
save(PREFERENCE_DISPENSER_URLS, setOf(Constants.URL_DISPENSER))
}
save(PREFERENCE_VENDING_VERSION, 0)
/*Customization*/
save(PREFERENCE_THEME_STYLE, 0)
save(PREFERENCE_DEFAULT_SELECTED_TAB, 0)
save(PREFERENCE_FOR_YOU, true)
save(PREFERENCE_SIMILAR, false)
/*Installer*/
save(PREFERENCE_AUTO_DELETE, true)
save(PREFERENCE_INSTALLER_ID, 0)
/*Updates*/
save(PREFERENCE_UPDATES_EXTENDED, false)
save(PREFERENCE_UPDATES_CHECK_INTERVAL, 3)
}
private fun setupAutoUpdates() {
val updateMode = when { val updateMode = when {
requireContext().isIgnoringBatteryOptimizations() -> UpdateMode.CHECK_AND_INSTALL requireContext().isIgnoringBatteryOptimizations() -> UpdateMode.CHECK_AND_INSTALL
requireContext().areNotificationsEnabled() -> UpdateMode.CHECK_AND_NOTIFY requireContext().areNotificationsEnabled() -> UpdateMode.CHECK_AND_NOTIFY
else -> UpdateMode.DISABLED else -> UpdateMode.DISABLED
} }
save(PREFERENCE_UPDATES_AUTO, updateMode.ordinal) save(PREFERENCE_UPDATES_AUTO, updateMode.ordinal)
viewModel.updateHelper.scheduleAutomatedCheck() viewModel.updateHelper.scheduleAutomatedCheck()
} }
internal class PagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
var items: List<Fragment>
) : FragmentStateAdapter(fragmentManager, lifecycle) {
override fun createFragment(position: Int): Fragment {
return items[position]
}
override fun getItemCount(): Int {
return items.size
}
}
} }

View File

@@ -1,45 +1,24 @@
/*
* 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.splash package com.aurora.store.view.ui.splash
import android.accounts.Account import android.accounts.Account
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.Intent
import android.net.UrlQuerySanitizer
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.extensions.hide import com.aurora.extensions.getPackageName
import com.aurora.extensions.isMAndAbove import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.isNAndAbove
import com.aurora.extensions.navigate import com.aurora.extensions.navigate
import com.aurora.extensions.show
import com.aurora.gplayapi.helpers.AuthHelper import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.compose.navigation.Screen import com.aurora.store.compose.navigation.Screen
@@ -56,22 +35,20 @@ import com.aurora.store.util.Preferences.PREFERENCE_INTRO
import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH import com.aurora.store.util.Preferences.PREFERENCE_MICROG_AUTH
import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.auth.AuthViewModel import com.aurora.store.viewmodel.auth.AuthViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@AndroidEntryPoint abstract class BaseFlavouredSplashFragment : BaseFragment<FragmentSplashBinding>() {
class SplashFragment : BaseFragment<FragmentSplashBinding>() {
private val TAG = SplashFragment::class.java.simpleName private val TAG = BaseFlavouredSplashFragment::class.java.simpleName
private val viewModel: AuthViewModel by activityViewModels() val viewModel: AuthViewModel by activityViewModels()
private val canLoginWithMicroG: Boolean val canLoginWithMicroG: Boolean
get() = isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext()) && get() = isMAndAbove && PackageUtil.hasSupportedMicroG(requireContext()) &&
Preferences.getBoolean(requireContext(), PREFERENCE_MICROG_AUTH, true) Preferences.getBoolean(requireContext(), PREFERENCE_MICROG_AUTH, true)
private val startForAccount = val startForAccount =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val accountName = it.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) val accountName = it.data?.getStringExtra(AccountManager.KEY_ACCOUNT_NAME)
if (!accountName.isNullOrBlank()) { if (!accountName.isNullOrBlank()) {
@@ -81,6 +58,15 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
} }
} }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSplashBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -115,9 +101,6 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
attachActions() attachActions()
// Show anonymous logins if we have dispenser URL
binding.btnAnonymous.isVisible = !viewModel.authProvider.dispenserURL.isNullOrBlank()
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.authState.collectLatest { viewModel.authState.collectLatest {
when (it) { when (it) {
@@ -128,7 +111,8 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
} }
AuthState.Valid -> { AuthState.Valid -> {
val packageName = getPackageName(requireActivity().intent) val packageName =
requireActivity().intent.getPackageName(requireArguments())
if (packageName.isNullOrBlank()) { if (packageName.isNullOrBlank()) {
navigateToDefaultTab() navigateToDefaultTab()
} else { } else {
@@ -152,7 +136,8 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
} }
AuthState.SignedIn -> { AuthState.SignedIn -> {
val packageName = getPackageName(requireActivity().intent) val packageName =
requireActivity().intent.getPackageName(requireArguments())
if (packageName.isNullOrBlank()) { if (packageName.isNullOrBlank()) {
navigateToDefaultTab() navigateToDefaultTab()
} else { } else {
@@ -193,55 +178,8 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
} }
private fun updateActionLayout(isVisible: Boolean) { private fun updateActionLayout(isVisible: Boolean) {
if (isVisible) { binding.layoutAction.isVisible = isVisible
binding.layoutAction.show() binding.toolbar.isVisible = isVisible
binding.toolbar.visibility = View.VISIBLE
} else {
binding.layoutAction.hide()
binding.toolbar.visibility = View.GONE
}
}
private fun attachActions() {
binding.btnAnonymous.addOnClickListener {
if (viewModel.authState.value != AuthState.Fetching) {
binding.btnAnonymous.updateProgress(true)
viewModel.buildAnonymousAuthData()
}
}
binding.btnGoogle.addOnClickListener {
if (viewModel.authState.value != AuthState.Fetching) {
binding.btnGoogle.updateProgress(true)
if (canLoginWithMicroG) {
Log.i(TAG, "Found supported microG, trying to request credentials")
val accountIntent = AccountManager.newChooseAccountIntent(
null,
null,
arrayOf(GOOGLE_ACCOUNT_TYPE),
null,
null,
null,
null
)
startForAccount.launch(accountIntent)
} else {
findNavController().navigate(R.id.googleFragment)
}
}
}
}
private fun resetActions() {
binding.btnGoogle.apply {
updateProgress(false)
isEnabled = true
}
binding.btnAnonymous.apply {
updateProgress(false)
isEnabled = true
}
} }
private fun navigateToDefaultTab() { private fun navigateToDefaultTab() {
@@ -262,29 +200,6 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
findNavController().navigate(directions) findNavController().navigate(directions)
} }
private fun getPackageName(intent: Intent): String? {
return when {
intent.action == Intent.ACTION_VIEW -> {
intent.data!!.getQueryParameter("id")
}
intent.action == Intent.ACTION_SEND -> {
val clipData = intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
UrlQuerySanitizer(clipData).getValue("id")
}
isNAndAbove && intent.action == Intent.ACTION_SHOW_APP_INFO -> {
intent.extras?.getString(Intent.EXTRA_PACKAGE_NAME)
}
intent.extras != null -> {
intent.extras?.getString("packageName")
}
else -> requireArguments().getString("packageName")
}
}
private fun requestAuthTokenForGoogle(accountName: String, oldToken: String? = null) { private fun requestAuthTokenForGoogle(accountName: String, oldToken: String? = null) {
try { try {
if (oldToken != null) { if (oldToken != null) {
@@ -317,4 +232,46 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
Log.e(TAG, "Failed to get authToken for Google login") Log.e(TAG, "Failed to get authToken for Google login")
} }
} }
open fun attachActions() {
binding.btnAnonymous.addOnClickListener {
if (viewModel.authState.value != AuthState.Fetching) {
binding.btnAnonymous.updateProgress(true)
viewModel.buildAnonymousAuthData()
}
}
binding.btnGoogle.addOnClickListener {
if (viewModel.authState.value != AuthState.Fetching) {
binding.btnGoogle.updateProgress(true)
if (canLoginWithMicroG) {
Log.i(TAG, "Found supported microG, trying to request credentials")
val accountIntent = AccountManager.newChooseAccountIntent(
null,
null,
arrayOf(GOOGLE_ACCOUNT_TYPE),
null,
null,
null,
null
)
startForAccount.launch(accountIntent)
} else {
findNavController().navigate(R.id.googleFragment)
}
}
}
}
open fun resetActions() {
binding.btnGoogle.apply {
updateProgress(false)
isEnabled = true
}
binding.btnAnonymous.apply {
updateProgress(false)
isEnabled = true
}
}
} }

View File

@@ -28,7 +28,6 @@ import com.aurora.gplayapi.helpers.web.WebStreamHelper
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -42,32 +41,36 @@ class StreamBrowseViewModel @Inject constructor(
private lateinit var streamCluster: StreamCluster private lateinit var streamCluster: StreamCluster
fun initCluster(cluster: StreamCluster) { fun seedCluster(cluster: StreamCluster) {
streamCluster = cluster streamCluster = cluster
liveData.postValue(streamCluster) liveData.postValue(streamCluster)
} }
fun nextCluster() { fun nextCluster() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { if (streamCluster.hasNext()) {
if (streamCluster.hasNext()) { val next = streamHelper.nextStreamCluster(streamCluster.clusterNextPageUrl)
val nextCluster = streamHelper.nextStreamCluster(
streamCluster.clusterNextPageUrl
)
streamCluster = streamCluster.copy( streamCluster = streamCluster.copy(
clusterNextPageUrl = nextCluster.clusterNextPageUrl, clusterNextPageUrl = next.clusterNextPageUrl,
clusterAppList = streamCluster.clusterAppList + nextCluster.clusterAppList clusterAppList = streamCluster.clusterAppList + next.clusterAppList
) )
liveData.postValue(streamCluster) liveData.postValue(streamCluster)
} else { } else {
Log.i(TAG, "End of Cluster") Log.i(TAG, "End of Cluster")
} postClusterEnd()
} catch (_: Exception) {
} }
} catch (_: Exception) {
} }
} }
} }
fun postClusterEnd() {
streamCluster = streamCluster.copy(
clusterNextPageUrl = ""
)
liveData.postValue(streamCluster)
}
} }

View File

@@ -107,9 +107,9 @@ class DetailsClusterViewModel @Inject constructor(
clusterNextPageUrl = newCluster.clusterNextPageUrl, clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
) )
val newStreamClusters = bundle.streamClusters.toMutableMap().also {
it.remove(clusterID) val newStreamClusters = bundle.streamClusters.toMutableMap().apply {
it[clusterID] = mergedCluster this[clusterID] = mergedCluster
} }
stash.put(url, bundle.copy(streamClusters = newStreamClusters)) stash.put(url, bundle.copy(streamClusters = newStreamClusters))

View File

@@ -90,9 +90,9 @@ class DevProfileViewModel @Inject constructor(
clusterNextPageUrl = newCluster.clusterNextPageUrl, clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
) )
val newStreamClusters = streamBundle.streamClusters.toMutableMap().also {
it.remove(newCluster.id) val newStreamClusters = streamBundle.streamClusters.toMutableMap().apply {
it[newCluster.id] = mergedCluster this[newCluster.id] = mergedCluster
} }
streamBundle = streamBundle.copy(streamClusters = newStreamClusters) streamBundle = streamBundle.copy(streamClusters = newStreamClusters)

View File

@@ -32,7 +32,8 @@ import com.aurora.store.data.model.ViewState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -44,11 +45,14 @@ class StreamViewModel @Inject constructor(
val liveData: MutableLiveData<ViewState> = MutableLiveData() val liveData: MutableLiveData<ViewState> = MutableLiveData()
private var stash: HomeStash = mutableMapOf() private val stash: HomeStash = mutableMapOf()
private val streamContract: StreamContract private val streamContract: StreamContract
get() = webStreamHelper get() = webStreamHelper
// Mutex to protect stash access for thread safety
private val stashMutex = Mutex()
fun getStreamBundle(category: StreamContract.Category, type: StreamContract.Type) { fun getStreamBundle(category: StreamContract.Category, type: StreamContract.Type) {
liveData.postValue(ViewState.Loading) liveData.postValue(ViewState.Loading)
observe(category, type) observe(category, type)
@@ -56,16 +60,18 @@ class StreamViewModel @Inject constructor(
fun observe(category: StreamContract.Category, type: StreamContract.Type) { fun observe(category: StreamContract.Category, type: StreamContract.Type) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
val bundle = targetBundle(category) stashMutex.withLock {
if (bundle.hasCluster()) { val bundle = targetBundle(category)
liveData.postValue(ViewState.Success(stash))
} // Post existing data if any clusters exist
if (bundle.hasCluster()) {
liveData.postValue(ViewState.Success(stash.toMap()))
}
try {
if (!bundle.hasCluster() || bundle.hasNext()) { if (!bundle.hasCluster() || bundle.hasNext()) {
//Fetch new stream bundle // Fetch new stream bundle
val newBundle = if (bundle.hasCluster()) { val newBundle = if (bundle.hasCluster()) {
streamContract.nextStreamBundle( streamContract.nextStreamBundle(
category, category,
@@ -75,41 +81,47 @@ class StreamViewModel @Inject constructor(
streamContract.fetch(type, category) streamContract.fetch(type, category)
} }
//Update old bundle // Update old bundle
val mergedBundle = bundle.copy( val mergedBundle = bundle.copy(
streamClusters = bundle.streamClusters + newBundle.streamClusters, streamClusters = bundle.streamClusters + newBundle.streamClusters,
streamNextPageUrl = newBundle.streamNextPageUrl streamNextPageUrl = newBundle.streamNextPageUrl
) )
stash[category] = mergedBundle stash[category] = mergedBundle
//Post updated to UI // Post updated to UI
liveData.postValue(ViewState.Success(stash)) liveData.postValue(ViewState.Success(stash.toMap()))
} else { } else {
Log.i(TAG, "End of Bundle") Log.i(TAG, "End of Bundle")
} }
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
} }
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
} }
} }
} }
fun observeCluster(category: StreamContract.Category, streamCluster: StreamCluster) { fun observeCluster(category: StreamContract.Category, streamCluster: StreamCluster) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { if (streamCluster.hasNext()) {
if (streamCluster.hasNext()) { val newCluster = streamContract.nextStreamCluster(
val newCluster = streamContract.nextStreamCluster( streamCluster.clusterNextPageUrl
streamCluster.clusterNextPageUrl )
)
stashMutex.withLock {
updateCluster(category, streamCluster.id, newCluster) updateCluster(category, streamCluster.id, newCluster)
liveData.postValue(ViewState.Success(stash))
} else {
Log.i(TAG, "End of cluster")
} }
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message)) liveData.postValue(ViewState.Success(stash.toMap()))
} else {
stashMutex.withLock {
postClusterEnd(category, streamCluster.id)
}
liveData.postValue(ViewState.Success(stash.toMap()))
} }
} catch (e: Exception) {
liveData.postValue(ViewState.Error(e.message))
} }
} }
} }
@@ -119,26 +131,34 @@ class StreamViewModel @Inject constructor(
clusterID: Int, clusterID: Int,
newCluster: StreamCluster newCluster: StreamCluster
) { ) {
val bundle = targetBundle(category) val bundle = stash[category] ?: return
bundle.streamClusters[clusterID]?.let { oldCluster -> val oldCluster = bundle.streamClusters[clusterID] ?: return
val mergedCluster = oldCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
)
val newStreamClusters = bundle.streamClusters.toMutableMap().also {
it.remove(clusterID)
it[clusterID] = mergedCluster
}
stash.put(category, bundle.copy(streamClusters = newStreamClusters)) val mergedCluster = oldCluster.copy(
clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
)
val updatedClusters = bundle.streamClusters.toMutableMap().apply {
this[clusterID] = mergedCluster
} }
stash[category] = bundle.copy(streamClusters = updatedClusters)
}
private fun postClusterEnd(category: StreamContract.Category, clusterID: Int) {
val bundle = stash[category] ?: return
val oldCluster = bundle.streamClusters[clusterID] ?: return
val updatedCluster = oldCluster.copy(clusterNextPageUrl = "")
val updatedClusters = bundle.streamClusters.toMutableMap().apply {
this[clusterID] = updatedCluster
}
stash[category] = bundle.copy(streamClusters = updatedClusters)
} }
private fun targetBundle(category: StreamContract.Category): StreamBundle { private fun targetBundle(category: StreamContract.Category): StreamBundle {
val streamBundle = stash.getOrPut(category) { return stash.getOrPut(category) { StreamBundle() }
StreamBundle()
}
return streamBundle
} }
} }

View File

@@ -19,6 +19,7 @@
package com.aurora.store.viewmodel.review package com.aurora.store.viewmodel.review
import android.util.Log
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -28,13 +29,13 @@ import com.aurora.gplayapi.helpers.ReviewsHelper
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ReviewViewModel @Inject constructor( class ReviewViewModel @Inject constructor(
private val reviewsHelper: ReviewsHelper private val reviewsHelper: ReviewsHelper
) : ViewModel() { ) : ViewModel() {
val TAG = javaClass.simpleName
val liveData: MutableLiveData<ReviewCluster> = MutableLiveData() val liveData: MutableLiveData<ReviewCluster> = MutableLiveData()
@@ -42,29 +43,29 @@ class ReviewViewModel @Inject constructor(
fun fetchReview(packageName: String, filter: Review.Filter) { fun fetchReview(packageName: String, filter: Review.Filter) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { reviewsCluster = reviewsHelper.getReviews(packageName, filter)
reviewsCluster = reviewsHelper.getReviews(packageName, filter) liveData.postValue(reviewsCluster)
liveData.postValue(reviewsCluster) } catch (e: Exception) {
} catch (_: Exception) { Log.e(TAG, "Failed to fetch reviews", e)
}
} }
} }
} }
fun next(nextReviewPageUrl: String) { fun next(nextReviewPageUrl: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
supervisorScope { try {
try { val currentCluster = reviewsCluster
val nextReviewCluster = reviewsHelper.next(nextReviewPageUrl) val nextReviewCluster = reviewsHelper.next(nextReviewPageUrl)
reviewsCluster = reviewsCluster.copy(
nextPageUrl = nextReviewCluster.nextPageUrl,
reviewList = nextReviewCluster.reviewList + nextReviewCluster.reviewList
)
liveData.postValue(reviewsCluster) reviewsCluster = currentCluster.copy(
} catch (_: Exception) { nextPageUrl = nextReviewCluster.nextPageUrl,
} reviewList = currentCluster.reviewList + nextReviewCluster.reviewList
)
liveData.postValue(reviewsCluster)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch next reviews $nextReviewPageUrl", e)
} }
} }
} }

View File

@@ -121,9 +121,9 @@ class CategoryStreamViewModel @Inject constructor(
clusterNextPageUrl = newCluster.clusterNextPageUrl, clusterNextPageUrl = newCluster.clusterNextPageUrl,
clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList
) )
val newStreamClusters = bundle.streamClusters.toMutableMap().also {
it.remove(clusterID) val newStreamClusters = bundle.streamClusters.toMutableMap().apply {
it[clusterID] = mergedCluster this[clusterID] = mergedCluster
} }
stash.put(browseUrl, bundle.copy(streamClusters = newStreamClusters)) stash.put(browseUrl, bundle.copy(streamClusters = newStreamClusters))

View File

@@ -91,7 +91,7 @@ class TopChartViewModel @Inject constructor(
clusterAppList = streamCluster.clusterAppList + newCluster.clusterAppList clusterAppList = streamCluster.clusterAppList + newCluster.clusterAppList
) )
stash[type] = mutableMapOf(chart to mergedCluster) stash[type]?.set(chart, mergedCluster)
} }
private fun targetCluster( private fun targetCluster(

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="match_parent"
android:orientation="vertical"
tools:context=".view.ui.onboarding.AppLinksFragment">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/appCompatTextView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="true"
android:paddingStart="@dimen/padding_normal"
android:paddingEnd="@dimen/padding_normal"
android:text="@string/app_links_title"
android:textAlignment="textStart"
android:textColor="?colorAccent"
android:textSize="42sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/appCompatTextView"
style="@style/AuroraTextStyle.Subtitle.Alt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/margin_small"
android:paddingStart="@dimen/padding_normal"
android:paddingEnd="@dimen/padding_normal"
android:text="@string/app_links_desc"
android:textAlignment="textStart"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appCompatTextView2" />
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="@+id/epoxyRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:overScrollMode="never"
android:padding="@dimen/padding_medium"
tools:listitem="@layout/view_permission_switch" />
</LinearLayout>

View File

@@ -357,6 +357,10 @@
<string name="android_market_desc">Android Market was an online store offering software applications designed for Android devices, retired in 2017.</string> <string name="android_market_desc">Android Market was an online store offering software applications designed for Android devices, retired in 2017.</string>
<string name="failed_apk_export">Failed to export APKs</string> <string name="failed_apk_export">Failed to export APKs</string>
<string name="no_apps_available">No apps available</string> <string name="no_apps_available">No apps available</string>
<string name="app_info_downloads">"Downloads"</string>
<string name="app_info_updated_on">"Last updated"</string>
<string name="app_info_min_android">"Minimum Android Version"</string>
<string name="app_info_target_android">"Target API Level"</string>
<!-- InstallerFragment --> <!-- InstallerFragment -->
<string name="session_installer_subtitle">Session based installer for bundled/split APKs</string> <string name="session_installer_subtitle">Session based installer for bundled/split APKs</string>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DELETE_PACKAGES" />
</manifest>

View File

@@ -0,0 +1,84 @@
/*
* 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.onboarding
import androidx.fragment.app.Fragment
import com.aurora.Constants
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.aurora.store.util.save
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class OnboardingFragment : BaseFlavouredOnboardingFragment() {
override fun loadDefaultPreferences() {
/*Filters*/
save(PREFERENCE_FILTER_AURORA_ONLY, false)
save(PREFERENCE_FILTER_FDROID, true)
/*Network*/
save(PREFERENCE_DISPENSER_URLS, setOf(Constants.URL_DISPENSER))
save(PREFERENCE_VENDING_VERSION, 0)
/*Customization*/
save(PREFERENCE_THEME_STYLE, 0)
save(PREFERENCE_DEFAULT_SELECTED_TAB, 0)
save(PREFERENCE_FOR_YOU, true)
save(PREFERENCE_SIMILAR, false)
/*Installer*/
save(PREFERENCE_AUTO_DELETE, true)
save(PREFERENCE_INSTALLER_ID, 0)
/*Updates*/
save(PREFERENCE_UPDATES_EXTENDED, false)
save(PREFERENCE_UPDATES_CHECK_INTERVAL, 3)
}
override fun onboardingPages(): List<Fragment> {
return listOf(
WelcomeFragment(),
PermissionsFragment.newInstance()
)
}
override fun setupAutoUpdates() {
super.setupAutoUpdates()
// Remove super & implement variant logic here
}
override fun finishOnboarding() {
super.finishOnboarding()
// Remove super & implement variant logic here
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.splash
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SplashFragment : BaseFlavouredSplashFragment()

View File

@@ -0,0 +1,84 @@
/*
* 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.onboarding
import androidx.fragment.app.Fragment
import com.aurora.Constants
import com.aurora.store.util.Preferences.PREFERENCE_AUTO_DELETE
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
import com.aurora.store.util.Preferences.PREFERENCE_DISPENSER_URLS
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_AURORA_ONLY
import com.aurora.store.util.Preferences.PREFERENCE_FILTER_FDROID
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
import com.aurora.store.util.Preferences.PREFERENCE_SIMILAR
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_CHECK_INTERVAL
import com.aurora.store.util.Preferences.PREFERENCE_UPDATES_EXTENDED
import com.aurora.store.util.Preferences.PREFERENCE_VENDING_VERSION
import com.aurora.store.util.save
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class OnboardingFragment : BaseFlavouredOnboardingFragment() {
override fun loadDefaultPreferences() {
/*Filters*/
save(PREFERENCE_FILTER_AURORA_ONLY, false)
save(PREFERENCE_FILTER_FDROID, true)
/*Network*/
save(PREFERENCE_DISPENSER_URLS, setOf(Constants.URL_DISPENSER))
save(PREFERENCE_VENDING_VERSION, 0)
/*Customization*/
save(PREFERENCE_THEME_STYLE, 0)
save(PREFERENCE_DEFAULT_SELECTED_TAB, 0)
save(PREFERENCE_FOR_YOU, true)
save(PREFERENCE_SIMILAR, false)
/*Installer*/
save(PREFERENCE_AUTO_DELETE, true)
save(PREFERENCE_INSTALLER_ID, 0)
/*Updates*/
save(PREFERENCE_UPDATES_EXTENDED, false)
save(PREFERENCE_UPDATES_CHECK_INTERVAL, 3)
}
override fun onboardingPages(): List<Fragment> {
return listOf(
WelcomeFragment(),
PermissionsFragment.newInstance()
)
}
override fun setupAutoUpdates() {
super.setupAutoUpdates()
// Remove super & implement variant logic here
}
override fun finishOnboarding() {
super.finishOnboarding()
// Remove super & implement variant logic here
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.splash
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SplashFragment : BaseFlavouredSplashFragment()

View File

@@ -9,7 +9,7 @@ composeBom = "2025.05.01"
core = "1.16.0" core = "1.16.0"
epoxy = "5.1.4" epoxy = "5.1.4"
espresso = "3.6.1" espresso = "3.6.1"
gplayapi = "3.5.2" gplayapi = "3.5.3"
hiddenapibypass = "6.1" hiddenapibypass = "6.1"
hilt = "2.56.2" hilt = "2.56.2"
hiltWork = "1.2.0" hiltWork = "1.2.0"