From b507cd6d73011ae3476e2919c8d5e681bd33f1cf Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 1 Jun 2025 20:10:23 +0530 Subject: [PATCH 1/5] gplayapi:3.5.5 --- .../controller/SearchCarouselController.kt | 80 ++++++ .../store/view/ui/details/DevAppsFragment.kt | 98 ++++--- .../view/ui/search/SearchResultsFragment.kt | 255 +++++------------- .../viewmodel/search/SearchResultViewModel.kt | 172 +++++++++--- .../res/layout/fragment_search_result.xml | 87 +++--- gradle/libs.versions.toml | 2 +- 6 files changed, 381 insertions(+), 313 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/view/epoxy/controller/SearchCarouselController.kt diff --git a/app/src/main/java/com/aurora/store/view/epoxy/controller/SearchCarouselController.kt b/app/src/main/java/com/aurora/store/view/epoxy/controller/SearchCarouselController.kt new file mode 100644 index 000000000..2662edcf7 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/controller/SearchCarouselController.kt @@ -0,0 +1,80 @@ +/* + * Aurora Store + * Copyright (C) 2021, Rahul Kumar Patel + * + * 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 . + * + */ + +package com.aurora.store.view.epoxy.controller + +import com.airbnb.epoxy.TypedEpoxyController +import com.aurora.gplayapi.data.models.StreamBundle +import com.aurora.store.R +import com.aurora.store.view.epoxy.controller.GenericCarouselController.Callbacks +import com.aurora.store.view.epoxy.groups.CarouselModelGroup +import com.aurora.store.view.epoxy.views.app.AppListViewModel_ +import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ +import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ + +open class SearchCarouselController(private val callbacks: Callbacks) : + + TypedEpoxyController() { + + override fun buildModels(streamBundle: StreamBundle?) { + setFilterDuplicates(true) + if (streamBundle == null) { + for (i in 1..6) { + add( + AppListViewShimmerModel_() + .id(i) + ) + } + } else { + if (streamBundle.streamClusters.isEmpty()) { + add( + NoAppViewModel_() + .id("no_app") + .icon(R.drawable.ic_apps) + .message(R.string.no_apps_available) + ) + } else { + streamBundle.streamClusters.values + .filter { it.clusterAppList.isNotEmpty() } // Filter out empty clusters, mostly related keywords + .forEach { + if (it.clusterTitle.isEmpty() or (it.clusterTitle == streamBundle.streamTitle)) { + if (it.clusterAppList.isNotEmpty()) { + it.clusterAppList.forEach { app -> + add( + AppListViewModel_() + .id(app.id) + .app(app) + .click { _ -> callbacks.onAppClick(app) } + ) + } + } + } else { + add(CarouselModelGroup(it, callbacks)) + } + } + + if (streamBundle.hasNext()) + add( + AppListViewShimmerModel_() + .id("progress") + ) + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt index 2de10d2c9..5c206dc7d 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt @@ -24,68 +24,84 @@ import android.view.View import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import com.aurora.gplayapi.data.models.SearchBundle +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.store.AppStreamStash +import com.aurora.store.data.model.ViewState +import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.databinding.FragmentGenericWithToolbarBinding import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener -import com.aurora.store.view.epoxy.views.AppProgressViewModel_ -import com.aurora.store.view.epoxy.views.app.AppListViewModel_ +import com.aurora.store.view.epoxy.controller.GenericCarouselController +import com.aurora.store.view.epoxy.controller.SearchCarouselController import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.search.SearchResultViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class DevAppsFragment : BaseFragment() { +class DevAppsFragment : BaseFragment(), + GenericCarouselController.Callbacks { private val args: DevAppsFragmentArgs by navArgs() + private val viewModel: SearchResultViewModel by viewModels() + private val controller = SearchCarouselController(this) + + private var query: String = "" + get() = "pub:${args.developerName}" + + private var scrollListener: EndlessRecyclerOnScrollListener = + object : EndlessRecyclerOnScrollListener(visibleThreshold = 4) { + override fun onLoadMore(currentPage: Int) { + viewModel.observe(query) + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.liveData.observe(viewLifecycleOwner) { - updateController(it) - } - - // Toolbar - binding.toolbar.apply { - title = args.developerName - setNavigationOnClickListener { findNavController().navigateUp() } - } - - // Recycler View - val endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() { - override fun onLoadMore(currentPage: Int) { - viewModel.liveData.value?.let { viewModel.next(it.subBundles) } + with(binding) { + toolbar.apply { + title = args.developerName + setNavigationOnClickListener { findNavController().navigateUp() } } + + recycler.setController(controller) + recycler.addOnScrollListener(scrollListener) } - binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener) - viewModel.observeSearchResults("pub:${args.developerName}") - } + with(viewModel) { + search("pub:${args.developerName}") - private fun updateController(searchBundle: SearchBundle) { - binding.recycler - .withModels { - setFilterDuplicates(true) - searchBundle.appList - .filter { it.displayName.isNotEmpty() } - .forEach { app -> - add( - AppListViewModel_() - .id(app.id) - .app(app) - .click(View.OnClickListener { - openDetailsFragment(app.packageName, app) - }) - ) + liveData.observe(viewLifecycleOwner) { + when (it) { + is ViewState.Loading -> { + controller.setData(null) } - if (searchBundle.subBundles.isNotEmpty()) { - add( - AppProgressViewModel_() - .id("progress") - ) + is ViewState.Success<*> -> { + val stash = it.getDataAs() + controller.setData(stash[query]) + } + + else -> {} } } + } + } + + override fun onHeaderClicked(streamCluster: StreamCluster) { + openStreamBrowseFragment(streamCluster) + } + + override fun onClusterScrolled(streamCluster: StreamCluster) { + viewModel.observeCluster(query, streamCluster) + } + + override fun onAppClick(app: App) { + openDetailsFragment(app.packageName, app) + } + + override fun onAppLongClick(app: App) { + } } diff --git a/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt index 118966b7e..a4992c218 100644 --- a/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/search/SearchResultsFragment.kt @@ -19,202 +19,100 @@ package com.aurora.store.view.ui.search -import android.content.SharedPreferences -import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.KeyEvent import android.view.View -import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.TextView -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.aurora.extensions.hideKeyboard import com.aurora.extensions.showKeyboard import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.SearchBundle +import com.aurora.gplayapi.data.models.StreamCluster +import com.aurora.store.AppStreamStash import com.aurora.store.R -import com.aurora.store.data.model.Filter -import com.aurora.store.data.providers.FilterProvider.Companion.PREFERENCE_FILTER +import com.aurora.store.data.model.ViewState +import com.aurora.store.data.model.ViewState.Loading.getDataAs import com.aurora.store.databinding.FragmentSearchResultBinding -import com.aurora.store.util.Preferences import com.aurora.store.view.custom.recycler.EndlessRecyclerOnScrollListener -import com.aurora.store.view.epoxy.views.AppProgressViewModel_ -import com.aurora.store.view.epoxy.views.app.AppListViewModel_ -import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ -import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ +import com.aurora.store.view.epoxy.controller.GenericCarouselController +import com.aurora.store.view.epoxy.controller.SearchCarouselController import com.aurora.store.view.ui.commons.BaseFragment import com.aurora.store.viewmodel.search.SearchResultViewModel import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class SearchResultsFragment : BaseFragment(), - OnSharedPreferenceChangeListener { + GenericCarouselController.Callbacks { - private val viewModel: SearchResultViewModel by viewModels() + private val viewModel: SearchResultViewModel by activityViewModels() + private val controller = SearchCarouselController(this) - private lateinit var sharedPreferences: SharedPreferences + private var query: String + get() = requireArguments().getString("query").orEmpty() + set(value) = requireArguments().putString("query", value) - private var query: String? = null - private var searchBundle: SearchBundle = SearchBundle() - - private var shimmerAnimationVisible = false + private var scrollListener: EndlessRecyclerOnScrollListener = + object : EndlessRecyclerOnScrollListener(visibleThreshold = 4) { + override fun onLoadMore(currentPage: Int) { + viewModel.observe(query) + } + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Adjust FAB margins for edgeToEdge display - ViewCompat.setOnApplyWindowInsetsListener(binding.filterFab) { _, windowInsets -> - val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()) - binding.filterFab.updateLayoutParams { - bottomMargin = insets.bottom + resources.getDimensionPixelSize(R.dimen.margin_normal) - } - WindowInsetsCompat.CONSUMED - } - - sharedPreferences = Preferences.getPrefs(view.context) - sharedPreferences.registerOnSharedPreferenceChangeListener(this) - - // Toolbar - binding.toolbar.apply { - setNavigationOnClickListener { - binding.searchBar.hideKeyboard() - findNavController().navigateUp() - } - setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_clear -> { - binding.searchBar.text?.clear() - binding.searchBar.showKeyboard() - } - R.id.action_download -> findNavController().navigate(R.id.downloadFragment) - } - true - } - } - // Search attachSearch() - - // RecyclerView - val endlessRecyclerOnScrollListener = object : EndlessRecyclerOnScrollListener() { - override fun onLoadMore(currentPage: Int) { - viewModel.next(searchBundle.subBundles) - } - } - binding.recycler.addOnScrollListener(endlessRecyclerOnScrollListener) - - // Filter - binding.filterFab.setOnClickListener { - findNavController().navigate(R.id.filterSheet) - } - - viewModel.liveData.observe(viewLifecycleOwner) { - if (shimmerAnimationVisible) { - endlessRecyclerOnScrollListener.resetPageCount() - binding.recycler.clear() - shimmerAnimationVisible = false - } - searchBundle = it - updateController(searchBundle) - } - - query = requireArguments().getString("query") - - // Don't fetch search results again when coming back to fragment - if (searchBundle.appList.isEmpty()) { - query?.let { updateQuery(it) } - } else { - updateController(searchBundle) - } - } - - override fun onDestroyView() { - sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - super.onDestroyView() - } - - override fun onDestroy() { - viewModel.filterProvider.saveFilter(Filter()) - super.onDestroy() - } - - private fun updateController(searchBundle: SearchBundle?) { - if (searchBundle == null) { - shimmerAnimationVisible = true - binding.recycler.withModels { - for (i in 1..10) { - add(AppListViewShimmerModel_().id(i)) + with(binding) { + toolbar.apply { + setNavigationOnClickListener { + binding.searchBar.hideKeyboard() + findNavController().navigateUp() } - } - return - } - - val filteredAppList = filter(searchBundle.appList) - - if (filteredAppList.isEmpty()) { - if (searchBundle.subBundles.isNotEmpty()) { - viewModel.next(searchBundle.subBundles) - binding.recycler.withModels { - setFilterDuplicates(true) - add( - AppProgressViewModel_() - .id("progress") - ) - } - } else { - binding.recycler.adapter?.let { - /*Show empty search list if nothing found or no app matches filter criterion*/ - if (it.itemCount == 1 && searchBundle.subBundles.isEmpty()) { - binding.recycler.withModels { - add( - NoAppViewModel_() - .id("no_app") - .message(R.string.details_no_app_match) - .icon(R.drawable.ic_round_search) - ) + setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_clear -> { + binding.searchBar.text?.clear() + binding.searchBar.showKeyboard() } + + R.id.action_download -> findNavController().navigate(R.id.downloadFragment) } - } - } - } else { - binding.recycler.withModels { - setFilterDuplicates(true) - - filteredAppList.forEach { app -> - add( - AppListViewModel_() - .id(app.id) - .app(app) - .click(View.OnClickListener { - binding.searchBar.hideKeyboard() - openDetailsFragment(app.packageName, app) - }) - ) - } - - if (searchBundle.subBundles.isNotEmpty()) { - add( - AppProgressViewModel_() - .id("progress") - ) + true } } - binding.recycler.adapter?.let { - if (it.itemCount < 10) { - viewModel.next(searchBundle.subBundles) + recycler.setController(controller) + recycler.addOnScrollListener(scrollListener) + } + + with(viewModel) { + search(query) + + liveData.observe(viewLifecycleOwner) { + when (it) { + is ViewState.Loading -> { + controller.setData(null) + } + + is ViewState.Success<*> -> { + val stash = it.getDataAs() + controller.setData(stash[query]) + } + + else -> {} } } } } private fun attachSearch() { + binding.searchBar.text = Editable.Factory.getInstance().newEditable(query) + binding.searchBar.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} @@ -230,44 +128,35 @@ class SearchResultsFragment : BaseFragment(), || actionId == KeyEvent.ACTION_DOWN || actionId == KeyEvent.KEYCODE_ENTER ) { + query = binding.searchBar.text.toString() - query?.let { - requireArguments().putString("query", it) - queryViewModel(it) - return@setOnEditorActionListener true - } + + queryViewModel(query) + + return@setOnEditorActionListener true } false } } - private fun updateQuery(query: String) { - binding.searchBar.text = Editable.Factory.getInstance().newEditable(query) - binding.searchBar.setSelection(query.length) - queryViewModel(query) - } - private fun queryViewModel(query: String) { - updateController(null) - viewModel.observeSearchResults(query) + scrollListener.resetPageCount() + viewModel.search(query) } - private fun filter(appList: List): List { - val filter = viewModel.filterProvider.getSavedFilter() - return appList - .asSequence() - .filter { app -> - app.displayName.isNotEmpty() && - (filter.paidApps || app.isFree) && - (filter.appsWithAds || !app.containsAds) && - (filter.gsfDependentApps || app.dependencies.dependentPackages.isEmpty()) && - (filter.rating <= 0 || app.rating.average >= filter.rating) && - (filter.downloads <= 0 || app.installs >= filter.downloads) - } - .toList() + override fun onHeaderClicked(streamCluster: StreamCluster) { + openStreamBrowseFragment(streamCluster) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - if (key == PREFERENCE_FILTER) query?.let { queryViewModel(it) } + override fun onClusterScrolled(streamCluster: StreamCluster) { + viewModel.observeCluster(query, streamCluster) + } + + override fun onAppClick(app: App) { + openDetailsFragment(app.packageName, app) + } + + override fun onAppLongClick(app: App) { + } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/search/SearchResultViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/search/SearchResultViewModel.kt index ae2ef978f..fdc1d3a49 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/search/SearchResultViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/search/SearchResultViewModel.kt @@ -23,21 +23,23 @@ import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.aurora.gplayapi.data.models.SearchBundle +import com.aurora.gplayapi.data.models.StreamBundle +import com.aurora.gplayapi.data.models.StreamCluster import com.aurora.gplayapi.helpers.SearchHelper import com.aurora.gplayapi.helpers.contracts.SearchContract import com.aurora.gplayapi.helpers.web.WebSearchHelper +import com.aurora.store.AppStreamStash +import com.aurora.store.data.model.ViewState import com.aurora.store.data.providers.AuthProvider -import com.aurora.store.data.providers.FilterProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import javax.inject.Inject @HiltViewModel class SearchResultViewModel @Inject constructor( - val filterProvider: FilterProvider, private val authProvider: AuthProvider, private val searchHelper: SearchHelper, private val webSearchHelper: WebSearchHelper @@ -45,50 +47,146 @@ class SearchResultViewModel @Inject constructor( private val TAG = SearchResultViewModel::class.java.simpleName - val liveData: MutableLiveData = MutableLiveData() + val liveData: MutableLiveData = MutableLiveData() - private var searchBundle: SearchBundle = SearchBundle() + private val stash: AppStreamStash = mutableMapOf() - private val helper: SearchContract + private val contract: SearchContract get() = if (authProvider.isAnonymous) webSearchHelper else searchHelper - fun observeSearchResults(query: String) { - viewModelScope.launch(Dispatchers.IO) { - supervisorScope { - try { - searchBundle = search(query) - liveData.postValue(searchBundle) - } catch (e: Exception) { - - } - } - } - } - - private fun search(query: String): SearchBundle { - return helper.searchResults(query) - } + private val stashMutex = Mutex() @Synchronized - fun next(nextSubBundleSet: Set) { + fun search(query: String) { viewModelScope.launch(Dispatchers.IO) { - supervisorScope { - try { - if (nextSubBundleSet.isNotEmpty()) { - val newSearchBundle = helper.next(nextSubBundleSet.toMutableSet()) - if (newSearchBundle.appList.isNotEmpty()) { - searchBundle = searchBundle.copy( - subBundles = newSearchBundle.subBundles, - appList = searchBundle.appList + newSearchBundle.appList - ) + try { + stashMutex.withLock { + liveData.postValue(ViewState.Loading) - liveData.postValue(searchBundle) - } + var bundle = targetBundle(query) + + // Post existing data if any clusters exist + if (bundle.hasCluster()) { + liveData.postValue(ViewState.Success(stash.toMap())) + return@launch } - } catch (e: Exception) { - Log.d(TAG, "Failed to get next bundle", e) + + // Fetch new stream bundle + val newBundle = contract.searchResults(query) + + bundle = bundle.copy( + streamClusters = newBundle.streamClusters, + streamNextPageUrl = newBundle.streamNextPageUrl + ) + + stash[query] = bundle + + liveData.postValue(ViewState.Success(stash.toMap())) } + } catch (e: Exception) { + liveData.postValue(ViewState.Error(e.message)) } } } + + fun observe(query: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + stashMutex.withLock { + var bundle = targetBundle(query) + + if (bundle.hasNext()) { + val newBundle = contract.nextStreamBundle( + query, + bundle.streamNextPageUrl + ) + + // Update old bundle + bundle = bundle.copy( + streamClusters = bundle.streamClusters + newBundle.streamClusters, + streamNextPageUrl = newBundle.streamNextPageUrl + ) + + stash[query] = bundle + + liveData.postValue(ViewState.Success(stash.toMap())) + } else { + Log.i(TAG, "End of Bundle") + + // If stream ends, likely there are clusters that need to be processed + bundle.streamClusters.values.forEach { + if (it.clusterNextPageUrl.isEmpty()) { + postClusterEnd(query, it.id) + } + + // Empty title or query as title indicates main stream cluster + if (it.clusterTitle.isEmpty() or (it.clusterTitle == bundle.streamTitle)) { + observeCluster(query, it) + } + } + } + } + } catch (e: Exception) { + liveData.postValue(ViewState.Error(e.message)) + } + } + } + + fun observeCluster(query: String, streamCluster: StreamCluster) { + viewModelScope.launch(Dispatchers.IO) { + try { + if (streamCluster.hasNext()) { + val newCluster = contract.nextStreamCluster( + query, + streamCluster.clusterNextPageUrl + ) + stashMutex.withLock { + updateCluster(query, streamCluster.id, newCluster) + } + + liveData.postValue(ViewState.Success(stash.toMap())) + } else { + stashMutex.withLock { + postClusterEnd(query, streamCluster.id) + } + + liveData.postValue(ViewState.Success(stash.toMap())) + } + } catch (e: Exception) { + liveData.postValue(ViewState.Error(e.message)) + } + } + } + + private fun updateCluster(query: String, clusterID: Int, newCluster: StreamCluster) { + val bundle = stash[query] ?: return + val oldCluster = bundle.streamClusters[clusterID] ?: return + + val mergedCluster = oldCluster.copy( + clusterNextPageUrl = newCluster.clusterNextPageUrl, + clusterAppList = oldCluster.clusterAppList + newCluster.clusterAppList + ) + + val updatedClusters = bundle.streamClusters.toMutableMap().apply { + this[clusterID] = mergedCluster + } + + stash[query] = bundle.copy(streamClusters = updatedClusters) + } + + private fun postClusterEnd(query: String, clusterID: Int) { + val bundle = stash[query] ?: return + val oldCluster = bundle.streamClusters[clusterID] ?: return + + val updatedCluster = oldCluster.copy(clusterNextPageUrl = "") + val updatedClusters = bundle.streamClusters.toMutableMap().apply { + this[clusterID] = updatedCluster + } + + stash[query] = bundle.copy(streamClusters = updatedClusters) + } + + private fun targetBundle(query: String): StreamBundle { + return stash.getOrPut(query.trim()) { StreamBundle(streamTitle = query) } + } } diff --git a/app/src/main/res/layout/fragment_search_result.xml b/app/src/main/res/layout/fragment_search_result.xml index 8935bd3ed..e636220a1 100644 --- a/app/src/main/res/layout/fragment_search_result.xml +++ b/app/src/main/res/layout/fragment_search_result.xml @@ -17,7 +17,7 @@ ~ --> - - + + + + + + + - - - - - - - - - - - - - + android:layout_below="@+id/divider" + android:clipToPadding="true" + android:paddingTop="@dimen/padding_normal" + android:paddingBottom="@dimen/height_bottom_adj" + tools:listitem="@layout/view_app_list" /> + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76d4a5e4d..e9b1e0b12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ composeBom = "2025.05.01" core = "1.16.0" epoxy = "5.1.4" espresso = "3.6.1" -gplayapi = "3.5.3" +gplayapi = "3.5.5" hiddenapibypass = "6.1" hilt = "2.56.2" hiltWork = "1.2.0" From 42800dd4eb54f9445be6eb4e123ccb6b5766c80d Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Sun, 1 Jun 2025 20:24:00 +0530 Subject: [PATCH 2/5] fix my folly --- .../java/com/aurora/store/view/ui/details/DevAppsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt index 5c206dc7d..c6f39e1b6 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/DevAppsFragment.kt @@ -70,7 +70,7 @@ class DevAppsFragment : BaseFragment(), } with(viewModel) { - search("pub:${args.developerName}") + search(query) liveData.observe(viewLifecycleOwner) { when (it) { From a90f1863f8b535ab6d51c621574c1ef40703b73e Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 2 Jun 2025 14:13:39 +0530 Subject: [PATCH 3/5] gplayapi:3.5.6 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9b1e0b12..69eda3fe6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ composeBom = "2025.05.01" core = "1.16.0" epoxy = "5.1.4" espresso = "3.6.1" -gplayapi = "3.5.5" +gplayapi = "3.5.6" hiddenapibypass = "6.1" hilt = "2.56.2" hiltWork = "1.2.0" From 50b35f8a7d24203de2d2851c8f47e3092920b838 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 5 Jun 2025 11:53:22 +0800 Subject: [PATCH 4/5] AuthProvider: Use actual device properties to generate anonymous auth Applying 3b41525c5041c66600e8af632518f5ac68d319dd again but with changes for kotlinx.serialization Signed-off-by: Aayush Gupta --- app/src/main/java/com/aurora/store/data/model/Auth.kt | 3 +++ .../java/com/aurora/store/data/providers/AuthProvider.kt | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/aurora/store/data/model/Auth.kt b/app/src/main/java/com/aurora/store/data/model/Auth.kt index b5fecd390..12de75e09 100644 --- a/app/src/main/java/com/aurora/store/data/model/Auth.kt +++ b/app/src/main/java/com/aurora/store/data/model/Auth.kt @@ -19,10 +19,13 @@ package com.aurora.store.data.model +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class Auth( val email: String, + + @SerialName("authToken") val auth: String ) diff --git a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt index 111eb8ff4..532f83e33 100644 --- a/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt +++ b/app/src/main/java/com/aurora/store/data/providers/AuthProvider.kt @@ -112,7 +112,10 @@ class AuthProvider @Inject constructor( suspend fun buildAnonymousAuthData(): Result { return withContext(Dispatchers.IO) { try { - val playResponse = httpClient.getAuth(dispenserURL!!).also { + val playResponse = httpClient.postAuth( + dispenserURL!!, + json.encodeToString(spoofProvider.deviceProperties).toByteArray() + ).also { if (!it.isSuccessful) throwError(it, context) } From d4d990cac01fbfa6f4127c518062b1742a80460c Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 5 Jun 2025 21:33:55 +0800 Subject: [PATCH 5/5] Minor string improvements Signed-off-by: Aayush Gupta --- app/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54747d80b..4e0cc2de0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -188,7 +188,7 @@ "Please, disable MIUI optimizations to allow installations, otherwise you can choose Root or Services installer." "Optionally you can choose Native installer, but then you can not install bundled (split) APKs, so choice is yours." "New update available" - "You can not install bundled(split) apps via Native Installer. Change your installer to Session, Services or Root." + "You can not install bundled (split) apps via Native Installer. Change your installer to Session, Services or Root." Account-related notification App export notification Install notification @@ -437,7 +437,7 @@ File exporter Hold on, exporting your file Successfully exported app bundle - Failed to exported app bundle + Failed to export app bundle Log out? @@ -490,8 +490,8 @@ %1$d apps installed - Use microG to sign-in to accounts - Authenticate using microG when signing into Google accounts for a simpler sign-in flow. + Use microG to sign in to accounts + Authenticate using microG when signing into Google accounts for a simpler sign-in flow Set Disable Proxy disabled successfully