From 9cbd56905967c652a0403593b7c4eef5259c8a43 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Mon, 15 Aug 2022 20:37:44 +0600 Subject: [PATCH 1/6] Refactoring applicationlistviewmodel (partially done) --- .../e/apps/api/fused/FusedAPIRepository.kt | 260 +++++++++++++++++- .../ApplicationListViewModel.kt | 4 +- 2 files changed, 261 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index 6502866f9..afb6badd1 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -136,7 +136,11 @@ class FusedAPIRepository @Inject constructor( homeUrl: String, currentStreamBundle: StreamBundle, ): ResultSupreme { - return fusedAPIImpl.getNextStreamBundle(authData, homeUrl, currentStreamBundle) + return fusedAPIImpl.getNextStreamBundle(authData, homeUrl, currentStreamBundle).apply { + if (isValidData()) streamBundle = data!! + hasNextStreamBundle = streamBundle.hasNext() + clusterPointer = 0 + } } suspend fun getAdjustedFirstCluster( @@ -183,4 +187,258 @@ class FusedAPIRepository @Inject constructor( fun isAnyAppInstallStatusChanged(currentList: List) = fusedAPIImpl.isAnyAppInstallStatusChanged(currentList) + + /** + * This is how the logic works: + * + * StreamBundles are obtained from "browseUrls". + * Each StreamBundle can contain + * - some StreamClusters, + * - point to a following StreamBundle with "streamNextPageUrl" + * (checked by StreamBundle.hasNext()) + * Each StreamCluster contain + * - apps to display + * - a "clusterBrowseUrl" + * - can point to a following StreamCluster with new app data using "clusterNextPageUrl" + * (checked by StreamCluster.hasNext()) + * + * -- browseUrl + * | + * StreamBundle 1 (streamNextPageUrl points to StreamBundle 2) + * StreamCluster 1 -> StreamCluster 1.1 -> StreamCluster 1.2 .... + * StreamCluster 2 -> StreamCluster 2.1 -> StreamCluster 2.2 .... + * StreamCluster 3 -> StreamCluster 3.1 -> StreamCluster 3.2 .... + * StreamBundle 2 + * StreamCluster 4 -> ... + * StreamCluster 5 -> ... + * + * + * - "browseUrl": looks like: homeV2?cat=SOCIAL&c=3 + * - "clusterBrowseUrl" (not used here): looks like: + * getBrowseStream?ecp=ChWiChIIARIGU09DSUFMKgIIB1ICCAE%3D + * getBrowseStream?ecp=CjOiCjAIARIGU09DSUFMGhwKFnJlY3NfdG9waWNfRjkxMjZNYVJ6S1UQOxgDKgIIB1ICCAI%3D + * - "clusterNextPageUrl" (not directly used here): looks like: + * getCluster?enpt=CkCC0_-4AzoKMfqegZ0DKwgIEKGz2kgQuMifuAcQ75So0QkQ6Ijz6gwQzvel8QQQprGBmgUQz938owMQyIeljYQwEAcaFaIKEggBEgZTT0NJQUwqAggHUgIIAQ&n=20 + * + * ========== Working logic ========== + * + * 1. [streamCluster] accumulates all data from all subsequent network calls. + * Its "clusterNextPageUrl" does point to the next StreamCluster, but its "clusterAppList" + * contains accumulated data of all previous network calls. + * + * 2. [streamBundle] is the same value received from [getNextStreamBundle]. + * + * 3. Initially [hasNextStreamCluster] is false, denoting [streamCluster] is empty. + * Initially [clusterPointer] = 0, [streamBundle].streamClusters.size = 0, + * hence 2nd case also does not execute. + * However, initially [hasNextStreamBundle] is true, thus [getNextStreamBundle] is called, + * fetching the first StreamBundle and storing the data in [streamBundle], and getting the first + * StreamCluster data using [getAdjustedFirstCluster]. + * + * NOTE: [getAdjustedFirstCluster] is used to fetch StreamCluster 1, 2, 3 .. in the above + * diagram with help of [clusterPointer]. For subsequent StreamCluster 1.1, 1.2 .. 2.1 .. + * [getNextStreamCluster] is used. + * + * 4. From now onwards, + * - [hasNextStreamBundle] is as good as [streamBundle].hasNext() + * - [hasNextStreamCluster] is as good as [streamCluster].hasNext() + * + * 5.1. When this method is again called when list reaches the end while scrolling on the UI, + * if [hasNextStreamCluster] is true, we will get the next StreamCluster under the current + * StreamBundle object. Once the last StreamCluster is reached, [hasNextStreamCluster] is + * false, we move to the next case. + * + * 5.2. In the step 5.1 we have been traversing along the path StreamCluster 1 -> 1.1 -> 1.2 .. + * Once that path reaches an end, we need to jump to StreamCluster 2 -> 2.1 -> 2.2 .. + * This is achieved by the second condition using [clusterPointer]. We increment the + * pointer and call [getAdjustedFirstCluster] again to start from StreamCluster 2. + * + * 5.3. Once we no longer have any more beginning StreamClusters, i.e + * [clusterPointer] exceeds [streamBundle].streamClusters size, the second condition no + * longer holds. Now we should try to go to a different StreamBundle. + * Using the above diagram, we move to StreamBundle 1 -> 2. + * We check [hasNextStreamBundle]. If that is true, we load the next StreamBundle. + * This also fetches the first StreamCluster of this bundle, thus re-initialising both + * [hasNextStreamCluster] and [hasNextStreamBundle]. + * + * 6. Once we reach the end of all StreamBundles and all StreamClusters, now calling + * this method makes no network calls. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + + private var streamBundle = StreamBundle() + private var streamCluster = StreamCluster() + + private var clusterPointer = 0 + + /** + * Variable denoting if we can call [getNextStreamCluster] to get a new StreamBundle. + * + * Initially set to true, so that we can get the first StreamBundle. + * Once the first StreamBundle is fetched, this variable value is same + * as [streamBundle].hasNext(). + * + * For more explanation on how [streamBundle] and [streamCluster] work, look at the + * documentation in [getNextDataSet]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + private var hasNextStreamBundle = true + + /** + * Variable denoting if we can call [getNextStreamCluster] to get a new StreamCluster. + * + * Initially set to false so that we get a StreamBundle first, because initially + * [streamCluster] is empty. Once [streamBundle] is fetched and [getAdjustedFirstCluster] + * is called, this variable value is same as [streamCluster].hasNext(). + * + * For more explanation on how [streamBundle] and [streamCluster] work, look at the + * documentation in [getNextDataSet]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + private var hasNextStreamCluster = false + + + suspend fun getNextDataSet( + authData: AuthData, + browseUrl: String, + ): ResultSupreme> { + if (hasNextStreamCluster) { + getNextStreamCluster(authData).run { + if (!isSuccess()) { + return ResultSupreme.replicate(this, listOf()) + } + } + } else if (clusterPointer < streamBundle.streamClusters.size) { + ++clusterPointer + getAdjustedFirstCluster(authData).run { + if (!isSuccess()) { + return ResultSupreme.replicate(this, listOf()) + } + } + } else if (hasNextStreamBundle) { + getNextStreamBundle(authData, browseUrl).run { + if (!isSuccess()) { + return ResultSupreme.replicate(this, listOf()) + } + getAdjustedFirstCluster(authData).run { + if (!isSuccess()) { + return ResultSupreme.replicate(this, listOf()) + } + } + } + } + return filterRestrictedGPlayApps(authData, streamCluster.clusterAppList).apply { + addPlaceHolderAppIfNeeded(this) + } + } + + /** + * Add a placeholder app at the end if more data can be loaded. + * "Placeholder" app shows a simple progress bar in the RecyclerView, indicating that + * more apps are being loaded. + * + * Note that it mutates the [ResultSupreme] object passed to it. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + * + * @param result object from [getNextDataSet]. Data of this object will be updated + * if [canLoadMore] is true. + * + * @return true if a placeholder app was added, false otherwise. + */ + private fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { + result.apply { + if (isSuccess() && canLoadMore()) { + // Add an empty app at the end if more data can be loaded on scroll + val newData = data!!.toMutableList() + newData.add(FusedApp(isPlaceHolder = true)) + setData(newData) + return true + } + } + return false + } + + /** + * Get the first StreamBundle object from the category browseUrl, or the subsequent + * StreamBundle objects from the "streamNextPageUrl" of current [streamBundle]. + * Also resets the [clusterPointer] to 0. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + * + * @see getNextDataSet + */ + private suspend fun getNextStreamBundle( + authData: AuthData, + browseUrl: String, + ): ResultSupreme { + return getNextStreamBundle(authData, browseUrl, streamBundle).apply { + if (isValidData()) streamBundle = data!! + hasNextStreamBundle = streamBundle.hasNext() + clusterPointer = 0 + } + } + + /** + * The first StreamCluster inside [streamBundle] may not have a "clusterNextPageUrl". + * This method tries to fix that. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + * + * @see getNextDataSet + */ + private suspend fun getAdjustedFirstCluster( + authData: AuthData, + ): ResultSupreme { + return getAdjustedFirstCluster(authData, streamBundle, clusterPointer) + .apply { + if (isValidData()) addNewClusterData(this.data!!) + } + } + + /** + * Get all subsequent StreamCluster of the current [streamBundle]. + * Accumulate the data in [streamCluster]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + * + * @see getNextDataSet + */ + private suspend fun getNextStreamCluster( + authData: AuthData, + ): ResultSupreme { + return getNextStreamCluster(authData, streamCluster).apply { + if (isValidData()) addNewClusterData(this.data!!) + } + } + + /** + * Method to add clusterAppList of [newCluster] to [streamCluster], + * but properly point to next StreamCluster. + * Also updates [hasNextStreamCluster]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + private fun addNewClusterData(newCluster: StreamCluster) { + newCluster.run { + streamCluster.clusterAppList.apply { + val addedList = this + newCluster.clusterAppList + clear() + addAll(addedList.distinctBy { it.packageName }) + } + streamCluster.clusterNextPageUrl = this.clusterNextPageUrl + streamCluster.clusterBrowseUrl = this.clusterBrowseUrl + } + hasNextStreamCluster = newCluster.hasNext() + } + + /** + * Function is used to check if we can load more data. + * It is also used to show a loading progress bar at the end of the list. + */ + fun canLoadMore(): Boolean = + hasNextStreamCluster || clusterPointer < streamBundle.streamClusters.size || hasNextStreamBundle } diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt index ad4884760..f7e47a3c1 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt @@ -88,7 +88,7 @@ class ApplicationListViewModel @Inject constructor( source ) } else { - getNextDataSet(authData, browseUrl).apply { + fusedAPIRepository.getNextDataSet(authData, browseUrl).apply { addPlaceHolderAppIfNeeded(this) } } @@ -138,7 +138,7 @@ class ApplicationListViewModel @Inject constructor( viewModelScope.launch { if (!isLoading) { val lastCount: Int = streamCluster.clusterAppList.size - val result = getNextDataSet(authData, browseUrl) + val result = fusedAPIRepository.getNextDataSet(authData, browseUrl) val newCount = streamCluster.clusterAppList.size appListLiveData.postValue(result) /* -- GitLab From 4d0f4f1f426b44a29a341bdb7b3a78b27eade09e Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 16 Aug 2022 22:53:19 +0600 Subject: [PATCH 2/6] refactoring applicationlist viewmodel --- .../e/apps/api/fused/FusedAPIRepository.kt | 124 ++++--- .../ApplicationListViewModel.kt | 304 +++--------------- 2 files changed, 122 insertions(+), 306 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index afb6badd1..3d16a64bc 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -35,6 +35,7 @@ import foundation.e.apps.utils.enums.FilterLevel import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -42,6 +43,45 @@ import javax.inject.Singleton class FusedAPIRepository @Inject constructor( private val fusedAPIImpl: FusedAPIImpl ) { + + var streamBundle = StreamBundle() + private set + var streamCluster = StreamCluster() + private set + + var clusterPointer = 0 + private set + + /** + * Variable denoting if we can call [getNextStreamCluster] to get a new StreamBundle. + * + * Initially set to true, so that we can get the first StreamBundle. + * Once the first StreamBundle is fetched, this variable value is same + * as [streamBundle].hasNext(). + * + * For more explanation on how [streamBundle] and [streamCluster] work, look at the + * documentation in [getNextDataSet]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + var hasNextStreamBundle = true + private set + + /** + * Variable denoting if we can call [getNextStreamCluster] to get a new StreamCluster. + * + * Initially set to false so that we get a StreamBundle first, because initially + * [streamCluster] is empty. Once [streamBundle] is fetched and [getAdjustedFirstCluster] + * is called, this variable value is same as [streamCluster].hasNext(). + * + * For more explanation on how [streamBundle] and [streamCluster] work, look at the + * documentation in [getNextDataSet]. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + var hasNextStreamCluster = false + private set + suspend fun getHomeScreenData(authData: AuthData): Pair, ResultStatus> { return fusedAPIImpl.getHomeScreenData(authData) } @@ -188,6 +228,38 @@ class FusedAPIRepository @Inject constructor( fun isAnyAppInstallStatusChanged(currentList: List) = fusedAPIImpl.isAnyAppInstallStatusChanged(currentList) + suspend fun getAppList( + category: String, + browseUrl: String, + authData: AuthData, + source: String + ): ResultSupreme> { + return if (source == "Open Source" || source == "PWA") { + getAppsListBasedOnCategory( + category, + browseUrl, + authData, + source + ) + } else { + getNextDataSet(authData, browseUrl).apply { + addPlaceHolderAppIfNeeded(this) + } + } + } + + /** + * @return a Pair, + * 1. first item is the data + * 1. second item is item count is changed or not + */ + suspend fun loadMore(authData: AuthData, browseUrl: String): Pair>, Boolean> { + val lastCount: Int = streamCluster.clusterAppList.size + val result = getNextDataSet(authData, browseUrl) + val newCount = streamCluster.clusterAppList.size + return Pair(result, lastCount != newCount) + } + /** * This is how the logic works: * @@ -267,44 +339,11 @@ class FusedAPIRepository @Inject constructor( * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] */ - private var streamBundle = StreamBundle() - private var streamCluster = StreamCluster() - - private var clusterPointer = 0 - - /** - * Variable denoting if we can call [getNextStreamCluster] to get a new StreamBundle. - * - * Initially set to true, so that we can get the first StreamBundle. - * Once the first StreamBundle is fetched, this variable value is same - * as [streamBundle].hasNext(). - * - * For more explanation on how [streamBundle] and [streamCluster] work, look at the - * documentation in [getNextDataSet]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - private var hasNextStreamBundle = true - - /** - * Variable denoting if we can call [getNextStreamCluster] to get a new StreamCluster. - * - * Initially set to false so that we get a StreamBundle first, because initially - * [streamCluster] is empty. Once [streamBundle] is fetched and [getAdjustedFirstCluster] - * is called, this variable value is same as [streamCluster].hasNext(). - * - * For more explanation on how [streamBundle] and [streamCluster] work, look at the - * documentation in [getNextDataSet]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - private var hasNextStreamCluster = false - - - suspend fun getNextDataSet( + private suspend fun getNextDataSet( authData: AuthData, browseUrl: String, ): ResultSupreme> { + Timber.d("hasNextStreamCluster: $hasNextStreamCluster hasNextStreamBundle: $hasNextStreamBundle clusterPointer: $clusterPointer: streambundleSize: ${streamBundle.streamClusters.size} streamClusterSize: ${streamCluster.clusterAppList.size}") if (hasNextStreamCluster) { getNextStreamCluster(authData).run { if (!isSuccess()) { @@ -330,9 +369,8 @@ class FusedAPIRepository @Inject constructor( } } } - return filterRestrictedGPlayApps(authData, streamCluster.clusterAppList).apply { - addPlaceHolderAppIfNeeded(this) - } + Timber.d("===> calling last segment") + return filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) } /** @@ -349,7 +387,7 @@ class FusedAPIRepository @Inject constructor( * * @return true if a placeholder app was added, false otherwise. */ - private fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { + fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { result.apply { if (isSuccess() && canLoadMore()) { // Add an empty app at the end if more data can be loaded on scroll @@ -441,4 +479,12 @@ class FusedAPIRepository @Inject constructor( */ fun canLoadMore(): Boolean = hasNextStreamCluster || clusterPointer < streamBundle.streamClusters.size || hasNextStreamBundle + + fun clearData() { + streamCluster = StreamCluster() + streamBundle = StreamBundle() + hasNextStreamBundle = true + hasNextStreamCluster = false + clusterPointer = 0 + } } diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt index f7e47a3c1..39ceb6651 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt @@ -31,6 +31,7 @@ import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.utils.enums.Origin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -40,60 +41,20 @@ class ApplicationListViewModel @Inject constructor( val appListLiveData: MutableLiveData>> = MutableLiveData() - private var streamBundle = StreamBundle() - private var streamCluster = StreamCluster() - - private var clusterPointer = 0 - var isLoading = false - /** - * Variable denoting if we can call [getNextStreamCluster] to get a new StreamBundle. - * - * Initially set to true, so that we can get the first StreamBundle. - * Once the first StreamBundle is fetched, this variable value is same - * as [streamBundle].hasNext(). - * - * For more explanation on how [streamBundle] and [streamCluster] work, look at the - * documentation in [getNextDataSet]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - private var hasNextStreamBundle = true - - /** - * Variable denoting if we can call [getNextStreamCluster] to get a new StreamCluster. - * - * Initially set to false so that we get a StreamBundle first, because initially - * [streamCluster] is empty. Once [streamBundle] is fetched and [getAdjustedFirstCluster] - * is called, this variable value is same as [streamCluster].hasNext(). - * - * For more explanation on how [streamBundle] and [streamCluster] work, look at the - * documentation in [getNextDataSet]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - private var hasNextStreamCluster = false - fun getList(category: String, browseUrl: String, authData: AuthData, source: String) { + Timber.d("===> getlist: $isLoading") if (isLoading) { return } viewModelScope.launch(Dispatchers.IO) { - val appsListData = if (source == "Open Source" || source == "PWA") { - fusedAPIRepository.getAppsListBasedOnCategory( - category, - browseUrl, - authData, - source - ) - } else { - fusedAPIRepository.getNextDataSet(authData, browseUrl).apply { - addPlaceHolderAppIfNeeded(this) - } + isLoading = true + fusedAPIRepository.getAppList(category, browseUrl, authData, source).apply { + isLoading = false + appListLiveData.postValue(this) + Timber.d("final result: ${this.data?.size}") } - - appListLiveData.postValue(appsListData) } } @@ -123,7 +84,7 @@ class ApplicationListViewModel @Inject constructor( */ private fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { result.apply { - if (isSuccess() && canLoadMore()) { + if (isSuccess() && fusedAPIRepository.canLoadMore()) { // Add an empty app at the end if more data can be loaded on scroll val newData = data!!.toMutableList() newData.add(FusedApp(isPlaceHolder = true)) @@ -136,234 +97,38 @@ class ApplicationListViewModel @Inject constructor( fun loadMore(authData: AuthData, browseUrl: String) { viewModelScope.launch { - if (!isLoading) { - val lastCount: Int = streamCluster.clusterAppList.size - val result = fusedAPIRepository.getNextDataSet(authData, browseUrl) - val newCount = streamCluster.clusterAppList.size - appListLiveData.postValue(result) - /* - * Check if a placeholder app is to be added at the end. - * If yes then post the updated result. - * We post this separately as it helps clear any previous placeholder app - * and ensures only a single placeholder app is present at the end of the - * list, and none at the middle of the list. - */ - if (addPlaceHolderAppIfNeeded(result)) { - appListLiveData.postValue(result) - } - - /* - * Old count and new count can be same if new StreamCluster has apps which - * are already shown, i.e. with duplicate package names. - * In that case, if we can load more data, we do it from here itself, - * because recyclerview scroll listener will not trigger itself twice - * for the same data. - */ - if (result.isSuccess() && lastCount == newCount && canLoadMore()) { - loadMore(authData, browseUrl) - } - } - } - } - - /** - * Get the first StreamBundle object from the category browseUrl, or the subsequent - * StreamBundle objects from the "streamNextPageUrl" of current [streamBundle]. - * Also resets the [clusterPointer] to 0. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - * - * @see getNextDataSet - */ - private suspend fun getNextStreamBundle( - authData: AuthData, - browseUrl: String, - ): ResultSupreme { - return fusedAPIRepository.getNextStreamBundle(authData, browseUrl, streamBundle).apply { - if (isValidData()) streamBundle = data!! - hasNextStreamBundle = streamBundle.hasNext() - clusterPointer = 0 - } - } - - /** - * The first StreamCluster inside [streamBundle] may not have a "clusterNextPageUrl". - * This method tries to fix that. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - * - * @see getNextDataSet - */ - private suspend fun getAdjustedFirstCluster( - authData: AuthData, - ): ResultSupreme { - return fusedAPIRepository.getAdjustedFirstCluster(authData, streamBundle, clusterPointer) - .apply { - if (isValidData()) addNewClusterData(this.data!!) + if (isLoading) { + return@launch } - } - - /** - * Get all subsequent StreamCluster of the current [streamBundle]. - * Accumulate the data in [streamCluster]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - * - * @see getNextDataSet - */ - private suspend fun getNextStreamCluster( - authData: AuthData, - ): ResultSupreme { - return fusedAPIRepository.getNextStreamCluster(authData, streamCluster).apply { - if (isValidData()) addNewClusterData(this.data!!) - } - } - /** - * Method to add clusterAppList of [newCluster] to [streamCluster], - * but properly point to next StreamCluster. - * Also updates [hasNextStreamCluster]. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - private fun addNewClusterData(newCluster: StreamCluster) { - newCluster.run { - streamCluster.clusterAppList.apply { - val addedList = this + newCluster.clusterAppList - clear() - addAll(addedList.distinctBy { it.packageName }) + isLoading = true + val result = fusedAPIRepository.loadMore(authData, browseUrl) + isLoading = false + appListLiveData.postValue(result.first!!) + /* + * Check if a placeholder app is to be added at the end. + * If yes then post the updated result. + * We post this separately as it helps clear any previous placeholder app + * and ensures only a single placeholder app is present at the end of the + * list, and none at the middle of the list. + */ + if (fusedAPIRepository.addPlaceHolderAppIfNeeded(result.first)) { + appListLiveData.postValue(result.first!!) } - streamCluster.clusterNextPageUrl = this.clusterNextPageUrl - streamCluster.clusterBrowseUrl = this.clusterBrowseUrl - } - hasNextStreamCluster = newCluster.hasNext() - } - - /** - * This is how the logic works: - * - * StreamBundles are obtained from "browseUrls". - * Each StreamBundle can contain - * - some StreamClusters, - * - point to a following StreamBundle with "streamNextPageUrl" - * (checked by StreamBundle.hasNext()) - * Each StreamCluster contain - * - apps to display - * - a "clusterBrowseUrl" - * - can point to a following StreamCluster with new app data using "clusterNextPageUrl" - * (checked by StreamCluster.hasNext()) - * - * -- browseUrl - * | - * StreamBundle 1 (streamNextPageUrl points to StreamBundle 2) - * StreamCluster 1 -> StreamCluster 1.1 -> StreamCluster 1.2 .... - * StreamCluster 2 -> StreamCluster 2.1 -> StreamCluster 2.2 .... - * StreamCluster 3 -> StreamCluster 3.1 -> StreamCluster 3.2 .... - * StreamBundle 2 - * StreamCluster 4 -> ... - * StreamCluster 5 -> ... - * - * - * - "browseUrl": looks like: homeV2?cat=SOCIAL&c=3 - * - "clusterBrowseUrl" (not used here): looks like: - * getBrowseStream?ecp=ChWiChIIARIGU09DSUFMKgIIB1ICCAE%3D - * getBrowseStream?ecp=CjOiCjAIARIGU09DSUFMGhwKFnJlY3NfdG9waWNfRjkxMjZNYVJ6S1UQOxgDKgIIB1ICCAI%3D - * - "clusterNextPageUrl" (not directly used here): looks like: - * getCluster?enpt=CkCC0_-4AzoKMfqegZ0DKwgIEKGz2kgQuMifuAcQ75So0QkQ6Ijz6gwQzvel8QQQprGBmgUQz938owMQyIeljYQwEAcaFaIKEggBEgZTT0NJQUwqAggHUgIIAQ&n=20 - * - * ========== Working logic ========== - * - * 1. [streamCluster] accumulates all data from all subsequent network calls. - * Its "clusterNextPageUrl" does point to the next StreamCluster, but its "clusterAppList" - * contains accumulated data of all previous network calls. - * - * 2. [streamBundle] is the same value received from [getNextStreamBundle]. - * - * 3. Initially [hasNextStreamCluster] is false, denoting [streamCluster] is empty. - * Initially [clusterPointer] = 0, [streamBundle].streamClusters.size = 0, - * hence 2nd case also does not execute. - * However, initially [hasNextStreamBundle] is true, thus [getNextStreamBundle] is called, - * fetching the first StreamBundle and storing the data in [streamBundle], and getting the first - * StreamCluster data using [getAdjustedFirstCluster]. - * - * NOTE: [getAdjustedFirstCluster] is used to fetch StreamCluster 1, 2, 3 .. in the above - * diagram with help of [clusterPointer]. For subsequent StreamCluster 1.1, 1.2 .. 2.1 .. - * [getNextStreamCluster] is used. - * - * 4. From now onwards, - * - [hasNextStreamBundle] is as good as [streamBundle].hasNext() - * - [hasNextStreamCluster] is as good as [streamCluster].hasNext() - * - * 5.1. When this method is again called when list reaches the end while scrolling on the UI, - * if [hasNextStreamCluster] is true, we will get the next StreamCluster under the current - * StreamBundle object. Once the last StreamCluster is reached, [hasNextStreamCluster] is - * false, we move to the next case. - * - * 5.2. In the step 5.1 we have been traversing along the path StreamCluster 1 -> 1.1 -> 1.2 .. - * Once that path reaches an end, we need to jump to StreamCluster 2 -> 2.1 -> 2.2 .. - * This is achieved by the second condition using [clusterPointer]. We increment the - * pointer and call [getAdjustedFirstCluster] again to start from StreamCluster 2. - * - * 5.3. Once we no longer have any more beginning StreamClusters, i.e - * [clusterPointer] exceeds [streamBundle].streamClusters size, the second condition no - * longer holds. Now we should try to go to a different StreamBundle. - * Using the above diagram, we move to StreamBundle 1 -> 2. - * We check [hasNextStreamBundle]. If that is true, we load the next StreamBundle. - * This also fetches the first StreamCluster of this bundle, thus re-initialising both - * [hasNextStreamCluster] and [hasNextStreamBundle]. - * - * 6. Once we reach the end of all StreamBundles and all StreamClusters, now calling - * this method makes no network calls. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - private suspend fun getNextDataSet( - authData: AuthData, - browseUrl: String, - ): ResultSupreme> { - isLoading = true - if (hasNextStreamCluster) { - getNextStreamCluster(authData).run { - if (!isSuccess()) { - return ResultSupreme.replicate(this, listOf()) - } - } - } else if (clusterPointer < streamBundle.streamClusters.size) { - ++clusterPointer - getAdjustedFirstCluster(authData).run { - if (!isSuccess()) { - return ResultSupreme.replicate(this, listOf()) - } - } - } else if (hasNextStreamBundle) { - getNextStreamBundle(authData, browseUrl).run { - if (!isSuccess()) { - return ResultSupreme.replicate(this, listOf()) - } - getAdjustedFirstCluster(authData).run { - if (!isSuccess()) { - return ResultSupreme.replicate(this, listOf()) - } - } + /* + * Old count and new count can be same if new StreamCluster has apps which + * are already shown, i.e. with duplicate package names. + * In that case, if we can load more data, we do it from here itself, + * because recyclerview scroll listener will not trigger itself twice + * for the same data. + */ + if (result.first.isSuccess() && !result.second && fusedAPIRepository.canLoadMore()) { + loadMore(authData, browseUrl) } } - return fusedAPIRepository.filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) - .apply { - isLoading = false - } } - /** - * Function is used to check if we can load more data. - * It is also used to show a loading progress bar at the end of the list. - */ - fun canLoadMore(): Boolean = - hasNextStreamCluster || clusterPointer < streamBundle.streamClusters.size || hasNextStreamBundle - - private fun getOrigin(source: String) = - if (source.contentEquals("Open Source")) Origin.CLEANAPK else Origin.GPLAY - /** * @return returns true if there is changes in data, otherwise false */ @@ -374,4 +139,9 @@ class ApplicationListViewModel @Inject constructor( fun hasAnyAppInstallStatusChanged(currentList: List) = fusedAPIRepository.isAnyAppInstallStatusChanged(currentList) + + override fun onCleared() { + fusedAPIRepository.clearData() + super.onCleared() + } } -- GitLab From c1fe865cbf6d44b0a7d73cc9c960ac5c5f900eec Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 17 Aug 2022 14:01:25 +0600 Subject: [PATCH 3/6] refactoring applistviewmodel & privacyinfoviewmodel --- .../foundation/e/apps/PrivacyInfoViewModel.kt | 58 ++----------------- .../AppPrivacyInfoRepositoryImpl.kt | 51 +++++++++++++++- .../repositories/IAppPrivacyInfoRepository.kt | 4 +- .../e/apps/api/fused/FusedAPIRepository.kt | 5 +- .../ApplicationListViewModel.kt | 33 ----------- .../model/ApplicationListRVAdapter.kt | 2 +- 6 files changed, 59 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/PrivacyInfoViewModel.kt b/app/src/main/java/foundation/e/apps/PrivacyInfoViewModel.kt index 01afa6db9..ae4322e21 100644 --- a/app/src/main/java/foundation/e/apps/PrivacyInfoViewModel.kt +++ b/app/src/main/java/foundation/e/apps/PrivacyInfoViewModel.kt @@ -8,10 +8,7 @@ import foundation.e.apps.api.Result import foundation.e.apps.api.exodus.models.AppPrivacyInfo import foundation.e.apps.api.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.utils.modules.CommonUtilsModule.LIST_OF_NULL import javax.inject.Inject -import kotlin.math.ceil -import kotlin.math.round @HiltViewModel class PrivacyInfoViewModel @Inject constructor( @@ -27,40 +24,17 @@ class PrivacyInfoViewModel @Inject constructor( private suspend fun fetchEmitAppPrivacyInfo( fusedApp: FusedApp ): Result { - if (fusedApp.trackers.isNotEmpty() && fusedApp.permsFromExodus.isNotEmpty()) { - val appInfo = AppPrivacyInfo(fusedApp.trackers, fusedApp.permsFromExodus) - return Result.success(appInfo) - } val appPrivacyPrivacyInfoResult = - privacyInfoRepository.getAppPrivacyInfo(fusedApp.package_name) - return handleAppPrivacyInfoResult(appPrivacyPrivacyInfoResult, fusedApp) + privacyInfoRepository.getAppPrivacyInfo(fusedApp, fusedApp.package_name) + return handleAppPrivacyInfoResult(appPrivacyPrivacyInfoResult) } private fun handleAppPrivacyInfoResult( appPrivacyPrivacyInfoResult: Result, - fusedApp: FusedApp ): Result { - return if (appPrivacyPrivacyInfoResult.isSuccess()) { - handleAppPrivacyInfoSuccess(appPrivacyPrivacyInfoResult, fusedApp) - } else { + return if (!appPrivacyPrivacyInfoResult.isSuccess()) { Result.error("Tracker not found!") - } - } - - private fun handleAppPrivacyInfoSuccess( - appPrivacyPrivacyInfoResult: Result, - fusedApp: FusedApp - ): Result { - fusedApp.trackers = appPrivacyPrivacyInfoResult.data?.trackerList ?: LIST_OF_NULL - fusedApp.permsFromExodus = appPrivacyPrivacyInfoResult.data?.permissionList ?: LIST_OF_NULL - if (fusedApp.perms.isEmpty() && fusedApp.permsFromExodus != LIST_OF_NULL) { - /* - * fusedApp.perms is generally populated from remote source like Play Store. - * If it is empty then set the value from permissions from exodus api. - */ - fusedApp.perms = fusedApp.permsFromExodus - } - return appPrivacyPrivacyInfoResult + } else appPrivacyPrivacyInfoResult } fun getTrackerListText(fusedApp: FusedApp?): String { @@ -74,30 +48,8 @@ class PrivacyInfoViewModel @Inject constructor( fun getPrivacyScore(fusedApp: FusedApp?): Int { fusedApp?.let { - return calculatePrivacyScore(it) + return privacyInfoRepository.calculatePrivacyScore(it) } return -1 } - - fun calculatePrivacyScore(fusedApp: FusedApp): Int { - if (fusedApp.permsFromExodus == LIST_OF_NULL) { - return -1 - } - val calculateTrackersScore = calculateTrackersScore(fusedApp.trackers.size) - val calculatePermissionsScore = calculatePermissionsScore( - countAndroidPermissions(fusedApp) - ) - return calculateTrackersScore + calculatePermissionsScore - } - - private fun countAndroidPermissions(fusedApp: FusedApp) = - fusedApp.permsFromExodus.filter { it.contains("android.permission") }.size - - private fun calculateTrackersScore(numberOfTrackers: Int): Int { - return if (numberOfTrackers > 5) 0 else 9 - numberOfTrackers - } - - private fun calculatePermissionsScore(numberOfPermission: Int): Int { - return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() - } } diff --git a/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt index 076b3b5f5..ecd684e13 100644 --- a/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt @@ -6,10 +6,13 @@ import foundation.e.apps.api.exodus.Report import foundation.e.apps.api.exodus.Tracker import foundation.e.apps.api.exodus.TrackerDao import foundation.e.apps.api.exodus.models.AppPrivacyInfo +import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.getResult import foundation.e.apps.utils.modules.CommonUtilsModule.LIST_OF_NULL import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.ceil +import kotlin.math.round @Singleton class AppPrivacyInfoRepositoryImpl @Inject constructor( @@ -18,14 +21,36 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( ) : IAppPrivacyInfoRepository { private var trackers: List = listOf() - override suspend fun getAppPrivacyInfo(appHandle: String): Result { + override suspend fun getAppPrivacyInfo(fusedApp: FusedApp, appHandle: String): Result { + if (fusedApp.trackers.isNotEmpty() && fusedApp.permsFromExodus.isNotEmpty()) { + val appInfo = AppPrivacyInfo(fusedApp.trackers, fusedApp.permsFromExodus) + return Result.success(appInfo) + } + val appTrackerInfoResult = getResult { exodusTrackerApi.getTrackerInfoOfApp(appHandle) } if (appTrackerInfoResult.isSuccess()) { - return handleAppPrivacyInfoResultSuccess(appTrackerInfoResult) + val appPrivacyPrivacyInfoResult = handleAppPrivacyInfoResultSuccess(appTrackerInfoResult) + updateFusedApp(fusedApp, appPrivacyPrivacyInfoResult) + return appPrivacyPrivacyInfoResult } return Result.error(extractErrorMessage(appTrackerInfoResult)) } + private fun updateFusedApp( + fusedApp: FusedApp, + appPrivacyPrivacyInfoResult: Result + ) { + fusedApp.trackers = appPrivacyPrivacyInfoResult.data?.trackerList ?: LIST_OF_NULL + fusedApp.permsFromExodus = appPrivacyPrivacyInfoResult.data?.permissionList ?: LIST_OF_NULL + if (fusedApp.perms.isEmpty() && fusedApp.permsFromExodus != LIST_OF_NULL) { + /* + * fusedApp.perms is generally populated from remote source like Play Store. + * If it is empty then set the value from permissions from exodus api. + */ + fusedApp.perms = fusedApp.permsFromExodus + } + } + private suspend fun handleAppPrivacyInfoResultSuccess( appTrackerResult: Result>, ): Result { @@ -95,4 +120,26 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( sortedTrackerData[0].trackers.contains(it.id) }.map { it.name } } + + override fun calculatePrivacyScore(fusedApp: FusedApp): Int { + if (fusedApp.permsFromExodus == LIST_OF_NULL) { + return -1 + } + val calculateTrackersScore = calculateTrackersScore(fusedApp.trackers.size) + val calculatePermissionsScore = calculatePermissionsScore( + countAndroidPermissions(fusedApp) + ) + return calculateTrackersScore + calculatePermissionsScore + } + + private fun countAndroidPermissions(fusedApp: FusedApp) = + fusedApp.permsFromExodus.filter { it.contains("android.permission") }.size + + private fun calculateTrackersScore(numberOfTrackers: Int): Int { + return if (numberOfTrackers > 5) 0 else 9 - numberOfTrackers + } + + private fun calculatePermissionsScore(numberOfPermission: Int): Int { + return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() + } } diff --git a/app/src/main/java/foundation/e/apps/api/exodus/repositories/IAppPrivacyInfoRepository.kt b/app/src/main/java/foundation/e/apps/api/exodus/repositories/IAppPrivacyInfoRepository.kt index d02337647..f14cb1f23 100644 --- a/app/src/main/java/foundation/e/apps/api/exodus/repositories/IAppPrivacyInfoRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/exodus/repositories/IAppPrivacyInfoRepository.kt @@ -2,7 +2,9 @@ package foundation.e.apps.api.exodus.repositories import foundation.e.apps.api.Result import foundation.e.apps.api.exodus.models.AppPrivacyInfo +import foundation.e.apps.api.fused.data.FusedApp interface IAppPrivacyInfoRepository { - suspend fun getAppPrivacyInfo(appHandle: String): Result + suspend fun getAppPrivacyInfo(fusedApp: FusedApp, appHandle: String): Result + fun calculatePrivacyScore(fusedApp: FusedApp): Int } diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index 3d16a64bc..c74e4add2 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -35,7 +35,6 @@ import foundation.e.apps.utils.enums.FilterLevel import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -343,7 +342,6 @@ class FusedAPIRepository @Inject constructor( authData: AuthData, browseUrl: String, ): ResultSupreme> { - Timber.d("hasNextStreamCluster: $hasNextStreamCluster hasNextStreamBundle: $hasNextStreamBundle clusterPointer: $clusterPointer: streambundleSize: ${streamBundle.streamClusters.size} streamClusterSize: ${streamCluster.clusterAppList.size}") if (hasNextStreamCluster) { getNextStreamCluster(authData).run { if (!isSuccess()) { @@ -369,7 +367,6 @@ class FusedAPIRepository @Inject constructor( } } } - Timber.d("===> calling last segment") return filterRestrictedGPlayApps(authData, streamCluster.clusterAppList) } @@ -387,7 +384,7 @@ class FusedAPIRepository @Inject constructor( * * @return true if a placeholder app was added, false otherwise. */ - fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { + fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { result.apply { if (isSuccess() && canLoadMore()) { // Add an empty app at the end if more data can be loaded on scroll diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt index 39ceb6651..106d47379 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt @@ -22,16 +22,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.StreamBundle -import com.aurora.gplayapi.data.models.StreamCluster import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.utils.enums.Origin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -44,7 +40,6 @@ class ApplicationListViewModel @Inject constructor( var isLoading = false fun getList(category: String, browseUrl: String, authData: AuthData, source: String) { - Timber.d("===> getlist: $isLoading") if (isLoading) { return } @@ -53,7 +48,6 @@ class ApplicationListViewModel @Inject constructor( fusedAPIRepository.getAppList(category, browseUrl, authData, source).apply { isLoading = false appListLiveData.postValue(this) - Timber.d("final result: ${this.data?.size}") } } } @@ -68,33 +62,6 @@ class ApplicationListViewModel @Inject constructor( return fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps) } - /** - * Add a placeholder app at the end if more data can be loaded. - * "Placeholder" app shows a simple progress bar in the RecyclerView, indicating that - * more apps are being loaded. - * - * Note that it mutates the [ResultSupreme] object passed to it. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - * - * @param result object from [getNextDataSet]. Data of this object will be updated - * if [canLoadMore] is true. - * - * @return true if a placeholder app was added, false otherwise. - */ - private fun addPlaceHolderAppIfNeeded(result: ResultSupreme>): Boolean { - result.apply { - if (isSuccess() && fusedAPIRepository.canLoadMore()) { - // Add an empty app at the end if more data can be loaded on scroll - val newData = data!!.toMutableList() - newData.add(FusedApp(isPlaceHolder = true)) - setData(newData) - return true - } - } - return false - } - fun loadMore(authData: AuthData, browseUrl: String) { viewModelScope.launch { if (isLoading) { diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt index 951be9aec..cf76ca1db 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt @@ -346,7 +346,7 @@ class ApplicationListRVAdapter( } privacyInfoViewModel.getAppPrivacyInfoLiveData(searchApp).observe(lifecycleOwner!!) { showPrivacyScore() - val calculatedScore = privacyInfoViewModel.calculatePrivacyScore(searchApp) + val calculatedScore = privacyInfoViewModel.getPrivacyScore(searchApp) if (it.isSuccess() && calculatedScore != -1) { searchApp.privacyScore = calculatedScore appPrivacyScore.text = view.context.getString( -- GitLab From 1350a08fde5451d43ed5c2805d2c7ec0e154b476 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 19 Aug 2022 20:02:57 +0600 Subject: [PATCH 4/6] AppInfoFetchViewModel is refactored --- .../e/apps/AppInfoFetchViewModel.kt | 43 ++----------------- .../e/apps/api/fdroid/FdroidRepository.kt | 24 ++++++++++- .../e/apps/application/ApplicationFragment.kt | 5 ++- .../model/ApplicationListRVAdapter.kt | 5 ++- 4 files changed, 32 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt b/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt index 9520922e2..1cc1de5f8 100644 --- a/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt @@ -1,24 +1,17 @@ package foundation.e.apps -import android.widget.TextView import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData -import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.faultyApps.FaultyAppRepository import foundation.e.apps.api.fdroid.FdroidRepository -import foundation.e.apps.api.fdroid.models.FdroidEntity import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.api.gplay.GPlayAPIRepository -import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.modules.DataStoreModule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject /** @@ -34,39 +27,9 @@ class AppInfoFetchViewModel @Inject constructor( private val gson: Gson ) : ViewModel() { - private val fdroidEntries = mutableMapOf() - - fun setAuthorNameIfNeeded(textView: TextView, fusedApp: FusedApp) { - viewModelScope.launch { - var authorNameToDisplay = textView.text - withContext(Dispatchers.Default) { - fusedApp.run { - try { - if (author == "unknown" && origin == Origin.CLEANAPK) { - - withContext(Dispatchers.Main) { - textView.text = FdroidEntity.DEFAULT_FDROID_AUTHOR_NAME - } - - var result = fdroidEntries[package_name] - if (result == null) { - result = fdroidRepository.getFdroidInfo(package_name)?.also { - fdroidEntries[package_name] = it - } - } - result?.authorName?.let { - authorNameToDisplay = it - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - withContext(Dispatchers.Main) { - textView.text = authorNameToDisplay - } - } + fun getAuthorName(fusedApp: FusedApp) = liveData { + val authorName = fdroidRepository.getAuthorName(fusedApp) + emit(authorName) } fun isAppPurchased(app: FusedApp): LiveData { diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt index d61d71791..8e4712103 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt @@ -1,6 +1,8 @@ package foundation.e.apps.api.fdroid import foundation.e.apps.api.fdroid.models.FdroidEntity +import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.utils.enums.Origin import javax.inject.Inject import javax.inject.Singleton @@ -10,13 +12,19 @@ class FdroidRepository @Inject constructor( private val fdroidDao: FdroidDao, ) { + companion object { + const val UNKNOWN = "unknown" + } + + private val fdroidEntries = mutableMapOf() + /** * Get Fdroid entity from DB is present. * If not present then make an API call, store the fetched result and return the result. * * Result may be null. */ - suspend fun getFdroidInfo(packageName: String): FdroidEntity? { + private suspend fun getFdroidInfo(packageName: String): FdroidEntity? { return fdroidDao.getFdroidEntityFromPackageName(packageName) ?: fdroidApi.getFdroidInfoForPackage(packageName)?.let { FdroidEntity(packageName, it.authorName).also { @@ -24,4 +32,18 @@ class FdroidRepository @Inject constructor( } } } + + suspend fun getAuthorName(fusedApp: FusedApp): String { + if (fusedApp.author != UNKNOWN || fusedApp.origin != Origin.CLEANAPK) { + return fusedApp.author.ifEmpty { UNKNOWN } + } + + var result = fdroidEntries[fusedApp.package_name] + if (result == null) { + result = getFdroidInfo(fusedApp.package_name)?.also { + fdroidEntries[fusedApp.package_name] = it + } + } + return result?.authorName ?: FdroidEntity.DEFAULT_FDROID_AUTHOR_NAME + } } diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index 555a107c5..762f72157 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -190,8 +190,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { binding.titleInclude.apply { applicationIcon = appIcon appName.text = it.name - appAuthor.text = it.author - appInfoFetchViewModel.setAuthorNameIfNeeded(appAuthor, it) + appInfoFetchViewModel.getAuthorName(it).observe(viewLifecycleOwner) { + appAuthor.text = it + } categoryTitle.text = it.category if (origin == Origin.CLEANAPK) { appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt index cf76ca1db..70fd3897a 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt @@ -150,8 +150,9 @@ class ApplicationListRVAdapter( action?.let { direction -> view.findNavController().navigate(direction) } } appTitle.text = searchApp.name - appAuthor.text = searchApp.author - appInfoFetchViewModel.setAuthorNameIfNeeded(appAuthor, searchApp) + appInfoFetchViewModel.getAuthorName(searchApp).observe(lifecycleOwner!!) { + appAuthor.text = it + } if (searchApp.ratings.usageQualityScore != -1.0) { appRating.text = searchApp.ratings.usageQualityScore.toString() appRatingBar.rating = searchApp.ratings.usageQualityScore.toFloat() -- GitLab From 079673f170ddc3bfed13d32cc815d7b8c7ce50b8 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Sun, 21 Aug 2022 15:54:45 +0600 Subject: [PATCH 5/6] Viewmodels are refactored, (test needed) --- .../foundation/e/apps/AppProgressViewModel.kt | 37 +-------- .../apps/application/ApplicationViewModel.kt | 29 +------ .../manager/fused/FusedManagerRepository.kt | 78 +++++++++++++++++++ .../e/apps/updates/UpdatesViewModel.kt | 7 +- .../manager/UpdatesManagerRepository.kt | 5 +- .../e/apps/updates/manager/UpdatesWorker.kt | 3 +- 6 files changed, 89 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/AppProgressViewModel.kt b/app/src/main/java/foundation/e/apps/AppProgressViewModel.kt index 63243ee37..2b791032e 100644 --- a/app/src/main/java/foundation/e/apps/AppProgressViewModel.kt +++ b/app/src/main/java/foundation/e/apps/AppProgressViewModel.kt @@ -38,41 +38,6 @@ class AppProgressViewModel @Inject constructor( fusedApp: FusedApp?, progress: DownloadProgress ): Int { - fusedApp?.let { app -> - val appDownload = fusedManagerRepository.getDownloadList() - .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } - ?: return 0 - - if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) { - return@let - } - - if (!isProgressValidForApp(fusedApp, progress)) { - return -1 - } - - val downloadingMap = progress.totalSizeBytes.filter { item -> - appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0 - } - - if (appDownload.downloadIdMap.size > downloadingMap.size) { // All files for download are not ready yet - return 0 - } - - val totalSizeBytes = downloadingMap.values.sum() - val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> - appDownload.downloadIdMap.keys.contains(item.key) - }.values.sum() - return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100).toInt() - } - return 0 - } - - private suspend fun isProgressValidForApp( - fusedApp: FusedApp, - downloadProgress: DownloadProgress - ): Boolean { - val download = fusedManagerRepository.getFusedDownload(downloadProgress.downloadId) - return download.id == fusedApp._id + return fusedManagerRepository.calculateProgress(fusedApp, progress) } } diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt index dc094e5ff..794904192 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt @@ -31,7 +31,6 @@ import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.manager.download.data.DownloadProgressLD import foundation.e.apps.manager.fused.FusedManagerRepository -import foundation.e.apps.manager.pkg.PkgManagerModule import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status @@ -44,7 +43,6 @@ class ApplicationViewModel @Inject constructor( downloadProgressLD: DownloadProgressLD, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, - private val pkgManagerModule: PkgManagerModule ) : ViewModel() { val fusedApp: MutableLiveData> = MutableLiveData() @@ -110,36 +108,17 @@ class ApplicationViewModel @Inject constructor( } fun handleRatingFormat(rating: Double): String { - return if (rating % 1 == 0.0) { - rating.toInt().toString() - } else { - rating.toString() - } + return fusedManagerRepository.handleRatingFormat(rating) } suspend fun calculateProgress(progress: DownloadProgress): Pair { - fusedApp.value?.first?.let { app -> - val appDownload = fusedManagerRepository.getDownloadList() - .singleOrNull { it.id.contentEquals(app._id) } - val downloadingMap = progress.totalSizeBytes.filter { item -> - appDownload?.downloadIdMap?.keys?.contains(item.key) == true - } - val totalSizeBytes = downloadingMap.values.sum() - val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> - appDownload?.downloadIdMap?.keys?.contains(item.key) == true - }.values.sum() - - return Pair(totalSizeBytes, downloadedSoFar) - } - return Pair(1, 0) + return fusedManagerRepository.getCalculateProgressWithTotalSize(fusedApp.value?.first, progress) } fun updateApplicationStatus(downloadList: List) { fusedApp.value?.first?.let { app -> - val downloadingItem = - downloadList.find { it.origin == app.origin && (it.packageName == app.package_name || it.id == app.package_name) } - appStatus.value = - downloadingItem?.status ?: fusedAPIRepository.getFusedAppInstallationStatus(app) + appStatus.value = fusedManagerRepository.getDownloadingItemStatus(app, downloadList) + ?: fusedAPIRepository.getFusedAppInstallationStatus(app) } } } diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt index 6ee2081a7..749f15f33 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt @@ -4,7 +4,9 @@ import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import androidx.lifecycle.asFlow +import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.database.fusedDownload.FusedDownload +import foundation.e.apps.manager.download.data.DownloadProgress import foundation.e.apps.utils.enums.Status import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -86,4 +88,80 @@ class FusedManagerRepository @Inject constructor( fun validateFusedDownload(fusedDownload: FusedDownload) = fusedDownload.packageName.isNotEmpty() && fusedDownload.downloadURLList.isNotEmpty() + + suspend fun calculateProgress( + fusedApp: FusedApp?, + progress: DownloadProgress + ): Int { + fusedApp?.let { app -> + val appDownload = getDownloadList() + .singleOrNull { it.id.contentEquals(app._id) && it.packageName.contentEquals(app.package_name) } + ?: return 0 + + if (!appDownload.id.contentEquals(app._id) || !appDownload.packageName.contentEquals(app.package_name)) { + return@let + } + + if (!isProgressValidForApp(fusedApp, progress)) { + return -1 + } + + val downloadingMap = progress.totalSizeBytes.filter { item -> + appDownload.downloadIdMap.keys.contains(item.key) && item.value > 0 + } + + if (appDownload.downloadIdMap.size > downloadingMap.size) { // All files for download are not ready yet + return 0 + } + + val totalSizeBytes = downloadingMap.values.sum() + val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> + appDownload.downloadIdMap.keys.contains(item.key) + }.values.sum() + return ((downloadedSoFar / totalSizeBytes.toDouble()) * 100).toInt() + } + return 0 + } + + private suspend fun isProgressValidForApp( + fusedApp: FusedApp, + downloadProgress: DownloadProgress + ): Boolean { + val download = getFusedDownload(downloadProgress.downloadId) + return download.id == fusedApp._id + } + + fun handleRatingFormat(rating: Double): String { + return if (rating % 1 == 0.0) { + rating.toInt().toString() + } else { + rating.toString() + } + } + + suspend fun getCalculateProgressWithTotalSize(fusedApp: FusedApp?, progress: DownloadProgress): Pair { + fusedApp?.let { app -> + val appDownload = getDownloadList() + .singleOrNull { it.id.contentEquals(app._id) } + val downloadingMap = progress.totalSizeBytes.filter { item -> + appDownload?.downloadIdMap?.keys?.contains(item.key) == true + } + val totalSizeBytes = downloadingMap.values.sum() + val downloadedSoFar = progress.bytesDownloadedSoFar.filter { item -> + appDownload?.downloadIdMap?.keys?.contains(item.key) == true + }.values.sum() + + return Pair(totalSizeBytes, downloadedSoFar) + } + return Pair(1, 0) + } + + fun getDownloadingItemStatus(fusedApp: FusedApp?, downloadList: List): Status? { + fusedApp?.let { app -> + val downloadingItem = + downloadList.find { it.origin == app.origin && (it.packageName == app.package_name || it.id == app.package_name) } + return downloadingItem?.status + } + return null + } } diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt index f630fda7a..fe1c036fa 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt @@ -43,12 +43,7 @@ class UpdatesViewModel @Inject constructor( fun getUpdates(authData: AuthData) { viewModelScope.launch { val updatesResult = updatesManagerRepository.getUpdates(authData) - updatesList.postValue( - Pair( - updatesResult.first.filter { !(!it.isFree && authData.isAnonymous) }, - updatesResult.second - ) - ) + updatesList.postValue(updatesResult) } } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt index 649c82261..dadc807ca 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt @@ -28,7 +28,10 @@ class UpdatesManagerRepository @Inject constructor( ) { suspend fun getUpdates(authData: AuthData): Pair, ResultStatus> { - return updatesManagerImpl.getUpdates(authData) + return updatesManagerImpl.getUpdates(authData).run { + val filteredApps = first.filter { !(!it.isFree && authData.isAnonymous) } + Pair(filteredApps, this.second) + } } fun getApplicationCategoryPreference(): String { diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt index f73370521..46597fee6 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt @@ -58,8 +58,7 @@ class UpdatesWorker @AssistedInject constructor( private suspend fun checkForUpdates() { loadSettings() val authData = getAuthData() - val appsNeededToUpdate = updatesManagerRepository.getUpdates(authData) - .first.filter { !(!it.isFree && authData.isAnonymous) } + val appsNeededToUpdate = updatesManagerRepository.getUpdates(authData).first val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) /* * Show notification only if enabled. -- GitLab From 6bfa83ca21aff1f383e14eb6e55c514a480a93e9 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 23 Aug 2022 11:36:45 +0600 Subject: [PATCH 6/6] refactoring of fragments & adapters --- .../AppPrivacyInfoRepositoryImpl.kt | 26 +- .../e/apps/application/ApplicationFragment.kt | 325 ++++++++++-------- .../ApplicationListFragment.kt | 17 +- .../model/ApplicationListRVAdapter.kt | 151 ++++---- .../categories/model/CategoriesRVAdapter.kt | 34 +- .../e/apps/search/SearchFragment.kt | 154 +++++---- .../e/apps/search/SearchViewModel.kt | 2 - 7 files changed, 420 insertions(+), 289 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt index ecd684e13..c53f6d252 100644 --- a/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt @@ -19,9 +19,24 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( private val exodusTrackerApi: ExodusTrackerApi, private val trackerDao: TrackerDao ) : IAppPrivacyInfoRepository { + + companion object { + private const val MAX_TRACKER_SCORE = 9 + private const val MIN_TRACKER_SCORE = 0 + private const val MAX_PERMISSION_SCORE = 10 + private const val MIN_PERMISSION_SCORE = 0 + private const val THRESHOLD_OF_NON_ZERO_TRACKER_SCORE = 5 + private const val THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE = 9 + private const val FACTOR_OF_PERMISSION_SCORE = 0.2 + private const val DIVIDER_OF_PERMISSION_SCORE = 2.0 + } + private var trackers: List = listOf() - override suspend fun getAppPrivacyInfo(fusedApp: FusedApp, appHandle: String): Result { + override suspend fun getAppPrivacyInfo( + fusedApp: FusedApp, + appHandle: String + ): Result { if (fusedApp.trackers.isNotEmpty() && fusedApp.permsFromExodus.isNotEmpty()) { val appInfo = AppPrivacyInfo(fusedApp.trackers, fusedApp.permsFromExodus) return Result.success(appInfo) @@ -29,7 +44,8 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( val appTrackerInfoResult = getResult { exodusTrackerApi.getTrackerInfoOfApp(appHandle) } if (appTrackerInfoResult.isSuccess()) { - val appPrivacyPrivacyInfoResult = handleAppPrivacyInfoResultSuccess(appTrackerInfoResult) + val appPrivacyPrivacyInfoResult = + handleAppPrivacyInfoResultSuccess(appTrackerInfoResult) updateFusedApp(fusedApp, appPrivacyPrivacyInfoResult) return appPrivacyPrivacyInfoResult } @@ -136,10 +152,12 @@ class AppPrivacyInfoRepositoryImpl @Inject constructor( fusedApp.permsFromExodus.filter { it.contains("android.permission") }.size private fun calculateTrackersScore(numberOfTrackers: Int): Int { - return if (numberOfTrackers > 5) 0 else 9 - numberOfTrackers + return if (numberOfTrackers > THRESHOLD_OF_NON_ZERO_TRACKER_SCORE) MIN_TRACKER_SCORE else MAX_TRACKER_SCORE - numberOfTrackers } private fun calculatePermissionsScore(numberOfPermission: Int): Int { - return if (numberOfPermission > 9) 0 else round(0.2 * ceil((10 - numberOfPermission) / 2.0)).toInt() + return if (numberOfPermission > THRESHOLD_OF_NON_ZERO_PERMISSION_SCORE) MIN_PERMISSION_SCORE else round( + FACTOR_OF_PERMISSION_SCORE * ceil((MAX_PERMISSION_SCORE - numberOfPermission) / DIVIDER_OF_PERMISSION_SCORE) + ).toInt() } } diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index 762f72157..365a5473e 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -73,8 +73,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private val args: ApplicationFragmentArgs by navArgs() private val TAG = ApplicationFragment::class.java.simpleName - private var _binding: FragmentApplicationBinding? = null + private val binding get() = _binding!! /* @@ -104,6 +104,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private var isDetailsLoaded = false + private lateinit var screenshotsRVAdapter: ApplicationScreenshotsRVAdapter + @Inject lateinit var pkgManagerModule: PkgManagerModule @@ -139,35 +141,30 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { refreshDataOrRefreshToken(mainActivityViewModel) } - val startDestination = findNavController().graph.startDestination - if (startDestination == R.id.applicationFragment) { - binding.toolbar.setNavigationOnClickListener { - val action = ApplicationFragmentDirections.actionApplicationFragmentToHomeFragment() - view.findNavController().navigate(action) - } - } else { - binding.toolbar.setNavigationOnClickListener { - view.findNavController().navigateUp() - } - } - - val notAvailable = getString(R.string.not_available) + setupToolbar(view) - val screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) - binding.recyclerView.apply { - adapter = screenshotsRVAdapter - layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - } + setupScreenshotRVAdapter() binding.applicationLayout.visibility = View.INVISIBLE applicationViewModel.fusedApp.observe(viewLifecycleOwner) { resultPair -> - if (resultPair.second != ResultStatus.OK) { - onTimeout() - return@observe - } + updateUi(resultPair) + } + + applicationViewModel.errorMessageLiveData.observe(viewLifecycleOwner) { + (requireActivity() as MainActivity).showSnackbarMessage(getString(it)) + } + } - /* + private fun updateUi( + resultPair: Pair, + ) { + if (resultPair.second != ResultStatus.OK) { + onTimeout() + return + } + + /* * Previously fusedApp only had instance of FusedApp. * As such previously all reference was simply using "it", the default variable in * the scope. But now "it" is Pair(FusedApp, ResultStatus), not an instance of FusedApp. @@ -176,142 +173,182 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 */ - val it = resultPair.first + val it = resultPair.first - dismissTimeoutDialog() + dismissTimeoutDialog() - isDetailsLoaded = true - if (applicationViewModel.appStatus.value == null) { - applicationViewModel.appStatus.value = it.status - } - screenshotsRVAdapter.setData(it.other_images_path) - - // Title widgets - binding.titleInclude.apply { - applicationIcon = appIcon - appName.text = it.name - appInfoFetchViewModel.getAuthorName(it).observe(viewLifecycleOwner) { - appAuthor.text = it - } - categoryTitle.text = it.category - if (origin == Origin.CLEANAPK) { - appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) - } else { - appIcon.load(it.icon_image_path) - } - } + isDetailsLoaded = true + if (applicationViewModel.appStatus.value == null) { + applicationViewModel.appStatus.value = it.status + } + screenshotsRVAdapter.setData(it.other_images_path) + + // Title widgets + updateAppTitlePanel(it) + + binding.downloadInclude.appSize.text = it.appSize - binding.downloadInclude.appSize.text = it.appSize + // Ratings widgets + updateAppRating(it) - // Ratings widgets - binding.ratingsInclude.apply { - if (it.ratings.usageQualityScore != -1.0) { - val rating = - applicationViewModel.handleRatingFormat(it.ratings.usageQualityScore) - appRating.text = - getString( - R.string.rating_out_of, rating - ) + updateAppDescriptionText(it) - appRating.setCompoundDrawablesWithIntrinsicBounds( - null, null, getRatingDrawable(rating), null + // Information widgets + updateAppInformation(it) + + // Privacy widgets + updatePrivacyPanel() + + if (appInfoFetchViewModel.isAppInBlockedList(it)) { + binding.snackbarLayout.visibility = View.VISIBLE + } + fetchAppTracker(it) + + mainActivityViewModel.downloadList.observe(viewLifecycleOwner) { list -> + applicationViewModel.updateApplicationStatus(list) + } + } + + private fun updateAppDescriptionText(it: FusedApp) { + binding.appDescription.text = + Html.fromHtml(it.description, Html.FROM_HTML_MODE_COMPACT) + + binding.appDescriptionMore.setOnClickListener { view -> + val action = + ApplicationFragmentDirections.actionApplicationFragmentToDescriptionFragment(it.description) + view.findNavController().navigate(action) + } + } + + private fun updatePrivacyPanel() { + binding.privacyInclude.apply { + appPermissions.setOnClickListener { _ -> + ApplicationDialogFragment( + R.drawable.ic_perm, + getString(R.string.permissions), + getPermissionListString() + ).show(childFragmentManager, TAG) + } + appTrackers.setOnClickListener { + val fusedApp = applicationViewModel.fusedApp.value?.first + var trackers = + privacyInfoViewModel.getTrackerListText(fusedApp) + + if (fusedApp?.trackers == LIST_OF_NULL) { + trackers = getString(R.string.tracker_information_not_found) + } else if (trackers.isNotEmpty()) { + trackers += "

" + getString( + R.string.privacy_computed_using_text, + EXODUS_URL ) - appRating.compoundDrawablePadding = 15 - } - appRatingLayout.setOnClickListener { - ApplicationDialogFragment( - R.drawable.ic_star, - getString(R.string.rating), - getString(R.string.rating_description) - ).show(childFragmentManager, TAG) + } else { + trackers = getString(R.string.no_tracker_found) } - appPrivacyScoreLayout.setOnClickListener { - ApplicationDialogFragment( - R.drawable.ic_lock, - getString(R.string.privacy_score), - getString( - R.string.privacy_description, - PRIVACY_SCORE_SOURCE_CODE_URL, - EXODUS_URL, - PRIVACY_GUIDELINE_URL - ) - ).show(childFragmentManager, TAG) - } + ApplicationDialogFragment( + R.drawable.ic_tracker, + getString(R.string.trackers_title), + trackers + ).show(childFragmentManager, TAG) } + } + } - binding.appDescription.text = - Html.fromHtml(it.description, Html.FROM_HTML_MODE_COMPACT) + private fun updateAppInformation( + it: FusedApp, + ) { + binding.infoInclude.apply { + appUpdatedOn.text = getString( + R.string.updated_on, + if (origin == Origin.CLEANAPK) it.updatedOn else it.last_modified + ) + val notAvailable = getString(R.string.not_available) + appRequires.text = getString(R.string.min_android_version, notAvailable) + appVersion.text = getString( + R.string.version, + if (it.latest_version_number == "-1") notAvailable else it.latest_version_number + ) + appLicense.text = getString( + R.string.license, + if (it.licence.isBlank() or (it.licence == "unknown")) notAvailable else it.licence + ) + appPackageName.text = getString(R.string.package_name, it.package_name) + } + } - binding.appDescriptionMore.setOnClickListener { view -> - val action = - ApplicationFragmentDirections.actionApplicationFragmentToDescriptionFragment(it.description) - view.findNavController().navigate(action) - } + private fun updateAppRating(it: FusedApp) { + binding.ratingsInclude.apply { + if (it.ratings.usageQualityScore != -1.0) { + val rating = + applicationViewModel.handleRatingFormat(it.ratings.usageQualityScore) + appRating.text = + getString( + R.string.rating_out_of, rating + ) - // Information widgets - binding.infoInclude.apply { - appUpdatedOn.text = getString( - R.string.updated_on, - if (origin == Origin.CLEANAPK) it.updatedOn else it.last_modified - ) - appRequires.text = getString(R.string.min_android_version, notAvailable) - appVersion.text = getString( - R.string.version, - if (it.latest_version_number == "-1") notAvailable else it.latest_version_number - ) - appLicense.text = getString( - R.string.license, - if (it.licence.isBlank() or (it.licence == "unknown")) notAvailable else it.licence + appRating.setCompoundDrawablesWithIntrinsicBounds( + null, null, getRatingDrawable(rating), null ) - appPackageName.text = getString(R.string.package_name, it.package_name) + appRating.compoundDrawablePadding = 15 } - - // Privacy widgets - binding.privacyInclude.apply { - appPermissions.setOnClickListener { _ -> - ApplicationDialogFragment( - R.drawable.ic_perm, - getString(R.string.permissions), - getPermissionListString() - ).show(childFragmentManager, TAG) - } - appTrackers.setOnClickListener { - val fusedApp = applicationViewModel.fusedApp.value?.first - var trackers = - privacyInfoViewModel.getTrackerListText(fusedApp) - - if (fusedApp?.trackers == LIST_OF_NULL) { - trackers = getString(R.string.tracker_information_not_found) - } else if (trackers.isNotEmpty()) { - trackers += "

" + getString( - R.string.privacy_computed_using_text, - EXODUS_URL - ) - } else { - trackers = getString(R.string.no_tracker_found) - } - - ApplicationDialogFragment( - R.drawable.ic_tracker, - getString(R.string.trackers_title), - trackers - ).show(childFragmentManager, TAG) - } + appRatingLayout.setOnClickListener { + ApplicationDialogFragment( + R.drawable.ic_star, + getString(R.string.rating), + getString(R.string.rating_description) + ).show(childFragmentManager, TAG) } - if (appInfoFetchViewModel.isAppInBlockedList(it)) { - binding.snackbarLayout.visibility = View.VISIBLE + appPrivacyScoreLayout.setOnClickListener { + ApplicationDialogFragment( + R.drawable.ic_lock, + getString(R.string.privacy_score), + getString( + R.string.privacy_description, + PRIVACY_SCORE_SOURCE_CODE_URL, + EXODUS_URL, + PRIVACY_GUIDELINE_URL + ) + ).show(childFragmentManager, TAG) } - fetchAppTracker(it) + } + } - mainActivityViewModel.downloadList.observe(viewLifecycleOwner) { list -> - applicationViewModel.updateApplicationStatus(list) + private fun updateAppTitlePanel(it: FusedApp) { + binding.titleInclude.apply { + applicationIcon = appIcon + appName.text = it.name + appInfoFetchViewModel.getAuthorName(it).observe(viewLifecycleOwner) { + appAuthor.text = it + } + categoryTitle.text = it.category + if (origin == Origin.CLEANAPK) { + appIcon.load(CleanAPKInterface.ASSET_URL + it.icon_image_path) + } else { + appIcon.load(it.icon_image_path) } } + } - applicationViewModel.errorMessageLiveData.observe(viewLifecycleOwner) { - (requireActivity() as MainActivity).showSnackbarMessage(getString(it)) + private fun setupScreenshotRVAdapter() { + screenshotsRVAdapter = ApplicationScreenshotsRVAdapter(origin) + binding.recyclerView.apply { + adapter = screenshotsRVAdapter + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } + } + + private fun setupToolbar(view: View) { + val startDestination = findNavController().graph.startDestination + if (startDestination == R.id.applicationFragment) { + binding.toolbar.setNavigationOnClickListener { + val action = ApplicationFragmentDirections.actionApplicationFragmentToHomeFragment() + view.findNavController().navigate(action) + } + } else { + binding.toolbar.setNavigationOnClickListener { + view.findNavController().navigateUp() + } } } @@ -381,7 +418,12 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { downloadPB, appSize ) - Status.UNAVAILABLE -> handleUnavaiable(installButton, fusedApp, downloadPB, appSize) + Status.UNAVAILABLE -> handleUnavaiable( + installButton, + fusedApp, + downloadPB, + appSize + ) Status.QUEUED, Status.AWAITING, Status.DOWNLOADED -> handleQueued( installButton, fusedApp, @@ -669,7 +711,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } private fun updatePrivacyScore() { - val privacyScore = privacyInfoViewModel.getPrivacyScore(applicationViewModel.fusedApp.value?.first) + val privacyScore = + privacyInfoViewModel.getPrivacyScore(applicationViewModel.fusedApp.value?.first) if (privacyScore != -1) { val appPrivacyScore = binding.ratingsInclude.appPrivacyScore appPrivacyScore.text = getString( diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt index eed527c4c..0158c66ea 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -78,13 +78,7 @@ class ApplicationListFragment : super.onViewCreated(view, savedInstanceState) _binding = FragmentApplicationListBinding.bind(view) - binding.toolbarTitleTV.text = args.translation - binding.toolbar.apply { - setNavigationOnClickListener { - view.findNavController().navigate(R.id.categoriesFragment) - } - } - + updateToolbar(view) setupRecyclerView(view) observeAppListLiveData() @@ -100,6 +94,15 @@ class ApplicationListFragment : } } + private fun updateToolbar(view: View) { + binding.toolbarTitleTV.text = args.translation + binding.toolbar.apply { + setNavigationOnClickListener { + view.findNavController().navigate(R.id.categoriesFragment) + } + } + } + private fun setupRecyclerView(view: View) { val recyclerView = initRecyclerView() findNavController().currentDestination?.id?.let { diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt index 70fd3897a..ecc072724 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt @@ -123,67 +123,13 @@ class ApplicationListRVAdapter( hidePrivacyScore() } applicationList.setOnClickListener { - val action = when (currentDestinationId) { - R.id.applicationListFragment -> { - ApplicationListFragmentDirections.actionApplicationListFragmentToApplicationFragment( - searchApp._id, - searchApp.package_name, - searchApp.origin - ) - } - R.id.searchFragment -> { - SearchFragmentDirections.actionSearchFragmentToApplicationFragment( - searchApp._id, - searchApp.package_name, - searchApp.origin - ) - } - R.id.updatesFragment -> { - UpdatesFragmentDirections.actionUpdatesFragmentToApplicationFragment( - searchApp._id, - searchApp.package_name, - searchApp.origin - ) - } - else -> null - } - action?.let { direction -> view.findNavController().navigate(direction) } - } - appTitle.text = searchApp.name - appInfoFetchViewModel.getAuthorName(searchApp).observe(lifecycleOwner!!) { - appAuthor.text = it - } - if (searchApp.ratings.usageQualityScore != -1.0) { - appRating.text = searchApp.ratings.usageQualityScore.toString() - appRatingBar.rating = searchApp.ratings.usageQualityScore.toFloat() - } - if (searchApp.ratings.privacyScore != -1.0) { - appPrivacyScore.text = view.context.getString( - R.string.privacy_rating_out_of, - searchApp.ratings.privacyScore.toInt().toString() - ) - } - - if (searchApp.source.isEmpty()) { - sourceTag.visibility = View.INVISIBLE - } else { - sourceTag.visibility = View.VISIBLE - } - sourceTag.text = searchApp.source - - when (searchApp.origin) { - Origin.GPLAY -> { - appIcon.load(searchApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } - Origin.CLEANAPK -> { - appIcon.load(CleanAPKInterface.ASSET_URL + searchApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } - else -> Log.wtf(TAG, "${searchApp.package_name} is from an unknown origin") + handleAppItemClick(searchApp, view) } + updateAppInfo(searchApp) + updateRating(searchApp) + updatePrivacyScore(searchApp, view) + updateSourceTag(searchApp) + setAppIcon(searchApp, shimmerDrawable) removeIsPurchasedObserver(holder) if (appInfoFetchViewModel.isAppInBlockedList(searchApp)) { @@ -198,6 +144,91 @@ class ApplicationListRVAdapter( } } + private fun ApplicationListItemBinding.setAppIcon( + searchApp: FusedApp, + shimmerDrawable: ShimmerDrawable + ) { + when (searchApp.origin) { + Origin.GPLAY -> { + appIcon.load(searchApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } + Origin.CLEANAPK -> { + appIcon.load(CleanAPKInterface.ASSET_URL + searchApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } + else -> Log.wtf(TAG, "${searchApp.package_name} is from an unknown origin") + } + } + + private fun ApplicationListItemBinding.updateAppInfo(searchApp: FusedApp) { + appTitle.text = searchApp.name + appInfoFetchViewModel.getAuthorName(searchApp).observe(lifecycleOwner!!) { + appAuthor.text = it + } + } + + private fun ApplicationListItemBinding.updateRating(searchApp: FusedApp) { + if (searchApp.ratings.usageQualityScore != -1.0) { + appRating.text = searchApp.ratings.usageQualityScore.toString() + appRatingBar.rating = searchApp.ratings.usageQualityScore.toFloat() + } + } + + private fun ApplicationListItemBinding.updatePrivacyScore( + searchApp: FusedApp, + view: View + ) { + if (searchApp.ratings.privacyScore != -1.0) { + appPrivacyScore.text = view.context.getString( + R.string.privacy_rating_out_of, + searchApp.ratings.privacyScore.toInt().toString() + ) + } + } + + private fun ApplicationListItemBinding.updateSourceTag(searchApp: FusedApp) { + if (searchApp.source.isEmpty()) { + sourceTag.visibility = View.INVISIBLE + } else { + sourceTag.visibility = View.VISIBLE + } + sourceTag.text = searchApp.source + } + + private fun handleAppItemClick( + searchApp: FusedApp, + view: View + ) { + val action = when (currentDestinationId) { + R.id.applicationListFragment -> { + ApplicationListFragmentDirections.actionApplicationListFragmentToApplicationFragment( + searchApp._id, + searchApp.package_name, + searchApp.origin + ) + } + R.id.searchFragment -> { + SearchFragmentDirections.actionSearchFragmentToApplicationFragment( + searchApp._id, + searchApp.package_name, + searchApp.origin + ) + } + R.id.updatesFragment -> { + UpdatesFragmentDirections.actionUpdatesFragmentToApplicationFragment( + searchApp._id, + searchApp.package_name, + searchApp.origin + ) + } + else -> null + } + action?.let { direction -> view.findNavController().navigate(direction) } + } + private fun removeIsPurchasedObserver(holder: ViewHolder) { lifecycleOwner?.let { holder.isPurchasedLiveData.removeObservers(it) diff --git a/app/src/main/java/foundation/e/apps/categories/model/CategoriesRVAdapter.kt b/app/src/main/java/foundation/e/apps/categories/model/CategoriesRVAdapter.kt index 02a02f3da..f98cf1ce7 100644 --- a/app/src/main/java/foundation/e/apps/categories/model/CategoriesRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/categories/model/CategoriesRVAdapter.kt @@ -59,20 +59,28 @@ class CategoriesRVAdapter : ) holder.itemView.findNavController().navigate(direction) } - if (oldList[position].drawable != -1) { - categoryIcon.load(oldList[position].drawable) - } else { - categoryIcon.load(oldList[position].imageUrl) - } + loadCategoryIcon(position) categoryTitle.text = oldList[position].title - val tag = oldList[position].tag - if (tag.displayTag.isNotBlank()) { - categoryTag.visibility = View.VISIBLE - categoryTag.text = tag.displayTag - } else { - categoryTag.visibility = View.INVISIBLE - categoryTag.text = "" - } + updateTag(position) + } + } + + private fun CategoriesListItemBinding.loadCategoryIcon(position: Int) { + if (oldList[position].drawable != -1) { + categoryIcon.load(oldList[position].drawable) + } else { + categoryIcon.load(oldList[position].imageUrl) + } + } + + private fun CategoriesListItemBinding.updateTag(position: Int) { + val tag = oldList[position].tag + if (tag.displayTag.isNotBlank()) { + categoryTag.visibility = View.VISIBLE + categoryTag.text = tag.displayTag + } else { + categoryTag.visibility = View.INVISIBLE + categoryTag.text = "" } } diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index dcf766038..545c13f67 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -46,6 +46,7 @@ import foundation.e.apps.AppProgressViewModel import foundation.e.apps.MainActivityViewModel import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R +import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.application.subFrags.ApplicationDialogFragment @@ -106,26 +107,82 @@ class SearchFragment : searchHintLayout = binding.searchHintLayout.root noAppsFoundLayout = binding.noAppsFoundLayout.root - // Setup SearchView - setHasOptionsMenu(true) - searchView?.setOnSuggestionListener(this) - searchView?.setOnQueryTextListener(this) - searchView?.let { configureCloseButton(it) } + setupSearchView() + setupSearchViewSuggestions() - // Setup SearchView Suggestions - val from = arrayOf(SUGGESTION_KEY) - val to = intArrayOf(android.R.id.text1) - searchView?.suggestionsAdapter = SimpleCursorAdapter( - context, - R.layout.custom_simple_list_item, null, from, to, - CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER - ) + // Setup Search Results + val listAdapter = setupSearchResult(view) - searchViewModel.searchSuggest.observe(viewLifecycleOwner) { - it?.let { populateSuggestionsAdapter(it) } + observeSearchResult(listAdapter) + } + + private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { + searchViewModel.searchResult.observe(viewLifecycleOwner) { + if (it.data?.first.isNullOrEmpty()) { + noAppsFoundLayout?.visibility = View.VISIBLE + } else { + if (!updateSearchResult(listAdapter, it)) return@observe + } + + listAdapter?.let { adapter -> + observeDownloadList(adapter) + } + + observeScrollOfSearchResult(listAdapter) + if (searchText.isNotBlank() && !it.isSuccess()) { + /* + * If blank check is not performed then timeout dialog keeps + * popping up whenever search tab is opened. + */ + onTimeout() + } } + } - // Setup Search Results + private fun observeScrollOfSearchResult(listAdapter: ApplicationListRVAdapter?) { + listAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + searchView?.run { + /* + * Only scroll back to 0 position for a new search. + * + * If we are getting new results from livedata for the old search query, + * do not scroll to top as the user may be scrolling to see already + * populated results. + */ + if (lastSearch != query?.toString()) { + recyclerView?.scrollToPosition(0) + lastSearch = query.toString() + } + } + } + }) + } + + /** + * @return true if Search result is updated, otherwise false + */ + private fun updateSearchResult( + listAdapter: ApplicationListRVAdapter?, + it: ResultSupreme, Boolean>> + ): Boolean { + val currentList = listAdapter?.currentList + if (it.data?.first != null && !currentList.isNullOrEmpty() && !searchViewModel.isAnyAppUpdated( + it.data?.first!!, + currentList + ) + ) { + return false + } + listAdapter?.setData(it.data!!.first) + binding.loadingProgressBar.isVisible = it.data!!.second + stopLoadingUI() + noAppsFoundLayout?.visibility = View.GONE + searchHintLayout?.visibility = View.GONE + return true + } + + private fun setupSearchResult(view: View): ApplicationListRVAdapter? { val listAdapter = findNavController().currentDestination?.id?.let { ApplicationListRVAdapter( this, @@ -145,57 +202,30 @@ class SearchFragment : adapter = listAdapter layoutManager = LinearLayoutManager(view.context) } + return listAdapter + } - searchViewModel.searchResult.observe(viewLifecycleOwner) { - if (it.data?.first.isNullOrEmpty()) { - noAppsFoundLayout?.visibility = View.VISIBLE - } else { - val currentList = listAdapter?.currentList - if (it.data?.first != null && !currentList.isNullOrEmpty() && !searchViewModel.isAnyAppUpdated( - it.data?.first!!, - currentList - ) - ) { - return@observe - } - listAdapter?.setData(it.data!!.first) - binding.loadingProgressBar.isVisible = it.data!!.second - stopLoadingUI() - noAppsFoundLayout?.visibility = View.GONE - searchHintLayout?.visibility = View.GONE - } - - listAdapter?.let { adapter -> - observeDownloadList(adapter) - } + private fun setupSearchViewSuggestions() { + val from = arrayOf(SUGGESTION_KEY) + val to = intArrayOf(android.R.id.text1) + searchView?.suggestionsAdapter = SimpleCursorAdapter( + context, + R.layout.custom_simple_list_item, null, from, to, + CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER + ) - listAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - searchView?.run { - /* - * Only scroll back to 0 position for a new search. - * - * If we are getting new results from livedata for the old search query, - * do not scroll to top as the user may be scrolling to see already - * populated results. - */ - if (lastSearch != query?.toString()) { - recyclerView?.scrollToPosition(0) - lastSearch = query.toString() - } - } - } - }) - if (searchText.isNotBlank() && !it.isSuccess()) { - /* - * If blank check is not performed then timeout dialog keeps - * popping up whenever search tab is opened. - */ - onTimeout() - } + searchViewModel.searchSuggest.observe(viewLifecycleOwner) { + it?.let { populateSuggestionsAdapter(it) } } } + private fun setupSearchView() { + setHasOptionsMenu(true) + searchView?.setOnSuggestionListener(this) + searchView?.setOnQueryTextListener(this) + searchView?.let { configureCloseButton(it) } + } + private fun showPaidAppMessage(fusedApp: FusedApp) { ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, fusedApp.name), diff --git a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt index 2c80d5b0c..1442ad98f 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt @@ -28,7 +28,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.manager.fused.FusedManagerRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -36,7 +35,6 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, - private val fusedManagerRepository: FusedManagerRepository ) : ViewModel() { val searchSuggest: MutableLiveData?> = MutableLiveData() -- GitLab