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) } 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..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 @@ -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(query) - 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/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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76d4a5e4d..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.3" +gplayapi = "3.5.6" hiddenapibypass = "6.1" hilt = "2.56.2" hiltWork = "1.2.0"