Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fe13e1cf authored by Sayantan Roychowdhury's avatar Sayantan Roychowdhury
Browse files

Merge branch '1845-429_banner_to_dialog' into 'main'

Issue 1845: Replace session error banner with dialog

See merge request !432
parents 9621f77e e10dadd4
Loading
Loading
Loading
Loading
Loading
+5 −29
Original line number Diff line number Diff line
<?xml version="1.0" ?>
<!--
  ~ Copyright (C) 2021-2024 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/>.
  ~
  -->

<SmellBaseline>
  <ManuallySuppressedIssues></ManuallySuppressedIssues>
  <CurrentIssues>
@@ -31,13 +13,13 @@
    <ID>InstanceOfCheckForException:GPlayHttpClient.kt$GPlayHttpClient$e is SocketTimeoutException</ID>
    <ID>InvalidPackageDeclaration:Trackers.kt$package foundation.e.apps.data.exodus</ID>
    <ID>LargeClass:ApplicationFragment.kt$ApplicationFragment : TimeoutFragment</ID>
    <ID>LongParameterList:ApplicationDialogFragment.kt$ApplicationDialogFragment$( drawable: Int = -1, title: String, message: String, positiveButtonText: String = "", positiveButtonAction: (() -&gt; Unit)? = null, cancelButtonText: String = "", cancelButtonAction: (() -&gt; Unit)? = null, cancellable: Boolean = true, onDismissListener: (() -&gt; Unit)? = null, )</ID>
    <ID>LongParameterList:ApplicationDialogFragment.kt$ApplicationDialogFragment$( drawable: Int = -1, title: String, message: String, positiveButtonText: String = "", positiveButtonAction: (() -&gt; Unit)? = null, cancelButtonText: String = "", cancelButtonAction: (() -&gt; Unit)? = null, cancelable: Boolean = true, onDismissListener: (() -&gt; Unit)? = null, )</ID>
    <ID>LongParameterList:ApplicationListRVAdapter.kt$ApplicationListRVAdapter$( private val applicationInstaller: ApplicationInstaller, private val privacyInfoViewModel: PrivacyInfoViewModel, private val appInfoFetchViewModel: AppInfoFetchViewModel, private val mainActivityViewModel: MainActivityViewModel, private val currentDestinationId: Int, private var lifecycleOwner: LifecycleOwner?, private var paidAppHandler: ((Application) -&gt; Unit)? = null )</ID>
    <ID>LongParameterList:ApplicationViewModel.kt$ApplicationViewModel$( id: String, packageName: String, origin: Origin, isFdroidLink: Boolean, authObjectList: List&lt;AuthObject&gt;, retryBlock: (failedObjects: List&lt;AuthObject&gt;) -&gt; Boolean, )</ID>
    <ID>LongParameterList:CleanApkRetrofit.kt$CleanApkRetrofit$( @Query("keyword") keyword: String, @Query("source") source: String = APP_SOURCE_FOSS, @Query("type") type: String = APP_TYPE_ANY, @Query("nres") nres: Int = 20, @Query("page") page: Int = 1, @Query("by") by: String? = null, )</ID>
    <ID>LongParameterList:EglExtensionProvider.kt$EglExtensionProvider$( egl10: EGL10, eglDisplay: EGLDisplay, eglConfig: EGLConfig?, ai: IntArray, ai1: IntArray?, set: MutableSet&lt;String&gt; )</ID>
    <ID>LongParameterList:FusedManagerImpl.kt$FusedManagerImpl$( @Named("cacheDir") private val cacheDir: String, private val downloadManager: DownloadManager, private val notificationManager: NotificationManager, private val fusedDownloadRepository: FusedDownloadRepository, private val pwaManager: PWAManager, private val appLoungePackageManager: AppLoungePackageManager, @Named("download") private val downloadNotificationChannel: NotificationChannel, @Named("update") private val updateNotificationChannel: NotificationChannel, @ApplicationContext private val context: Context )</ID>
    <ID>LongParameterList:MainActivityViewModel.kt$MainActivityViewModel$( private val appLoungeDataStore: AppLoungeDataStore, private val applicationRepository: ApplicationRepository, private val fusedManagerRepository: FusedManagerRepository, private val appLoungePackageManager: AppLoungePackageManager, private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val appInstallProcessor: AppInstallProcessor, private val sessionPreference: SessionPreference )</ID>
    <ID>LongParameterList:MainActivityViewModel.kt$MainActivityViewModel$( private val appLoungeDataStore: AppLoungeDataStore, private val applicationRepository: ApplicationRepository, private val fusedManagerRepository: FusedManagerRepository, private val appLoungePackageManager: AppLoungePackageManager, private val pwaManager: PWAManager, private val ecloudRepository: EcloudRepository, private val blockedAppRepository: BlockedAppRepository, private val appInstallProcessor: AppInstallProcessor, )</ID>
    <ID>LongParameterList:UpdatesManagerImpl.kt$UpdatesManagerImpl$( @ApplicationContext private val context: Context, private val appLoungePackageManager: AppLoungePackageManager, private val applicationRepository: ApplicationRepository, private val faultyAppRepository: FaultyAppRepository, private val appLoungePreference: AppLoungePreference, private val fdroidRepository: FdroidRepository, private val blockedAppRepository: BlockedAppRepository, )</ID>
    <ID>LongParameterList:UpdatesWorker.kt$UpdatesWorker$( @Assisted private val context: Context, @Assisted private val params: WorkerParameters, private val updatesManagerRepository: UpdatesManagerRepository, private val dataStoreManager: DataStoreManager, private val authenticatorRepository: AuthenticatorRepository, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, )</ID>
    <ID>MagicNumber:AnonymousLoginManager.kt$AnonymousLoginManager$200</ID>
@@ -74,7 +56,6 @@
    <ID>MagicNumber:PackageInstallerService.kt$PackageInstallerService$69</ID>
    <ID>MagicNumber:PkgManagerBR.kt$PkgManagerBR$69</ID>
    <ID>MagicNumber:PlayStoreLoginWrapper.kt$PlayStoreLoginWrapper$200</ID>
    <ID>MagicNumber:RetrofitModule.kt$RetrofitModule$999</ID>
    <ID>MagicNumber:ScreenshotRVAdapter.kt$ScreenshotRVAdapter$10f</ID>
    <ID>MagicNumber:ScreenshotRVAdapter.kt$ScreenshotRVAdapter$50f</ID>
    <ID>MagicNumber:StorageComputer.kt$StorageComputer$1000</ID>
@@ -83,12 +64,10 @@
    <ID>MagicNumber:StorageComputer.kt$StorageComputer$999950</ID>
    <ID>MagicNumber:TOSFragment.kt$TOSFragment$20</ID>
    <ID>MaxLineLength:AppInstallProcessor.kt$AppInstallProcessor$"Enqueuing App install work is failed for ${fusedDownload.packageName} exception: ${e.localizedMessage}"</ID>
    <ID>MaxLineLength:AppInstallProcessor.kt$AppInstallProcessor$fusedDownload.areFilesDownloaded() &amp;&amp; (!fusedManagerRepository.isFusedDownloadInstalled(fusedDownload) || fusedDownload.status == Status.INSTALLING)</ID>
    <ID>MaxLineLength:AppPrivacyInfo.kt$AppPrivacyInfo</ID>
    <ID>MaxLineLength:ApplicationFragment.kt$ApplicationFragment.Companion$"https://gitlab.e.foundation/e/os/apps/-/blob/main/app/src/main/java/foundation/e/apps/data/exodus/repositories/PrivacyScoreRepositoryImpl.kt"</ID>
    <ID>MaxLineLength:CommonUtilsModule.kt$CommonUtilsModule$*</ID>
    <ID>MaxLineLength:DownloadManager.kt$DownloadManager$Timber.e("Download Issue: $downloadId : DownloadManager returns status: $status but the failed because: reason: $reason")</ID>
    <ID>MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$"Download failed for ${fusedDownload.packageName}, " + "reason: " + "${downloadManager.getDownloadFailureReason(downloadId)}"</ID>
    <ID>MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$Timber.d("===&gt; updateDownloadStatus: ${fusedDownload.name}: $downloadId: $numberOfDownloadedItems/${fusedDownload.downloadIdMap.size}")</ID>
    <ID>MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$if</ID>
    <ID>MaxLineLength:DownloadManagerUtils.kt$DownloadManagerUtils$numberOfDownloadedItems == fusedDownload.downloadIdMap.size &amp;&amp; numberOfDownloadedItems == fusedDownload.downloadURLList.size</ID>
@@ -112,13 +91,11 @@
    <ID>PrintStackTrace:CommonUtilsModule.kt$CommonUtilsModule$e</ID>
    <ID>PrintStackTrace:EcloudRepository.kt$EcloudRepository$e</ID>
    <ID>PrintStackTrace:InstallWorkManager.kt$InstallWorkManager$e</ID>
    <ID>PrintStackTrace:AppLoungePackageManager.kt$PkgManagerModule$e</ID>
    <ID>PrintStackTrace:PlayStoreAuthenticator.kt$PlayStoreAuthenticator$e</ID>
    <ID>PrintStackTrace:SystemInfoProvider.kt$SystemInfoProvider$e</ID>
    <ID>ProtectedMemberInFinalClass:ApplicationListFragment.kt$ApplicationListFragment$// protected to avoid SyntheticAccessor protected val args: ApplicationListFragmentArgs by navArgs()</ID>
    <ID>ProtectedMemberInFinalClass:ApplicationListFragment.kt$ApplicationListFragment$// protected to avoid SyntheticAccessor protected val viewModel: ApplicationListViewModel by viewModels()</ID>
    <ID>ProtectedMemberInFinalClass:GoogleSignInFragment.kt$GoogleSignInFragment$// protected to avoid SyntheticAccessor protected val viewModel: LoginViewModel by lazy { ViewModelProvider(requireActivity())[LoginViewModel::class.java] }</ID>
    <ID>ProtectedMemberInFinalClass:MainActivityViewModel.kt$MainActivityViewModel$protected fun ProducerScope&lt;Boolean&gt;.sendInternetStatus(connectivityManager: ConnectivityManager)</ID>
    <ID>ProtectedMemberInFinalClass:SearchFragment.kt$SearchFragment$protected val searchViewModel: SearchViewModel by viewModels()</ID>
    <ID>ReturnCount:ApkSignatureManager.kt$ApkSignatureManager$private fun verifyAPKSignature( apkInputStream: BufferedInputStream, apkSignatureInputStream: InputStream, publicKeyInputStream: InputStream, packageName: String ): Boolean</ID>
    <ID>ReturnCount:AppInstallProcessor.kt$AppInstallProcessor$private suspend fun updateDownloadUrls(fusedDownload: FusedDownload): Boolean</ID>
@@ -140,18 +117,19 @@
    <ID>SpreadOperator:NativeDeviceInfoProviderModule.kt$NativeDeviceInfoProviderModule$(*systemSharedLibraryNames)</ID>
    <ID>SwallowedException:AppInfoFetchViewModel.kt$AppInfoFetchViewModel$e: Exception</ID>
    <ID>SwallowedException:AppInstallProcessor.kt$AppInstallProcessor$e: ApiException.AppNotPurchased</ID>
    <ID>SwallowedException:AppLoungePackageManager.kt$AppLoungePackageManager$e: PackageManager.NameNotFoundException</ID>
    <ID>SwallowedException:ApplicationViewModel.kt$ApplicationViewModel$e: ApiException.AppNotFound</ID>
    <ID>SwallowedException:ApplicationViewModel.kt$ApplicationViewModel$e: Exception</ID>
    <ID>SwallowedException:GPlayHttpClient.kt$GPlayHttpClient$e: Exception</ID>
    <ID>SwallowedException:NativeDeviceInfoProviderModule.kt$NativeDeviceInfoProviderModule$e: Exception</ID>
    <ID>SwallowedException:NativeGsfVersionProvider.kt$NativeGsfVersionProvider$e: PackageManager.NameNotFoundException</ID>
    <ID>SwallowedException:AppLoungePackageManager.kt$AppLoungePackageManager$e: PackageManager.NameNotFoundException</ID>
    <ID>SwallowedException:UpdatesManagerImpl.kt$UpdatesManagerImpl$e: Exception</ID>
    <ID>TooGenericExceptionCaught:AnonymousLoginManager.kt$AnonymousLoginManager$e: Exception</ID>
    <ID>TooGenericExceptionCaught:ApiCaller.kt$e: Exception</ID>
    <ID>TooGenericExceptionCaught:ApkSignatureManager.kt$ApkSignatureManager$e: Exception</ID>
    <ID>TooGenericExceptionCaught:AppInfoFetchViewModel.kt$AppInfoFetchViewModel$e: Exception</ID>
    <ID>TooGenericExceptionCaught:AppInstallProcessor.kt$AppInstallProcessor$e: Exception</ID>
    <ID>TooGenericExceptionCaught:AppLoungePackageManager.kt$AppLoungePackageManager$e: Exception</ID>
    <ID>TooGenericExceptionCaught:ApplicationViewModel.kt$ApplicationViewModel$e: Exception</ID>
    <ID>TooGenericExceptionCaught:BlockedAppRepository.kt$BlockedAppRepository$exception: Exception</ID>
    <ID>TooGenericExceptionCaught:CommonUtilsModule.kt$CommonUtilsModule$e: Exception</ID>
@@ -170,15 +148,14 @@
    <ID>TooGenericExceptionCaught:NetworkHandler.kt$e: Exception</ID>
    <ID>TooGenericExceptionCaught:PWAManager.kt$PWAManager$e: Exception</ID>
    <ID>TooGenericExceptionCaught:PWAPlayerStatusReceiver.kt$PWAPlayerStatusReceiver$e: Exception</ID>
    <ID>TooGenericExceptionCaught:AppLoungePackageManager.kt$AppLoungePackageManager$e: Exception</ID>
    <ID>TooGenericExceptionCaught:PlayStoreAuthenticator.kt$PlayStoreAuthenticator$e: Exception</ID>
    <ID>TooGenericExceptionCaught:RetrofitModule.kt$RetrofitModule$e: Exception</ID>
    <ID>TooGenericExceptionCaught:SystemInfoProvider.kt$SystemInfoProvider$e: Exception</ID>
    <ID>TooGenericExceptionCaught:UpdatesManagerImpl.kt$UpdatesManagerImpl$e: Exception</ID>
    <ID>TooGenericExceptionCaught:UpdatesWorker.kt$UpdatesWorker$e: Throwable</ID>
    <ID>TooGenericExceptionThrown:AnonymousLoginManager.kt$AnonymousLoginManager$throw Exception( "Error fetching Anonymous credentials\n" + "Network code: ${response.code}\n" + "Success: ${response.isSuccessful}" + response.errorString.run { if (isNotBlank()) "\nError message: $this" else "" } )</ID>
    <ID>TooGenericExceptionThrown:PlayStoreLoginWrapper.kt$PlayStoreLoginWrapper$throw Exception("Validation network code: ${response.code}")</ID>
    <ID>TooGenericExceptionThrown:PlayStoreLoginWrapper.kt$PlayStoreLoginWrapper$throw Exception(error)</ID>
    <ID>TooManyFunctions:AppLoungePackageManager.kt$AppLoungePackageManager</ID>
    <ID>TooManyFunctions:ApplicationListFragment.kt$ApplicationListFragment : TimeoutFragmentApplicationInstaller</ID>
    <ID>TooManyFunctions:ApplicationRepository.kt$ApplicationRepository</ID>
    <ID>TooManyFunctions:FusedManagerImpl.kt$FusedManagerImpl : IFusedManager</ID>
@@ -186,7 +163,6 @@
    <ID>TooManyFunctions:HomeFragment.kt$HomeFragment : TimeoutFragmentApplicationInstaller</ID>
    <ID>TooManyFunctions:IFusedManager.kt$IFusedManager</ID>
    <ID>TooManyFunctions:MainActivityViewModel.kt$MainActivityViewModel : ViewModel</ID>
    <ID>TooManyFunctions:AppLoungePackageManager.kt$AppLoungePackageManager</ID>
    <ID>TooManyFunctions:SearchFragment.kt$SearchFragment : TimeoutFragmentOnQueryTextListenerOnSuggestionListenerApplicationInstaller</ID>
    <ID>TooManyFunctions:TimeoutFragment.kt$TimeoutFragment : Fragment</ID>
    <ID>TooManyFunctions:UpdatesFragment.kt$UpdatesFragment : TimeoutFragmentApplicationInstaller</ID>
+30 −25
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ class MainActivity : AppCompatActivity() {

    companion object {
        private val TAG = MainActivity::class.java.simpleName
        private const val SESSION_DIALOG_TAG = "session_dialog"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
@@ -123,17 +124,6 @@ class MainActivity : AppCompatActivity() {
        observeEvents()
    }

    override fun onStart() {
        super.onStart()
        checkSessionRefresh()
    }

    private fun checkSessionRefresh() {
        if (viewModel.shouldRefreshSession()) {
            refreshSession()
        }
    }

    private fun refreshSession() {
        loginViewModel.startLoginFlow(listOf(PlayStoreAuthenticator::class.java.simpleName))
    }
@@ -145,6 +135,7 @@ class MainActivity : AppCompatActivity() {
    override fun onBackPressed() {
        if (isInitialScreen()) {
            resetIgnoreStatusForSessionRefresh()
            finish()
        }
        super.onBackPressed()
    }
@@ -159,7 +150,7 @@ class MainActivity : AppCompatActivity() {
    }

    private fun resetIgnoreStatusForSessionRefresh() {
        viewModel.updateIgnoreRefreshPreference(ignore = false)
        viewModel.shouldIgnoreSessionError = false
    }

    @Suppress("DEPRECATION")
@@ -167,7 +158,7 @@ class MainActivity : AppCompatActivity() {
        if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
            onBackInvokedDispatcher.registerOnBackInvokedCallback(PRIORITY_DEFAULT) {
                resetIgnoreStatusForSessionRefresh()
                super.onBackPressed() // Deprecated for Android 13+
                finish()
            }
        }
    }
@@ -406,23 +397,37 @@ class MainActivity : AppCompatActivity() {
        EventBus.events.filter { appEvent ->
            appEvent is AppEvent.TooManyRequests
        }.collectLatest {
            val shouldShowDialog = viewModel.shouldRefreshSession()
            if (shouldShowDialog) {
                binding.sessionErrorLayout.visibility = View.VISIBLE
                binding.retrySessionButton.setOnClickListener { onRefreshSessionClick() }
                binding.ignoreSessionButton.setOnClickListener { onIgnoreSessionClick() }
            }
            handleRefreshSessionEvent()
        }
    }

    private fun onIgnoreSessionClick() {
        viewModel.updateIgnoreRefreshPreference(true)
        binding.sessionErrorLayout.visibility = View.GONE
    private fun handleRefreshSessionEvent() {
        val shouldShowDialog = !viewModel.shouldIgnoreSessionError
        val isDialogShowing = supportFragmentManager.findFragmentByTag(SESSION_DIALOG_TAG) != null
        if (shouldShowDialog && !isDialogShowing) {
            showRefreshSessionDialog()
        }
    }

    private fun onRefreshSessionClick() {
        binding.sessionErrorLayout.visibility = View.GONE
    private fun showRefreshSessionDialog() {
        ApplicationDialogFragment(
            title = getString(R.string.account_unavailable),
            message = getString(R.string.too_many_requests_desc),
            drawable = R.drawable.ic_warning,
            positiveButtonText = getString(R.string.refresh_session),
            positiveButtonAction = {
                refreshSession()
            },
            cancelButtonText = getString(R.string.ignore).uppercase(),
            cancelButtonAction = {
                onIgnoreSessionClick()
            },
            cancelable = true,
        ).show(supportFragmentManager, SESSION_DIALOG_TAG)
    }

    private fun onIgnoreSessionClick() {
        viewModel.shouldIgnoreSessionError = true
    }

    private fun setupBottomNavItemSelectedListener(
+0 −2
Original line number Diff line number Diff line
@@ -30,6 +30,4 @@ object Constants {

    const val ACTION_DUMP_APP_INSTALL_STATE = "foundation.e.apps.action.APP_INSTALL_STATE"
    const val TAG_APP_INSTALL_STATE = "APP_INSTALL_STATE"

    const val PREFERENCE_IGNORE_SESSION_REFRESH = "ignoreSessionRefresh"
}
+0 −41
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021-2024 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.preference

import android.content.Context
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.data.Constants.PREFERENCE_IGNORE_SESSION_REFRESH
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SessionPreference @Inject constructor(
    @ApplicationContext private val context: Context
) {
    private val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context)

    fun shouldIgnoreSessionRefresh(): Boolean {
        return preferenceManager.getBoolean(PREFERENCE_IGNORE_SESSION_REFRESH, false)
    }

    fun updateIgnoreSessionRefreshPreference(ignore: Boolean) {
        preferenceManager.edit().putBoolean(PREFERENCE_IGNORE_SESSION_REFRESH, ignore).apply()
    }
}
+2 −10
Original line number Diff line number Diff line
@@ -42,7 +42,6 @@ import foundation.e.apps.data.enums.isUnFiltered
import foundation.e.apps.data.fusedDownload.FusedManagerRepository
import foundation.e.apps.data.fusedDownload.models.FusedDownload
import foundation.e.apps.data.preference.AppLoungeDataStore
import foundation.e.apps.data.preference.SessionPreference
import foundation.e.apps.data.preference.getSync
import foundation.e.apps.install.pkg.AppLoungePackageManager
import foundation.e.apps.install.pkg.PWAManager
@@ -61,7 +60,6 @@ class MainActivityViewModel @Inject constructor(
    private val ecloudRepository: EcloudRepository,
    private val blockedAppRepository: BlockedAppRepository,
    private val appInstallProcessor: AppInstallProcessor,
    private val sessionPreference: SessionPreference
) : ViewModel() {

    val tocStatus: LiveData<Boolean> = appLoungeDataStore.tocStatus.asLiveData()
@@ -84,6 +82,8 @@ class MainActivityViewModel @Inject constructor(

    lateinit var connectivityManager: ConnectivityManager

    var shouldIgnoreSessionError = false

    fun getUser(): User {
        return appLoungeDataStore.getUserType()
    }
@@ -240,12 +240,4 @@ class MainActivityViewModel @Inject constructor(
    fun launchPwa(application: Application) {
        pwaManager.launchPwa(application)
    }

    fun updateIgnoreRefreshPreference(ignore: Boolean) {
        sessionPreference.updateIgnoreSessionRefreshPreference(ignore)
    }

    fun shouldRefreshSession(): Boolean {
        return !sessionPreference.shouldIgnoreSessionRefresh()
    }
}
Loading