From 49bdeb0f37d1f226d0da956e645233dd0556e60c Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 18 Jan 2024 05:55:12 +0530 Subject: [PATCH 01/26] chip layout --- .../main/res/color/chip_background_color.xml | 5 +++ app/src/main/res/color/chip_border_color.xml | 5 +++ app/src/main/res/color/chip_text_color.xml | 5 +++ app/src/main/res/layout/fragment_search.xml | 36 +++++++++++++++++++ app/src/main/res/values/dimens.xml | 4 +++ app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/values/themes.xml | 13 +++++++ 7 files changed, 70 insertions(+) create mode 100644 app/src/main/res/color/chip_background_color.xml create mode 100644 app/src/main/res/color/chip_border_color.xml create mode 100644 app/src/main/res/color/chip_text_color.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/src/main/res/color/chip_background_color.xml b/app/src/main/res/color/chip_background_color.xml new file mode 100644 index 000000000..4b121e9be --- /dev/null +++ b/app/src/main/res/color/chip_background_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/chip_border_color.xml b/app/src/main/res/color/chip_border_color.xml new file mode 100644 index 000000000..0e1ffb342 --- /dev/null +++ b/app/src/main/res/color/chip_border_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/chip_text_color.xml b/app/src/main/res/color/chip_text_color.xml new file mode 100644 index 000000000..e4b3fed74 --- /dev/null +++ b/app/src/main/res/color/chip_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 07322322d..49dc21378 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -48,19 +48,55 @@ android:layout_marginTop="8dp" android:background="@color/colorGrey" /> + + + + + + + + + + + + 16dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aeb2e0597..6a5387047 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,8 @@ Search for an app No apps found… + No Trackers + Applications Games diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1523963d7..dc5f734d8 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -71,4 +71,17 @@ @color/install_button_background + + \ No newline at end of file -- GitLab From 0ed3a6e3828765dce174695afa05ff176650fd91 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 18 Jan 2024 05:56:06 +0530 Subject: [PATCH 02/26] PrivacyInfoViewModel - getAppPrivacyInfo --- .../main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt index 46c07546d..e74a72737 100644 --- a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt @@ -29,6 +29,10 @@ class PrivacyInfoViewModel @Inject constructor( } } + suspend fun getAppPrivacyInfo(application: Application): Result { + return fetchEmitAppPrivacyInfo(application) + } + fun getSingularAppPrivacyInfoLiveData(application: Application?): LiveData> { fetchPrivacyInfo(application) return singularAppPrivacyInfoLiveData -- GitLab From 2925b7869bc6a0308b73ef8f0fa7388ec2d99dca Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 18 Jan 2024 05:57:51 +0530 Subject: [PATCH 03/26] SearchViewModel - setFilterFlags --- .../e/apps/ui/search/SearchViewModel.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index c46fa4847..f92a9d142 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -25,10 +25,13 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.Result import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Origin +import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException @@ -57,10 +60,31 @@ class SearchViewModel @Inject constructor( private var isLoading: Boolean = false + private var flagNoTrackers: Boolean = false + private var flagOpenSource: Boolean = false + private var flagPWA: Boolean = false + + private var getAppPrivacyInfo: (suspend (application: Application) -> Result)? = null + private var getPrivacyScore: ((application: Application?) -> Int)? = null + companion object { private const val DATA_LOAD_ERROR = "Data load error" } + fun setFilterFlags( + flagNoTrackers: Boolean = false, + flagOpenSource: Boolean = false, + flagPWA: Boolean = false, + getAppPrivacyInfo: suspend (application: Application) -> Result, + getPrivacyScore: (application: Application?) -> Int, + ) { + this.flagNoTrackers = flagNoTrackers + this.flagOpenSource = flagOpenSource + this.flagPWA = flagPWA + this.getAppPrivacyInfo = getAppPrivacyInfo + this.getPrivacyScore = getPrivacyScore + } + fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { viewModelScope.launch(Dispatchers.IO) { if (gPlayAuth.result.isSuccess()) -- GitLab From c3f93d5f92ef847bd6f28596d40c9bb62bb155cf Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 18 Jan 2024 06:00:39 +0530 Subject: [PATCH 04/26] SearchViewModel - auxiliary methods --- .../e/apps/ui/search/SearchViewModel.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index f92a9d142..0a0040d74 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -19,6 +19,7 @@ package foundation.e.apps.ui.search import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.SearchSuggestEntry @@ -36,6 +37,7 @@ import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.login.exceptions.UnknownSourceException +import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,6 +45,10 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext @HiltViewModel class SearchViewModel @Inject constructor( @@ -60,6 +66,8 @@ class SearchViewModel @Inject constructor( private var isLoading: Boolean = false + val accumulatedList = mutableListOf() + private var flagNoTrackers: Boolean = false private var flagOpenSource: Boolean = false private var flagPWA: Boolean = false @@ -224,4 +232,49 @@ class SearchViewModel @Inject constructor( fun isAuthObjectListSame(authObjectList: List?): Boolean { return lastAuthObjects == authObjectList } + + private fun hasTrackers(app: Application): Boolean { + return when { + app.trackers == LIST_OF_NULL -> true // Tracker data unavailable, don't show + app.trackers.isNotEmpty() -> true // Trackers present + app.privacyScore == 0 -> true // Manually blocked apps (Facebook etc.) + else -> false + } + } + + private suspend fun fetchTrackersForApp(app: Application) { + if (app.isPlaceHolder) return + getAppPrivacyInfo?.invoke(app).let { + val calculatedScore = getPrivacyScore?.invoke(app) ?: 0 + app.privacyScore = calculatedScore + } + } + + private suspend fun getFilteredList(): List { + return accumulatedList.filter { + when { + flagOpenSource && flagPWA-> it.is_pwa || it.origin == Origin.CLEANAPK + flagPWA -> it.is_pwa + flagOpenSource -> !it.is_pwa && it.origin == Origin.CLEANAPK + else -> true + } + }.run { + if (!flagNoTrackers) return this + + val trackerCheckedApps = withContext(IO) { + val deferredCheck = this@run.map { + async { + if (it.privacyScore == -1) { + fetchTrackersForApp(it) + } + it + } + } + deferredCheck.awaitAll() + } + + trackerCheckedApps.filter { !hasTrackers(it) } + } + + } } -- GitLab From e99e96dd069918183ef3c3dbb14a08a9f4c14049 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 18 Jan 2024 06:03:01 +0530 Subject: [PATCH 05/26] SearchViewModel - emitFilteredResults --- .../e/apps/ui/search/SearchViewModel.kt | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 0a0040d74..747a314f9 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -57,8 +57,9 @@ class SearchViewModel @Inject constructor( val searchSuggest: MutableLiveData?> = MutableLiveData() - val searchResult: MutableLiveData, Boolean>>> = + private val _searchResult: MutableLiveData, Boolean>>> = MutableLiveData() + val searchResult: LiveData, Boolean>>> = _searchResult private var lastAuthObjects: List? = null @@ -91,6 +92,10 @@ class SearchViewModel @Inject constructor( this.flagPWA = flagPWA this.getAppPrivacyInfo = getAppPrivacyInfo this.getPrivacyScore = getPrivacyScore + + viewModelScope.launch { + emitFilteredResults(null) + } } fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { @@ -142,7 +147,7 @@ class SearchViewModel @Inject constructor( authData ?: AuthData("", "") ) - searchResult.postValue(searchResultSupreme) + emitFilteredResults(searchResultSupreme) if (!searchResultSupreme.isSuccess()) { val exception = @@ -204,13 +209,12 @@ class SearchViewModel @Inject constructor( Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) ) - this@SearchViewModel.searchResult.postValue(finalResult) + emitFilteredResults(finalResult) isLoading = false } private fun updateCurrentAppList(gplaySearchResult: GplaySearchResult): List { - val currentSearchResult = searchResult.value?.data - val currentAppList = currentSearchResult?.first?.toMutableList() ?: mutableListOf() + val currentAppList = accumulatedList currentAppList.removeIf { item -> item.isPlaceHolder } currentAppList.addAll(gplaySearchResult.data?.first ?: emptyList()) return currentAppList.distinctBy { it.package_name } @@ -277,4 +281,38 @@ class SearchViewModel @Inject constructor( } } + + /** + * Pass [result] as null to re-emit already loaded search results with new filters. + */ + private suspend fun emitFilteredResults( + result: ResultSupreme, Boolean>>? = null + ) { + + // When filters are changed but no data is fetched yet + if (result == null && _searchResult.value == null) { + return + } + + if (result != null && !result.isSuccess()) { + _searchResult.postValue(result) + return + } + + if (result != null) { + result.data?.first?.let { + accumulatedList.clear() + accumulatedList.addAll(it) + } + } + + val filteredList = getFilteredList() + val isMoreDataLoading = result?.data?.second ?: _searchResult.value?.data?.second ?: false + + _searchResult.postValue( + ResultSupreme.Success( + Pair(filteredList.toList(), isMoreDataLoading) + ) + ) + } } -- GitLab From 02269261a63ec74abab22ac156b8cc972b5dffd5 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 18 Jan 2024 06:03:37 +0530 Subject: [PATCH 06/26] Integrate with SearchFragment --- .../e/apps/ui/search/SearchFragment.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 0c743352e..ef65657c3 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -25,6 +25,7 @@ import android.os.Bundle import android.provider.BaseColumns import android.view.View import android.view.inputmethod.InputMethodManager +import android.widget.CompoundButton.OnCheckedChangeListener import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout @@ -40,6 +41,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.aurora.gplayapi.SearchSuggestEntry import com.facebook.shimmer.ShimmerFrameLayout +import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.enums.Status @@ -90,6 +92,10 @@ class SearchFragment : private var searchHintLayout: LinearLayout? = null private var noAppsFoundLayout: LinearLayout? = null + private var filterChipNoTrackers: Chip? = null + private var filterChipOpenSource: Chip? = null + private var filterChipPWA: Chip? = null + /* * Store the string from onQueryTextSubmit() and access it from loadData() * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 @@ -106,6 +112,10 @@ class SearchFragment : searchHintLayout = binding.searchHintLayout.root noAppsFoundLayout = binding.noAppsFoundLayout.root + filterChipNoTrackers = binding.filterChipNoTrackers + filterChipOpenSource = binding.filterChipOpenSource + filterChipPWA = binding.filterChipPWA + setupSearchView() setupSearchViewSuggestions() @@ -114,6 +124,8 @@ class SearchFragment : observeSearchResult(listAdapter) + setupSearchFilters() + setupListening() authObjects.observe(viewLifecycleOwner) { @@ -255,6 +267,22 @@ class SearchFragment : } } + private fun setupSearchFilters() { + val listener = OnCheckedChangeListener { _, _ -> + searchViewModel.setFilterFlags( + flagNoTrackers = filterChipNoTrackers?.isChecked ?: false, + flagOpenSource = filterChipOpenSource?.isChecked ?: false, + flagPWA = filterChipPWA?.isChecked ?: false, + privacyInfoViewModel::getAppPrivacyInfo, + privacyInfoViewModel::getPrivacyScore, + ) + } + + filterChipNoTrackers?.setOnCheckedChangeListener(listener) + filterChipOpenSource?.setOnCheckedChangeListener(listener) + filterChipPWA?.setOnCheckedChangeListener(listener) + } + private fun setupSearchView() { setHasOptionsMenu(true) searchView?.setOnSuggestionListener(this) -- GitLab From 1e6fb253d202467eac8c8a29f9c89943e0b4569a Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 24 Jan 2024 09:31:06 +0530 Subject: [PATCH 07/26] rearrange filter logic --- .../e/apps/ui/search/SearchViewModel.kt | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 747a314f9..05b75e40f 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -254,32 +254,25 @@ class SearchViewModel @Inject constructor( } } - private suspend fun getFilteredList(): List { - return accumulatedList.filter { - when { - flagOpenSource && flagPWA-> it.is_pwa || it.origin == Origin.CLEANAPK - flagPWA -> it.is_pwa - flagOpenSource -> !it.is_pwa && it.origin == Origin.CLEANAPK - else -> true - } - }.run { - if (!flagNoTrackers) return this - - val trackerCheckedApps = withContext(IO) { - val deferredCheck = this@run.map { - async { - if (it.privacyScore == -1) { - fetchTrackersForApp(it) - } - it + private suspend fun getFilteredList(): List = withContext(IO) { + if (flagNoTrackers) { + val deferredCheck = accumulatedList.map { + async { + if (it.privacyScore == -1) { + fetchTrackersForApp(it) } + it } - deferredCheck.awaitAll() } - - trackerCheckedApps.filter { !hasTrackers(it) } + deferredCheck.awaitAll() + } + accumulatedList.filter { + if (!flagNoTrackers && !flagOpenSource && !flagPWA) return@filter true + if (flagNoTrackers && !hasTrackers(it)) return@filter true + if (flagOpenSource && !it.is_pwa && it.origin == Origin.CLEANAPK) return@filter true + if (flagPWA && it.is_pwa) return@filter true + false } - } /** -- GitLab From 8cfa80d6fb7b1437e72cd2589314e031a832019f Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Sun, 28 Jan 2024 20:11:19 +0530 Subject: [PATCH 08/26] fetch trackers if tracker list is empty --- .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 05b75e40f..535cac8fb 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -258,7 +258,7 @@ class SearchViewModel @Inject constructor( if (flagNoTrackers) { val deferredCheck = accumulatedList.map { async { - if (it.privacyScore == -1) { + if (it.trackers.isEmpty()) { fetchTrackersForApp(it) } it -- GitLab From eaf92c456a992f976f0df4897896ed7929c10c5a Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Sun, 28 Jan 2024 20:16:24 +0530 Subject: [PATCH 09/26] showLoadingUI() if any filter is clicked --- app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index ef65657c3..953ccc844 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -269,6 +269,7 @@ class SearchFragment : private fun setupSearchFilters() { val listener = OnCheckedChangeListener { _, _ -> + showLoadingUI() searchViewModel.setFilterFlags( flagNoTrackers = filterChipNoTrackers?.isChecked ?: false, flagOpenSource = filterChipOpenSource?.isChecked ?: false, -- GitLab From 2e9bf0729452ef7ad5571a764bf3b4c1154bf912 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 29 Jan 2024 00:19:02 +0530 Subject: [PATCH 10/26] copy tracker and perm info --- .../e/apps/ui/applicationlist/ApplicationListRVAdapter.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 40572c6a3..43cd66851 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -535,6 +535,9 @@ class ApplicationListRVAdapter( currentList.forEach { newList.find { item -> item._id == it._id }?.let { foundItem -> foundItem.privacyScore = it.privacyScore + foundItem.trackers = it.trackers + foundItem.perms = it.perms + foundItem.permsFromExodus = it.permsFromExodus } } this.submitList(newList.map { it.copy() }) -- GitLab From 78f12a4e34a5af5ad6d402213b2ec6304a3d1edb Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 29 Jan 2024 00:21:15 +0530 Subject: [PATCH 11/26] Revert "fetch trackers if tracker list is empty" This reverts commit 62c86e2f5fd51817f8d64c14a126c2118f2aade9. --- .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 535cac8fb..05b75e40f 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -258,7 +258,7 @@ class SearchViewModel @Inject constructor( if (flagNoTrackers) { val deferredCheck = accumulatedList.map { async { - if (it.trackers.isEmpty()) { + if (it.privacyScore == -1) { fetchTrackersForApp(it) } it -- GitLab From 6ba13d12127f801594a408bb56c261a69c5a638e Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 29 Jan 2024 04:13:01 +0530 Subject: [PATCH 12/26] load more gplay results if less than 10 results are shown --- .../java/foundation/e/apps/data/Constants.kt | 2 ++ .../e/apps/ui/search/SearchFragment.kt | 14 ++++++++++++ .../e/apps/ui/search/SearchViewModel.kt | 22 ++++++++++--------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 20e9c0bc8..10b09b4f2 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -9,4 +9,6 @@ object Constants { const val ACTION_AUTHDATA_DUMP = "foundation.e.apps.action.DUMP_GACCOUNT_INFO" const val TAG_AUTHDATA_DUMP = "AUTHDATA_DUMP" + + const val MIN_SEARCH_DISPLAY_ITEMS = 10 } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 953ccc844..0c0e7d740 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -44,6 +44,7 @@ import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R +import foundation.e.apps.data.Constants.MIN_SEARCH_DISPLAY_ITEMS import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application @@ -122,6 +123,7 @@ class SearchFragment : // Setup Search Results val listAdapter = setupSearchResult(view) + observeMinResultLoad() observeSearchResult(listAdapter) setupSearchFilters() @@ -184,6 +186,18 @@ class SearchFragment : } } + private fun observeMinResultLoad() { + searchViewModel.gplaySearchLoaded.observe(viewLifecycleOwner) { + if (!it) return@observe + val searchList = + searchViewModel.searchResult.value?.data?.first?.toMutableList() ?: emptyList() + val canLoadMore = searchViewModel.searchResult.value?.data?.second ?: false + if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { + searchViewModel.loadMore(searchText, autoTriggered = true) + } + } + } + private fun observeScrollOfSearchResult(listAdapter: ApplicationListRVAdapter?) { listAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 05b75e40f..ce7d0fc3a 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -39,15 +39,14 @@ import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.ui.parentFragment.LoadingViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -import kotlin.coroutines.coroutineContext import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @HiltViewModel @@ -61,11 +60,14 @@ class SearchViewModel @Inject constructor( MutableLiveData() val searchResult: LiveData, Boolean>>> = _searchResult + val gplaySearchLoaded = MutableLiveData(false) + private var lastAuthObjects: List? = null private var nextSubBundle: Set? = null private var isLoading: Boolean = false + private var hasGPlayBeenFetched = false val accumulatedList = mutableListOf() @@ -147,6 +149,7 @@ class SearchViewModel @Inject constructor( authData ?: AuthData("", "") ) + hasGPlayBeenFetched = false emitFilteredResults(searchResultSupreme) if (!searchResultSupreme.isSuccess()) { @@ -175,13 +178,16 @@ class SearchViewModel @Inject constructor( } } - fun loadMore(query: String) { + fun loadMore(query: String, autoTriggered: Boolean = false) { if (isLoading) { Timber.d("Search result is loading....") return } viewModelScope.launch(Dispatchers.IO) { + if (autoTriggered) { + delay(1000) // prevent overloading APIs and 401 error code + } fetchGplayData(query) } } @@ -197,19 +203,14 @@ class SearchViewModel @Inject constructor( val isFirstFetch = nextSubBundle == null nextSubBundle = gplaySearchResult.data?.second - // first page has less data, then fetch next page data without waiting for users' scroll - if (isFirstFetch && gplaySearchResult.isSuccess()) { - CoroutineScope(coroutineContext).launch { - fetchGplayData(query) - } - } - val currentAppList = updateCurrentAppList(gplaySearchResult) val finalResult = ResultSupreme.Success( Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) ) + hasGPlayBeenFetched = true emitFilteredResults(finalResult) + isLoading = false } @@ -307,5 +308,6 @@ class SearchViewModel @Inject constructor( Pair(filteredList.toList(), isMoreDataLoading) ) ) + gplaySearchLoaded.postValue(hasGPlayBeenFetched) } } -- GitLab From ed0728c2ad4f0dd12c6ab232ce8d1d3d25570acc Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 29 Feb 2024 19:54:13 +0530 Subject: [PATCH 13/26] set single selection for the time --- .../main/java/foundation/e/apps/ui/search/SearchFragment.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 0c0e7d740..182cf206d 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -282,6 +282,9 @@ class SearchFragment : } private fun setupSearchFilters() { + + binding.filterChipGroup.isSingleSelection = true + val listener = OnCheckedChangeListener { _, _ -> showLoadingUI() searchViewModel.setFilterFlags( -- GitLab From cad7ec917507721418cf1cb61ae6c89170b0f611 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Thu, 7 Mar 2024 18:39:41 +0530 Subject: [PATCH 14/26] fix lintRelease errors --- .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 4 ++-- .../java/foundation/e/apps/ui/updates/UpdatesViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index ce7d0fc3a..04fa2f76b 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -60,7 +60,7 @@ class SearchViewModel @Inject constructor( MutableLiveData() val searchResult: LiveData, Boolean>>> = _searchResult - val gplaySearchLoaded = MutableLiveData(false) + val gplaySearchLoaded : MutableLiveData = MutableLiveData(false) private var lastAuthObjects: List? = null @@ -289,7 +289,7 @@ class SearchViewModel @Inject constructor( } if (result != null && !result.isSuccess()) { - _searchResult.postValue(result) + _searchResult.postValue(result!!) return } diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt index 12efeb8f6..77214dc20 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt @@ -57,7 +57,7 @@ class UpdatesViewModel @Inject constructor( } successAuthList.find { it is AuthObject.CleanApk }?.run { - getUpdates(AuthData("", "")) + getUpdates(null) return@onLoadData } }, retryBlock) -- GitLab From 84cf69f486333151b277bd5624d4c3f1e5919514 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 11 Mar 2024 15:56:39 +0530 Subject: [PATCH 15/26] use lateinit var for chips --- .../e/apps/ui/search/SearchFragment.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 182cf206d..e423bfce1 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -93,9 +93,9 @@ class SearchFragment : private var searchHintLayout: LinearLayout? = null private var noAppsFoundLayout: LinearLayout? = null - private var filterChipNoTrackers: Chip? = null - private var filterChipOpenSource: Chip? = null - private var filterChipPWA: Chip? = null + lateinit var filterChipNoTrackers: Chip + lateinit var filterChipOpenSource: Chip + lateinit var filterChipPWA: Chip /* * Store the string from onQueryTextSubmit() and access it from loadData() @@ -288,17 +288,17 @@ class SearchFragment : val listener = OnCheckedChangeListener { _, _ -> showLoadingUI() searchViewModel.setFilterFlags( - flagNoTrackers = filterChipNoTrackers?.isChecked ?: false, - flagOpenSource = filterChipOpenSource?.isChecked ?: false, - flagPWA = filterChipPWA?.isChecked ?: false, + flagNoTrackers = filterChipNoTrackers.isChecked, + flagOpenSource = filterChipOpenSource.isChecked, + flagPWA = filterChipPWA.isChecked, privacyInfoViewModel::getAppPrivacyInfo, privacyInfoViewModel::getPrivacyScore, ) } - filterChipNoTrackers?.setOnCheckedChangeListener(listener) - filterChipOpenSource?.setOnCheckedChangeListener(listener) - filterChipPWA?.setOnCheckedChangeListener(listener) + filterChipNoTrackers.setOnCheckedChangeListener(listener) + filterChipOpenSource.setOnCheckedChangeListener(listener) + filterChipPWA.setOnCheckedChangeListener(listener) } private fun setupSearchView() { -- GitLab From 009d88bdd6c4175d8386ea375c97c2e783121304 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 11 Mar 2024 16:13:07 +0530 Subject: [PATCH 16/26] fix detekt --- app/src/main/java/foundation/e/apps/data/Constants.kt | 2 ++ app/src/main/java/foundation/e/apps/data/NetworkHandler.kt | 2 +- .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 10b09b4f2..a0ee85269 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -11,4 +11,6 @@ object Constants { const val TAG_AUTHDATA_DUMP = "AUTHDATA_DUMP" const val MIN_SEARCH_DISPLAY_ITEMS = 10 + + const val ONE_SECOND_IN_MILLIS = 1000L } diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index f5c5cdd50..ea29bd423 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -18,6 +18,7 @@ package foundation.e.apps.data +import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.login.exceptions.GPlayException @@ -31,7 +32,6 @@ private const val STATUS = "Status:" private const val ERROR_GPLAY_API = "Gplay api has faced error!" private const val REGEX_429_OR_401 = "429|401" private const val MAX_RETRY_DELAY_IN_SECONDS = 300 -private const val ONE_SECOND_IN_MILLIS = 1000L private const val INITIAL_DELAY_RETRY_IN_SECONDS = 10 suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 04fa2f76b..10aa8b23a 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -26,6 +26,7 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS import foundation.e.apps.data.Result import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository @@ -186,7 +187,7 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { if (autoTriggered) { - delay(1000) // prevent overloading APIs and 401 error code + delay(ONE_SECOND_IN_MILLIS) // prevent overloading APIs and 401 error code } fetchGplayData(query) } @@ -200,7 +201,6 @@ class SearchViewModel @Inject constructor( handleException(gplaySearchResult.exception ?: UnknownSourceException()) } - val isFirstFetch = nextSubBundle == null nextSubBundle = gplaySearchResult.data?.second val currentAppList = updateCurrentAppList(gplaySearchResult) -- GitLab From 8d0e7b8599e985fd2061563f87d562285ca50ecf Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 11 Mar 2024 16:18:01 +0530 Subject: [PATCH 17/26] change method name observeMinResultLoad -> preventLoadingLessResults --- .../java/foundation/e/apps/ui/search/SearchFragment.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index e423bfce1..5919b4023 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -123,7 +123,7 @@ class SearchFragment : // Setup Search Results val listAdapter = setupSearchResult(view) - observeMinResultLoad() + preventLoadingLessResults() observeSearchResult(listAdapter) setupSearchFilters() @@ -186,12 +186,15 @@ class SearchFragment : } } - private fun observeMinResultLoad() { + private fun preventLoadingLessResults() { searchViewModel.gplaySearchLoaded.observe(viewLifecycleOwner) { + if (!it) return@observe + val searchList = searchViewModel.searchResult.value?.data?.first?.toMutableList() ?: emptyList() val canLoadMore = searchViewModel.searchResult.value?.data?.second ?: false + if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { searchViewModel.loadMore(searchText, autoTriggered = true) } -- GitLab From 09f0da33d6e794dab6470c027b061c6bbc813272 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 11 Mar 2024 16:43:34 +0530 Subject: [PATCH 18/26] use typealias for ResultSupreme, Boolean>> --- .../e/apps/data/application/ApplicationRepository.kt | 3 ++- .../e/apps/data/application/search/SearchApi.kt | 3 ++- .../e/apps/data/application/search/SearchApiImpl.kt | 12 ++++++------ .../foundation/e/apps/ui/search/SearchViewModel.kt | 8 +++++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt index 5808dda59..343f67743 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt @@ -39,6 +39,7 @@ import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.application.search.SearchApi import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.fusedDownload.models.FusedDownload +import foundation.e.apps.ui.search.SearchDataForUI import javax.inject.Inject import javax.inject.Singleton @@ -119,7 +120,7 @@ class ApplicationRepository @Inject constructor( suspend fun getCleanApkSearchResults( query: String, authData: AuthData - ): ResultSupreme, Boolean>> { + ): SearchDataForUI { return searchAPIImpl.getCleanApkSearchResults(query, authData) } diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt index f539f613c..95adde3fb 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt @@ -23,6 +23,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.data.Application +import foundation.e.apps.ui.search.SearchDataForUI typealias GplaySearchResult = ResultSupreme, Set>> @@ -45,7 +46,7 @@ interface SearchApi { suspend fun getCleanApkSearchResults( query: String, authData: AuthData - ): ResultSupreme, Boolean>> + ): SearchDataForUI suspend fun getGplaySearchResult( query: String, diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index c7379ff43..5894f13e9 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -39,6 +39,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.ui.search.SearchDataForUI import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Deferred @@ -85,9 +86,8 @@ class SearchApiImpl @Inject constructor( override suspend fun getCleanApkSearchResults( query: String, authData: AuthData - ): ResultSupreme, Boolean>> { - var finalSearchResult: ResultSupreme, Boolean>> = - ResultSupreme.Error() + ): SearchDataForUI { + var finalSearchResult: SearchDataForUI = ResultSupreme.Error() val packageSpecificResults = fetchPackageSpecificResult(authData, query).data?.first ?: emptyList() @@ -125,7 +125,7 @@ class SearchApiImpl @Inject constructor( query: String, searchResult: MutableList, packageSpecificResults: List - ): ResultSupreme, Boolean>> { + ): SearchDataForUI { val pwaApps: MutableList = mutableListOf() val result = handleNetworkResult { val apps = @@ -159,7 +159,7 @@ class SearchApiImpl @Inject constructor( query: String, searchResult: MutableList, packageSpecificResults: List - ): ResultSupreme, Boolean>> { + ): SearchDataForUI { val cleanApkResults = mutableListOf() val result = handleNetworkResult { @@ -187,7 +187,7 @@ class SearchApiImpl @Inject constructor( private suspend fun fetchPackageSpecificResult( authData: AuthData, query: String, - ): ResultSupreme, Boolean>> { + ): SearchDataForUI { val packageSpecificResults: MutableList = mutableListOf() var gplayPackageResult: Application? = null var cleanapkPackageResult: Application? = null diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 10aa8b23a..fe00c07d8 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -50,6 +50,8 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +typealias SearchDataForUI = ResultSupreme, Boolean>> + @HiltViewModel class SearchViewModel @Inject constructor( private val applicationRepository: ApplicationRepository, @@ -57,9 +59,9 @@ class SearchViewModel @Inject constructor( val searchSuggest: MutableLiveData?> = MutableLiveData() - private val _searchResult: MutableLiveData, Boolean>>> = + private val _searchResult: MutableLiveData = MutableLiveData() - val searchResult: LiveData, Boolean>>> = _searchResult + val searchResult: LiveData = _searchResult val gplaySearchLoaded : MutableLiveData = MutableLiveData(false) @@ -280,7 +282,7 @@ class SearchViewModel @Inject constructor( * Pass [result] as null to re-emit already loaded search results with new filters. */ private suspend fun emitFilteredResults( - result: ResultSupreme, Boolean>>? = null + result: SearchDataForUI? = null ) { // When filters are changed but no data is fetched yet -- GitLab From c339a1ed6b3e0769ae802a658ef2d7f23cc8d714 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 13 Mar 2024 17:40:15 +0530 Subject: [PATCH 19/26] scroll to top on filters change --- app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 5919b4023..ac01e65ec 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -297,6 +297,8 @@ class SearchFragment : privacyInfoViewModel::getAppPrivacyInfo, privacyInfoViewModel::getPrivacyScore, ) + + recyclerView?.scrollToPosition(0) } filterChipNoTrackers.setOnCheckedChangeListener(listener) -- GitLab From d237d86ad2125bace80195cb6b22c5918c6b8bba Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 13 Mar 2024 19:23:14 +0530 Subject: [PATCH 20/26] use IAppPrivacyInfoRepository and PrivacyScoreRepository --- .../e/apps/ui/search/SearchFragment.kt | 2 -- .../e/apps/ui/search/SearchViewModel.kt | 17 ++++++----------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index ac01e65ec..bd697751d 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -294,8 +294,6 @@ class SearchFragment : flagNoTrackers = filterChipNoTrackers.isChecked, flagOpenSource = filterChipOpenSource.isChecked, flagPWA = filterChipPWA.isChecked, - privacyInfoViewModel::getAppPrivacyInfo, - privacyInfoViewModel::getPrivacyScore, ) recyclerView?.scrollToPosition(0) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index fe00c07d8..93a9fd149 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -27,13 +27,13 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS -import foundation.e.apps.data.Result import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Origin -import foundation.e.apps.data.exodus.models.AppPrivacyInfo +import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository +import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException @@ -55,6 +55,8 @@ typealias SearchDataForUI = ResultSupreme, Boolean>> @HiltViewModel class SearchViewModel @Inject constructor( private val applicationRepository: ApplicationRepository, + private val privacyScoreRepository: PrivacyScoreRepository, + private val appPrivacyInfoRepository: IAppPrivacyInfoRepository ) : LoadingViewModel() { val searchSuggest: MutableLiveData?> = MutableLiveData() @@ -78,9 +80,6 @@ class SearchViewModel @Inject constructor( private var flagOpenSource: Boolean = false private var flagPWA: Boolean = false - private var getAppPrivacyInfo: (suspend (application: Application) -> Result)? = null - private var getPrivacyScore: ((application: Application?) -> Int)? = null - companion object { private const val DATA_LOAD_ERROR = "Data load error" } @@ -89,14 +88,10 @@ class SearchViewModel @Inject constructor( flagNoTrackers: Boolean = false, flagOpenSource: Boolean = false, flagPWA: Boolean = false, - getAppPrivacyInfo: suspend (application: Application) -> Result, - getPrivacyScore: (application: Application?) -> Int, ) { this.flagNoTrackers = flagNoTrackers this.flagOpenSource = flagOpenSource this.flagPWA = flagPWA - this.getAppPrivacyInfo = getAppPrivacyInfo - this.getPrivacyScore = getPrivacyScore viewModelScope.launch { emitFilteredResults(null) @@ -251,8 +246,8 @@ class SearchViewModel @Inject constructor( private suspend fun fetchTrackersForApp(app: Application) { if (app.isPlaceHolder) return - getAppPrivacyInfo?.invoke(app).let { - val calculatedScore = getPrivacyScore?.invoke(app) ?: 0 + appPrivacyInfoRepository.getAppPrivacyInfo(app, app.package_name).let { + val calculatedScore = privacyScoreRepository.calculatePrivacyScore(app) app.privacyScore = calculatedScore } } -- GitLab From b9a07d642642524a2b53becf63da5ca45ceb2dbd Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 13 Mar 2024 19:26:55 +0530 Subject: [PATCH 21/26] move checking less results to viewmodel --- .../foundation/e/apps/ui/search/SearchFragment.kt | 9 +-------- .../foundation/e/apps/ui/search/SearchViewModel.kt | 11 +++++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index bd697751d..314e6de78 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -188,16 +188,9 @@ class SearchFragment : private fun preventLoadingLessResults() { searchViewModel.gplaySearchLoaded.observe(viewLifecycleOwner) { - if (!it) return@observe - val searchList = - searchViewModel.searchResult.value?.data?.first?.toMutableList() ?: emptyList() - val canLoadMore = searchViewModel.searchResult.value?.data?.second ?: false - - if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { - searchViewModel.loadMore(searchText, autoTriggered = true) - } + searchViewModel.loadMoreDataIfNeeded(searchText) } } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 93a9fd149..89bf82e69 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -26,6 +26,7 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.Constants.MIN_SEARCH_DISPLAY_ITEMS import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository @@ -307,4 +308,14 @@ class SearchViewModel @Inject constructor( ) gplaySearchLoaded.postValue(hasGPlayBeenFetched) } + + fun loadMoreDataIfNeeded(searchText: String) { + val searchList = + searchResult.value?.data?.first?.toMutableList() ?: emptyList() + val canLoadMore = searchResult.value?.data?.second ?: false + + if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { + loadMore(searchText, autoTriggered = true) + } + } } -- GitLab From 2c98fcd17792bfc407540f88633ee6e42dd3daaf Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 13 Mar 2024 19:32:14 +0530 Subject: [PATCH 22/26] move MIN_SEARCH_DISPLAY_ITEMS to SearchViewModel --- app/src/main/java/foundation/e/apps/data/Constants.kt | 2 -- app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt | 1 - .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index a0ee85269..46ed6551c 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -10,7 +10,5 @@ object Constants { const val ACTION_AUTHDATA_DUMP = "foundation.e.apps.action.DUMP_GACCOUNT_INFO" const val TAG_AUTHDATA_DUMP = "AUTHDATA_DUMP" - const val MIN_SEARCH_DISPLAY_ITEMS = 10 - const val ONE_SECOND_IN_MILLIS = 1000L } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index 314e6de78..45aaa7cfd 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -44,7 +44,6 @@ import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.chip.Chip import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R -import foundation.e.apps.data.Constants.MIN_SEARCH_DISPLAY_ITEMS import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 89bf82e69..19283af28 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -26,7 +26,6 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.Constants.MIN_SEARCH_DISPLAY_ITEMS import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository @@ -83,6 +82,7 @@ class SearchViewModel @Inject constructor( companion object { private const val DATA_LOAD_ERROR = "Data load error" + private const val MIN_SEARCH_DISPLAY_ITEMS = 10 } fun setFilterFlags( -- GitLab From e9908eb8d67e456adb21ffde8f845f0746c543f3 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 13 Mar 2024 14:02:46 +0000 Subject: [PATCH 23/26] Apply 1 suggestion(s) to 1 file(s) --- .../e/apps/ui/search/SearchViewModel.kt | 643 +++++++++--------- 1 file changed, 322 insertions(+), 321 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 19283af28..1a6c5b87c 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -1,321 +1,322 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program 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 3 of the License, or - * (at your option) any later version. - * - * This program 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 this program. If not, see . - */ - -package foundation.e.apps.ui.search - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import com.aurora.gplayapi.SearchSuggestEntry -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.SearchBundle -import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.search.GplaySearchResult -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.enums.Origin -import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository -import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository -import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.login.exceptions.UnknownSourceException -import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL -import foundation.e.apps.ui.parentFragment.LoadingViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext - -typealias SearchDataForUI = ResultSupreme, Boolean>> - -@HiltViewModel -class SearchViewModel @Inject constructor( - private val applicationRepository: ApplicationRepository, - private val privacyScoreRepository: PrivacyScoreRepository, - private val appPrivacyInfoRepository: IAppPrivacyInfoRepository -) : LoadingViewModel() { - - val searchSuggest: MutableLiveData?> = MutableLiveData() - - private val _searchResult: MutableLiveData = - MutableLiveData() - val searchResult: LiveData = _searchResult - - val gplaySearchLoaded : MutableLiveData = MutableLiveData(false) - - private var lastAuthObjects: List? = null - - private var nextSubBundle: Set? = null - - private var isLoading: Boolean = false - private var hasGPlayBeenFetched = false - - val accumulatedList = mutableListOf() - - private var flagNoTrackers: Boolean = false - private var flagOpenSource: Boolean = false - private var flagPWA: Boolean = false - - companion object { - private const val DATA_LOAD_ERROR = "Data load error" - private const val MIN_SEARCH_DISPLAY_ITEMS = 10 - } - - fun setFilterFlags( - flagNoTrackers: Boolean = false, - flagOpenSource: Boolean = false, - flagPWA: Boolean = false, - ) { - this.flagNoTrackers = flagNoTrackers - this.flagOpenSource = flagOpenSource - this.flagPWA = flagPWA - - viewModelScope.launch { - emitFilteredResults(null) - } - } - - fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { - viewModelScope.launch(Dispatchers.IO) { - if (gPlayAuth.result.isSuccess()) - searchSuggest.postValue( - applicationRepository.getSearchSuggestions(query) - ) - } - } - - fun loadData( - query: String, - lifecycleOwner: LifecycleOwner, - authObjectList: List, - retryBlock: (failedObjects: List) -> Boolean - ) { - - if (query.isBlank()) return - - this.lastAuthObjects = authObjectList - super.onLoadData(authObjectList, { successAuthList, _ -> - - successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getSearchResults(query, result.data!! as AuthData) - return@onLoadData - } - - successAuthList.find { it is AuthObject.CleanApk }?.run { - getSearchResults(query, null) - return@onLoadData - } - }, retryBlock) - } - - /* - * Observe data from Fused API and publish the result in searchResult. - * This allows us to show apps as they are being fetched from the network, - * without having to wait for all of the apps. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 - */ - private fun getSearchResults( - query: String, - authData: AuthData? - ) { - viewModelScope.launch(Dispatchers.IO) { - val searchResultSupreme = applicationRepository.getCleanApkSearchResults( - query, - authData ?: AuthData("", "") - ) - - hasGPlayBeenFetched = false - emitFilteredResults(searchResultSupreme) - - if (!searchResultSupreme.isSuccess()) { - val exception = - if (authData != null) { - GPlayException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } - ) - } else { - CleanApkException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } - ) - } - - handleException(exception) - } - - if (authData == null) { - return@launch - } - - nextSubBundle = null - fetchGplayData(query) - } - } - - fun loadMore(query: String, autoTriggered: Boolean = false) { - if (isLoading) { - Timber.d("Search result is loading....") - return - } - - viewModelScope.launch(Dispatchers.IO) { - if (autoTriggered) { - delay(ONE_SECOND_IN_MILLIS) // prevent overloading APIs and 401 error code - } - fetchGplayData(query) - } - } - - private suspend fun fetchGplayData(query: String) { - isLoading = true - val gplaySearchResult = applicationRepository.getGplaySearchResults(query, nextSubBundle) - - if (!gplaySearchResult.isSuccess()) { - handleException(gplaySearchResult.exception ?: UnknownSourceException()) - } - - nextSubBundle = gplaySearchResult.data?.second - - val currentAppList = updateCurrentAppList(gplaySearchResult) - val finalResult = ResultSupreme.Success( - Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) - ) - - hasGPlayBeenFetched = true - emitFilteredResults(finalResult) - - isLoading = false - } - - private fun updateCurrentAppList(gplaySearchResult: GplaySearchResult): List { - val currentAppList = accumulatedList - currentAppList.removeIf { item -> item.isPlaceHolder } - currentAppList.addAll(gplaySearchResult.data?.first ?: emptyList()) - return currentAppList.distinctBy { it.package_name } - } - - private fun handleException(exception: Exception) { - exceptionsList.add(exception) - exceptionsLiveData.postValue(exceptionsList) - } - - /** - * @return returns true if there is changes in data, otherwise false - */ - fun isAnyAppUpdated( - newApplications: List, - oldApplications: List - ) = applicationRepository.isAnyFusedAppUpdated(newApplications, oldApplications) - - fun isAuthObjectListSame(authObjectList: List?): Boolean { - return lastAuthObjects == authObjectList - } - - private fun hasTrackers(app: Application): Boolean { - return when { - app.trackers == LIST_OF_NULL -> true // Tracker data unavailable, don't show - app.trackers.isNotEmpty() -> true // Trackers present - app.privacyScore == 0 -> true // Manually blocked apps (Facebook etc.) - else -> false - } - } - - private suspend fun fetchTrackersForApp(app: Application) { - if (app.isPlaceHolder) return - appPrivacyInfoRepository.getAppPrivacyInfo(app, app.package_name).let { - val calculatedScore = privacyScoreRepository.calculatePrivacyScore(app) - app.privacyScore = calculatedScore - } - } - - private suspend fun getFilteredList(): List = withContext(IO) { - if (flagNoTrackers) { - val deferredCheck = accumulatedList.map { - async { - if (it.privacyScore == -1) { - fetchTrackersForApp(it) - } - it - } - } - deferredCheck.awaitAll() - } - accumulatedList.filter { - if (!flagNoTrackers && !flagOpenSource && !flagPWA) return@filter true - if (flagNoTrackers && !hasTrackers(it)) return@filter true - if (flagOpenSource && !it.is_pwa && it.origin == Origin.CLEANAPK) return@filter true - if (flagPWA && it.is_pwa) return@filter true - false - } - } - - /** - * Pass [result] as null to re-emit already loaded search results with new filters. - */ - private suspend fun emitFilteredResults( - result: SearchDataForUI? = null - ) { - - // When filters are changed but no data is fetched yet - if (result == null && _searchResult.value == null) { - return - } - - if (result != null && !result.isSuccess()) { - _searchResult.postValue(result!!) - return - } - - if (result != null) { - result.data?.first?.let { - accumulatedList.clear() - accumulatedList.addAll(it) - } - } - - val filteredList = getFilteredList() - val isMoreDataLoading = result?.data?.second ?: _searchResult.value?.data?.second ?: false - - _searchResult.postValue( - ResultSupreme.Success( - Pair(filteredList.toList(), isMoreDataLoading) - ) - ) - gplaySearchLoaded.postValue(hasGPlayBeenFetched) - } - - fun loadMoreDataIfNeeded(searchText: String) { - val searchList = - searchResult.value?.data?.first?.toMutableList() ?: emptyList() - val canLoadMore = searchResult.value?.data?.second ?: false - - if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { - loadMore(searchText, autoTriggered = true) - } - } -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program 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 3 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see . + */ + +package foundation.e.apps.ui.search + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.aurora.gplayapi.SearchSuggestEntry +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.data.models.SearchBundle +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.search.GplaySearchResult +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Origin +import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository +import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository +import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.login.exceptions.CleanApkException +import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.login.exceptions.UnknownSourceException +import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL +import foundation.e.apps.ui.parentFragment.LoadingViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext + +typealias SearchDataForUI = ResultSupreme, Boolean>> + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val applicationRepository: ApplicationRepository, + private val privacyScoreRepository: PrivacyScoreRepository, + private val appPrivacyInfoRepository: IAppPrivacyInfoRepository +) : LoadingViewModel() { + + val searchSuggest: MutableLiveData?> = MutableLiveData() + + private val _searchResult: MutableLiveData = + MutableLiveData() + val searchResult: LiveData = _searchResult + + val gplaySearchLoaded : MutableLiveData = MutableLiveData(false) + + private var lastAuthObjects: List? = null + + private var nextSubBundle: Set? = null + + private var isLoading: Boolean = false + private var hasGPlayBeenFetched = false + + val accumulatedList = mutableListOf() + + private var flagNoTrackers: Boolean = false + private var flagOpenSource: Boolean = false + private var flagPWA: Boolean = false + + companion object { + private const val DATA_LOAD_ERROR = "Data load error" + private const val MIN_SEARCH_DISPLAY_ITEMS = 10 + } + + fun setFilterFlags( + flagNoTrackers: Boolean = false, + flagOpenSource: Boolean = false, + flagPWA: Boolean = false, + ) { + this.flagNoTrackers = flagNoTrackers + this.flagOpenSource = flagOpenSource + this.flagPWA = flagPWA + + viewModelScope.launch { + emitFilteredResults(null) + } + } + + fun getSearchSuggestions(query: String, gPlayAuth: AuthObject.GPlayAuth) { + viewModelScope.launch(Dispatchers.IO) { + if (gPlayAuth.result.isSuccess()) + searchSuggest.postValue( + applicationRepository.getSearchSuggestions(query) + ) + } + } + + fun loadData( + query: String, + lifecycleOwner: LifecycleOwner, + authObjectList: List, + retryBlock: (failedObjects: List) -> Boolean + ) { + + if (query.isBlank()) return + + this.lastAuthObjects = authObjectList + super.onLoadData(authObjectList, { successAuthList, _ -> + + successAuthList.find { it is AuthObject.GPlayAuth }?.run { + getSearchResults(query, result.data!! as AuthData) + return@onLoadData + } + + successAuthList.find { it is AuthObject.CleanApk }?.run { + getSearchResults(query, null) + return@onLoadData + } + }, retryBlock) + } + + /* + * Observe data from Fused API and publish the result in searchResult. + * This allows us to show apps as they are being fetched from the network, + * without having to wait for all of the apps. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 + */ + private fun getSearchResults( + query: String, + authData: AuthData? + ) { + viewModelScope.launch(Dispatchers.IO) { + val searchResultSupreme = applicationRepository.getCleanApkSearchResults( + query, + authData ?: AuthData("", "") + ) + + hasGPlayBeenFetched = false + emitFilteredResults(searchResultSupreme) + + if (!searchResultSupreme.isSuccess()) { + val exception = + if (authData != null) { + GPlayException( + searchResultSupreme.isTimeout(), + searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } + ) + } else { + CleanApkException( + searchResultSupreme.isTimeout(), + searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } + ) + } + + handleException(exception) + } + + if (authData == null) { + return@launch + } + + nextSubBundle = null + fetchGplayData(query) + } + } + + fun loadMore(query: String, autoTriggered: Boolean = false) { + if (isLoading) { + Timber.d("Search result is loading....") + return + } + + viewModelScope.launch(Dispatchers.IO) { + if (autoTriggered) { + delay(ONE_SECOND_IN_MILLIS) // prevent overloading APIs and 401 error code + } + fetchGplayData(query) + } + } + + private suspend fun fetchGplayData(query: String) { + isLoading = true + val gplaySearchResult = applicationRepository.getGplaySearchResults(query, nextSubBundle) + + if (!gplaySearchResult.isSuccess()) { + handleException(gplaySearchResult.exception ?: UnknownSourceException()) + } + + nextSubBundle = gplaySearchResult.data?.second + + val currentAppList = updateCurrentAppList(gplaySearchResult) + val finalResult = ResultSupreme.Success( + Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) + ) + + hasGPlayBeenFetched = true + emitFilteredResults(finalResult) + + isLoading = false + } + + private fun updateCurrentAppList(gplaySearchResult: GplaySearchResult): List { + val currentAppList = accumulatedList + currentAppList.removeIf { item -> item.isPlaceHolder } + currentAppList.addAll(gplaySearchResult.data?.first ?: emptyList()) + return currentAppList.distinctBy { it.package_name } + } + + private fun handleException(exception: Exception) { + exceptionsList.add(exception) + exceptionsLiveData.postValue(exceptionsList) + } + + /** + * @return returns true if there is changes in data, otherwise false + */ + fun isAnyAppUpdated( + newApplications: List, + oldApplications: List + ) = applicationRepository.isAnyFusedAppUpdated(newApplications, oldApplications) + + fun isAuthObjectListSame(authObjectList: List?): Boolean { + return lastAuthObjects == authObjectList + } + + private fun hasTrackers(app: Application): Boolean { + return when { + app.trackers == LIST_OF_NULL -> true // Tracker data unavailable, don't show + app.trackers.isNotEmpty() -> true // Trackers present + app.privacyScore == 0 -> true // Manually blocked apps (Facebook etc.) + else -> false + } + } + + private suspend fun fetchTrackersForApp(app: Application) { + if (app.isPlaceHolder) return + appPrivacyInfoRepository.getAppPrivacyInfo(app, app.package_name).let { + val calculatedScore = privacyScoreRepository.calculatePrivacyScore(app) + app.privacyScore = calculatedScore + } + } + + private suspend fun getFilteredList(): List = withContext(IO) { + if (flagNoTrackers) { + val deferredCheck = accumulatedList.map { + async { + if (it.privacyScore == -1) { + fetchTrackersForApp(it) + } + it + } + } + deferredCheck.awaitAll() + } + + accumulatedList.filter { + if (!flagNoTrackers && !flagOpenSource && !flagPWA) return@filter true + if (flagNoTrackers && !hasTrackers(it)) return@filter true + if (flagOpenSource && !it.is_pwa && it.origin == Origin.CLEANAPK) return@filter true + if (flagPWA && it.is_pwa) return@filter true + false + } + } + + /** + * Pass [result] as null to re-emit already loaded search results with new filters. + */ + private suspend fun emitFilteredResults( + result: SearchDataForUI? = null + ) { + + // When filters are changed but no data is fetched yet + if (result == null && _searchResult.value == null) { + return + } + + if (result != null && !result.isSuccess()) { + _searchResult.postValue(result!!) + return + } + + if (result != null) { + result.data?.first?.let { + accumulatedList.clear() + accumulatedList.addAll(it) + } + } + + val filteredList = getFilteredList() + val isMoreDataLoading = result?.data?.second ?: _searchResult.value?.data?.second ?: false + + _searchResult.postValue( + ResultSupreme.Success( + Pair(filteredList.toList(), isMoreDataLoading) + ) + ) + gplaySearchLoaded.postValue(hasGPlayBeenFetched) + } + + fun loadMoreDataIfNeeded(searchText: String) { + val searchList = + searchResult.value?.data?.first?.toMutableList() ?: emptyList() + val canLoadMore = searchResult.value?.data?.second ?: false + + if (searchList.size < MIN_SEARCH_DISPLAY_ITEMS && canLoadMore) { + loadMore(searchText, autoTriggered = true) + } + } +} -- GitLab From 322639af6f13c32b1799a4c8760e2f1d941c39fb Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Wed, 13 Mar 2024 21:42:42 +0530 Subject: [PATCH 24/26] minor comment change --- .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 1a6c5b87c..2742bfcdf 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -185,7 +185,7 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { if (autoTriggered) { - delay(ONE_SECOND_IN_MILLIS) // prevent overloading APIs and 401 error code + delay(ONE_SECOND_IN_MILLIS) // prevent overloading APIs and 429 error code } fetchGplayData(query) } -- GitLab From d957b4e6c1a35ad3137c6c8bb32698086c254260 Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 18 Mar 2024 13:46:10 +0530 Subject: [PATCH 25/26] remove ONE_SECOND_IN_MILLIS from Constants class --- app/src/main/java/foundation/e/apps/data/Constants.kt | 2 -- app/src/main/java/foundation/e/apps/data/NetworkHandler.kt | 2 +- .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/Constants.kt b/app/src/main/java/foundation/e/apps/data/Constants.kt index 46ed6551c..20e9c0bc8 100644 --- a/app/src/main/java/foundation/e/apps/data/Constants.kt +++ b/app/src/main/java/foundation/e/apps/data/Constants.kt @@ -9,6 +9,4 @@ object Constants { const val ACTION_AUTHDATA_DUMP = "foundation.e.apps.action.DUMP_GACCOUNT_INFO" const val TAG_AUTHDATA_DUMP = "AUTHDATA_DUMP" - - const val ONE_SECOND_IN_MILLIS = 1000L } diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index ea29bd423..f5c5cdd50 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -18,7 +18,6 @@ package foundation.e.apps.data -import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.login.exceptions.GPlayException @@ -32,6 +31,7 @@ private const val STATUS = "Status:" private const val ERROR_GPLAY_API = "Gplay api has faced error!" private const val REGEX_429_OR_401 = "429|401" private const val MAX_RETRY_DELAY_IN_SECONDS = 300 +private const val ONE_SECOND_IN_MILLIS = 1000L private const val INITIAL_DELAY_RETRY_IN_SECONDS = 10 suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 2742bfcdf..f88e5516a 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -26,7 +26,6 @@ import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.Constants.ONE_SECOND_IN_MILLIS import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.search.GplaySearchResult @@ -83,6 +82,7 @@ class SearchViewModel @Inject constructor( companion object { private const val DATA_LOAD_ERROR = "Data load error" private const val MIN_SEARCH_DISPLAY_ITEMS = 10 + private const val PREVENT_HTTP_429_DELAY_IN_MS = 1000L } fun setFilterFlags( @@ -185,7 +185,7 @@ class SearchViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { if (autoTriggered) { - delay(ONE_SECOND_IN_MILLIS) // prevent overloading APIs and 429 error code + delay(PREVENT_HTTP_429_DELAY_IN_MS) } fetchGplayData(query) } -- GitLab From 7bfcf9a3ffb8a71bc05d68dc24dd104883f669ec Mon Sep 17 00:00:00 2001 From: Sayantan Roychowdhury Date: Mon, 18 Mar 2024 15:09:45 +0530 Subject: [PATCH 26/26] change SearchDataForUI to SearchResult --- .../e/apps/data/application/ApplicationRepository.kt | 4 ++-- .../e/apps/data/application/search/SearchApi.kt | 4 ++-- .../e/apps/data/application/search/SearchApiImpl.kt | 12 ++++++------ .../foundation/e/apps/ui/search/SearchViewModel.kt | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt index 343f67743..a43a4e841 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt @@ -39,7 +39,7 @@ import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.application.search.SearchApi import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.fusedDownload.models.FusedDownload -import foundation.e.apps.ui.search.SearchDataForUI +import foundation.e.apps.ui.search.SearchResult import javax.inject.Inject import javax.inject.Singleton @@ -120,7 +120,7 @@ class ApplicationRepository @Inject constructor( suspend fun getCleanApkSearchResults( query: String, authData: AuthData - ): SearchDataForUI { + ): SearchResult { return searchAPIImpl.getCleanApkSearchResults(query, authData) } diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt index 95adde3fb..34898eefd 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt @@ -23,7 +23,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.data.Application -import foundation.e.apps.ui.search.SearchDataForUI +import foundation.e.apps.ui.search.SearchResult typealias GplaySearchResult = ResultSupreme, Set>> @@ -46,7 +46,7 @@ interface SearchApi { suspend fun getCleanApkSearchResults( query: String, authData: AuthData - ): SearchDataForUI + ): SearchResult suspend fun getGplaySearchResult( query: String, diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index 5894f13e9..bf19c7af8 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -39,7 +39,7 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.ui.search.SearchDataForUI +import foundation.e.apps.ui.search.SearchResult import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Deferred @@ -86,8 +86,8 @@ class SearchApiImpl @Inject constructor( override suspend fun getCleanApkSearchResults( query: String, authData: AuthData - ): SearchDataForUI { - var finalSearchResult: SearchDataForUI = ResultSupreme.Error() + ): SearchResult { + var finalSearchResult: SearchResult = ResultSupreme.Error() val packageSpecificResults = fetchPackageSpecificResult(authData, query).data?.first ?: emptyList() @@ -125,7 +125,7 @@ class SearchApiImpl @Inject constructor( query: String, searchResult: MutableList, packageSpecificResults: List - ): SearchDataForUI { + ): SearchResult { val pwaApps: MutableList = mutableListOf() val result = handleNetworkResult { val apps = @@ -159,7 +159,7 @@ class SearchApiImpl @Inject constructor( query: String, searchResult: MutableList, packageSpecificResults: List - ): SearchDataForUI { + ): SearchResult { val cleanApkResults = mutableListOf() val result = handleNetworkResult { @@ -187,7 +187,7 @@ class SearchApiImpl @Inject constructor( private suspend fun fetchPackageSpecificResult( authData: AuthData, query: String, - ): SearchDataForUI { + ): SearchResult { val packageSpecificResults: MutableList = mutableListOf() var gplayPackageResult: Application? = null var cleanapkPackageResult: Application? = null diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index f88e5516a..2f8e171c0 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -49,7 +49,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -typealias SearchDataForUI = ResultSupreme, Boolean>> +typealias SearchResult = ResultSupreme, Boolean>> @HiltViewModel class SearchViewModel @Inject constructor( @@ -60,9 +60,9 @@ class SearchViewModel @Inject constructor( val searchSuggest: MutableLiveData?> = MutableLiveData() - private val _searchResult: MutableLiveData = + private val _searchResult: MutableLiveData = MutableLiveData() - val searchResult: LiveData = _searchResult + val searchResult: LiveData = _searchResult val gplaySearchLoaded : MutableLiveData = MutableLiveData(false) @@ -279,7 +279,7 @@ class SearchViewModel @Inject constructor( * Pass [result] as null to re-emit already loaded search results with new filters. */ private suspend fun emitFilteredResults( - result: SearchDataForUI? = null + result: SearchResult? = null ) { // When filters are changed but no data is fetched yet -- GitLab