diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 3366ac70d298fa13f2a1f2cc7dc6618e5d0d0d3a..b30a1f2e3a2ac3670f2a89127fae66989710f4d0 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -36,51 +36,13 @@ ChainWrapping:DownloadManagerUtils.kt$DownloadManagerUtils$|| ChainWrapping:HomeViewModel.kt$HomeViewModel$|| ChainWrapping:NetworkHandler.kt$&& - ChainWrapping:PlayStoreRepository.kt$PlayStoreRepository$&& ChainWrapping:ValidateAppAgeLimitUseCase.kt$ValidateAppAgeLimitUseCase$&& - CommentSpacing:ParentalControlAuthenticator.kt$ParentalControlAuthenticator$//Parental Control Key - CommentSpacing:ParentalControlAuthenticator.kt$ParentalControlAuthenticator$//Parental Control intent EmptyCatchBlock:NativeDeviceInfoProviderModule.kt$NativeDeviceInfoProviderModule${ } EmptyFunctionBlock:CleanApkAuthenticator.kt$CleanApkAuthenticator${} FinalNewline:ContentRatingValidity.kt$foundation.e.apps.domain.model.ContentRatingValidity.kt - FinalNewline:SystemAppsUpdatesRepository.kt$foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository.kt FunctionReturnTypeSpacing:AppPrivacyInfoRepositoryImpl.kt$AppPrivacyInfoRepositoryImpl$private suspend fun fetchReports(packageName: String) : List<Report> FunctionReturnTypeSpacing:CategoryApiImpl.kt$CategoryApiImpl$override suspend fun getCategoriesList(type: CategoryType) : List<CategoriesResponse> FunctionStartOfBodySpacing:RetrofitApiModule.kt$RetrofitApiModule$@Singleton @Provides fun provideCleanApkApi( okHttpClient: OkHttpClient, @Named("gsonCustomAdapter") gson: Gson ): CleanApkRetrofit - ImportOrdering:AppDatabase.kt$import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import foundation.e.apps.data.database.install.AppInstallConverter import foundation.e.apps.data.exodus.Tracker import foundation.e.apps.data.exodus.TrackerDao import foundation.e.apps.data.faultyApps.FaultyApp import foundation.e.apps.data.faultyApps.FaultyAppDao import foundation.e.apps.data.fdroid.FdroidDao import foundation.e.apps.data.fdroid.models.FdroidEntity import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.data.parentalcontrol.ContentRatingDao import foundation.e.apps.data.parentalcontrol.FDroidNsfwApp import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup - ImportOrdering:AppInfoFetchViewModel.kt$import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.faultyApps.FaultyAppRepository import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.playstore.PlayStoreRepository import javax.inject.Inject - ImportOrdering:AppInstallProcessor.kt$import android.content.Context import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.Type import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.User import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.domain.ValidateAppAgeLimitUseCase import foundation.e.apps.domain.model.ContentRatingValidity import foundation.e.apps.install.AppInstallComponents import foundation.e.apps.install.download.DownloadManagerUtils import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.updates.UpdatesNotifier import foundation.e.apps.utils.ParentalControlAuthenticator import foundation.e.apps.utils.StorageComputer import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.getFormattedString import foundation.e.apps.utils.isNetworkAvailable import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.flow.transformWhile import timber.log.Timber import java.text.NumberFormat import java.util.Date import javax.inject.Inject - ImportOrdering:AppLoungePackageManager.kt$import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageInstaller.Session import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import androidx.core.content.pm.PackageInfoCompat import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting import foundation.e.apps.data.application.search.SearchRepository import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall import java.io.File import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.DelicateCoroutinesApi import timber.log.Timber - ImportOrdering:AppManagerImpl.kt$import android.app.DownloadManager import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.net.Uri import android.os.Build import android.os.Environment import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.Type import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.ContentRatingDao import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.install.download.data.DownloadProgressLD import foundation.e.apps.install.pkg.PwaManager import foundation.e.apps.install.pkg.AppLoungePackageManager import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber import java.io.File import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton import com.aurora.gplayapi.data.models.PlayFile as AuroraFile - ImportOrdering:AppManagerWrapper.kt$import android.content.Context import android.os.Build import androidx.annotation.RequiresApi import androidx.lifecycle.LiveData import foundation.e.apps.OpenForTesting import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.workmanager.InstallWorkManager import javax.inject.Inject import javax.inject.Singleton - ImportOrdering:AppPrivacyInfoRepositoryImpl.kt$import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import foundation.e.apps.data.exodus.Report import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.exodus.ApiResponse import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.lang.reflect.Modifier import javax.inject.Inject import javax.inject.Singleton import foundation.e.apps.data.Result import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext - ImportOrdering:ApplicationListFragment.kt$import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentApplicationListBinding import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PwaManager import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.parentFragment.TimeoutFragment import kotlinx.coroutines.launch import java.util.Locale import javax.inject.Inject - ImportOrdering:ApplicationListRVAdapter.kt$import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.view.children import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.navigation.findNavController import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import coil.load import com.facebook.shimmer.Shimmer import com.facebook.shimmer.Shimmer.Direction.LEFT_TO_RIGHT import com.facebook.shimmer.ShimmerDrawable import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import foundation.e.apps.R import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.User import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.databinding.ApplicationListItemBinding import foundation.e.apps.install.pkg.InstallerService import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.search.SearchFragmentDirections import foundation.e.apps.ui.updates.UpdatesFragmentDirections import foundation.e.apps.utils.disableInstallButton import foundation.e.apps.utils.enableInstallButton import timber.log.Timber import javax.inject.Singleton - ImportOrdering:ApplicationListViewModel.kt$import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject - ImportOrdering:CategoriesViewModel.kt$import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Category import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.enums.Source import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException import kotlinx.coroutines.launch import javax.inject.Inject - ImportOrdering:DownloadInfoApiImpl.kt$import foundation.e.apps.data.AppSourcesContainer import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.enums.Source import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.handleNetworkResult import javax.inject.Inject - ImportOrdering:DownloadManagerUtils.kt$import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R import foundation.e.apps.data.DownloadManager import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import android.app.DownloadManager as PlatformDownloadManager import timber.log.Timber import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton - ImportOrdering:FDroidRepository.kt$import android.content.Context import foundation.e.apps.data.cleanapk.ApkSignatureManager import foundation.e.apps.data.fdroid.models.BuildInfo import foundation.e.apps.data.fdroid.models.FdroidEntity import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import javax.inject.Inject import javax.inject.Singleton - ImportOrdering:GoogleSignInFragment.kt$import android.annotation.SuppressLint import android.os.Build import android.os.Bundle import android.view.View import android.webkit.CookieManager import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.playstore.utils.AC2DMUtil import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.databinding.FragmentGoogleSigninBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate - ImportOrdering:GplayApiExtensions.kt$import android.content.Context import android.text.format.Formatter import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork import com.aurora.gplayapi.data.models.Category import foundation.e.apps.data.application.data.Category as AppLoungeCategory import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings - ImportOrdering:HomeChildRVAdapter.kt$import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.lifecycle.LifecycleOwner import androidx.navigation.findNavController import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import coil.load import com.facebook.shimmer.Shimmer import com.facebook.shimmer.ShimmerDrawable import com.google.android.material.button.MaterialButton import com.google.android.material.snackbar.Snackbar import foundation.e.apps.R import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.User import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.databinding.HomeChildListItemBinding import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.applicationlist.ApplicationDiffUtil import foundation.e.apps.ui.home.HomeFragmentDirections import foundation.e.apps.utils.disableInstallButton import foundation.e.apps.utils.enableInstallButton - ImportOrdering:IAppPrivacyInfoRepository.kt$import foundation.e.apps.data.Result import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.application.data.Application - ImportOrdering:IFdroidRepository.kt$import android.content.Context import foundation.e.apps.data.fdroid.models.FdroidEntity import foundation.e.apps.data.application.data.Application - ImportOrdering:InstallerService.kt$import android.app.Service import android.content.Intent import android.content.pm.PackageInstaller import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.data.faultyApps.FaultyAppRepository import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject - ImportOrdering:ParentalControlRepository.kt$import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton import androidx.core.net.toUri - ImportOrdering:PlayStoreLoginWrapper.kt$import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.User import foundation.e.apps.data.playstore.utils.AC2DMUtil import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import java.util.Locale - ImportOrdering:PrivacyInfoViewModel.kt$import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.Result import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.application.data.Application import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject - ImportOrdering:PwaManager.kt$import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.content.Intent import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.models.AppInstall import kotlinx.coroutines.delay import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.IOException import java.net.URL import javax.inject.Inject import javax.inject.Singleton - ImportOrdering:RepositoryModule.kt$import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.exodus.repositories.AppPrivacyInfoRepositoryImpl import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepositoryImpl import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.fdroid.IFdroidRepository import foundation.e.apps.data.install.AppManagerImpl import foundation.e.apps.data.install.AppManager import javax.inject.Singleton - ImportOrdering:RetrofitApiModule.kt$import com.google.gson.Gson import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.fdroid.FdroidApiInterface import foundation.e.apps.data.gitlab.ReleaseInfoApi import foundation.e.apps.data.gitlab.UpdatableSystemAppsApi import foundation.e.apps.data.gitlab.SystemAppDefinitionApi import foundation.e.apps.data.parentalcontrol.fdroid.FDroidMonitorApi import foundation.e.apps.data.parentalcontrol.googleplay.AgeGroupApi import foundation.e.apps.di.network.NetworkModule.getYamlFactory import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory import javax.inject.Named import javax.inject.Singleton - ImportOrdering:SignInFragment.kt$import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.databinding.FragmentSignInBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate import foundation.e.apps.utils.showGoogleSignInAlertDialog - ImportOrdering:SystemAppsUpdatesRepository.kt$import android.content.Context import android.os.Build import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Status import foundation.e.apps.data.gitlab.UpdatableSystemAppsApi.* import foundation.e.apps.data.gitlab.models.OsReleaseType import foundation.e.apps.data.gitlab.models.SystemAppInfo import foundation.e.apps.data.gitlab.models.SystemAppProject import foundation.e.apps.data.gitlab.models.toApplication import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.utils.SystemInfoProvider import javax.inject.Inject import javax.inject.Singleton import timber.log.Timber - ImportOrdering:UpdatesFragment.kt$import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentUpdatesBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate import foundation.e.apps.install.download.data.DownloadProgress import foundation.e.apps.install.pkg.PwaManager import foundation.e.apps.install.updates.UpdatesWorkManager import foundation.e.apps.install.workmanager.InstallWorkManager.INSTALL_WORK_NAME import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter import foundation.e.apps.ui.parentFragment.TimeoutFragment import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.toast import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import timber.log.Timber import java.util.Locale import javax.inject.Inject - ImportOrdering:UpdatesManagerImpl.kt$import android.content.Context import android.content.pm.ApplicationInfo import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.cleanapk.ApkSignatureManager import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status import foundation.e.apps.data.enums.isUnFiltered import foundation.e.apps.data.faultyApps.FaultyAppRepository import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.install.pkg.AppLoungePackageManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject - ImportOrdering:UpdatesManagerRepository.kt$import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.application.UpdatesDao import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import javax.inject.Inject - ImportOrdering:UpdatesNotifier.kt$import android.Manifest import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import foundation.e.apps.ui.MainActivity import foundation.e.apps.R - ImportOrdering:UpdatesViewModel.kt$import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.StoreRepository import foundation.e.apps.data.Stores import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source import foundation.e.apps.data.login.exceptions.CleanApkException import foundation.e.apps.data.login.exceptions.GPlayException import foundation.e.apps.data.updates.UpdatesManagerRepository import kotlinx.coroutines.launch import javax.inject.Inject - ImportOrdering:UpdatesWorker.kt$import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.NetworkCapabilities import androidx.hilt.work.HiltWorker import androidx.preference.PreferenceManager import androidx.work.CoroutineWorker import androidx.work.WorkInfo.State import androidx.work.WorkManager import androidx.work.WorkerParameters import com.aurora.gplayapi.data.models.AuthData import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.install.workmanager.AppInstallProcessor import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import timber.log.Timber Indentation:AppInstallProcessor.kt$AppInstallProcessor$ Indentation:AuthDataProvider.kt$AuthDataProvider$ Indentation:ContentRatingValidity.kt$ContentRatingValidity$ @@ -89,7 +51,6 @@ Indentation:InstallAppWorker.kt$InstallAppWorker$ Indentation:MainActivity.kt$MainActivity$ Indentation:NetworkHandler.kt$ - Indentation:PlayStoreRepository.kt$PlayStoreRepository$ Indentation:PwaManager.kt$PwaManager$ Indentation:RetrofitApiModule.kt$RetrofitApiModule$ Indentation:Source.kt$Source$ @@ -233,7 +194,6 @@ MultiLineIfElse:SettingsFragment.kt$SettingsFragment$true MultiLineIfElse:ValidateAppAgeLimitUseCase.kt$ValidateAppAgeLimitUseCase$fetchedContentRating NewLineAtEndOfFile:ContentRatingValidity.kt$foundation.e.apps.domain.model.ContentRatingValidity.kt - NewLineAtEndOfFile:SystemAppsUpdatesRepository.kt$foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository.kt NoBlankLineBeforeRbrace:AgeGroupApi.kt$AgeGroupApi$ NoBlankLineBeforeRbrace:AgeRatingProvider.kt$AgeRatingProvider$ NoBlankLineBeforeRbrace:ApplicationListRVAdapter.kt$ApplicationListRVAdapter$ @@ -242,12 +202,10 @@ NoBlankLineBeforeRbrace:FDroidMonitorApi.kt$FDroidMonitorApi$ NoBlankLineBeforeRbrace:NetworkHandler.kt$ NoBlankLineBeforeRbrace:RetrofitApiModule.kt$RetrofitApiModule$ - NoBlankLineBeforeRbrace:SearchViewModel.kt$SearchViewModel$ NoBlankLineBeforeRbrace:SystemAppDefinitionApi.kt$SystemAppDefinitionApi$ NoBlankLineBeforeRbrace:UpdatableSystemAppsApi.kt$UpdatableSystemAppsApi$ NoBlankLineBeforeRbrace:UpdatesManagerRepository.kt$UpdatesManagerRepository$ NoBlankLineBeforeRbrace:ValidateAppAgeLimitUseCase.kt$ValidateAppAgeLimitUseCase$ - NoConsecutiveBlankLines:ApplicationFragment.kt$ NoConsecutiveBlankLines:ApplicationRepository.kt$ApplicationRepository$ NoConsecutiveBlankLines:AuthDataProvider.kt$ NoConsecutiveBlankLines:CategoriesViewModel.kt$CategoriesViewModel$ @@ -291,7 +249,6 @@ NoSemicolons:AgeRatingProvider.kt$AgeRatingProvider.UriCode.AgeRating$; NoSemicolons:AuthDataProvider.kt$AuthDataProvider$; NoTrailingSpaces:DownloadInfoApiImpl.kt$DownloadInfoApiImpl$ - NoUnusedImports:AppInstallProcessor.kt$foundation.e.apps.install.workmanager.AppInstallProcessor.kt NoUnusedImports:AppLoungePackageManager.kt$foundation.e.apps.install.pkg.AppLoungePackageManager.kt NoUnusedImports:AppManagerImpl.kt$foundation.e.apps.data.install.AppManagerImpl.kt NoUnusedImports:AppManagerWrapper.kt$foundation.e.apps.data.install.AppManagerWrapper.kt @@ -345,7 +302,6 @@ SpacingAroundColon:GPlayException.kt$GPlayIOException$: SpacingAroundComma:HomeFragment.kt$HomeFragment$, SpacingAroundComma:StorageComputer.kt$StorageComputer$, - SpacingAroundCurly:AppInstallProcessor.kt$AppInstallProcessor${ SpacingAroundCurly:ApplicationListRVAdapter.kt$ApplicationListRVAdapter${ SpacingAroundCurly:RetrofitApiModule.kt$RetrofitApiModule${ SpacingAroundKeyword:SystemAppsUpdatesRepository.kt$SystemAppsUpdatesRepository$catch @@ -428,7 +384,6 @@ Wrapping:InstallAppWorker.kt$InstallAppWorker$( Wrapping:MainActivity.kt$MainActivity$( Wrapping:MainActivity.kt$MainActivity$(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (isInitialScreen()) { resetIgnoreStatusForSessionRefresh() finish() } else { // Let the system handle the back press isEnabled = false onBackPressedDispatcher.onBackPressed() } } }) - Wrapping:PlayStoreRepository.kt$PlayStoreRepository$( Wrapping:SearchFragment.kt$SearchFragment$Fragment(R.layout.fragment_search), ApplicationInstaller, SearchViewHandler.SearchViewListener Wrapping:SystemAppsUpdatesRepository.kt$SystemAppsUpdatesRepository$( Wrapping:ValidateAppAgeLimitUseCase.kt$ValidateAppAgeLimitUseCase$( diff --git a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt index 1c36aac865d47ffbced95167d958515ef4ffeae1..0bc6ae9c7f8b4765d056c109f92a3edaf559e53a 100644 --- a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt @@ -21,8 +21,8 @@ package foundation.e.apps.data.application.downloadInfo import foundation.e.apps.data.AppSourcesContainer import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.install.models.AppInstall import javax.inject.Inject class DownloadInfoApiImpl @Inject constructor( diff --git a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt index 3263a4ffe99afec90c0b8962aa43f7191222fdb1..a1052a0176008e4da31cf0790354ea38ad7eb1c1 100644 --- a/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt +++ b/app/src/main/java/foundation/e/apps/data/application/utils/GplayApiExtensions.kt @@ -23,9 +23,9 @@ import android.text.format.Formatter import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork import com.aurora.gplayapi.data.models.Category -import foundation.e.apps.data.application.data.Category as AppLoungeCategory import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Ratings +import foundation.e.apps.data.application.data.Category as AppLoungeCategory fun App.toApplication(context: Context): Application { val app = Application( diff --git a/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt b/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt index 76e2cba0c5d18bbbad150bdb382c7cf24944a619..6990dbbc0d191efbdfbf0a21bf48060b4f3f746e 100644 --- a/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt +++ b/app/src/main/java/foundation/e/apps/data/database/AppDatabase.kt @@ -14,8 +14,8 @@ import foundation.e.apps.data.faultyApps.FaultyApp import foundation.e.apps.data.faultyApps.FaultyAppDao import foundation.e.apps.data.fdroid.FdroidDao import foundation.e.apps.data.fdroid.models.FdroidEntity -import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.ContentRatingEntity import foundation.e.apps.data.parentalcontrol.FDroidNsfwApp import foundation.e.apps.data.parentalcontrol.googleplay.GPlayContentRatingGroup diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt index 593b1861abf569850d79de433502d415508b9aa4..6fdb98d223bcb6b09e3a3c55b6a03035a1e3ed31 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/AppPrivacyInfoRepositoryImpl.kt @@ -21,10 +21,13 @@ package foundation.e.apps.data.exodus.repositories import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken -import foundation.e.apps.data.exodus.Report -import foundation.e.apps.data.exodus.models.AppPrivacyInfo +import foundation.e.apps.data.Result import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.exodus.ApiResponse +import foundation.e.apps.data.exodus.Report +import foundation.e.apps.data.exodus.models.AppPrivacyInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -32,9 +35,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import java.lang.reflect.Modifier import javax.inject.Inject import javax.inject.Singleton -import foundation.e.apps.data.Result -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Singleton class AppPrivacyInfoRepositoryImpl @Inject constructor( diff --git a/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt b/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt index 564512d9d5589ed1ea95a9c131665dc66389241d..8eebdf4aca09009a5dffca6fc549b1cd2b882ff1 100644 --- a/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/exodus/repositories/IAppPrivacyInfoRepository.kt @@ -1,8 +1,8 @@ package foundation.e.apps.data.exodus.repositories import foundation.e.apps.data.Result -import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.exodus.models.AppPrivacyInfo interface IAppPrivacyInfoRepository { suspend fun getAppPrivacyInfo(application: Application, appHandle: String): Result diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt b/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt index dbb9397affaa29657140e7d36aa42f8c88f2c9b8..29e1bbca92054650bb4b166398a65a14c86f5f11 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt @@ -1,11 +1,11 @@ package foundation.e.apps.data.fdroid import android.content.Context +import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.cleanapk.ApkSignatureManager +import foundation.e.apps.data.enums.Source import foundation.e.apps.data.fdroid.models.BuildInfo import foundation.e.apps.data.fdroid.models.FdroidEntity -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.enums.Source import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/IFdroidRepository.kt b/app/src/main/java/foundation/e/apps/data/fdroid/IFdroidRepository.kt index 84f411e4477296da22c3a4595ab7af893b48d078..923f17a37dff90e4fc91a4d30468441a7744f3b4 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/IFdroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/IFdroidRepository.kt @@ -19,8 +19,8 @@ package foundation.e.apps.data.fdroid import android.content.Context -import foundation.e.apps.data.fdroid.models.FdroidEntity import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.fdroid.models.FdroidEntity interface IFdroidRepository { /** diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt index e6f9e290ac50d619492af63f26d530ed27715ab3..556608c9f388634b4e2a82571df6803a6f7237a3 100644 --- a/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt @@ -32,9 +32,9 @@ import foundation.e.apps.data.gitlab.models.toApplication import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.install.pkg.AppLoungePackageManager import foundation.e.apps.utils.SystemInfoProvider +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -import timber.log.Timber @Singleton class SystemAppsUpdatesRepository @Inject constructor( @@ -264,4 +264,4 @@ class SystemAppsUpdatesRepository @Inject constructor( } } -private class UnsupportedAndroidApiException(message: String) : RuntimeException(message) \ No newline at end of file +private class UnsupportedAndroidApiException(message: String) : RuntimeException(message) diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt index 3f05ff0bb6fc3a25a70a2399b6d8384fe0f1ab90..17651d4950c51bbeb181ee7b3c6de4b829a181b1 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerImpl.kt @@ -1,310 +1,310 @@ -/* - * Copyright ECORP SAS 2022 - * Apps Quickly and easily install Android apps onto your device! - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.data.install - -import android.app.DownloadManager -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import androidx.annotation.RequiresApi -import androidx.lifecycle.LiveData -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.R -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.data.parentalcontrol.ContentRatingDao -import foundation.e.apps.data.parentalcontrol.ContentRatingEntity -import foundation.e.apps.install.download.data.DownloadProgressLD -import foundation.e.apps.install.pkg.PwaManager -import foundation.e.apps.install.pkg.AppLoungePackageManager -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import timber.log.Timber -import java.io.File -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton -import com.aurora.gplayapi.data.models.PlayFile as AuroraFile - -@Singleton -class AppManagerImpl @Inject constructor( - @Named("cacheDir") private val cacheDir: String, - private val downloadManager: DownloadManager, - private val notificationManager: NotificationManager, - private val appInstallRepository: AppInstallRepository, - 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 -) : AppManager { - - @Inject - lateinit var contentRatingDao: ContentRatingDao - - private val mutex = Mutex() - - override fun createNotificationChannels() { - notificationManager.apply { - createNotificationChannel(downloadNotificationChannel) - createNotificationChannel(updateNotificationChannel) - } - } - - override suspend fun addDownload(appInstall: AppInstall) { - appInstall.status = Status.QUEUED - appInstallRepository.addDownload(appInstall) - } - - override suspend fun getDownloadById(appInstall: AppInstall): AppInstall? { - return appInstallRepository.getDownloadById(appInstall.id) - } - - override suspend fun getDownloadList(): List { - return appInstallRepository.getDownloadList() - } - - override fun getDownloadLiveList(): LiveData> { - return appInstallRepository.getDownloadLiveList() - } - - @OptIn(DelicateCoroutinesApi::class) - override suspend fun updateDownloadStatus(appInstall: AppInstall, status: Status) { - if (status == Status.INSTALLED) { - appInstall.status = status - insertContentRating(appInstall) - flushOldDownload(appInstall.packageName) - appInstallRepository.deleteDownload(appInstall) - } else if (status == Status.INSTALLING) { - appInstall.downloadIdMap.all { true } - appInstall.status = status - val isSelfUpdate = appInstall.packageName == context.packageName - if (isSelfUpdate) { - appInstallRepository.deleteDownload(appInstall) - } else { - appInstall.status = status - appInstallRepository.updateDownload(appInstall) - } - installApp(appInstall) - } - } - - private suspend fun insertContentRating(appInstall: AppInstall) { - contentRatingDao.insertContentRating( - ContentRatingEntity( - appInstall.packageName, - appInstall.contentRating.id, - appInstall.contentRating.title - ) - ) - Timber.d("inserted age rating: ${appInstall.contentRating.title}") - } - - override suspend fun downloadApp(appInstall: AppInstall) { - mutex.withLock { - when (appInstall.type) { - Type.NATIVE -> downloadNativeApp(appInstall) - Type.PWA -> pwaManager.installPWAApp(appInstall) - } - } - } - - override suspend fun installApp(appInstall: AppInstall) { - val list = mutableListOf() - when (appInstall.type) { - Type.NATIVE -> { - val parentPathFile = File("$cacheDir/${appInstall.packageName}") - parentPathFile.listFiles()?.let { list.addAll(it) } - list.sort() - - if (list.size != 0) { - try { - Timber.i("installApp: STARTED ${appInstall.name} ${list.size}") - appLoungePackageManager.installApplication(list, appInstall.packageName) - Timber.i("installApp: ENDED ${appInstall.name} ${list.size}") - } catch (e: Exception) { - Timber.i(">>> installApp app failed ") - installationIssue(appInstall) - throw e - } - } - } - else -> { - Timber.d("Unsupported application type!") - appInstall.status = Status.INSTALLATION_ISSUE - appInstallRepository.updateDownload(appInstall) - } - } - } - - @OptIn(DelicateCoroutinesApi::class) - override suspend fun cancelDownload(appInstall: AppInstall, packageName: String) { - mutex.withLock { - if (appInstall.id.isNotBlank()) { - removeFusedDownload(appInstall) - } else { - Timber.d("Unable to cancel download!") - } - contentRatingDao.deleteContentRating(packageName) - } - } - - private suspend fun removeFusedDownload(appInstall: AppInstall) { - appInstall.downloadIdMap.forEach { (key, _) -> - downloadManager.remove(key) - } - DownloadProgressLD.setDownloadId(-1) - - if (appInstall.status != Status.INSTALLATION_ISSUE) { - appInstallRepository.deleteDownload(appInstall) - } - - flushOldDownload(appInstall.packageName) - } - - override suspend fun getFusedDownload(downloadId: Long, packageName: String): AppInstall { - val downloadList = getDownloadList() - var appInstall = AppInstall() - downloadList.forEach { - if (downloadId != 0L) { - if (it.downloadIdMap.contains(downloadId)) { - appInstall = it - } - } else if (packageName.isNotBlank()) { - if (it.packageName == packageName) { - appInstall = it - } - } - } - return appInstall - } - - override fun flushOldDownload(packageName: String) { - val parentPathFile = File("$cacheDir/$packageName") - if (parentPathFile.exists()) parentPathFile.deleteRecursively() - } - - override suspend fun downloadNativeApp(appInstall: AppInstall) { - var count = 0 - var parentPath = "$cacheDir/${appInstall.packageName}" - - // Clean old downloads and re-create download dir - flushOldDownload(appInstall.packageName) - File(parentPath).mkdirs() - - appInstall.status = Status.DOWNLOADING - appInstallRepository.updateDownload(appInstall) - DownloadProgressLD.setDownloadId(-1) - appInstall.downloadURLList.forEach { - count += 1 - val packagePath: File = if (appInstall.files.isNotEmpty()) { - getGplayInstallationPackagePath(appInstall, it, parentPath, count) - } else { - File(parentPath, "${appInstall.packageName}_$count.apk") - } - val request = DownloadManager.Request(Uri.parse(it)) - .setTitle(if (count == 1) appInstall.name else context.getString(R.string.additional_file_for, appInstall.name)) - .setDestinationUri(Uri.fromFile(packagePath)) - val requestId = downloadManager.enqueue(request) - DownloadProgressLD.setDownloadId(requestId) - appInstall.downloadIdMap[requestId] = false - } - appInstallRepository.updateDownload(appInstall) - } - - override fun getGplayInstallationPackagePath( - appInstall: AppInstall, - it: String, - parentPath: String, - count: Int - ): File { - val downloadingFile = appInstall.files[appInstall.downloadURLList.indexOf(it)] - return if (downloadingFile.type == AuroraFile.Type.BASE || downloadingFile.type == AuroraFile.Type.SPLIT) { - File(parentPath, "${appInstall.packageName}_$count.apk") - } else { - createObbFileForDownload(appInstall, it) - } - } - - override fun createObbFileForDownload( - appInstall: AppInstall, - url: String - ): File { - val parentPath = - context.getExternalFilesDir(null)?.absolutePath + "/Android/obb/" + appInstall.packageName - File(parentPath).mkdirs() - val obbFile = appInstall.files[appInstall.downloadURLList.indexOf(url)] - return File(parentPath, obbFile.name) - } - - override fun moveOBBFilesToOBBDirectory(appInstall: AppInstall) { - appInstall.files.forEach { - val parentPath = - context.getExternalFilesDir(null)?.absolutePath + "/Android/obb/" + appInstall.packageName - val file = File(parentPath, it.name) - if (file.exists()) { - val destinationDirectory = Environment.getExternalStorageDirectory() - .toString() + "/Android/obb/" + appInstall.packageName - File(destinationDirectory).mkdirs() - FileManager.moveFile("$parentPath/", it.name, "$destinationDirectory/") - } - } - } - - override fun getBaseApkPath(appInstall: AppInstall) = - "$cacheDir/${appInstall.packageName}/${appInstall.packageName}_1.apk" - - override suspend fun installationIssue(appInstall: AppInstall) { - appInstall.status = Status.INSTALLATION_ISSUE - appInstallRepository.updateDownload(appInstall) - flushOldDownload(appInstall.packageName) - } - - override suspend fun updateAwaiting(appInstall: AppInstall) { - appInstall.status = Status.AWAITING - appInstallRepository.updateDownload(appInstall) - } - - override suspend fun updateUnavailable(appInstall: AppInstall) { - appInstall.status = Status.UNAVAILABLE - appInstallRepository.updateDownload(appInstall) - } - - override suspend fun updateAppInstall(appInstall: AppInstall) { - appInstallRepository.updateDownload(appInstall) - } - - override suspend fun insertAppInstallPurchaseNeeded(appInstall: AppInstall) { - appInstall.status = Status.PURCHASE_NEEDED - appInstallRepository.addDownload(appInstall) - } - - override fun isAppInstalled(appInstall: AppInstall): Boolean { - return appLoungePackageManager.isInstalled(appInstall.packageName) - } - - override fun getInstallationStatus(appInstall: AppInstall): Status { - return appLoungePackageManager.getPackageStatus(appInstall.packageName, appInstall.versionCode) - } -} +/* + * Copyright ECORP SAS 2022 + * Apps Quickly and easily install Android apps onto your device! + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.install + +import android.app.DownloadManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import androidx.annotation.RequiresApi +import androidx.lifecycle.LiveData +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.data.parentalcontrol.ContentRatingDao +import foundation.e.apps.data.parentalcontrol.ContentRatingEntity +import foundation.e.apps.install.download.data.DownloadProgressLD +import foundation.e.apps.install.pkg.AppLoungePackageManager +import foundation.e.apps.install.pkg.PwaManager +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import com.aurora.gplayapi.data.models.PlayFile as AuroraFile + +@Singleton +class AppManagerImpl @Inject constructor( + @Named("cacheDir") private val cacheDir: String, + private val downloadManager: DownloadManager, + private val notificationManager: NotificationManager, + private val appInstallRepository: AppInstallRepository, + 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 +) : AppManager { + + @Inject + lateinit var contentRatingDao: ContentRatingDao + + private val mutex = Mutex() + + override fun createNotificationChannels() { + notificationManager.apply { + createNotificationChannel(downloadNotificationChannel) + createNotificationChannel(updateNotificationChannel) + } + } + + override suspend fun addDownload(appInstall: AppInstall) { + appInstall.status = Status.QUEUED + appInstallRepository.addDownload(appInstall) + } + + override suspend fun getDownloadById(appInstall: AppInstall): AppInstall? { + return appInstallRepository.getDownloadById(appInstall.id) + } + + override suspend fun getDownloadList(): List { + return appInstallRepository.getDownloadList() + } + + override fun getDownloadLiveList(): LiveData> { + return appInstallRepository.getDownloadLiveList() + } + + @OptIn(DelicateCoroutinesApi::class) + override suspend fun updateDownloadStatus(appInstall: AppInstall, status: Status) { + if (status == Status.INSTALLED) { + appInstall.status = status + insertContentRating(appInstall) + flushOldDownload(appInstall.packageName) + appInstallRepository.deleteDownload(appInstall) + } else if (status == Status.INSTALLING) { + appInstall.downloadIdMap.all { true } + appInstall.status = status + val isSelfUpdate = appInstall.packageName == context.packageName + if (isSelfUpdate) { + appInstallRepository.deleteDownload(appInstall) + } else { + appInstall.status = status + appInstallRepository.updateDownload(appInstall) + } + installApp(appInstall) + } + } + + private suspend fun insertContentRating(appInstall: AppInstall) { + contentRatingDao.insertContentRating( + ContentRatingEntity( + appInstall.packageName, + appInstall.contentRating.id, + appInstall.contentRating.title + ) + ) + Timber.d("inserted age rating: ${appInstall.contentRating.title}") + } + + override suspend fun downloadApp(appInstall: AppInstall) { + mutex.withLock { + when (appInstall.type) { + Type.NATIVE -> downloadNativeApp(appInstall) + Type.PWA -> pwaManager.installPWAApp(appInstall) + } + } + } + + override suspend fun installApp(appInstall: AppInstall) { + val list = mutableListOf() + when (appInstall.type) { + Type.NATIVE -> { + val parentPathFile = File("$cacheDir/${appInstall.packageName}") + parentPathFile.listFiles()?.let { list.addAll(it) } + list.sort() + + if (list.size != 0) { + try { + Timber.i("installApp: STARTED ${appInstall.name} ${list.size}") + appLoungePackageManager.installApplication(list, appInstall.packageName) + Timber.i("installApp: ENDED ${appInstall.name} ${list.size}") + } catch (e: Exception) { + Timber.i(">>> installApp app failed ") + installationIssue(appInstall) + throw e + } + } + } + else -> { + Timber.d("Unsupported application type!") + appInstall.status = Status.INSTALLATION_ISSUE + appInstallRepository.updateDownload(appInstall) + } + } + } + + @OptIn(DelicateCoroutinesApi::class) + override suspend fun cancelDownload(appInstall: AppInstall, packageName: String) { + mutex.withLock { + if (appInstall.id.isNotBlank()) { + removeFusedDownload(appInstall) + } else { + Timber.d("Unable to cancel download!") + } + contentRatingDao.deleteContentRating(packageName) + } + } + + private suspend fun removeFusedDownload(appInstall: AppInstall) { + appInstall.downloadIdMap.forEach { (key, _) -> + downloadManager.remove(key) + } + DownloadProgressLD.setDownloadId(-1) + + if (appInstall.status != Status.INSTALLATION_ISSUE) { + appInstallRepository.deleteDownload(appInstall) + } + + flushOldDownload(appInstall.packageName) + } + + override suspend fun getFusedDownload(downloadId: Long, packageName: String): AppInstall { + val downloadList = getDownloadList() + var appInstall = AppInstall() + downloadList.forEach { + if (downloadId != 0L) { + if (it.downloadIdMap.contains(downloadId)) { + appInstall = it + } + } else if (packageName.isNotBlank()) { + if (it.packageName == packageName) { + appInstall = it + } + } + } + return appInstall + } + + override fun flushOldDownload(packageName: String) { + val parentPathFile = File("$cacheDir/$packageName") + if (parentPathFile.exists()) parentPathFile.deleteRecursively() + } + + override suspend fun downloadNativeApp(appInstall: AppInstall) { + var count = 0 + var parentPath = "$cacheDir/${appInstall.packageName}" + + // Clean old downloads and re-create download dir + flushOldDownload(appInstall.packageName) + File(parentPath).mkdirs() + + appInstall.status = Status.DOWNLOADING + appInstallRepository.updateDownload(appInstall) + DownloadProgressLD.setDownloadId(-1) + appInstall.downloadURLList.forEach { + count += 1 + val packagePath: File = if (appInstall.files.isNotEmpty()) { + getGplayInstallationPackagePath(appInstall, it, parentPath, count) + } else { + File(parentPath, "${appInstall.packageName}_$count.apk") + } + val request = DownloadManager.Request(Uri.parse(it)) + .setTitle(if (count == 1) appInstall.name else context.getString(R.string.additional_file_for, appInstall.name)) + .setDestinationUri(Uri.fromFile(packagePath)) + val requestId = downloadManager.enqueue(request) + DownloadProgressLD.setDownloadId(requestId) + appInstall.downloadIdMap[requestId] = false + } + appInstallRepository.updateDownload(appInstall) + } + + override fun getGplayInstallationPackagePath( + appInstall: AppInstall, + it: String, + parentPath: String, + count: Int + ): File { + val downloadingFile = appInstall.files[appInstall.downloadURLList.indexOf(it)] + return if (downloadingFile.type == AuroraFile.Type.BASE || downloadingFile.type == AuroraFile.Type.SPLIT) { + File(parentPath, "${appInstall.packageName}_$count.apk") + } else { + createObbFileForDownload(appInstall, it) + } + } + + override fun createObbFileForDownload( + appInstall: AppInstall, + url: String + ): File { + val parentPath = + context.getExternalFilesDir(null)?.absolutePath + "/Android/obb/" + appInstall.packageName + File(parentPath).mkdirs() + val obbFile = appInstall.files[appInstall.downloadURLList.indexOf(url)] + return File(parentPath, obbFile.name) + } + + override fun moveOBBFilesToOBBDirectory(appInstall: AppInstall) { + appInstall.files.forEach { + val parentPath = + context.getExternalFilesDir(null)?.absolutePath + "/Android/obb/" + appInstall.packageName + val file = File(parentPath, it.name) + if (file.exists()) { + val destinationDirectory = Environment.getExternalStorageDirectory() + .toString() + "/Android/obb/" + appInstall.packageName + File(destinationDirectory).mkdirs() + FileManager.moveFile("$parentPath/", it.name, "$destinationDirectory/") + } + } + } + + override fun getBaseApkPath(appInstall: AppInstall) = + "$cacheDir/${appInstall.packageName}/${appInstall.packageName}_1.apk" + + override suspend fun installationIssue(appInstall: AppInstall) { + appInstall.status = Status.INSTALLATION_ISSUE + appInstallRepository.updateDownload(appInstall) + flushOldDownload(appInstall.packageName) + } + + override suspend fun updateAwaiting(appInstall: AppInstall) { + appInstall.status = Status.AWAITING + appInstallRepository.updateDownload(appInstall) + } + + override suspend fun updateUnavailable(appInstall: AppInstall) { + appInstall.status = Status.UNAVAILABLE + appInstallRepository.updateDownload(appInstall) + } + + override suspend fun updateAppInstall(appInstall: AppInstall) { + appInstallRepository.updateDownload(appInstall) + } + + override suspend fun insertAppInstallPurchaseNeeded(appInstall: AppInstall) { + appInstall.status = Status.PURCHASE_NEEDED + appInstallRepository.addDownload(appInstall) + } + + override fun isAppInstalled(appInstall: AppInstall): Boolean { + return appLoungePackageManager.isInstalled(appInstall.packageName) + } + + override fun getInstallationStatus(appInstall: AppInstall): Status { + return appLoungePackageManager.getPackageStatus(appInstall.packageName, appInstall.versionCode) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt index bba3754f8f5349a0a2151a6dfbd797d2f0273fb2..f17ebdec51f20a5ea835923c0060190052a54a3c 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt @@ -1,202 +1,202 @@ -package foundation.e.apps.data.install - -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.lifecycle.LiveData -import foundation.e.apps.OpenForTesting -import foundation.e.apps.data.Constants.MIN_VALID_RATING -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.fdroid.FDroidRepository -import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.install.download.data.DownloadProgress -import foundation.e.apps.install.workmanager.InstallWorkManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -@OpenForTesting -class AppManagerWrapper @Inject constructor( - private val appManager: AppManager, - private val fDroidRepository: FDroidRepository -) { - - fun createNotificationChannels() { - return appManager.createNotificationChannels() - } - - suspend fun downloadApp(appInstall: AppInstall) { - return appManager.downloadApp(appInstall) - } - - fun moveOBBFileToOBBDirectory(appInstall: AppInstall) { - return appManager.moveOBBFilesToOBBDirectory(appInstall) - } - - suspend fun addDownload(appInstall: AppInstall): Boolean { - val existingFusedDownload = appManager.getDownloadById(appInstall) - if (isInstallWorkRunning(existingFusedDownload, appInstall)) { - return false - } - - // We don't want to add any thing, if it already exists without INSTALLATION_ISSUE - if (existingFusedDownload != null && !isStatusEligibleToInstall(existingFusedDownload)) { - return false - } - - appManager.addDownload(appInstall) - return true - } - - private fun isStatusEligibleToInstall(existingAppInstall: AppInstall) = - listOf( - Status.UNAVAILABLE, - Status.INSTALLATION_ISSUE, - Status.PURCHASE_NEEDED - ).contains(existingAppInstall.status) - - private fun isInstallWorkRunning( - existingAppInstall: AppInstall?, - appInstall: AppInstall - ) = - existingAppInstall != null && InstallWorkManager.checkWorkIsAlreadyAvailable( - appInstall.id - ) - - suspend fun addFusedDownloadPurchaseNeeded(appInstall: AppInstall) { - appManager.insertAppInstallPurchaseNeeded(appInstall) - } - - suspend fun getDownloadList(): List { - return appManager.getDownloadList() - } - - fun getDownloadLiveList(): LiveData> { - return appManager.getDownloadLiveList() - } - - suspend fun getFusedDownload(downloadId: Long = 0, packageName: String = ""): AppInstall { - return appManager.getFusedDownload(downloadId, packageName) - } - - suspend fun updateDownloadStatus(appInstall: AppInstall, status: Status) { - return appManager.updateDownloadStatus(appInstall, status) - } - - suspend fun cancelDownload(appInstall: AppInstall, packageName: String = "") { - return appManager.cancelDownload(appInstall, packageName) - } - - suspend fun installationIssue(appInstall: AppInstall) { - return appManager.installationIssue(appInstall) - } - - suspend fun updateAwaiting(appInstall: AppInstall) { - appManager.updateAwaiting(appInstall) - } - - suspend fun updateUnavailable(appInstall: AppInstall) { - appManager.updateUnavailable(appInstall) - } - - suspend fun updateFusedDownload(appInstall: AppInstall) { - appManager.updateAppInstall(appInstall) - } - - fun validateFusedDownload(appInstall: AppInstall) = - appInstall.packageName.isNotEmpty() && appInstall.downloadURLList.isNotEmpty() - - suspend fun calculateProgress( - application: Application?, - progress: DownloadProgress - ): Int { - application?.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(application, 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( - application: Application, - downloadProgress: DownloadProgress - ): Boolean { - val download = getFusedDownload(downloadProgress.downloadId) - return download.id == application._id - } - - fun handleRatingFormat(rating: Double): String? { - return if (rating >= MIN_VALID_RATING) { - if (rating % 1 == 0.0) { - rating.toInt().toString() - } else { - rating.toString() - } - } else { - null - } - } - - suspend fun getCalculateProgressWithTotalSize(application: Application?, progress: DownloadProgress): Pair { - application?.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(application: Application?, downloadList: List): Status? { - application?.let { app -> - val downloadingItem = - downloadList.find { it.packageName == app.package_name || it.id == app.package_name } - return downloadingItem?.status - } - return null - } - - suspend fun isFDroidApplicationSigned(context: Context, appInstall: AppInstall): Boolean { - val apkFilePath = appManager.getBaseApkPath(appInstall) - return fDroidRepository.isFDroidApplicationSigned(context, appInstall.packageName, apkFilePath, appInstall.signature) - } - - fun isFusedDownloadInstalled(appInstall: AppInstall): Boolean { - return appManager.isAppInstalled(appInstall) - } - - fun getFusedDownloadPackageStatus(appInstall: AppInstall): Status { - return appManager.getInstallationStatus(appInstall) - } -} +package foundation.e.apps.data.install + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.lifecycle.LiveData +import foundation.e.apps.OpenForTesting +import foundation.e.apps.data.Constants.MIN_VALID_RATING +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.fdroid.FDroidRepository +import foundation.e.apps.data.install.models.AppInstall +import foundation.e.apps.install.download.data.DownloadProgress +import foundation.e.apps.install.workmanager.InstallWorkManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +@OpenForTesting +class AppManagerWrapper @Inject constructor( + private val appManager: AppManager, + private val fDroidRepository: FDroidRepository +) { + + fun createNotificationChannels() { + return appManager.createNotificationChannels() + } + + suspend fun downloadApp(appInstall: AppInstall) { + return appManager.downloadApp(appInstall) + } + + fun moveOBBFileToOBBDirectory(appInstall: AppInstall) { + return appManager.moveOBBFilesToOBBDirectory(appInstall) + } + + suspend fun addDownload(appInstall: AppInstall): Boolean { + val existingFusedDownload = appManager.getDownloadById(appInstall) + if (isInstallWorkRunning(existingFusedDownload, appInstall)) { + return false + } + + // We don't want to add any thing, if it already exists without INSTALLATION_ISSUE + if (existingFusedDownload != null && !isStatusEligibleToInstall(existingFusedDownload)) { + return false + } + + appManager.addDownload(appInstall) + return true + } + + private fun isStatusEligibleToInstall(existingAppInstall: AppInstall) = + listOf( + Status.UNAVAILABLE, + Status.INSTALLATION_ISSUE, + Status.PURCHASE_NEEDED + ).contains(existingAppInstall.status) + + private fun isInstallWorkRunning( + existingAppInstall: AppInstall?, + appInstall: AppInstall + ) = + existingAppInstall != null && InstallWorkManager.checkWorkIsAlreadyAvailable( + appInstall.id + ) + + suspend fun addFusedDownloadPurchaseNeeded(appInstall: AppInstall) { + appManager.insertAppInstallPurchaseNeeded(appInstall) + } + + suspend fun getDownloadList(): List { + return appManager.getDownloadList() + } + + fun getDownloadLiveList(): LiveData> { + return appManager.getDownloadLiveList() + } + + suspend fun getFusedDownload(downloadId: Long = 0, packageName: String = ""): AppInstall { + return appManager.getFusedDownload(downloadId, packageName) + } + + suspend fun updateDownloadStatus(appInstall: AppInstall, status: Status) { + return appManager.updateDownloadStatus(appInstall, status) + } + + suspend fun cancelDownload(appInstall: AppInstall, packageName: String = "") { + return appManager.cancelDownload(appInstall, packageName) + } + + suspend fun installationIssue(appInstall: AppInstall) { + return appManager.installationIssue(appInstall) + } + + suspend fun updateAwaiting(appInstall: AppInstall) { + appManager.updateAwaiting(appInstall) + } + + suspend fun updateUnavailable(appInstall: AppInstall) { + appManager.updateUnavailable(appInstall) + } + + suspend fun updateFusedDownload(appInstall: AppInstall) { + appManager.updateAppInstall(appInstall) + } + + fun validateFusedDownload(appInstall: AppInstall) = + appInstall.packageName.isNotEmpty() && appInstall.downloadURLList.isNotEmpty() + + suspend fun calculateProgress( + application: Application?, + progress: DownloadProgress + ): Int { + application?.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(application, 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( + application: Application, + downloadProgress: DownloadProgress + ): Boolean { + val download = getFusedDownload(downloadProgress.downloadId) + return download.id == application._id + } + + fun handleRatingFormat(rating: Double): String? { + return if (rating >= MIN_VALID_RATING) { + if (rating % 1 == 0.0) { + rating.toInt().toString() + } else { + rating.toString() + } + } else { + null + } + } + + suspend fun getCalculateProgressWithTotalSize(application: Application?, progress: DownloadProgress): Pair { + application?.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(application: Application?, downloadList: List): Status? { + application?.let { app -> + val downloadingItem = + downloadList.find { it.packageName == app.package_name || it.id == app.package_name } + return downloadingItem?.status + } + return null + } + + suspend fun isFDroidApplicationSigned(context: Context, appInstall: AppInstall): Boolean { + val apkFilePath = appManager.getBaseApkPath(appInstall) + return fDroidRepository.isFDroidApplicationSigned(context, appInstall.packageName, apkFilePath, appInstall.signature) + } + + fun isFusedDownloadInstalled(appInstall: AppInstall): Boolean { + return appManager.isAppInstalled(appInstall) + } + + fun getFusedDownloadPackageStatus(appInstall: AppInstall): Status { + return appManager.getInstallationStatus(appInstall) + } +} diff --git a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt index 4d013b2f058457ef1b0d7617826c8c586fa3367d..bf8820e024f1cefa62fa67208341426bb8704e6e 100644 --- a/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt +++ b/app/src/main/java/foundation/e/apps/data/login/api/PlayStoreLoginWrapper.kt @@ -21,9 +21,9 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.PlayResponse import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.enums.User -import foundation.e.apps.data.playstore.utils.AC2DMUtil import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.exceptions.GPlayLoginException +import foundation.e.apps.data.playstore.utils.AC2DMUtil import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import java.util.Locale diff --git a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt index 1966edd13fe0cdfa5e11e129053754d7fc33f772..986bd2a83a70430ae574e1d39094bfe9066b1b08 100644 --- a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt @@ -1,416 +1,416 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.data.updates - -import android.content.Context -import android.content.pm.ApplicationInfo -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.blockedApps.BlockedAppRepository -import foundation.e.apps.data.cleanapk.ApkSignatureManager -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.isUnFiltered -import foundation.e.apps.data.faultyApps.FaultyAppRepository -import foundation.e.apps.data.fdroid.FDroidRepository -import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository -import foundation.e.apps.data.handleNetworkResult -import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.install.pkg.AppLoungePackageManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber -import javax.inject.Inject - -class UpdatesManagerImpl @Inject constructor( - @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, - private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, -) { - - companion object { - const val PACKAGE_NAME_F_DROID = "org.fdroid.fdroid" - const val PACKAGE_NAME_F_DROID_PRIVILEGED = "org.fdroid.fdroid.privileged" - const val PACKAGE_NAME_ANDROID_VENDING = "com.android.vending" - } - - private val userApplications: List - get() = appLoungePackageManager.getAllUserApps() - - suspend fun getUpdates(): Pair, ResultStatus> { - val updateList = mutableListOf() - var status = ResultStatus.OK - - val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() - val gPlayInstalledApps = getGPlayInstalledApps().toMutableList() - - if (appLoungePreference.shouldUpdateAppsFromOtherStores()) { - withContext(Dispatchers.IO) { - val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() - - // This list is based on app signatures - val updatableFDroidApps = - findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) - - openSourceInstalledApps.addAll(updatableFDroidApps) - - otherStoresInstalledApps.removeAll(updatableFDroidApps) - gPlayInstalledApps.addAll(otherStoresInstalledApps) - } - } - - openSourceInstalledApps.removeIf { - blockedAppRepository.isBlockedApp(it) - } - - gPlayInstalledApps.removeIf { - blockedAppRepository.isBlockedApp(it) - } - - // Get open source app updates - if (openSourceInstalledApps.isNotEmpty()) { - status = getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) - } - - // Get GPlay app updates - if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) && - gPlayInstalledApps.isNotEmpty() - ) { - - val gplayStatus = getUpdatesFromApi({ - getGPlayUpdates( - gPlayInstalledApps - ) - }, updateList) - - /** - If any one of the sources is successful, status should be [ResultStatus.OK] - **/ - status = if (status == ResultStatus.OK) status else gplayStatus - } - - val systemApps = getSystemAppUpdates() - val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) - - addSystemApps(updateList, nonFaultyUpdateList, systemApps) - - return Pair(updateList, status) - } - - suspend fun getUpdatesOSS(): Pair, ResultStatus> { - val updateList = mutableListOf() - var status = ResultStatus.OK - - val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() - - if (appLoungePreference.shouldUpdateAppsFromOtherStores()) { - val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() - - // This list is based on app signatures - val updatableFDroidApps = - findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) - - openSourceInstalledApps.addAll(updatableFDroidApps) - } - - openSourceInstalledApps.removeIf { - blockedAppRepository.isBlockedApp(it) - } - - if (openSourceInstalledApps.isNotEmpty()) { - status = getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) - } - - val systemApps = getSystemAppUpdates() - val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) - - addSystemApps(updateList, nonFaultyUpdateList, systemApps) - - return Pair(updateList, status) - } - - private suspend fun getSystemAppUpdates(): List { - val systemApps = mutableListOf() - getUpdatesFromApi({ - Pair(systemAppsUpdatesRepository.getSystemUpdates(), ResultStatus.OK) - }, systemApps) - return systemApps - } - - /** - * This method adds the system app updates at the beginning of the update list. - * It will ensure our system apps are updated first, followed by other apps, - * avoiding potential conflicts. - * - * Since installing an App Lounge update will cause App Lounge to be closed by the system, - * it is added at the end of the list. - */ - private fun addSystemApps( - updateList: MutableList, - nonFaultyApps: List, - systemApps: List, - ) { - updateList.clear() - updateList.addAll(systemApps) - updateList.addAll(nonFaultyApps) - - // Move App Lounge to the end of the list - val appLoungeItem = updateList.find { - it.isSystemApp && it.package_name == context.packageName - } ?: return - updateList.remove(appLoungeItem) - updateList.add(appLoungeItem) - } - - /** - * Lists apps directly updatable by App Lounge from the Open Source category. - * (This includes apps installed by F-Droid client app, if used by the user; - * F-Droid is not considered a third party source.) - */ - private fun getOpenSourceInstalledApps(): List { - return userApplications.filter { - appLoungePackageManager.getInstallerName(it.packageName) in listOf( - context.packageName, - PACKAGE_NAME_F_DROID, - PACKAGE_NAME_F_DROID_PRIVILEGED, - ) - }.map { it.packageName } - } - - /** - * Lists GPlay apps directly updatable by App Lounge. - * - * GPlay apps installed by App Lounge alone can have their installer package - * set as "com.android.vending". - */ - private fun getGPlayInstalledApps(): List { - return userApplications.filter { - appLoungePackageManager.getInstallerName(it.packageName) in listOf( - PACKAGE_NAME_ANDROID_VENDING, - ) - }.map { it.packageName } - } - - /** - * Lists apps installed from other app stores. - * (F-Droid client is not considered a third party source.) - * - * @return List of package names of apps installed from other app stores like - * Aurora Store, Apk mirror, apps installed from browser, apps from ADB etc. - */ - private fun getAppsFromOtherStores(): List { - val gplayAndOpenSourceInstalledApps = getGPlayInstalledApps() + getOpenSourceInstalledApps() - return userApplications.filter { - it.packageName !in gplayAndOpenSourceInstalledApps - }.map { it.packageName } - } - - /** - * Runs API (GPlay api or CleanApk) and accumulates the updatable apps - * into a provided list. - * - * @param apiFunction Function that calls an API method to fetch update information. - * Apps returned is filtered to get only the apps which can be downloaded and updated. - * @param updateAccumulationList A list into which the filtered results from - * [apiFunction] is stored. The caller needs to read this list to get the update info. - * - * @return ResultStatus from calling [apiFunction]. - */ - private suspend fun getUpdatesFromApi( - apiFunction: suspend () -> Pair, ResultStatus>, - updateAccumulationList: MutableList, - ): ResultStatus { - val apiResult = apiFunction() - val updatableApps = apiResult.first.filter { - it.status == Status.UPDATABLE && (it.filterLevel.isUnFiltered() || it.isFDroidApp) - } - updateAccumulationList.addAll(updatableApps) - return apiResult.second - } - - private suspend fun getGPlayUpdates( - packageNames: List, - ): Pair, ResultStatus> { - val appsResults = applicationRepository.getApplicationDetails( - packageNames, - Source.PLAY_STORE - ) - return Pair(appsResults.first, appsResults.second) - } - - /** - * Takes a list of package names and for the apps present on F-Droid, - * returns key value pairs of package names and their signatures. - * - * The signature for an app corresponds to the version currently - * installed on the device. - * If the current installed version for an app is (say) 7, then even if - * the latest version is 10, we try to find the signature of version 7. - * If signature for version 7 of the app is unavailable, then we put blank. - * - * If none of the apps mentioned in [installedPackageNames] are present on F-Droid, - * then it returns an empty Map. - * - * Map is String : String = package name : signature - */ - private suspend fun getFDroidAppsAndSignatures(installedPackageNames: List): Map { - val appsAndSignatures = hashMapOf() - for (packageName in installedPackageNames) { - updateAppsWithPGPSignature(packageName, appsAndSignatures) - } - return appsAndSignatures - } - - private suspend fun updateAppsWithPGPSignature( - packageName: String, - appsAndSignatures: HashMap - ) { - val apps = applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first - if (apps.isEmpty()) { - return - } - - if (apps[0].package_name.isBlank()) { - return - } - appsAndSignatures[packageName] = getPgpSignature(apps[0]) - } - - private suspend fun getPgpSignature(cleanApkApplication: Application): String { - val installedVersionSignature = calculateSignatureVersion(cleanApkApplication) - - val downloadInfoResult = handleNetworkResult { - applicationRepository - .getOSSDownloadInfo(cleanApkApplication._id, installedVersionSignature) - .body()?.download_data - } - - val pgpSignature = downloadInfoResult.data?.signature ?: "" - - Timber.i( - "Signature calculated for : ${cleanApkApplication.package_name}, " + - "signature version: $installedVersionSignature, " + - "is sig blank: ${pgpSignature.isBlank()}" - ) - - return pgpSignature - } - - /** - * Returns list of packages whose signature matches with the available listing on F-Droid. - * - * Example: If Element (im.vector.app) is installed from ApkMirror, then it's signature - * will not match with the version of Element on F-Droid. So if Element is present - * in [installedPackageNames], it will not be present in the list returned by this method. - */ - private suspend fun findPackagesMatchingFDroidSignatures( - installedPackageNames: List, - ): List { - val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) - - val fDroidUpdatablePackageNames = fDroidAppsAndSignatures.filter { - if (it.value.isEmpty()) return@filter false - - // For each installed app also present on F-droid, check signature of base APK. - val baseApkPath = appLoungePackageManager.getBaseApkPath(it.key) - if (baseApkPath.isEmpty()) return@filter false - - ApkSignatureManager.verifyFdroidSignature(context, baseApkPath, it.value, it.key) - }.map { it.key } - - return fDroidUpdatablePackageNames - } - - /** - * Get signature version for the installed version of the app. - * A signature version is like "update_XX" where XX is a 2 digit number. - * - * Example: - * The installed versionCode of an app is (say) 7. - * The latest available version is (say) 10, we need to update to this version. - * The latest signature version is (say) "update_33". - * Available builds of F-droid are (say): - * version 10 - * version 9 - * version 8 - * version 7 - * ... - * Index of version 7 from top is 3 (index of version 10 is 0). - * So the corresponding signature version will be "update_(33-3)" = "update_30" - */ - private suspend fun calculateSignatureVersion(latestCleanapkApp: Application): String { - val packageName = latestCleanapkApp.package_name - val latestSignatureVersion = latestCleanapkApp.latest_downloaded_version - - Timber.i("Latest signature version for $packageName : $latestSignatureVersion") - - val installedVersionCode = appLoungePackageManager.getVersionCode(packageName) - val installedVersionName = appLoungePackageManager.getVersionName(packageName) - - Timber.i("Calculate signature for $packageName : $installedVersionCode, $installedVersionName") - - val latestSignatureVersionNumber = try { - latestSignatureVersion.split("_")[1].toInt() - } catch (e: Exception) { - return "" - } - - // Received list has build info of the latest version at the bottom. - // We want it at the top. - val builds = handleNetworkResult { - fDroidRepository.getBuildVersionInfo(packageName)?.asReversed() ?: listOf() - }.data - - val matchingIndex = builds?.find { - it.versionCode == installedVersionCode && it.versionName == installedVersionName - }?.run { - builds.indexOf(this) - } ?: return "" - - Timber.i("Build info match at index: $matchingIndex") - - /* If latest latest signature version is (say) "update_33" - * corresponding to (say) versionCode 10, and we need to find signature - * version of (say) versionCode 7, then we calculate signature version as: - * "update_" + [33 (latestSignatureVersionNumber) - 3 (i.e. matchingIndex)] = "update_30" - */ - return "update_${latestSignatureVersionNumber - matchingIndex}" - } - - fun getApplicationCategoryPreference(): List { - return applicationRepository.getSelectedAppTypes() - } -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.updates + +import android.content.Context +import android.content.pm.ApplicationInfo +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.blockedApps.BlockedAppRepository +import foundation.e.apps.data.cleanapk.ApkSignatureManager +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.isUnFiltered +import foundation.e.apps.data.faultyApps.FaultyAppRepository +import foundation.e.apps.data.fdroid.FDroidRepository +import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository +import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.data.preference.AppLoungePreference +import foundation.e.apps.install.pkg.AppLoungePackageManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +class UpdatesManagerImpl @Inject constructor( + @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, + private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, +) { + + companion object { + const val PACKAGE_NAME_F_DROID = "org.fdroid.fdroid" + const val PACKAGE_NAME_F_DROID_PRIVILEGED = "org.fdroid.fdroid.privileged" + const val PACKAGE_NAME_ANDROID_VENDING = "com.android.vending" + } + + private val userApplications: List + get() = appLoungePackageManager.getAllUserApps() + + suspend fun getUpdates(): Pair, ResultStatus> { + val updateList = mutableListOf() + var status = ResultStatus.OK + + val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() + val gPlayInstalledApps = getGPlayInstalledApps().toMutableList() + + if (appLoungePreference.shouldUpdateAppsFromOtherStores()) { + withContext(Dispatchers.IO) { + val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + + // This list is based on app signatures + val updatableFDroidApps = + findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) + + openSourceInstalledApps.addAll(updatableFDroidApps) + + otherStoresInstalledApps.removeAll(updatableFDroidApps) + gPlayInstalledApps.addAll(otherStoresInstalledApps) + } + } + + openSourceInstalledApps.removeIf { + blockedAppRepository.isBlockedApp(it) + } + + gPlayInstalledApps.removeIf { + blockedAppRepository.isBlockedApp(it) + } + + // Get open source app updates + if (openSourceInstalledApps.isNotEmpty()) { + status = getUpdatesFromApi({ + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + }, updateList) + } + + // Get GPlay app updates + if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) && + gPlayInstalledApps.isNotEmpty() + ) { + + val gplayStatus = getUpdatesFromApi({ + getGPlayUpdates( + gPlayInstalledApps + ) + }, updateList) + + /** + If any one of the sources is successful, status should be [ResultStatus.OK] + **/ + status = if (status == ResultStatus.OK) status else gplayStatus + } + + val systemApps = getSystemAppUpdates() + val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) + + addSystemApps(updateList, nonFaultyUpdateList, systemApps) + + return Pair(updateList, status) + } + + suspend fun getUpdatesOSS(): Pair, ResultStatus> { + val updateList = mutableListOf() + var status = ResultStatus.OK + + val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() + + if (appLoungePreference.shouldUpdateAppsFromOtherStores()) { + val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + + // This list is based on app signatures + val updatableFDroidApps = + findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) + + openSourceInstalledApps.addAll(updatableFDroidApps) + } + + openSourceInstalledApps.removeIf { + blockedAppRepository.isBlockedApp(it) + } + + if (openSourceInstalledApps.isNotEmpty()) { + status = getUpdatesFromApi({ + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + }, updateList) + } + + val systemApps = getSystemAppUpdates() + val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) + + addSystemApps(updateList, nonFaultyUpdateList, systemApps) + + return Pair(updateList, status) + } + + private suspend fun getSystemAppUpdates(): List { + val systemApps = mutableListOf() + getUpdatesFromApi({ + Pair(systemAppsUpdatesRepository.getSystemUpdates(), ResultStatus.OK) + }, systemApps) + return systemApps + } + + /** + * This method adds the system app updates at the beginning of the update list. + * It will ensure our system apps are updated first, followed by other apps, + * avoiding potential conflicts. + * + * Since installing an App Lounge update will cause App Lounge to be closed by the system, + * it is added at the end of the list. + */ + private fun addSystemApps( + updateList: MutableList, + nonFaultyApps: List, + systemApps: List, + ) { + updateList.clear() + updateList.addAll(systemApps) + updateList.addAll(nonFaultyApps) + + // Move App Lounge to the end of the list + val appLoungeItem = updateList.find { + it.isSystemApp && it.package_name == context.packageName + } ?: return + updateList.remove(appLoungeItem) + updateList.add(appLoungeItem) + } + + /** + * Lists apps directly updatable by App Lounge from the Open Source category. + * (This includes apps installed by F-Droid client app, if used by the user; + * F-Droid is not considered a third party source.) + */ + private fun getOpenSourceInstalledApps(): List { + return userApplications.filter { + appLoungePackageManager.getInstallerName(it.packageName) in listOf( + context.packageName, + PACKAGE_NAME_F_DROID, + PACKAGE_NAME_F_DROID_PRIVILEGED, + ) + }.map { it.packageName } + } + + /** + * Lists GPlay apps directly updatable by App Lounge. + * + * GPlay apps installed by App Lounge alone can have their installer package + * set as "com.android.vending". + */ + private fun getGPlayInstalledApps(): List { + return userApplications.filter { + appLoungePackageManager.getInstallerName(it.packageName) in listOf( + PACKAGE_NAME_ANDROID_VENDING, + ) + }.map { it.packageName } + } + + /** + * Lists apps installed from other app stores. + * (F-Droid client is not considered a third party source.) + * + * @return List of package names of apps installed from other app stores like + * Aurora Store, Apk mirror, apps installed from browser, apps from ADB etc. + */ + private fun getAppsFromOtherStores(): List { + val gplayAndOpenSourceInstalledApps = getGPlayInstalledApps() + getOpenSourceInstalledApps() + return userApplications.filter { + it.packageName !in gplayAndOpenSourceInstalledApps + }.map { it.packageName } + } + + /** + * Runs API (GPlay api or CleanApk) and accumulates the updatable apps + * into a provided list. + * + * @param apiFunction Function that calls an API method to fetch update information. + * Apps returned is filtered to get only the apps which can be downloaded and updated. + * @param updateAccumulationList A list into which the filtered results from + * [apiFunction] is stored. The caller needs to read this list to get the update info. + * + * @return ResultStatus from calling [apiFunction]. + */ + private suspend fun getUpdatesFromApi( + apiFunction: suspend () -> Pair, ResultStatus>, + updateAccumulationList: MutableList, + ): ResultStatus { + val apiResult = apiFunction() + val updatableApps = apiResult.first.filter { + it.status == Status.UPDATABLE && (it.filterLevel.isUnFiltered() || it.isFDroidApp) + } + updateAccumulationList.addAll(updatableApps) + return apiResult.second + } + + private suspend fun getGPlayUpdates( + packageNames: List, + ): Pair, ResultStatus> { + val appsResults = applicationRepository.getApplicationDetails( + packageNames, + Source.PLAY_STORE + ) + return Pair(appsResults.first, appsResults.second) + } + + /** + * Takes a list of package names and for the apps present on F-Droid, + * returns key value pairs of package names and their signatures. + * + * The signature for an app corresponds to the version currently + * installed on the device. + * If the current installed version for an app is (say) 7, then even if + * the latest version is 10, we try to find the signature of version 7. + * If signature for version 7 of the app is unavailable, then we put blank. + * + * If none of the apps mentioned in [installedPackageNames] are present on F-Droid, + * then it returns an empty Map. + * + * Map is String : String = package name : signature + */ + private suspend fun getFDroidAppsAndSignatures(installedPackageNames: List): Map { + val appsAndSignatures = hashMapOf() + for (packageName in installedPackageNames) { + updateAppsWithPGPSignature(packageName, appsAndSignatures) + } + return appsAndSignatures + } + + private suspend fun updateAppsWithPGPSignature( + packageName: String, + appsAndSignatures: HashMap + ) { + val apps = applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first + if (apps.isEmpty()) { + return + } + + if (apps[0].package_name.isBlank()) { + return + } + appsAndSignatures[packageName] = getPgpSignature(apps[0]) + } + + private suspend fun getPgpSignature(cleanApkApplication: Application): String { + val installedVersionSignature = calculateSignatureVersion(cleanApkApplication) + + val downloadInfoResult = handleNetworkResult { + applicationRepository + .getOSSDownloadInfo(cleanApkApplication._id, installedVersionSignature) + .body()?.download_data + } + + val pgpSignature = downloadInfoResult.data?.signature ?: "" + + Timber.i( + "Signature calculated for : ${cleanApkApplication.package_name}, " + + "signature version: $installedVersionSignature, " + + "is sig blank: ${pgpSignature.isBlank()}" + ) + + return pgpSignature + } + + /** + * Returns list of packages whose signature matches with the available listing on F-Droid. + * + * Example: If Element (im.vector.app) is installed from ApkMirror, then it's signature + * will not match with the version of Element on F-Droid. So if Element is present + * in [installedPackageNames], it will not be present in the list returned by this method. + */ + private suspend fun findPackagesMatchingFDroidSignatures( + installedPackageNames: List, + ): List { + val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) + + val fDroidUpdatablePackageNames = fDroidAppsAndSignatures.filter { + if (it.value.isEmpty()) return@filter false + + // For each installed app also present on F-droid, check signature of base APK. + val baseApkPath = appLoungePackageManager.getBaseApkPath(it.key) + if (baseApkPath.isEmpty()) return@filter false + + ApkSignatureManager.verifyFdroidSignature(context, baseApkPath, it.value, it.key) + }.map { it.key } + + return fDroidUpdatablePackageNames + } + + /** + * Get signature version for the installed version of the app. + * A signature version is like "update_XX" where XX is a 2 digit number. + * + * Example: + * The installed versionCode of an app is (say) 7. + * The latest available version is (say) 10, we need to update to this version. + * The latest signature version is (say) "update_33". + * Available builds of F-droid are (say): + * version 10 + * version 9 + * version 8 + * version 7 + * ... + * Index of version 7 from top is 3 (index of version 10 is 0). + * So the corresponding signature version will be "update_(33-3)" = "update_30" + */ + private suspend fun calculateSignatureVersion(latestCleanapkApp: Application): String { + val packageName = latestCleanapkApp.package_name + val latestSignatureVersion = latestCleanapkApp.latest_downloaded_version + + Timber.i("Latest signature version for $packageName : $latestSignatureVersion") + + val installedVersionCode = appLoungePackageManager.getVersionCode(packageName) + val installedVersionName = appLoungePackageManager.getVersionName(packageName) + + Timber.i("Calculate signature for $packageName : $installedVersionCode, $installedVersionName") + + val latestSignatureVersionNumber = try { + latestSignatureVersion.split("_")[1].toInt() + } catch (e: Exception) { + return "" + } + + // Received list has build info of the latest version at the bottom. + // We want it at the top. + val builds = handleNetworkResult { + fDroidRepository.getBuildVersionInfo(packageName)?.asReversed() ?: listOf() + }.data + + val matchingIndex = builds?.find { + it.versionCode == installedVersionCode && it.versionName == installedVersionName + }?.run { + builds.indexOf(this) + } ?: return "" + + Timber.i("Build info match at index: $matchingIndex") + + /* If latest latest signature version is (say) "update_33" + * corresponding to (say) versionCode 10, and we need to find signature + * version of (say) versionCode 7, then we calculate signature version as: + * "update_" + [33 (latestSignatureVersionNumber) - 3 (i.e. matchingIndex)] = "update_30" + */ + return "update_${latestSignatureVersionNumber - matchingIndex}" + } + + fun getApplicationCategoryPreference(): List { + return applicationRepository.getSelectedAppTypes() + } +} diff --git a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerRepository.kt b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerRepository.kt index 2ce742136e0d9b53c60058b60e6150dd0e5244bc..54326d68350d0a04897b6cf150b4e62be55e29d4 100644 --- a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerRepository.kt @@ -1,47 +1,47 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.data.updates - -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.application.UpdatesDao -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.enums.Source -import javax.inject.Inject - -class UpdatesManagerRepository @Inject constructor( - private val updatesManagerImpl: UpdatesManagerImpl -) { - - suspend fun getUpdates(): Pair, ResultStatus> { - if (UpdatesDao.hasAnyAppsForUpdate()) { - return Pair(UpdatesDao.appsAwaitingForUpdate, ResultStatus.OK) - } - return updatesManagerImpl.getUpdates().run { - val filteredApps = - first.filter { !(!it.isFree && it.source == Source.PLAY_STORE && !it.isPurchased) } - UpdatesDao.addItemsForUpdate(filteredApps) - Pair(filteredApps, this.second) - } - } - - suspend fun getUpdatesOSS(): Pair, ResultStatus> { - return updatesManagerImpl.getUpdatesOSS() - } - -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.data.updates + +import foundation.e.apps.data.application.UpdatesDao +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source +import javax.inject.Inject + +class UpdatesManagerRepository @Inject constructor( + private val updatesManagerImpl: UpdatesManagerImpl +) { + + suspend fun getUpdates(): Pair, ResultStatus> { + if (UpdatesDao.hasAnyAppsForUpdate()) { + return Pair(UpdatesDao.appsAwaitingForUpdate, ResultStatus.OK) + } + return updatesManagerImpl.getUpdates().run { + val filteredApps = + first.filter { !(!it.isFree && it.source == Source.PLAY_STORE && !it.isPurchased) } + UpdatesDao.addItemsForUpdate(filteredApps) + Pair(filteredApps, this.second) + } + } + + suspend fun getUpdatesOSS(): Pair, ResultStatus> { + return updatesManagerImpl.getUpdatesOSS() + } + +} diff --git a/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt b/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt index 0eb1d9a690712169f524965c798cd24615c64235..0038958d12fba4cc8fcc754842f0a54014335e73 100644 --- a/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt +++ b/app/src/main/java/foundation/e/apps/di/RepositoryModule.kt @@ -28,8 +28,8 @@ import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepositoryImpl import foundation.e.apps.data.fdroid.FDroidRepository import foundation.e.apps.data.fdroid.IFdroidRepository -import foundation.e.apps.data.install.AppManagerImpl import foundation.e.apps.data.install.AppManager +import foundation.e.apps.data.install.AppManagerImpl import javax.inject.Singleton @Module diff --git a/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt b/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt index 6fa6aca5ede08e2fcac86452298637ab4641d96c..90be997047ff2c682e991847ac90b92184f17b8a 100644 --- a/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt +++ b/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt @@ -28,8 +28,8 @@ import foundation.e.apps.data.cleanapk.CleanApkRetrofit import foundation.e.apps.data.ecloud.EcloudApiInterface import foundation.e.apps.data.fdroid.FdroidApiInterface import foundation.e.apps.data.gitlab.ReleaseInfoApi -import foundation.e.apps.data.gitlab.UpdatableSystemAppsApi import foundation.e.apps.data.gitlab.SystemAppDefinitionApi +import foundation.e.apps.data.gitlab.UpdatableSystemAppsApi import foundation.e.apps.data.parentalcontrol.fdroid.FDroidMonitorApi import foundation.e.apps.data.parentalcontrol.googleplay.AgeGroupApi import foundation.e.apps.di.network.NetworkModule.getYamlFactory diff --git a/app/src/main/java/foundation/e/apps/install/pkg/AppLoungePackageManager.kt b/app/src/main/java/foundation/e/apps/install/pkg/AppLoungePackageManager.kt index d2a6b25636e4d0ba75fccabd50983dc466d2461f..1f519e41abc7359020107c12a21ef5302f5bbe8d 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/AppLoungePackageManager.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/AppLoungePackageManager.kt @@ -1,267 +1,267 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.install.pkg - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo -import android.content.pm.PackageInstaller.Session -import android.content.pm.PackageInstaller.SessionParams -import android.content.pm.PackageManager -import android.content.pm.PackageManager.NameNotFoundException -import android.os.Build -import androidx.core.content.pm.PackageInfoCompat -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.OpenForTesting -import foundation.e.apps.data.application.search.SearchRepository -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.Type -import foundation.e.apps.data.install.models.AppInstall -import java.io.File -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.DelicateCoroutinesApi -import timber.log.Timber - -@Singleton -@OpenForTesting -class AppLoungePackageManager @Inject constructor( - @ApplicationContext private val context: Context -) { - companion object { - const val ERROR_PACKAGE_INSTALL = "ERROR_PACKAGE_INSTALL" - const val PACKAGE_NAME = "packageName" - const val FAKE_STORE_PACKAGE_NAME = "com.android.vending" - private const val UNKNOWN_VALUE = "" - } - - private val packageManager = context.packageManager - - fun isInstalled(packageName: String): Boolean { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0L)) - } else { - packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) - } - - true - } catch (e: PackageManager.NameNotFoundException) { - false - } - } - - private fun isUpdatable(packageName: String, versionCode: Long): Boolean { - val packageInfo = getPackageInfo(packageName) ?: return false - val installedVersionNumber = PackageInfoCompat.getLongVersionCode(packageInfo) - return versionCode > installedVersionNumber - } - - fun getLaunchIntent(packageName: String): Intent? { - return packageManager.getLaunchIntentForPackage(packageName) - } - - private fun getPackageInfo(packageName: String): PackageInfo? { - return try { - packageManager.getPackageInfo(packageName, 0) - } catch (e: NameNotFoundException) { - Timber.e("getPackageInfo: ${e.localizedMessage}") - null - } - } - - /** - * This method should be only used for native apps! - * If you are using for any FusedApp, please consider that it can be a PWA! - */ - fun getPackageStatus( - packageName: String, - versionCode: Long, - ): Status { - return if (isInstalled(packageName)) { - if (isUpdatable(packageName, versionCode)) { - Status.UPDATABLE - } else { - Status.INSTALLED - } - } else { - Status.UNAVAILABLE - } - } - - /** - * Sets an installed app's installer as FakeStore if its source / origin is from Google play. - * If the origin is not Google play, no operation is performed. - * - * [See issue 2237](https://gitlab.e.foundation/e/backlog/-/issues/2237) - * - * Surrounded by try-catch to prevent exception is case App Lounge and FakeStore's - * signing certificate is not the same. - */ - fun setFakeStoreAsInstallerIfNeeded(appInstall: AppInstall?) { - if (appInstall == null || appInstall.packageName.isBlank()) { - return - } - if (appInstall.source == Source.PLAY_STORE) { - if (appInstall.type == Type.NATIVE && isInstalled(FAKE_STORE_PACKAGE_NAME)) { - val targetPackage = appInstall.packageName - try { - packageManager.setInstallerPackageName(targetPackage, FAKE_STORE_PACKAGE_NAME) - Timber.d("Changed installer to $FAKE_STORE_PACKAGE_NAME for $targetPackage") - } catch (e: Exception) { - Timber.w(e) - } - } - } - } - - fun getInstallerName(packageName: String): String { - return try { - val installerInfo = packageManager.getInstallSourceInfo(packageName) - installerInfo.originatingPackageName ?: installerInfo.installingPackageName ?: UNKNOWN_VALUE - } catch (e: NameNotFoundException) { - Timber.e("getInstallerName -> $packageName : ${e.localizedMessage}") - UNKNOWN_VALUE - } catch (e: IllegalArgumentException) { - Timber.e("getInstallerName -> $packageName : ${e.localizedMessage}") - UNKNOWN_VALUE - } - } - - /** - * For an installed app, get the path to the base.apk. - */ - fun getBaseApkPath(packageName: String): String { - val packageInfo = getPackageInfo(packageName) - return packageInfo?.applicationInfo?.publicSourceDir ?: UNKNOWN_VALUE - } - - fun getVersionCode(packageName: String): String { - val packageInfo = getPackageInfo(packageName) - return packageInfo?.longVersionCode?.toString() ?: UNKNOWN_VALUE - } - - fun getVersionName(packageName: String): String { - val packageInfo = getPackageInfo(packageName) - return packageInfo?.versionName?.toString() ?: UNKNOWN_VALUE - } - - /** - * Installs the given package using system API - * @param list List of [File] to be written to install session. - */ - @OptIn(DelicateCoroutinesApi::class) - fun installApplication(list: List, packageName: String) { - - val sessionId = createInstallSession(packageName, SessionParams.MODE_FULL_INSTALL) - val session = packageManager.packageInstaller.openSession(sessionId) - - try { - // Install the package using the provided stream - list.forEach { - syncFile(session, it) - } - - val callBackIntent = Intent(context, InstallerService::class.java) - callBackIntent.putExtra(PACKAGE_NAME, packageName) - - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else - PendingIntent.FLAG_UPDATE_CURRENT - val servicePendingIntent = PendingIntent.getService( - context, - sessionId, - callBackIntent, - flags - ) - session.commit(servicePendingIntent.intentSender) - } catch (e: Exception) { - Timber.e( - "Initiating Install Failed for $packageName exception: ${e.localizedMessage}", - e - ) - val pendingIntent = PendingIntent.getBroadcast( - context, - sessionId, - Intent(ERROR_PACKAGE_INSTALL), - PendingIntent.FLAG_IMMUTABLE - ) - session.commit(pendingIntent.intentSender) - session.abandon() - throw e - } finally { - session.close() - } - } - - private fun createInstallSession(packageName: String, mode: Int): Int { - - val packageInstaller = packageManager.packageInstaller - val params = SessionParams(mode).apply { - setAppPackageName(packageName) - setOriginatingUid(android.os.Process.myUid()) - setInstallReason(PackageManager.INSTALL_REASON_USER) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) - } - } - - return packageInstaller.createSession(params) - } - - private fun syncFile(session: Session, file: File) { - - val inputStream = file.inputStream() - val outputStream = session.openWrite(file.nameWithoutExtension, 0, -1) - inputStream.copyTo(outputStream) - session.fsync(outputStream) - inputStream.close() - outputStream.close() - } - - fun getFilter(): IntentFilter { - val filter = IntentFilter() - filter.addDataScheme("package") - filter.addAction(Intent.ACTION_PACKAGE_REMOVED) - filter.addAction(Intent.ACTION_PACKAGE_ADDED) - filter.addAction(ERROR_PACKAGE_INSTALL) - return filter - } - - fun getAllUserApps(): List { - val userPackages = mutableListOf() - val allPackages = packageManager.getInstalledApplications(0) - allPackages.forEach { - if (it.flags and ApplicationInfo.FLAG_SYSTEM == 0) userPackages.add(it) - } - return userPackages - } - - fun getAppNameFromPackageName(packageName: String): String { - val packageManager = context.packageManager - return packageManager.getApplicationLabel( - packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) - ).toString() - } -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.install.pkg + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller.Session +import android.content.pm.PackageInstaller.SessionParams +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.os.Build +import androidx.core.content.pm.PackageInfoCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.OpenForTesting +import foundation.e.apps.data.application.search.SearchRepository +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.Type +import foundation.e.apps.data.install.models.AppInstall +import kotlinx.coroutines.DelicateCoroutinesApi +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +@OpenForTesting +class AppLoungePackageManager @Inject constructor( + @ApplicationContext private val context: Context +) { + companion object { + const val ERROR_PACKAGE_INSTALL = "ERROR_PACKAGE_INSTALL" + const val PACKAGE_NAME = "packageName" + const val FAKE_STORE_PACKAGE_NAME = "com.android.vending" + private const val UNKNOWN_VALUE = "" + } + + private val packageManager = context.packageManager + + fun isInstalled(packageName: String): Boolean { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0L)) + } else { + packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA) + } + + true + } catch (e: PackageManager.NameNotFoundException) { + false + } + } + + private fun isUpdatable(packageName: String, versionCode: Long): Boolean { + val packageInfo = getPackageInfo(packageName) ?: return false + val installedVersionNumber = PackageInfoCompat.getLongVersionCode(packageInfo) + return versionCode > installedVersionNumber + } + + fun getLaunchIntent(packageName: String): Intent? { + return packageManager.getLaunchIntentForPackage(packageName) + } + + private fun getPackageInfo(packageName: String): PackageInfo? { + return try { + packageManager.getPackageInfo(packageName, 0) + } catch (e: NameNotFoundException) { + Timber.e("getPackageInfo: ${e.localizedMessage}") + null + } + } + + /** + * This method should be only used for native apps! + * If you are using for any FusedApp, please consider that it can be a PWA! + */ + fun getPackageStatus( + packageName: String, + versionCode: Long, + ): Status { + return if (isInstalled(packageName)) { + if (isUpdatable(packageName, versionCode)) { + Status.UPDATABLE + } else { + Status.INSTALLED + } + } else { + Status.UNAVAILABLE + } + } + + /** + * Sets an installed app's installer as FakeStore if its source / origin is from Google play. + * If the origin is not Google play, no operation is performed. + * + * [See issue 2237](https://gitlab.e.foundation/e/backlog/-/issues/2237) + * + * Surrounded by try-catch to prevent exception is case App Lounge and FakeStore's + * signing certificate is not the same. + */ + fun setFakeStoreAsInstallerIfNeeded(appInstall: AppInstall?) { + if (appInstall == null || appInstall.packageName.isBlank()) { + return + } + if (appInstall.source == Source.PLAY_STORE) { + if (appInstall.type == Type.NATIVE && isInstalled(FAKE_STORE_PACKAGE_NAME)) { + val targetPackage = appInstall.packageName + try { + packageManager.setInstallerPackageName(targetPackage, FAKE_STORE_PACKAGE_NAME) + Timber.d("Changed installer to $FAKE_STORE_PACKAGE_NAME for $targetPackage") + } catch (e: Exception) { + Timber.w(e) + } + } + } + } + + fun getInstallerName(packageName: String): String { + return try { + val installerInfo = packageManager.getInstallSourceInfo(packageName) + installerInfo.originatingPackageName ?: installerInfo.installingPackageName ?: UNKNOWN_VALUE + } catch (e: NameNotFoundException) { + Timber.e("getInstallerName -> $packageName : ${e.localizedMessage}") + UNKNOWN_VALUE + } catch (e: IllegalArgumentException) { + Timber.e("getInstallerName -> $packageName : ${e.localizedMessage}") + UNKNOWN_VALUE + } + } + + /** + * For an installed app, get the path to the base.apk. + */ + fun getBaseApkPath(packageName: String): String { + val packageInfo = getPackageInfo(packageName) + return packageInfo?.applicationInfo?.publicSourceDir ?: UNKNOWN_VALUE + } + + fun getVersionCode(packageName: String): String { + val packageInfo = getPackageInfo(packageName) + return packageInfo?.longVersionCode?.toString() ?: UNKNOWN_VALUE + } + + fun getVersionName(packageName: String): String { + val packageInfo = getPackageInfo(packageName) + return packageInfo?.versionName?.toString() ?: UNKNOWN_VALUE + } + + /** + * Installs the given package using system API + * @param list List of [File] to be written to install session. + */ + @OptIn(DelicateCoroutinesApi::class) + fun installApplication(list: List, packageName: String) { + + val sessionId = createInstallSession(packageName, SessionParams.MODE_FULL_INSTALL) + val session = packageManager.packageInstaller.openSession(sessionId) + + try { + // Install the package using the provided stream + list.forEach { + syncFile(session, it) + } + + val callBackIntent = Intent(context, InstallerService::class.java) + callBackIntent.putExtra(PACKAGE_NAME, packageName) + + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE else + PendingIntent.FLAG_UPDATE_CURRENT + val servicePendingIntent = PendingIntent.getService( + context, + sessionId, + callBackIntent, + flags + ) + session.commit(servicePendingIntent.intentSender) + } catch (e: Exception) { + Timber.e( + "Initiating Install Failed for $packageName exception: ${e.localizedMessage}", + e + ) + val pendingIntent = PendingIntent.getBroadcast( + context, + sessionId, + Intent(ERROR_PACKAGE_INSTALL), + PendingIntent.FLAG_IMMUTABLE + ) + session.commit(pendingIntent.intentSender) + session.abandon() + throw e + } finally { + session.close() + } + } + + private fun createInstallSession(packageName: String, mode: Int): Int { + + val packageInstaller = packageManager.packageInstaller + val params = SessionParams(mode).apply { + setAppPackageName(packageName) + setOriginatingUid(android.os.Process.myUid()) + setInstallReason(PackageManager.INSTALL_REASON_USER) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) + } + } + + return packageInstaller.createSession(params) + } + + private fun syncFile(session: Session, file: File) { + + val inputStream = file.inputStream() + val outputStream = session.openWrite(file.nameWithoutExtension, 0, -1) + inputStream.copyTo(outputStream) + session.fsync(outputStream) + inputStream.close() + outputStream.close() + } + + fun getFilter(): IntentFilter { + val filter = IntentFilter() + filter.addDataScheme("package") + filter.addAction(Intent.ACTION_PACKAGE_REMOVED) + filter.addAction(Intent.ACTION_PACKAGE_ADDED) + filter.addAction(ERROR_PACKAGE_INSTALL) + return filter + } + + fun getAllUserApps(): List { + val userPackages = mutableListOf() + val allPackages = packageManager.getInstalledApplications(0) + allPackages.forEach { + if (it.flags and ApplicationInfo.FLAG_SYSTEM == 0) userPackages.add(it) + } + return userPackages + } + + fun getAppNameFromPackageName(packageName: String): String { + val packageManager = context.packageManager + return packageManager.getApplicationLabel( + packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + ).toString() + } +} diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt index b1ea00b57efa1d0e304d4eb5db118b0dad21ad95..5799396060d7b8cc4d18c43e7d59a7e11ad3c5e0 100644 --- a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt +++ b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt @@ -13,8 +13,8 @@ import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.OpenForTesting -import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.AppInstallRepository import foundation.e.apps.data.install.models.AppInstall import kotlinx.coroutines.delay diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt index 3f55ae189387f028dc47c35a7db0315519c60c6b..ba7fbb5c558111ace39c8ee84510ae20e61090db 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt @@ -29,8 +29,8 @@ import android.os.Build import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import foundation.e.apps.ui.MainActivity import foundation.e.apps.R +import foundation.e.apps.ui.MainActivity object UpdatesNotifier { const val UPDATES_NOTIFICATION_CLICK_EXTRA = "updates_notification_click_extra" diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index d40a26b8dc23369130828c9769911c66ca700283..1bd7deded4bfd261a34d790247feed481241abd5 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -16,10 +16,10 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User -import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.preference.AppLoungeDataStore diff --git a/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt index 168bb76e07b3a7e7a6ea1d183112b867e2f19994..d1a73dfe6b24a3e2ede92504f1b738ee529bcbdc 100644 --- a/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/AppInfoFetchViewModel.kt @@ -4,10 +4,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.liveData import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.faultyApps.FaultyAppRepository import foundation.e.apps.data.fdroid.FDroidRepository -import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.playstore.PlayStoreRepository import javax.inject.Inject diff --git a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt index e43c108580ca8fa2fd37c69f3ed62b6d2cfb032e..c21b0f85da27162f15f62a2ca5d8490bfe154521 100644 --- a/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/PrivacyInfoViewModel.kt @@ -6,10 +6,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.Result +import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.exodus.models.AppPrivacyInfo import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository -import foundation.e.apps.data.application.data.Application import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt index 6ee84a431687a482c3c3cdc4fd79d74279a53ab1..c9bfa454d88b6d03962a3e171f7e63a0cc086b26 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt @@ -32,15 +32,15 @@ import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.Status import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.databinding.FragmentApplicationListBinding import foundation.e.apps.install.download.data.DownloadProgress -import foundation.e.apps.install.pkg.PwaManager import foundation.e.apps.install.pkg.AppLoungePackageManager +import foundation.e.apps.install.pkg.PwaManager import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.MainActivityViewModel diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 4dee4b59cef1003d5a8481a7ae61c7432b7b5103..6e0d6c2c31fdca061f214f19cb0dedde0a1fd71d 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -1,546 +1,545 @@ -/* - * Copyright (C) 2022 ECORP - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.ui.applicationlist - -import android.content.Context -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.view.children -import androidx.core.view.isVisible -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.navigation.findNavController -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import coil.load -import com.facebook.shimmer.Shimmer -import com.facebook.shimmer.Shimmer.Direction.LEFT_TO_RIGHT -import com.facebook.shimmer.ShimmerDrawable -import com.google.android.material.button.MaterialButton -import com.google.android.material.snackbar.Snackbar -import foundation.e.apps.R -import foundation.e.apps.data.application.ApplicationInstaller -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.cleanapk.CleanApkRetrofit -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.User -import foundation.e.apps.databinding.ApplicationListItemBinding -import foundation.e.apps.install.pkg.InstallerService -import foundation.e.apps.ui.AppInfoFetchViewModel -import foundation.e.apps.ui.MainActivityViewModel -import foundation.e.apps.ui.PrivacyInfoViewModel -import foundation.e.apps.ui.applicationlist.diffUtils.ConciseAppDiffUtils -import foundation.e.apps.ui.search.SearchFragmentDirections -import foundation.e.apps.ui.updates.UpdatesFragmentDirections -import foundation.e.apps.utils.disableInstallButton -import foundation.e.apps.utils.enableInstallButton -import timber.log.Timber -import javax.inject.Singleton - -@Singleton -class 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) -> Unit)? = null -) : ListAdapter(ConciseAppDiffUtils()) { - - private var optionalCategory = "" - - private val shimmer = Shimmer.ColorHighlightBuilder() - .setDuration(500) - .setBaseAlpha(0.7f) - .setDirection(LEFT_TO_RIGHT) - .setHighlightAlpha(0.6f) - .setAutoStart(true) - .build() - - var onPlaceHolderShow: (() -> Unit)? = null - - inner class ViewHolder( - val binding: ApplicationListItemBinding - ) : RecyclerView.ViewHolder(binding.root) { - var isPurchasedLiveData: LiveData = MutableLiveData() - lateinit var app: Application - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder( - ApplicationListItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val view = holder.itemView - val searchApp = getItem(position) - holder.app = searchApp - val shimmerDrawable = ShimmerDrawable().apply { setShimmer(shimmer) } - - /* - * A placeholder entry is one where we only show a loading progress bar, - * instead of an app entry. - * It is usually done to signify more apps are being loaded at the end of the list. - * - * We hide all view elements other than the circular progress bar. - * - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] - */ - if (searchApp.isPlaceHolder) { - val progressBar = holder.binding.placeholderProgressBar - holder.binding.root.children.forEach { - it.visibility = if (it != progressBar) View.INVISIBLE - else View.VISIBLE - } - onPlaceHolderShow?.invoke() - // Do not process anything else for this entry - return - } else { - val progressBar = holder.binding.placeholderProgressBar - holder.binding.root.children.forEach { - it.visibility = if (it != progressBar) View.VISIBLE - else View.INVISIBLE - } - } - - holder.binding.apply { - applicationList.setOnClickListener { - handleAppItemClick(searchApp, view) - } - updateAppInfo(searchApp) - updateRating(searchApp) - updateSourceTag(searchApp) - setAppIcon(searchApp, shimmerDrawable) - removeIsPurchasedObserver(holder) - - setInstallButtonDimensions(view) - - if (appInfoFetchViewModel.isAppInBlockedList(searchApp)) { - setupShowMoreButton() - } else { - mainActivityViewModel.verifyUiFilter(searchApp) { - setupInstallButton(searchApp, view, holder) - } - } - - } - } - - private fun ApplicationListItemBinding.setInstallButtonDimensions(item: View) { - item.post { - val maxAllowedWidth = item.measuredWidth / 2 - installButton.apply { - if (width > maxAllowedWidth) - width = maxAllowedWidth - } - } - } - - private fun ApplicationListItemBinding.setAppIcon( - searchApp: Application, - shimmerDrawable: ShimmerDrawable - ) { - when (searchApp.source) { - Source.PLAY_STORE -> { - appIcon.load(searchApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } - Source.PWA -> { - appIcon.load(CleanApkRetrofit.ASSET_URL + searchApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } - Source.OPEN_SOURCE -> { - appIcon.load(CleanApkRetrofit.ASSET_URL + searchApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } - Source.SYSTEM_APP -> { - appIcon.load(getAppIcon(appIcon.context, searchApp.package_name)) { - placeholder(shimmerDrawable) - } - } - } - } - - private fun getAppIcon(context: Context, packageName: String): Drawable? { - return try { - context.packageManager.getApplicationIcon(packageName) - } catch (e: PackageManager.NameNotFoundException) { - Timber.w("Icon could not be set for system app - $packageName: ${e.message}") - null - } - } - - private fun ApplicationListItemBinding.updateAppInfo(searchApp: Application) { - appTitle.text = searchApp.name - appInfoFetchViewModel.getAuthorName(searchApp).observe(lifecycleOwner!!) { - appAuthor.text = it - } - } - - private fun ApplicationListItemBinding.updateRating(searchApp: Application) { - if (searchApp.isSystemApp) { - iconStar.isVisible = false - appRating.isVisible = false - return - } - val formattedRating = mainActivityViewModel.handleRatingFormat(searchApp.ratings.usageQualityScore) - appRating.text = formattedRating ?: root.context.getString(R.string.not_available) - } - - private fun ApplicationListItemBinding.updateSourceTag(searchApp: Application) { - sourceTag.visibility = View.INVISIBLE - val tag = searchApp.source.toString() - if (tag.isNotBlank()) { - sourceTag.text = tag - sourceTag.visibility = View.VISIBLE - } - } - - private fun handleAppItemClick( - searchApp: Application, - view: View - ) { - if (searchApp.isSystemApp) { - return - } - val catText = searchApp.category.ifBlank { optionalCategory } - val action = when (currentDestinationId) { - R.id.applicationListFragment -> { - ApplicationListFragmentDirections.actionApplicationListFragmentToApplicationFragment( - searchApp.package_name, - searchApp._id, - searchApp.source, - catText, - searchApp.isGplayReplaced, - searchApp.isPurchased - ) - } - R.id.searchFragment -> { - SearchFragmentDirections.actionSearchFragmentToApplicationFragment( - searchApp.package_name, - searchApp._id, - searchApp.source, - catText, - searchApp.isGplayReplaced, - searchApp.isPurchased - ) - } - R.id.updatesFragment -> { - UpdatesFragmentDirections.actionUpdatesFragmentToApplicationFragment( - searchApp.package_name, - searchApp._id, - searchApp.source, - catText, - searchApp.isGplayReplaced, - searchApp.isPurchased - ) - } - else -> null - } - action?.let { direction -> view.findNavController().navigate(direction) } - } - - private fun removeIsPurchasedObserver(holder: ViewHolder) { - lifecycleOwner?.let { - holder.isPurchasedLiveData.removeObservers(it) - } - } - - private fun ApplicationListItemBinding.setupInstallButton( - searchApp: Application, - view: View, - holder: ViewHolder - ) { - installButton.visibility = View.VISIBLE - showMore.visibility = View.INVISIBLE - when (searchApp.status) { - Status.INSTALLED -> { - handleInstalled(searchApp) - } - Status.UPDATABLE -> { - handleUpdatable(searchApp) - } - Status.UNAVAILABLE -> { - handleUnavailable(searchApp, holder) - } - Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> { - handleDownloading(searchApp) - } - Status.INSTALLING -> { - handleInstalling() - } - Status.BLOCKED -> { - handleBlocked(view) - } - Status.INSTALLATION_ISSUE -> { - handleInstallationIssue(view, searchApp) - } - else -> { - Timber.w("ApplicationListRVAdapter: Unknown status") - } - } - } - - private fun ApplicationListItemBinding.setupShowMoreButton() { - installButton.visibility = View.INVISIBLE - showMore.visibility = View.VISIBLE - progressBarInstall.visibility = View.GONE - } - - private fun ApplicationListItemBinding.handleInstallationIssue( - view: View, - searchApp: Application, - ) { - progressBarInstall.visibility = View.GONE - if (lifecycleOwner == null) { - return - } - - appInfoFetchViewModel.isAppFaulty(searchApp).observe(lifecycleOwner!!) { - updateInstallButton(it, view, searchApp) - } - } - - private fun ApplicationListItemBinding.updateInstallButton( - faultyAppResult: Pair, - view: View, - searchApp: Application - ) { - installButton.apply { - if (faultyAppResult.first) disableInstallButton() else enableInstallButton() - text = getInstallationIssueText(faultyAppResult, view) - backgroundTintList = - ContextCompat.getColorStateList(view.context, android.R.color.transparent) - setOnClickListener { - installApplication(searchApp) - } - } - } - - private fun getInstallationIssueText( - faultyAppResult: Pair, - view: View - ) = - if (faultyAppResult.second.contentEquals(InstallerService.INSTALL_FAILED_UPDATE_INCOMPATIBLE)) - view.context.getText(R.string.update) - else - view.context.getString(R.string.retry) - - private fun ApplicationListItemBinding.handleBlocked(view: View) { - installButton.apply { - isEnabled = true - setOnClickListener { - val errorMsg = when (mainActivityViewModel.getUser()) { - User.ANONYMOUS, - User.NO_GOOGLE, - User.UNAVAILABLE -> view.context.getString(R.string.install_blocked_anonymous) - User.GOOGLE -> view.context.getString(R.string.install_blocked_google) - } - if (errorMsg.isNotBlank()) { - Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() - } - } - } - progressBarInstall.visibility = View.GONE - } - - private fun ApplicationListItemBinding.handleInstalling() { - installButton.apply { - disableInstallButton() - text = context.getText(R.string.installing) - } - progressBarInstall.visibility = View.GONE - } - - private fun ApplicationListItemBinding.handleDownloading( - searchApp: Application, - ) { - installButton.apply { - enableInstallButton() - text = context.getString(R.string.cancel) - setOnClickListener { - cancelDownload(searchApp) - } - progressBarInstall.visibility = View.GONE - } - progressBarInstall.visibility = View.GONE - } - - private fun ApplicationListItemBinding.handleUnavailable( - searchApp: Application, - holder: ViewHolder, - ) { - installButton.apply { - updateUIByPaymentType(searchApp, this, this@handleUnavailable, holder) - setOnClickListener { - if (mainActivityViewModel.checkUnsupportedApplication(searchApp, context)) { - return@setOnClickListener - } - if (searchApp.isFree || searchApp.isPurchased) { - disableInstallButton() - text = context.getText(R.string.cancel) - installApplication(searchApp) - } else { - paidAppHandler?.invoke(searchApp) - } - } - } - } - - private fun updateUIByPaymentType( - searchApp: Application, - materialButton: MaterialButton, - applicationListItemBinding: ApplicationListItemBinding, - holder: ViewHolder - ) { - when { - mainActivityViewModel.checkUnsupportedApplication(searchApp) -> { - materialButton.enableInstallButton() - materialButton.text = materialButton.context.getString(R.string.not_available) - applicationListItemBinding.progressBarInstall.visibility = View.GONE - } - searchApp.isFree -> { - materialButton.enableInstallButton() - materialButton.text = materialButton.context.getString(R.string.install) - materialButton.strokeColor = - ContextCompat.getColorStateList(holder.itemView.context, R.color.light_grey) - applicationListItemBinding.progressBarInstall.visibility = View.GONE - } - else -> { - materialButton.disableInstallButton() - materialButton.text = "" - applicationListItemBinding.progressBarInstall.visibility = View.VISIBLE - if (appInfoFetchViewModel.isAnonymousUser()) { - materialButton.enableInstallButton() - applicationListItemBinding.progressBarInstall.visibility = View.GONE - materialButton.text = searchApp.price - return - } - holder.isPurchasedLiveData = appInfoFetchViewModel.isAppPurchased(searchApp) - if (lifecycleOwner == null) { - return - } - holder.isPurchasedLiveData.observe(lifecycleOwner!!) { - materialButton.enableInstallButton() - applicationListItemBinding.progressBarInstall.visibility = View.GONE - materialButton.text = - if (it) materialButton.context.getString(R.string.install) else searchApp.price - } - } - } - } - - private fun ApplicationListItemBinding.handleUpdatable( - searchApp: Application - ) { - installButton.apply { - enableInstallButton(Status.UPDATABLE) - text = if (mainActivityViewModel.checkUnsupportedApplication(searchApp)) - context.getString(R.string.not_available) - else context.getString(R.string.update) - setOnClickListener { - if (mainActivityViewModel.checkUnsupportedApplication(searchApp, context)) { - return@setOnClickListener - } - if (searchApp.package_name == context.packageName) { - showUpdateConfirmationDialog(context, searchApp) - } else { - installApplication(searchApp) - } - } - } - progressBarInstall.visibility = View.GONE - } - - private fun ApplicationListItemBinding.handleInstalled( - searchApp: Application, - ) { - installButton.apply { - enableInstallButton(Status.INSTALLED) - text = context.getString(R.string.open) - setOnClickListener { - if (searchApp.is_pwa) { - mainActivityViewModel.launchPwa(searchApp) - } else { - mainActivityViewModel.getLaunchIntentForPackageName(searchApp.package_name)?.let { - context.startActivity(it) - } - } - } - } - progressBarInstall.visibility = View.GONE - } - - private fun showUpdateConfirmationDialog(context: Context, searchApp: Application) { - AlertDialog.Builder(context).apply { - setTitle(R.string.own_update_warning_title) - setMessage(R.string.own_update_warning_description) - setPositiveButton(android.R.string.ok) {_, _ -> - installApplication(searchApp) - } - setNegativeButton(android.R.string.cancel, null) - }.show() - } - - fun setData(newList: List, optionalCategory: String? = null) { - optionalCategory?.let { - this.optionalCategory = it - } - currentList.forEach { - newList.find { item -> item._id == it._id }?.let { foundItem -> - foundItem.privacyScore = it.privacyScore - } - } - this.submitList(newList.map { it.copy() }) - } - - private fun installApplication(searchApp: Application) { - applicationInstaller.installApplication(searchApp) - } - - private fun cancelDownload(searchApp: Application) { - applicationInstaller.cancelDownload(searchApp) - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - super.onDetachedFromRecyclerView(recyclerView) - lifecycleOwner = null - paidAppHandler = null - } - - override fun onViewRecycled(holder: ViewHolder) { - privacyInfoViewModel.cancelAppPrivacyInfoFetch(holder.app) - super.onViewRecycled(holder) - } -} +/* + * Copyright (C) 2022 ECORP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.applicationlist + +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.view.children +import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.navigation.findNavController +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.facebook.shimmer.Shimmer +import com.facebook.shimmer.Shimmer.Direction.LEFT_TO_RIGHT +import com.facebook.shimmer.ShimmerDrawable +import com.google.android.material.button.MaterialButton +import com.google.android.material.snackbar.Snackbar +import foundation.e.apps.R +import foundation.e.apps.data.application.ApplicationInstaller +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.CleanApkRetrofit +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.User +import foundation.e.apps.databinding.ApplicationListItemBinding +import foundation.e.apps.install.pkg.InstallerService +import foundation.e.apps.ui.AppInfoFetchViewModel +import foundation.e.apps.ui.MainActivityViewModel +import foundation.e.apps.ui.PrivacyInfoViewModel +import foundation.e.apps.ui.applicationlist.diffUtils.ConciseAppDiffUtils +import foundation.e.apps.ui.search.SearchFragmentDirections +import foundation.e.apps.ui.updates.UpdatesFragmentDirections +import foundation.e.apps.utils.disableInstallButton +import foundation.e.apps.utils.enableInstallButton +import timber.log.Timber +import javax.inject.Singleton +@Singleton +class 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) -> Unit)? = null +) : ListAdapter(ConciseAppDiffUtils()) { + + private var optionalCategory = "" + + private val shimmer = Shimmer.ColorHighlightBuilder() + .setDuration(500) + .setBaseAlpha(0.7f) + .setDirection(LEFT_TO_RIGHT) + .setHighlightAlpha(0.6f) + .setAutoStart(true) + .build() + + var onPlaceHolderShow: (() -> Unit)? = null + + inner class ViewHolder( + val binding: ApplicationListItemBinding + ) : RecyclerView.ViewHolder(binding.root) { + var isPurchasedLiveData: LiveData = MutableLiveData() + lateinit var app: Application + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ApplicationListItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val view = holder.itemView + val searchApp = getItem(position) + holder.app = searchApp + val shimmerDrawable = ShimmerDrawable().apply { setShimmer(shimmer) } + + /* + * A placeholder entry is one where we only show a loading progress bar, + * instead of an app entry. + * It is usually done to signify more apps are being loaded at the end of the list. + * + * We hide all view elements other than the circular progress bar. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5131 [2] + */ + if (searchApp.isPlaceHolder) { + val progressBar = holder.binding.placeholderProgressBar + holder.binding.root.children.forEach { + it.visibility = if (it != progressBar) View.INVISIBLE + else View.VISIBLE + } + onPlaceHolderShow?.invoke() + // Do not process anything else for this entry + return + } else { + val progressBar = holder.binding.placeholderProgressBar + holder.binding.root.children.forEach { + it.visibility = if (it != progressBar) View.VISIBLE + else View.INVISIBLE + } + } + + holder.binding.apply { + applicationList.setOnClickListener { + handleAppItemClick(searchApp, view) + } + updateAppInfo(searchApp) + updateRating(searchApp) + updateSourceTag(searchApp) + setAppIcon(searchApp, shimmerDrawable) + removeIsPurchasedObserver(holder) + + setInstallButtonDimensions(view) + + if (appInfoFetchViewModel.isAppInBlockedList(searchApp)) { + setupShowMoreButton() + } else { + mainActivityViewModel.verifyUiFilter(searchApp) { + setupInstallButton(searchApp, view, holder) + } + } + + } + } + + private fun ApplicationListItemBinding.setInstallButtonDimensions(item: View) { + item.post { + val maxAllowedWidth = item.measuredWidth / 2 + installButton.apply { + if (width > maxAllowedWidth) + width = maxAllowedWidth + } + } + } + + private fun ApplicationListItemBinding.setAppIcon( + searchApp: Application, + shimmerDrawable: ShimmerDrawable + ) { + when (searchApp.source) { + Source.PLAY_STORE -> { + appIcon.load(searchApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } + Source.PWA -> { + appIcon.load(CleanApkRetrofit.ASSET_URL + searchApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } + Source.OPEN_SOURCE -> { + appIcon.load(CleanApkRetrofit.ASSET_URL + searchApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } + Source.SYSTEM_APP -> { + appIcon.load(getAppIcon(appIcon.context, searchApp.package_name)) { + placeholder(shimmerDrawable) + } + } + } + } + + private fun getAppIcon(context: Context, packageName: String): Drawable? { + return try { + context.packageManager.getApplicationIcon(packageName) + } catch (e: PackageManager.NameNotFoundException) { + Timber.w("Icon could not be set for system app - $packageName: ${e.message}") + null + } + } + + private fun ApplicationListItemBinding.updateAppInfo(searchApp: Application) { + appTitle.text = searchApp.name + appInfoFetchViewModel.getAuthorName(searchApp).observe(lifecycleOwner!!) { + appAuthor.text = it + } + } + + private fun ApplicationListItemBinding.updateRating(searchApp: Application) { + if (searchApp.isSystemApp) { + iconStar.isVisible = false + appRating.isVisible = false + return + } + val formattedRating = mainActivityViewModel.handleRatingFormat(searchApp.ratings.usageQualityScore) + appRating.text = formattedRating ?: root.context.getString(R.string.not_available) + } + + private fun ApplicationListItemBinding.updateSourceTag(searchApp: Application) { + sourceTag.visibility = View.INVISIBLE + val tag = searchApp.source.toString() + if (tag.isNotBlank()) { + sourceTag.text = tag + sourceTag.visibility = View.VISIBLE + } + } + + private fun handleAppItemClick( + searchApp: Application, + view: View + ) { + if (searchApp.isSystemApp) { + return + } + val catText = searchApp.category.ifBlank { optionalCategory } + val action = when (currentDestinationId) { + R.id.applicationListFragment -> { + ApplicationListFragmentDirections.actionApplicationListFragmentToApplicationFragment( + searchApp.package_name, + searchApp._id, + searchApp.source, + catText, + searchApp.isGplayReplaced, + searchApp.isPurchased + ) + } + R.id.searchFragment -> { + SearchFragmentDirections.actionSearchFragmentToApplicationFragment( + searchApp.package_name, + searchApp._id, + searchApp.source, + catText, + searchApp.isGplayReplaced, + searchApp.isPurchased + ) + } + R.id.updatesFragment -> { + UpdatesFragmentDirections.actionUpdatesFragmentToApplicationFragment( + searchApp.package_name, + searchApp._id, + searchApp.source, + catText, + searchApp.isGplayReplaced, + searchApp.isPurchased + ) + } + else -> null + } + action?.let { direction -> view.findNavController().navigate(direction) } + } + + private fun removeIsPurchasedObserver(holder: ViewHolder) { + lifecycleOwner?.let { + holder.isPurchasedLiveData.removeObservers(it) + } + } + + private fun ApplicationListItemBinding.setupInstallButton( + searchApp: Application, + view: View, + holder: ViewHolder + ) { + installButton.visibility = View.VISIBLE + showMore.visibility = View.INVISIBLE + when (searchApp.status) { + Status.INSTALLED -> { + handleInstalled(searchApp) + } + Status.UPDATABLE -> { + handleUpdatable(searchApp) + } + Status.UNAVAILABLE -> { + handleUnavailable(searchApp, holder) + } + Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> { + handleDownloading(searchApp) + } + Status.INSTALLING -> { + handleInstalling() + } + Status.BLOCKED -> { + handleBlocked(view) + } + Status.INSTALLATION_ISSUE -> { + handleInstallationIssue(view, searchApp) + } + else -> { + Timber.w("ApplicationListRVAdapter: Unknown status") + } + } + } + + private fun ApplicationListItemBinding.setupShowMoreButton() { + installButton.visibility = View.INVISIBLE + showMore.visibility = View.VISIBLE + progressBarInstall.visibility = View.GONE + } + + private fun ApplicationListItemBinding.handleInstallationIssue( + view: View, + searchApp: Application, + ) { + progressBarInstall.visibility = View.GONE + if (lifecycleOwner == null) { + return + } + + appInfoFetchViewModel.isAppFaulty(searchApp).observe(lifecycleOwner!!) { + updateInstallButton(it, view, searchApp) + } + } + + private fun ApplicationListItemBinding.updateInstallButton( + faultyAppResult: Pair, + view: View, + searchApp: Application + ) { + installButton.apply { + if (faultyAppResult.first) disableInstallButton() else enableInstallButton() + text = getInstallationIssueText(faultyAppResult, view) + backgroundTintList = + ContextCompat.getColorStateList(view.context, android.R.color.transparent) + setOnClickListener { + installApplication(searchApp) + } + } + } + + private fun getInstallationIssueText( + faultyAppResult: Pair, + view: View + ) = + if (faultyAppResult.second.contentEquals(InstallerService.INSTALL_FAILED_UPDATE_INCOMPATIBLE)) + view.context.getText(R.string.update) + else + view.context.getString(R.string.retry) + + private fun ApplicationListItemBinding.handleBlocked(view: View) { + installButton.apply { + isEnabled = true + setOnClickListener { + val errorMsg = when (mainActivityViewModel.getUser()) { + User.ANONYMOUS, + User.NO_GOOGLE, + User.UNAVAILABLE -> view.context.getString(R.string.install_blocked_anonymous) + User.GOOGLE -> view.context.getString(R.string.install_blocked_google) + } + if (errorMsg.isNotBlank()) { + Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() + } + } + } + progressBarInstall.visibility = View.GONE + } + + private fun ApplicationListItemBinding.handleInstalling() { + installButton.apply { + disableInstallButton() + text = context.getText(R.string.installing) + } + progressBarInstall.visibility = View.GONE + } + + private fun ApplicationListItemBinding.handleDownloading( + searchApp: Application, + ) { + installButton.apply { + enableInstallButton() + text = context.getString(R.string.cancel) + setOnClickListener { + cancelDownload(searchApp) + } + progressBarInstall.visibility = View.GONE + } + progressBarInstall.visibility = View.GONE + } + + private fun ApplicationListItemBinding.handleUnavailable( + searchApp: Application, + holder: ViewHolder, + ) { + installButton.apply { + updateUIByPaymentType(searchApp, this, this@handleUnavailable, holder) + setOnClickListener { + if (mainActivityViewModel.checkUnsupportedApplication(searchApp, context)) { + return@setOnClickListener + } + if (searchApp.isFree || searchApp.isPurchased) { + disableInstallButton() + text = context.getText(R.string.cancel) + installApplication(searchApp) + } else { + paidAppHandler?.invoke(searchApp) + } + } + } + } + + private fun updateUIByPaymentType( + searchApp: Application, + materialButton: MaterialButton, + applicationListItemBinding: ApplicationListItemBinding, + holder: ViewHolder + ) { + when { + mainActivityViewModel.checkUnsupportedApplication(searchApp) -> { + materialButton.enableInstallButton() + materialButton.text = materialButton.context.getString(R.string.not_available) + applicationListItemBinding.progressBarInstall.visibility = View.GONE + } + searchApp.isFree -> { + materialButton.enableInstallButton() + materialButton.text = materialButton.context.getString(R.string.install) + materialButton.strokeColor = + ContextCompat.getColorStateList(holder.itemView.context, R.color.light_grey) + applicationListItemBinding.progressBarInstall.visibility = View.GONE + } + else -> { + materialButton.disableInstallButton() + materialButton.text = "" + applicationListItemBinding.progressBarInstall.visibility = View.VISIBLE + if (appInfoFetchViewModel.isAnonymousUser()) { + materialButton.enableInstallButton() + applicationListItemBinding.progressBarInstall.visibility = View.GONE + materialButton.text = searchApp.price + return + } + holder.isPurchasedLiveData = appInfoFetchViewModel.isAppPurchased(searchApp) + if (lifecycleOwner == null) { + return + } + holder.isPurchasedLiveData.observe(lifecycleOwner!!) { + materialButton.enableInstallButton() + applicationListItemBinding.progressBarInstall.visibility = View.GONE + materialButton.text = + if (it) materialButton.context.getString(R.string.install) else searchApp.price + } + } + } + } + + private fun ApplicationListItemBinding.handleUpdatable( + searchApp: Application + ) { + installButton.apply { + enableInstallButton(Status.UPDATABLE) + text = if (mainActivityViewModel.checkUnsupportedApplication(searchApp)) + context.getString(R.string.not_available) + else context.getString(R.string.update) + setOnClickListener { + if (mainActivityViewModel.checkUnsupportedApplication(searchApp, context)) { + return@setOnClickListener + } + if (searchApp.package_name == context.packageName) { + showUpdateConfirmationDialog(context, searchApp) + } else { + installApplication(searchApp) + } + } + } + progressBarInstall.visibility = View.GONE + } + + private fun ApplicationListItemBinding.handleInstalled( + searchApp: Application, + ) { + installButton.apply { + enableInstallButton(Status.INSTALLED) + text = context.getString(R.string.open) + setOnClickListener { + if (searchApp.is_pwa) { + mainActivityViewModel.launchPwa(searchApp) + } else { + mainActivityViewModel.getLaunchIntentForPackageName(searchApp.package_name)?.let { + context.startActivity(it) + } + } + } + } + progressBarInstall.visibility = View.GONE + } + + private fun showUpdateConfirmationDialog(context: Context, searchApp: Application) { + AlertDialog.Builder(context).apply { + setTitle(R.string.own_update_warning_title) + setMessage(R.string.own_update_warning_description) + setPositiveButton(android.R.string.ok) {_, _ -> + installApplication(searchApp) + } + setNegativeButton(android.R.string.cancel, null) + }.show() + } + + fun setData(newList: List, optionalCategory: String? = null) { + optionalCategory?.let { + this.optionalCategory = it + } + currentList.forEach { + newList.find { item -> item._id == it._id }?.let { foundItem -> + foundItem.privacyScore = it.privacyScore + } + } + this.submitList(newList.map { it.copy() }) + } + + private fun installApplication(searchApp: Application) { + applicationInstaller.installApplication(searchApp) + } + + private fun cancelDownload(searchApp: Application) { + applicationInstaller.cancelDownload(searchApp) + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + lifecycleOwner = null + paidAppHandler = null + } + + override fun onViewRecycled(holder: ViewHolder) { + privacyInfoViewModel.cancelAppPrivacyInfoFetch(holder.app) + super.onViewRecycled(holder) + } +} diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt index d23390c89f34ce90f347ff89fa4e36d07c4e99f1..172c3b56755a277fbf3e2e46c6874cc3a2c32e2a 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListViewModel.kt @@ -1,133 +1,133 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.ui.applicationlist - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class ApplicationListViewModel @Inject constructor( - private val applicationRepository: ApplicationRepository -) : ViewModel() { - - val appListLiveData: MutableLiveData>?> = MutableLiveData() - val exceptionsLiveData: MutableLiveData> = MutableLiveData() - val exceptions = ArrayList() - - private var isLoading = false - - private var nextPageUrl: String? = null - - fun loadList(category: String, source: String) { - if (isLoading) { - return - } - val sourceType = Source.fromString(source) - val isCleanApk = sourceType != Source.PLAY_STORE - - viewModelScope.launch(Dispatchers.IO) { - isLoading = true - val result = applicationRepository.getAppsListBasedOnCategory( - category, - nextPageUrl, - sourceType - ).apply { - isLoading = false - } - - result.data?.let { - appListLiveData.postValue(ResultSupreme.create(ResultStatus.OK, it.first)) - updateNextPageUrl(it.second) - } - - if (!result.isSuccess()) { - val exception = - if (isCleanApk) CleanApkException( - result.isTimeout(), - result.message.ifBlank { "Data load error" } - ) else GPlayException( - result.isTimeout(), - result.message.ifBlank { "Data load error" } - ) - exceptions.add(exception) - exceptionsLiveData.postValue(exceptions) - } - } - } - - private fun updateNextPageUrl(nextPageUrl: String?) { - this.nextPageUrl = nextPageUrl - } - - /** - * @return returns true if there is changes in data, otherwise false - */ - fun isFusedAppUpdated( - newApplications: List, - oldApplications: List - ): Boolean { - return applicationRepository.isAnyFusedAppUpdated(newApplications, oldApplications) - } - - fun loadMore(category: String) { - viewModelScope.launch(Dispatchers.IO) { - - if (isLoading || nextPageUrl.isNullOrEmpty()) { - return@launch - } - - isLoading = true - val result = applicationRepository.getAppsListBasedOnCategory( - category, - nextPageUrl, - Source.PLAY_STORE - ) - isLoading = false - - result.data?.let { - val appList = appendAppList(it) - val resultSupreme = ResultSupreme.create(ResultStatus.OK, appList) - appListLiveData.postValue(resultSupreme) - - updateNextPageUrl(it.second) - } - } - } - - private fun appendAppList(it: Pair, String>): List? { - val currentAppList = appListLiveData.value?.data?.toMutableList() - currentAppList?.removeIf { item -> item.isPlaceHolder } - return currentAppList?.plus(it.first) - } - - fun hasAnyAppInstallStatusChanged(currentList: List) = - applicationRepository.isAnyAppInstallStatusChanged(currentList) -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.applicationlist + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.ResultSupreme +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.login.exceptions.CleanApkException +import foundation.e.apps.data.login.exceptions.GPlayException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ApplicationListViewModel @Inject constructor( + private val applicationRepository: ApplicationRepository +) : ViewModel() { + + val appListLiveData: MutableLiveData>?> = MutableLiveData() + val exceptionsLiveData: MutableLiveData> = MutableLiveData() + val exceptions = ArrayList() + + private var isLoading = false + + private var nextPageUrl: String? = null + + fun loadList(category: String, source: String) { + if (isLoading) { + return + } + val sourceType = Source.fromString(source) + val isCleanApk = sourceType != Source.PLAY_STORE + + viewModelScope.launch(Dispatchers.IO) { + isLoading = true + val result = applicationRepository.getAppsListBasedOnCategory( + category, + nextPageUrl, + sourceType + ).apply { + isLoading = false + } + + result.data?.let { + appListLiveData.postValue(ResultSupreme.create(ResultStatus.OK, it.first)) + updateNextPageUrl(it.second) + } + + if (!result.isSuccess()) { + val exception = + if (isCleanApk) CleanApkException( + result.isTimeout(), + result.message.ifBlank { "Data load error" } + ) else GPlayException( + result.isTimeout(), + result.message.ifBlank { "Data load error" } + ) + exceptions.add(exception) + exceptionsLiveData.postValue(exceptions) + } + } + } + + private fun updateNextPageUrl(nextPageUrl: String?) { + this.nextPageUrl = nextPageUrl + } + + /** + * @return returns true if there is changes in data, otherwise false + */ + fun isFusedAppUpdated( + newApplications: List, + oldApplications: List + ): Boolean { + return applicationRepository.isAnyFusedAppUpdated(newApplications, oldApplications) + } + + fun loadMore(category: String) { + viewModelScope.launch(Dispatchers.IO) { + + if (isLoading || nextPageUrl.isNullOrEmpty()) { + return@launch + } + + isLoading = true + val result = applicationRepository.getAppsListBasedOnCategory( + category, + nextPageUrl, + Source.PLAY_STORE + ) + isLoading = false + + result.data?.let { + val appList = appendAppList(it) + val resultSupreme = ResultSupreme.create(ResultStatus.OK, appList) + appListLiveData.postValue(resultSupreme) + + updateNextPageUrl(it.second) + } + } + } + + private fun appendAppList(it: Pair, String>): List? { + val currentAppList = appListLiveData.value?.data?.toMutableList() + currentAppList?.removeIf { item -> item.isPlaceHolder } + return currentAppList?.plus(it.first) + } + + fun hasAnyAppInstallStatusChanged(currentList: List) = + applicationRepository.isAnyAppInstallStatusChanged(currentList) +} diff --git a/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt index 85dc5fde1dcfdfc91f579464ec5086cfa8111807..5b497276034a04ced2944d2c489f6d8412ec9015 100644 --- a/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/categories/CategoriesViewModel.kt @@ -1,72 +1,72 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.ui.categories - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.data.Category -import foundation.e.apps.data.application.utils.CategoryType -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class CategoriesViewModel @Inject constructor( - private val applicationRepository: ApplicationRepository -) : ViewModel() { - - val categoriesList: MutableLiveData> = - MutableLiveData() - val exceptionsLiveData: MutableLiveData> = MutableLiveData() - val exceptions = ArrayList() - - - fun loadCategoriesList(type: CategoryType) { - viewModelScope.launch { - val categoriesData = applicationRepository.getCategoriesList(type) - val categories: MutableList = mutableListOf() - exceptions.clear() - - for (data in categoriesData) { - if (data.status != ResultStatus.OK) { - val error = if (data.source == Source.PLAY_STORE) GPlayException( - data.status == ResultStatus.TIMEOUT, - data.status.message.ifBlank { "Data load error" } - ) else CleanApkException( - data.status == ResultStatus.TIMEOUT, - data.status.message.ifBlank { "Data load error" } - ) - exceptions.add(error) - continue - } - categories.addAll(data.categories) - } - - categories.sortBy { item -> item.title.lowercase() } - categoriesList.postValue(categories) - exceptionsLiveData.postValue(exceptions) - } - } -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.categories + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Category +import foundation.e.apps.data.application.utils.CategoryType +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.login.exceptions.CleanApkException +import foundation.e.apps.data.login.exceptions.GPlayException +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class CategoriesViewModel @Inject constructor( + private val applicationRepository: ApplicationRepository +) : ViewModel() { + + val categoriesList: MutableLiveData> = + MutableLiveData() + val exceptionsLiveData: MutableLiveData> = MutableLiveData() + val exceptions = ArrayList() + + + fun loadCategoriesList(type: CategoryType) { + viewModelScope.launch { + val categoriesData = applicationRepository.getCategoriesList(type) + val categories: MutableList = mutableListOf() + exceptions.clear() + + for (data in categoriesData) { + if (data.status != ResultStatus.OK) { + val error = if (data.source == Source.PLAY_STORE) GPlayException( + data.status == ResultStatus.TIMEOUT, + data.status.message.ifBlank { "Data load error" } + ) else CleanApkException( + data.status == ResultStatus.TIMEOUT, + data.status.message.ifBlank { "Data load error" } + ) + exceptions.add(error) + continue + } + categories.addAll(data.categories) + } + + categories.sortBy { item -> item.title.lowercase() } + categoriesList.postValue(categories) + exceptionsLiveData.postValue(exceptions) + } + } +} diff --git a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt index a98aaca4154c8a8f2c5e72e5abd76ad4f3dda54b..39507124cb992243a46b48e4ebe9e542bc0411aa 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/model/HomeChildRVAdapter.kt @@ -1,299 +1,299 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.ui.home.model - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleOwner -import androidx.navigation.findNavController -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import coil.load -import com.facebook.shimmer.Shimmer -import com.facebook.shimmer.ShimmerDrawable -import com.google.android.material.button.MaterialButton -import com.google.android.material.snackbar.Snackbar -import foundation.e.apps.R -import foundation.e.apps.data.cleanapk.CleanApkRetrofit -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.enums.User -import foundation.e.apps.data.application.ApplicationInstaller -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.enums.Source -import foundation.e.apps.databinding.HomeChildListItemBinding -import foundation.e.apps.ui.AppInfoFetchViewModel -import foundation.e.apps.ui.MainActivityViewModel -import foundation.e.apps.ui.applicationlist.ApplicationDiffUtil -import foundation.e.apps.ui.home.HomeFragmentDirections -import foundation.e.apps.utils.disableInstallButton -import foundation.e.apps.utils.enableInstallButton - -class HomeChildRVAdapter( - private var applicationInstaller: ApplicationInstaller?, - private val appInfoFetchViewModel: AppInfoFetchViewModel, - private val mainActivityViewModel: MainActivityViewModel, - private var lifecycleOwner: LifecycleOwner?, - private var paidAppHandler: ((Application) -> Unit)? = null -) : ListAdapter(ApplicationDiffUtil()) { - - private val shimmer = Shimmer.ColorHighlightBuilder() - .setDuration(500) - .setBaseAlpha(0.7f) - .setDirection(Shimmer.Direction.LEFT_TO_RIGHT) - .setHighlightAlpha(0.6f) - .setAutoStart(true) - .build() - - inner class ViewHolder(val binding: HomeChildListItemBinding) : - RecyclerView.ViewHolder(binding.root) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val viewHolder = ViewHolder( - HomeChildListItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) - return viewHolder - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val homeApp = getItem(position) - val shimmerDrawable = ShimmerDrawable().apply { setShimmer(shimmer) } - - holder.binding.apply { - if (homeApp.source == Source.PWA || homeApp.source == Source.OPEN_SOURCE) { - appIcon.load(CleanApkRetrofit.ASSET_URL + homeApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } else { - appIcon.load(homeApp.icon_image_path) { - placeholder(shimmerDrawable) - } - } - appName.text = homeApp.name - homeLayout.setOnClickListener { - val action = HomeFragmentDirections.actionHomeFragmentToApplicationFragment( - homeApp.package_name, - homeApp._id, - homeApp.source, - homeApp.category, - homeApp.isGplayReplaced, - homeApp.isPurchased - ) - holder.itemView.findNavController().navigate(action) - } - - when (homeApp.status) { - Status.INSTALLED -> { - handleInstalled(homeApp) - } - Status.UPDATABLE -> { - handleUpdatable(homeApp) - } - Status.UNAVAILABLE -> { - handleUnavailable(homeApp, holder) - } - Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> { - handleQueued(homeApp) - } - Status.INSTALLING -> { - handleInstalling() - } - Status.BLOCKED -> { - handleBlocked() - } - Status.INSTALLATION_ISSUE -> { - handleInstallationIssue(homeApp) - } - else -> {} - } - } - } - - private fun HomeChildListItemBinding.handleInstallationIssue( - homeApp: Application - ) { - installButton.apply { - enableInstallButton() - text = context.getString(R.string.retry) - setOnClickListener { - installApplication(homeApp) - } - } - progressBarInstall.visibility = View.GONE - } - - private fun HomeChildListItemBinding.handleBlocked() { - val view = this.root - installButton.enableInstallButton() - installButton.setOnClickListener { - val errorMsg = when (mainActivityViewModel.getUser()) { - User.ANONYMOUS, - User.NO_GOOGLE, - User.UNAVAILABLE -> view.context.getString(R.string.install_blocked_anonymous) - User.GOOGLE -> view.context.getString(R.string.install_blocked_google) - } - if (errorMsg.isNotBlank()) { - Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() - } - } - progressBarInstall.visibility = View.GONE - } - - private fun HomeChildListItemBinding.handleInstalling() { - installButton.apply { - disableInstallButton() - text = context.getString(R.string.installing) - } - progressBarInstall.visibility = View.GONE - } - - private fun HomeChildListItemBinding.handleQueued( - homeApp: Application - ) { - installButton.apply { - enableInstallButton() - text = context.getString(R.string.cancel) - setTextColor(context.getColor(R.color.colorAccent)) - backgroundTintList = ContextCompat.getColorStateList( - context, - android.R.color.transparent - ) - strokeColor = - ContextCompat.getColorStateList(context, R.color.colorAccent) - - setOnClickListener { - cancelDownload(homeApp) - } - } - progressBarInstall.visibility = View.GONE - } - - private fun HomeChildListItemBinding.handleUnavailable( - homeApp: Application, - holder: ViewHolder, - ) { - installButton.apply { - updateUIByPaymentType(homeApp, this, holder.binding) - setOnClickListener { - if (mainActivityViewModel.checkUnsupportedApplication(homeApp, context)) { - return@setOnClickListener - } - if (homeApp.isFree) { - disableInstallButton() - text = context.getString(R.string.cancel) - installApplication(homeApp) - } else { - paidAppHandler?.invoke(homeApp) - } - } - } - } - - private fun HomeChildListItemBinding.handleUpdatable( - homeApp: Application - ) { - installButton.apply { - enableInstallButton(Status.UPDATABLE) - text = if (mainActivityViewModel.checkUnsupportedApplication(homeApp)) - context.getString(R.string.not_available) - else context.getString(R.string.update) - setOnClickListener { - if (mainActivityViewModel.checkUnsupportedApplication(homeApp, context)) { - return@setOnClickListener - } - installApplication(homeApp) - } - } - progressBarInstall.visibility = View.GONE - } - - private fun HomeChildListItemBinding.handleInstalled( - homeApp: Application - ) { - installButton.apply { - enableInstallButton(Status.INSTALLED) - text = context.getString(R.string.open) - setOnClickListener { - if (homeApp.is_pwa) { - mainActivityViewModel.launchPwa(homeApp) - } else { - mainActivityViewModel.getLaunchIntentForPackageName(homeApp.package_name)?.let { - context.startActivity(it) - } - } - } - } - progressBarInstall.visibility = View.GONE - } - - private fun updateUIByPaymentType( - homeApp: Application, - materialButton: MaterialButton, - homeChildListItemBinding: HomeChildListItemBinding - ) { - when { - mainActivityViewModel.checkUnsupportedApplication(homeApp) -> { - materialButton.enableInstallButton() - materialButton.text = materialButton.context.getString(R.string.not_available) - } - homeApp.isFree -> { - materialButton.enableInstallButton() - materialButton.text = materialButton.context.getString(R.string.install) - homeChildListItemBinding.progressBarInstall.visibility = View.GONE - } - else -> { - materialButton.disableInstallButton() - materialButton.text = "" - homeChildListItemBinding.progressBarInstall.visibility = View.VISIBLE - lifecycleOwner?.let { - appInfoFetchViewModel.isAppPurchased(homeApp).observe(it) { - homeChildListItemBinding.progressBarInstall.visibility = View.GONE - materialButton.enableInstallButton() - materialButton.text = - if (it) materialButton.context.getString(R.string.install) else homeApp.price - } - } - } - } - } - - fun setData(newList: List) { - this.submitList(newList.map { it.copy() }) - } - - private fun installApplication(homeApp: Application) { - applicationInstaller?.installApplication(homeApp) - } - - private fun cancelDownload(homeApp: Application) { - applicationInstaller?.cancelDownload(homeApp) - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - super.onDetachedFromRecyclerView(recyclerView) - lifecycleOwner = null - paidAppHandler = null - applicationInstaller = null - } -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.home.model + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import androidx.navigation.findNavController +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.facebook.shimmer.Shimmer +import com.facebook.shimmer.ShimmerDrawable +import com.google.android.material.button.MaterialButton +import com.google.android.material.snackbar.Snackbar +import foundation.e.apps.R +import foundation.e.apps.data.application.ApplicationInstaller +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.CleanApkRetrofit +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.enums.User +import foundation.e.apps.databinding.HomeChildListItemBinding +import foundation.e.apps.ui.AppInfoFetchViewModel +import foundation.e.apps.ui.MainActivityViewModel +import foundation.e.apps.ui.applicationlist.ApplicationDiffUtil +import foundation.e.apps.ui.home.HomeFragmentDirections +import foundation.e.apps.utils.disableInstallButton +import foundation.e.apps.utils.enableInstallButton + +class HomeChildRVAdapter( + private var applicationInstaller: ApplicationInstaller?, + private val appInfoFetchViewModel: AppInfoFetchViewModel, + private val mainActivityViewModel: MainActivityViewModel, + private var lifecycleOwner: LifecycleOwner?, + private var paidAppHandler: ((Application) -> Unit)? = null +) : ListAdapter(ApplicationDiffUtil()) { + + private val shimmer = Shimmer.ColorHighlightBuilder() + .setDuration(500) + .setBaseAlpha(0.7f) + .setDirection(Shimmer.Direction.LEFT_TO_RIGHT) + .setHighlightAlpha(0.6f) + .setAutoStart(true) + .build() + + inner class ViewHolder(val binding: HomeChildListItemBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val viewHolder = ViewHolder( + HomeChildListItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + return viewHolder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val homeApp = getItem(position) + val shimmerDrawable = ShimmerDrawable().apply { setShimmer(shimmer) } + + holder.binding.apply { + if (homeApp.source == Source.PWA || homeApp.source == Source.OPEN_SOURCE) { + appIcon.load(CleanApkRetrofit.ASSET_URL + homeApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } else { + appIcon.load(homeApp.icon_image_path) { + placeholder(shimmerDrawable) + } + } + appName.text = homeApp.name + homeLayout.setOnClickListener { + val action = HomeFragmentDirections.actionHomeFragmentToApplicationFragment( + homeApp.package_name, + homeApp._id, + homeApp.source, + homeApp.category, + homeApp.isGplayReplaced, + homeApp.isPurchased + ) + holder.itemView.findNavController().navigate(action) + } + + when (homeApp.status) { + Status.INSTALLED -> { + handleInstalled(homeApp) + } + Status.UPDATABLE -> { + handleUpdatable(homeApp) + } + Status.UNAVAILABLE -> { + handleUnavailable(homeApp, holder) + } + Status.QUEUED, Status.AWAITING, Status.DOWNLOADING, Status.DOWNLOADED -> { + handleQueued(homeApp) + } + Status.INSTALLING -> { + handleInstalling() + } + Status.BLOCKED -> { + handleBlocked() + } + Status.INSTALLATION_ISSUE -> { + handleInstallationIssue(homeApp) + } + else -> {} + } + } + } + + private fun HomeChildListItemBinding.handleInstallationIssue( + homeApp: Application + ) { + installButton.apply { + enableInstallButton() + text = context.getString(R.string.retry) + setOnClickListener { + installApplication(homeApp) + } + } + progressBarInstall.visibility = View.GONE + } + + private fun HomeChildListItemBinding.handleBlocked() { + val view = this.root + installButton.enableInstallButton() + installButton.setOnClickListener { + val errorMsg = when (mainActivityViewModel.getUser()) { + User.ANONYMOUS, + User.NO_GOOGLE, + User.UNAVAILABLE -> view.context.getString(R.string.install_blocked_anonymous) + User.GOOGLE -> view.context.getString(R.string.install_blocked_google) + } + if (errorMsg.isNotBlank()) { + Snackbar.make(view, errorMsg, Snackbar.LENGTH_SHORT).show() + } + } + progressBarInstall.visibility = View.GONE + } + + private fun HomeChildListItemBinding.handleInstalling() { + installButton.apply { + disableInstallButton() + text = context.getString(R.string.installing) + } + progressBarInstall.visibility = View.GONE + } + + private fun HomeChildListItemBinding.handleQueued( + homeApp: Application + ) { + installButton.apply { + enableInstallButton() + text = context.getString(R.string.cancel) + setTextColor(context.getColor(R.color.colorAccent)) + backgroundTintList = ContextCompat.getColorStateList( + context, + android.R.color.transparent + ) + strokeColor = + ContextCompat.getColorStateList(context, R.color.colorAccent) + + setOnClickListener { + cancelDownload(homeApp) + } + } + progressBarInstall.visibility = View.GONE + } + + private fun HomeChildListItemBinding.handleUnavailable( + homeApp: Application, + holder: ViewHolder, + ) { + installButton.apply { + updateUIByPaymentType(homeApp, this, holder.binding) + setOnClickListener { + if (mainActivityViewModel.checkUnsupportedApplication(homeApp, context)) { + return@setOnClickListener + } + if (homeApp.isFree) { + disableInstallButton() + text = context.getString(R.string.cancel) + installApplication(homeApp) + } else { + paidAppHandler?.invoke(homeApp) + } + } + } + } + + private fun HomeChildListItemBinding.handleUpdatable( + homeApp: Application + ) { + installButton.apply { + enableInstallButton(Status.UPDATABLE) + text = if (mainActivityViewModel.checkUnsupportedApplication(homeApp)) + context.getString(R.string.not_available) + else context.getString(R.string.update) + setOnClickListener { + if (mainActivityViewModel.checkUnsupportedApplication(homeApp, context)) { + return@setOnClickListener + } + installApplication(homeApp) + } + } + progressBarInstall.visibility = View.GONE + } + + private fun HomeChildListItemBinding.handleInstalled( + homeApp: Application + ) { + installButton.apply { + enableInstallButton(Status.INSTALLED) + text = context.getString(R.string.open) + setOnClickListener { + if (homeApp.is_pwa) { + mainActivityViewModel.launchPwa(homeApp) + } else { + mainActivityViewModel.getLaunchIntentForPackageName(homeApp.package_name)?.let { + context.startActivity(it) + } + } + } + } + progressBarInstall.visibility = View.GONE + } + + private fun updateUIByPaymentType( + homeApp: Application, + materialButton: MaterialButton, + homeChildListItemBinding: HomeChildListItemBinding + ) { + when { + mainActivityViewModel.checkUnsupportedApplication(homeApp) -> { + materialButton.enableInstallButton() + materialButton.text = materialButton.context.getString(R.string.not_available) + } + homeApp.isFree -> { + materialButton.enableInstallButton() + materialButton.text = materialButton.context.getString(R.string.install) + homeChildListItemBinding.progressBarInstall.visibility = View.GONE + } + else -> { + materialButton.disableInstallButton() + materialButton.text = "" + homeChildListItemBinding.progressBarInstall.visibility = View.VISIBLE + lifecycleOwner?.let { + appInfoFetchViewModel.isAppPurchased(homeApp).observe(it) { + homeChildListItemBinding.progressBarInstall.visibility = View.GONE + materialButton.enableInstallButton() + materialButton.text = + if (it) materialButton.context.getString(R.string.install) else homeApp.price + } + } + } + } + } + + fun setData(newList: List) { + this.submitList(newList.map { it.copy() }) + } + + private fun installApplication(homeApp: Application) { + applicationInstaller?.installApplication(homeApp) + } + + private fun cancelDownload(homeApp: Application) { + applicationInstaller?.cancelDownload(homeApp) + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + lifecycleOwner = null + paidAppHandler = null + applicationInstaller = null + } +} diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt index 7ad1f6270823a595a0b4ff74ccfa2331ca5cafe6..7b6711bcea59da5b8a5d472e76cdb3946c3c8666 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/SignInFragment.kt @@ -7,9 +7,9 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R -import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.databinding.FragmentSignInBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate +import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.utils.showGoogleSignInAlertDialog @AndroidEntryPoint diff --git a/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt b/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt index fcd2e3d3a4553bb10e9f9b01ca5edef7971e3419..ae6f987d4dc46a79aa968ccd675a383fbedabe36 100644 --- a/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/setup/signin/google/GoogleSignInFragment.kt @@ -32,9 +32,9 @@ import androidx.navigation.findNavController import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.playstore.utils.AC2DMUtil -import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.databinding.FragmentGoogleSigninBinding import foundation.e.apps.di.CommonUtilsModule.safeNavigate +import foundation.e.apps.ui.LoginViewModel @AndroidEntryPoint class GoogleSignInFragment : Fragment(R.layout.fragment_google_signin) { diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt index c4c607812041303eb4a0d9c8de1967139a2b06aa..de4ca4a5f1c704270b0bab2999109ad5a6d126ea 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt @@ -36,10 +36,10 @@ import androidx.work.WorkManager import dagger.hilt.android.AndroidEntryPoint import foundation.e.apps.R import foundation.e.apps.data.ResultSupreme -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Status import foundation.e.apps.data.application.ApplicationInstaller import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Status import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.exceptions.GPlayException diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt index 4e8d4c9dd72fbf468a3001defc9fa8fd2cdf5edb..0d05577e4ea0a12b79f633d6ba0339d80cf7fed1 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt @@ -1,139 +1,139 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.apps.ui.updates - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.work.WorkInfo -import dagger.hilt.android.lifecycle.HiltViewModel -import foundation.e.apps.data.StoreRepository -import foundation.e.apps.data.Stores -import foundation.e.apps.data.enums.ResultStatus -import foundation.e.apps.data.enums.Status -import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.data.Application -import foundation.e.apps.data.enums.Source -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.updates.UpdatesManagerRepository -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class UpdatesViewModel @Inject constructor( - private val updatesManagerRepository: UpdatesManagerRepository, - private val applicationRepository: ApplicationRepository, - private val stores: Stores -) : ViewModel() { - - val updatesList: MutableLiveData> = MutableLiveData() - val exceptionsLiveData: MutableLiveData> = MutableLiveData() - val exceptionsList = ArrayList() - - private var previousStores = mapOf() - - fun haveSourcesChanged(): Boolean { - val newStores = stores.getStores() - if (newStores == previousStores) { - return false - } - - previousStores = newStores.toMutableMap() - return true - } - - fun loadUpdates() { - viewModelScope.launch { - exceptionsList.clear() - val updatesResult = updatesManagerRepository.getUpdates() - val ossUpdatesResult = updatesManagerRepository.getUpdatesOSS() - - updatesList.postValue( - mutableListOf().apply { - addAll(updatesResult.first) - addAll(ossUpdatesResult.first) - }.toList() - ) - - if (updatesResult.second != ResultStatus.OK) { - val status = updatesResult.second - exceptionsList.add( - GPlayException( - updatesResult.second == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } - ) - ) - } - if (ossUpdatesResult.second != ResultStatus.OK) { - val status = ossUpdatesResult.second - exceptionsList.add( - CleanApkException( - updatesResult.second == ResultStatus.TIMEOUT, - status.message.ifBlank { "Data load error" } - ) - ) - } - - exceptionsLiveData.postValue(exceptionsList) - } - } - - fun checkWorkInfoListHasAnyUpdatableWork(workInfoList: List): Boolean { - workInfoList.forEach { workInfo -> - if (listOf( - WorkInfo.State.ENQUEUED, - WorkInfo.State.RUNNING - ).contains(workInfo.state) && checkWorkIsForUpdateByTag(workInfo.tags.toList()) - ) { - return true - } - } - return false - } - - private fun checkWorkIsForUpdateByTag(tags: List): Boolean { - updatesList.value?.let { - it.find { fusedApp -> tags.contains(fusedApp._id) }?.let { foundApp -> - return listOf( - Status.INSTALLED, - Status.UPDATABLE - ).contains(applicationRepository.getFusedAppInstallationStatus(foundApp)) - } - } - return false - } - - fun hasAnyUpdatableApp(): Boolean { - return updatesList.value?.any { - it.status == Status.UPDATABLE || it.status == Status.INSTALLATION_ISSUE - } == true - } - - fun hasAnyPendingAppsForUpdate(): Boolean { - val pendingStatesForUpdate = listOf( - Status.QUEUED, - Status.AWAITING, - Status.DOWNLOADING, - Status.DOWNLOADED, - Status.INSTALLING - ) - return updatesList.value?.any { pendingStatesForUpdate.contains(it.status) } == true - } -} +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.apps.ui.updates + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.work.WorkInfo +import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.data.StoreRepository +import foundation.e.apps.data.Stores +import foundation.e.apps.data.application.ApplicationRepository +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.ResultStatus +import foundation.e.apps.data.enums.Source +import foundation.e.apps.data.enums.Status +import foundation.e.apps.data.login.exceptions.CleanApkException +import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.updates.UpdatesManagerRepository +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class UpdatesViewModel @Inject constructor( + private val updatesManagerRepository: UpdatesManagerRepository, + private val applicationRepository: ApplicationRepository, + private val stores: Stores +) : ViewModel() { + + val updatesList: MutableLiveData> = MutableLiveData() + val exceptionsLiveData: MutableLiveData> = MutableLiveData() + val exceptionsList = ArrayList() + + private var previousStores = mapOf() + + fun haveSourcesChanged(): Boolean { + val newStores = stores.getStores() + if (newStores == previousStores) { + return false + } + + previousStores = newStores.toMutableMap() + return true + } + + fun loadUpdates() { + viewModelScope.launch { + exceptionsList.clear() + val updatesResult = updatesManagerRepository.getUpdates() + val ossUpdatesResult = updatesManagerRepository.getUpdatesOSS() + + updatesList.postValue( + mutableListOf().apply { + addAll(updatesResult.first) + addAll(ossUpdatesResult.first) + }.toList() + ) + + if (updatesResult.second != ResultStatus.OK) { + val status = updatesResult.second + exceptionsList.add( + GPlayException( + updatesResult.second == ResultStatus.TIMEOUT, + status.message.ifBlank { "Data load error" } + ) + ) + } + if (ossUpdatesResult.second != ResultStatus.OK) { + val status = ossUpdatesResult.second + exceptionsList.add( + CleanApkException( + updatesResult.second == ResultStatus.TIMEOUT, + status.message.ifBlank { "Data load error" } + ) + ) + } + + exceptionsLiveData.postValue(exceptionsList) + } + } + + fun checkWorkInfoListHasAnyUpdatableWork(workInfoList: List): Boolean { + workInfoList.forEach { workInfo -> + if (listOf( + WorkInfo.State.ENQUEUED, + WorkInfo.State.RUNNING + ).contains(workInfo.state) && checkWorkIsForUpdateByTag(workInfo.tags.toList()) + ) { + return true + } + } + return false + } + + private fun checkWorkIsForUpdateByTag(tags: List): Boolean { + updatesList.value?.let { + it.find { fusedApp -> tags.contains(fusedApp._id) }?.let { foundApp -> + return listOf( + Status.INSTALLED, + Status.UPDATABLE + ).contains(applicationRepository.getFusedAppInstallationStatus(foundApp)) + } + } + return false + } + + fun hasAnyUpdatableApp(): Boolean { + return updatesList.value?.any { + it.status == Status.UPDATABLE || it.status == Status.INSTALLATION_ISSUE + } == true + } + + fun hasAnyPendingAppsForUpdate(): Boolean { + val pendingStatesForUpdate = listOf( + Status.QUEUED, + Status.AWAITING, + Status.DOWNLOADING, + Status.DOWNLOADED, + Status.INSTALLING + ) + return updatesList.value?.any { pendingStatesForUpdate.contains(it.status) } == true + } +}