Loading app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +33 −19 Original line number Diff line number Diff line Loading @@ -21,8 +21,8 @@ package foundation.e.apps.data.fused import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App Loading Loading @@ -62,6 +62,7 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.fused.utils.CategoryUtils import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.gplay.GplayStoreRepository import foundation.e.apps.data.gplay.utils.runFlowWithTimeout import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule Loading @@ -79,7 +80,7 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton typealias GplaySearchResultFlow = Flow<ResultSupreme<Pair<List<FusedApp>, Boolean>>> typealias GplaySearchResultLiveData = LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> typealias FusedHomeDeferred = Deferred<ResultSupreme<List<FusedHome>>> @Singleton Loading Loading @@ -254,8 +255,9 @@ class FusedApiImpl @Inject constructor( */ return liveData { val packageSpecificResults = ArrayList<FusedApp>() fetchPackageSpecificResult(authData, query, packageSpecificResults).let { if (it.data?.second == true) { // if there are no data to load if (it.data?.second != true) { // if there are no data to load emit(it) return@liveData } Loading Loading @@ -289,7 +291,7 @@ class FusedApiImpl @Inject constructor( query, searchResult, packageSpecificResults ).asLiveData() ) ) } } Loading Loading @@ -333,10 +335,17 @@ class FusedApiImpl @Inject constructor( query: String, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): GplaySearchResultFlow = getGplaySearchResult(query).map { if (it.first.isNotEmpty()) { searchResult.addAll(it.first) ): GplaySearchResultLiveData { return runFlowWithTimeout({ getGplaySearchResult(query) }, { it.second }, { Pair(listOf(), false) // empty data for timeout } ).map { if (it.isSuccess()) { searchResult.addAll(it.data!!.first) ResultSupreme.Success( Pair( filterWithKeywordSearch( Loading @@ -344,9 +353,14 @@ class FusedApiImpl @Inject constructor( packageSpecificResults, query ), it.second it.data!!.second ) ) } else { it } } } private suspend fun fetchOpenSourceSearchResult( Loading Loading @@ -409,10 +423,10 @@ class FusedApiImpl @Inject constructor( * Also send true in the pair to signal more results being loaded. */ if (status != ResultStatus.OK) { return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } return ResultSupreme.create(status, Pair(packageSpecificResults, false)) } return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } /* * The list packageSpecificResults may contain apps with duplicate package names. Loading app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +18 −2 Original line number Diff line number Diff line Loading @@ -52,11 +52,10 @@ class GplayStoreRepositoryImpl @Inject constructor( private val loginSourceRepository: LoginSourceRepository ) : GplayStoreRepository { private val authData by lazy { loginSourceRepository.gplayAuth!! } override suspend fun getHomeScreenData(): Any { val homeScreenData = mutableMapOf<String, List<App>>() val homeElements = createTopChartElements() val authData = loginSourceRepository.gplayAuth ?: return homeScreenData homeElements.forEach { val chart = it.value.keys.iterator().next() Loading @@ -81,10 +80,13 @@ class GplayStoreRepositoryImpl @Inject constructor( query: String, ): Flow<Pair<List<App>, Boolean>> { return flow { /* * Variable names and logic made same as that of Aurora store. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ var authData = loginSourceRepository.gplayAuth ?: return@flow val searchHelper = SearchHelper(authData).using(gPlayHttpClient) val searchBundle = searchHelper.searchResults(query) Loading Loading @@ -148,6 +150,8 @@ class GplayStoreRepositoryImpl @Inject constructor( } override suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry> { val authData = loginSourceRepository.gplayAuth ?: return listOf() val searchData = mutableListOf<SearchSuggestEntry>() withContext(Dispatchers.IO) { val searchHelper = SearchHelper(authData).using(gPlayHttpClient) Loading @@ -157,6 +161,8 @@ class GplayStoreRepositoryImpl @Inject constructor( } override suspend fun getAppsByCategory(category: String, pageUrl: String?): StreamCluster { val authData = loginSourceRepository.gplayAuth ?: return StreamCluster() val subCategoryHelper = CategoryAppsHelper(authData).using(gPlayHttpClient) Loading @@ -173,6 +179,8 @@ class GplayStoreRepositoryImpl @Inject constructor( return categoryList } val authData = loginSourceRepository.gplayAuth ?: return categoryList withContext(Dispatchers.IO) { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) categoryList.addAll(categoryHelper.getAllCategoriesList(getCategoryType(type))) Loading @@ -182,6 +190,8 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getAppDetails(packageNameOrId: String): App? { var appDetails: App? val authData = loginSourceRepository.gplayAuth ?: return null withContext(Dispatchers.IO) { val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) appDetails = appDetailsHelper.getAppByPackageName(packageNameOrId) Loading @@ -191,6 +201,8 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getAppsDetails(packageNamesOrIds: List<String>): List<App> { val appDetailsList = mutableListOf<App>() val authData = loginSourceRepository.gplayAuth ?: return appDetailsList withContext(Dispatchers.IO) { val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) appDetailsList.addAll(appDetailsHelper.getAppByPackageName(packageNamesOrIds)) Loading Loading @@ -267,6 +279,8 @@ class GplayStoreRepositoryImpl @Inject constructor( offerType: Int ): List<File> { val downloadData = mutableListOf<File>() val authData = loginSourceRepository.gplayAuth ?: return downloadData withContext(Dispatchers.IO) { val version = versionCode?.let { it as Int } ?: -1 val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) Loading @@ -282,6 +296,8 @@ class GplayStoreRepositoryImpl @Inject constructor( offerType: Int ): List<File> { val downloadData = mutableListOf<File>() val authData = loginSourceRepository.gplayAuth ?: return downloadData withContext(Dispatchers.IO) { val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) downloadData.addAll( Loading app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt 0 → 100644 +106 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019-2023 MURENA SAS * * 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 <https://www.gnu.org/licenses/>. */ package foundation.e.apps.data.gplay.utils import android.os.CountDownTimer import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import foundation.e.apps.data.Constants import foundation.e.apps.data.ResultSupreme import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * Observing a LiveData/Flow for timeout is not easy. * We use a [CountDownTimer] to keep track of intervals between two emissions of data. * Also we had to collect items from Flow and emit it to a LiveData to avoid missing data emissions. * * @param block Code block producing the Flow * @param moreItemsToLoad Code block evaluating if more items will be loaded in the Flow. * Check if the item passed is the last item or not. If it is the last item, return false. * @param timeoutBlock Mandatory code block to execute for timeout. * Pass empty data from this block or any other data. * @param exceptionBlock Optional code block to execute for any other error. * * @return LiveData containing items from the Flow from [block], each item * wrapped in [ResultSupreme]. */ suspend fun <T> runFlowWithTimeout( block: suspend () -> Flow<T>, moreItemsToLoad: suspend (item: T) -> Boolean, timeoutBlock: () -> T, exceptionBlock: ((e: Exception) -> T?)? = null, ): LiveData<ResultSupreme<T>> { return liveData { withContext(Dispatchers.Main) { val timer = Timer(this) { emit(ResultSupreme.Timeout(timeoutBlock())) cancel() } try { withContext(Dispatchers.IO) { timer.start() block().collect { item -> timer.cancel() emit(ResultSupreme.Success(item)) if (!moreItemsToLoad(item)) { cancel() } timer.start() } } } catch (e: Exception) { if (e is CancellationException) { return@withContext } runCatching { emit( ResultSupreme.Error<T>(e.stackTraceToString()).apply { exceptionBlock?.invoke(e)?.let { setData(it) } } ) } } } } } private class Timer( private val scope: CoroutineScope, private val onTimeout: suspend () -> Unit, ): CountDownTimer(Constants.timeoutDurationInMillis, 1000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { scope.launch { onTimeout() } } } No newline at end of file app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt +3 −1 Original line number Diff line number Diff line Loading @@ -67,6 +67,8 @@ class LoginSourceRepository @Inject constructor( suspend fun getValidatedAuthData(): ResultSupreme<AuthData?> { val authDataValidator = (sources.find { it is AuthDataValidator } as AuthDataValidator) return authDataValidator.validateAuthData() val validateAuthData = authDataValidator.validateAuthData() this.gplayAuth = validateAuthData.data return validateAuthData } } app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +13 −3 Original line number Diff line number Diff line Loading @@ -118,7 +118,10 @@ class SearchFragment : authObjects.observe(viewLifecycleOwner) { val currentQuery = searchView?.query?.toString() ?: "" if (it == null || (currentQuery.isNotEmpty() && lastSearch == currentQuery)) return@observe if (it == null || shouldIgnore(it, currentQuery)) { return@observe } loadDataWhenNetworkAvailable(it) } Loading @@ -127,6 +130,12 @@ class SearchFragment : } } private fun shouldIgnore( authObjectList: List<AuthObject>?, currentQuery: String ) = currentQuery.isNotEmpty() && searchViewModel.isAuthObjectListSame(authObjectList) && lastSearch == currentQuery private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.data?.first.isNullOrEmpty() && it.data?.second == false) { Loading Loading @@ -427,7 +436,8 @@ class SearchFragment : } private fun showKeyboard() { val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager searchView?.javaClass?.getDeclaredField("mSearchSrcTextView")?.runCatching { isAccessible = true get(searchView) as EditText Loading Loading
app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +33 −19 Original line number Diff line number Diff line Loading @@ -21,8 +21,8 @@ package foundation.e.apps.data.fused import android.content.Context import android.text.format.Formatter import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.map import com.aurora.gplayapi.Constants import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App Loading Loading @@ -62,6 +62,7 @@ import foundation.e.apps.data.fused.utils.CategoryType import foundation.e.apps.data.fused.utils.CategoryUtils import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.gplay.GplayStoreRepository import foundation.e.apps.data.gplay.utils.runFlowWithTimeout import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule Loading @@ -79,7 +80,7 @@ import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton typealias GplaySearchResultFlow = Flow<ResultSupreme<Pair<List<FusedApp>, Boolean>>> typealias GplaySearchResultLiveData = LiveData<ResultSupreme<Pair<List<FusedApp>, Boolean>>> typealias FusedHomeDeferred = Deferred<ResultSupreme<List<FusedHome>>> @Singleton Loading Loading @@ -254,8 +255,9 @@ class FusedApiImpl @Inject constructor( */ return liveData { val packageSpecificResults = ArrayList<FusedApp>() fetchPackageSpecificResult(authData, query, packageSpecificResults).let { if (it.data?.second == true) { // if there are no data to load if (it.data?.second != true) { // if there are no data to load emit(it) return@liveData } Loading Loading @@ -289,7 +291,7 @@ class FusedApiImpl @Inject constructor( query, searchResult, packageSpecificResults ).asLiveData() ) ) } } Loading Loading @@ -333,10 +335,17 @@ class FusedApiImpl @Inject constructor( query: String, searchResult: MutableList<FusedApp>, packageSpecificResults: ArrayList<FusedApp> ): GplaySearchResultFlow = getGplaySearchResult(query).map { if (it.first.isNotEmpty()) { searchResult.addAll(it.first) ): GplaySearchResultLiveData { return runFlowWithTimeout({ getGplaySearchResult(query) }, { it.second }, { Pair(listOf(), false) // empty data for timeout } ).map { if (it.isSuccess()) { searchResult.addAll(it.data!!.first) ResultSupreme.Success( Pair( filterWithKeywordSearch( Loading @@ -344,9 +353,14 @@ class FusedApiImpl @Inject constructor( packageSpecificResults, query ), it.second it.data!!.second ) ) } else { it } } } private suspend fun fetchOpenSourceSearchResult( Loading Loading @@ -409,10 +423,10 @@ class FusedApiImpl @Inject constructor( * Also send true in the pair to signal more results being loaded. */ if (status != ResultStatus.OK) { return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } return ResultSupreme.create(status, Pair(packageSpecificResults, false)) } return ResultSupreme.create(status, Pair(packageSpecificResults, true)) } /* * The list packageSpecificResults may contain apps with duplicate package names. Loading
app/src/main/java/foundation/e/apps/data/gplay/GplayStoreRepositoryImpl.kt +18 −2 Original line number Diff line number Diff line Loading @@ -52,11 +52,10 @@ class GplayStoreRepositoryImpl @Inject constructor( private val loginSourceRepository: LoginSourceRepository ) : GplayStoreRepository { private val authData by lazy { loginSourceRepository.gplayAuth!! } override suspend fun getHomeScreenData(): Any { val homeScreenData = mutableMapOf<String, List<App>>() val homeElements = createTopChartElements() val authData = loginSourceRepository.gplayAuth ?: return homeScreenData homeElements.forEach { val chart = it.value.keys.iterator().next() Loading @@ -81,10 +80,13 @@ class GplayStoreRepositoryImpl @Inject constructor( query: String, ): Flow<Pair<List<App>, Boolean>> { return flow { /* * Variable names and logic made same as that of Aurora store. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ var authData = loginSourceRepository.gplayAuth ?: return@flow val searchHelper = SearchHelper(authData).using(gPlayHttpClient) val searchBundle = searchHelper.searchResults(query) Loading Loading @@ -148,6 +150,8 @@ class GplayStoreRepositoryImpl @Inject constructor( } override suspend fun getSearchSuggestions(query: String): List<SearchSuggestEntry> { val authData = loginSourceRepository.gplayAuth ?: return listOf() val searchData = mutableListOf<SearchSuggestEntry>() withContext(Dispatchers.IO) { val searchHelper = SearchHelper(authData).using(gPlayHttpClient) Loading @@ -157,6 +161,8 @@ class GplayStoreRepositoryImpl @Inject constructor( } override suspend fun getAppsByCategory(category: String, pageUrl: String?): StreamCluster { val authData = loginSourceRepository.gplayAuth ?: return StreamCluster() val subCategoryHelper = CategoryAppsHelper(authData).using(gPlayHttpClient) Loading @@ -173,6 +179,8 @@ class GplayStoreRepositoryImpl @Inject constructor( return categoryList } val authData = loginSourceRepository.gplayAuth ?: return categoryList withContext(Dispatchers.IO) { val categoryHelper = CategoryHelper(authData).using(gPlayHttpClient) categoryList.addAll(categoryHelper.getAllCategoriesList(getCategoryType(type))) Loading @@ -182,6 +190,8 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getAppDetails(packageNameOrId: String): App? { var appDetails: App? val authData = loginSourceRepository.gplayAuth ?: return null withContext(Dispatchers.IO) { val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) appDetails = appDetailsHelper.getAppByPackageName(packageNameOrId) Loading @@ -191,6 +201,8 @@ class GplayStoreRepositoryImpl @Inject constructor( override suspend fun getAppsDetails(packageNamesOrIds: List<String>): List<App> { val appDetailsList = mutableListOf<App>() val authData = loginSourceRepository.gplayAuth ?: return appDetailsList withContext(Dispatchers.IO) { val appDetailsHelper = AppDetailsHelper(authData).using(gPlayHttpClient) appDetailsList.addAll(appDetailsHelper.getAppByPackageName(packageNamesOrIds)) Loading Loading @@ -267,6 +279,8 @@ class GplayStoreRepositoryImpl @Inject constructor( offerType: Int ): List<File> { val downloadData = mutableListOf<File>() val authData = loginSourceRepository.gplayAuth ?: return downloadData withContext(Dispatchers.IO) { val version = versionCode?.let { it as Int } ?: -1 val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) Loading @@ -282,6 +296,8 @@ class GplayStoreRepositoryImpl @Inject constructor( offerType: Int ): List<File> { val downloadData = mutableListOf<File>() val authData = loginSourceRepository.gplayAuth ?: return downloadData withContext(Dispatchers.IO) { val purchaseHelper = PurchaseHelper(authData).using(gPlayHttpClient) downloadData.addAll( Loading
app/src/main/java/foundation/e/apps/data/gplay/utils/TimeoutEvaluation.kt 0 → 100644 +106 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019-2023 MURENA SAS * * 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 <https://www.gnu.org/licenses/>. */ package foundation.e.apps.data.gplay.utils import android.os.CountDownTimer import androidx.lifecycle.LiveData import androidx.lifecycle.liveData import foundation.e.apps.data.Constants import foundation.e.apps.data.ResultSupreme import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * Observing a LiveData/Flow for timeout is not easy. * We use a [CountDownTimer] to keep track of intervals between two emissions of data. * Also we had to collect items from Flow and emit it to a LiveData to avoid missing data emissions. * * @param block Code block producing the Flow * @param moreItemsToLoad Code block evaluating if more items will be loaded in the Flow. * Check if the item passed is the last item or not. If it is the last item, return false. * @param timeoutBlock Mandatory code block to execute for timeout. * Pass empty data from this block or any other data. * @param exceptionBlock Optional code block to execute for any other error. * * @return LiveData containing items from the Flow from [block], each item * wrapped in [ResultSupreme]. */ suspend fun <T> runFlowWithTimeout( block: suspend () -> Flow<T>, moreItemsToLoad: suspend (item: T) -> Boolean, timeoutBlock: () -> T, exceptionBlock: ((e: Exception) -> T?)? = null, ): LiveData<ResultSupreme<T>> { return liveData { withContext(Dispatchers.Main) { val timer = Timer(this) { emit(ResultSupreme.Timeout(timeoutBlock())) cancel() } try { withContext(Dispatchers.IO) { timer.start() block().collect { item -> timer.cancel() emit(ResultSupreme.Success(item)) if (!moreItemsToLoad(item)) { cancel() } timer.start() } } } catch (e: Exception) { if (e is CancellationException) { return@withContext } runCatching { emit( ResultSupreme.Error<T>(e.stackTraceToString()).apply { exceptionBlock?.invoke(e)?.let { setData(it) } } ) } } } } } private class Timer( private val scope: CoroutineScope, private val onTimeout: suspend () -> Unit, ): CountDownTimer(Constants.timeoutDurationInMillis, 1000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { scope.launch { onTimeout() } } } No newline at end of file
app/src/main/java/foundation/e/apps/data/login/LoginSourceRepository.kt +3 −1 Original line number Diff line number Diff line Loading @@ -67,6 +67,8 @@ class LoginSourceRepository @Inject constructor( suspend fun getValidatedAuthData(): ResultSupreme<AuthData?> { val authDataValidator = (sources.find { it is AuthDataValidator } as AuthDataValidator) return authDataValidator.validateAuthData() val validateAuthData = authDataValidator.validateAuthData() this.gplayAuth = validateAuthData.data return validateAuthData } }
app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +13 −3 Original line number Diff line number Diff line Loading @@ -118,7 +118,10 @@ class SearchFragment : authObjects.observe(viewLifecycleOwner) { val currentQuery = searchView?.query?.toString() ?: "" if (it == null || (currentQuery.isNotEmpty() && lastSearch == currentQuery)) return@observe if (it == null || shouldIgnore(it, currentQuery)) { return@observe } loadDataWhenNetworkAvailable(it) } Loading @@ -127,6 +130,12 @@ class SearchFragment : } } private fun shouldIgnore( authObjectList: List<AuthObject>?, currentQuery: String ) = currentQuery.isNotEmpty() && searchViewModel.isAuthObjectListSame(authObjectList) && lastSearch == currentQuery private fun observeSearchResult(listAdapter: ApplicationListRVAdapter?) { searchViewModel.searchResult.observe(viewLifecycleOwner) { if (it.data?.first.isNullOrEmpty() && it.data?.second == false) { Loading Loading @@ -427,7 +436,8 @@ class SearchFragment : } private fun showKeyboard() { val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager searchView?.javaClass?.getDeclaredField("mSearchSrcTextView")?.runCatching { isAccessible = true get(searchView) as EditText Loading