diff --git a/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt b/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt index b8a9c1e8dc4907a728b6cf2a560c1e6533f16551..5bd0e8eb07e78ddff202872fa9db26f8b14c474b 100644 --- a/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/AppInfoFetchViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.fdroid.FdroidRepository import foundation.e.apps.api.fdroid.models.FdroidEntity import foundation.e.apps.api.fused.data.FusedApp @@ -27,6 +28,7 @@ class AppInfoFetchViewModel @Inject constructor( private val fdroidRepository: FdroidRepository, private val gPlayAPIRepository: GPlayAPIRepository, private val dataStoreModule: DataStoreModule, + private val blockedAppRepository: BlockedAppRepository, private val gson: Gson ) : ViewModel() { @@ -83,4 +85,8 @@ class AppInfoFetchViewModel @Inject constructor( } } } + + fun isAppInBlockedList(fusedApp: FusedApp): Boolean { + return blockedAppRepository.getBlockedAppPackages().contains(fusedApp.package_name) + } } diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index f1e809af4675c51520e3f59f398cbd506e40ebcc..57360afde4bc9c4abd492033ea23d8c943a77982 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -227,6 +227,8 @@ class MainActivity : AppCompatActivity() { if (!CommonUtilsModule.isNetworkAvailable(this)) { showNoInternet() } + + viewModel.updateAppWarningList() } private fun handleFusedDownloadQueued( diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt index 14ca337f5444cc52bdc87d3a095c4e8e0797d1d6..2012bc9efc2e27a6018954745274d5c8de30940f 100644 --- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt @@ -41,6 +41,7 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.exceptions.ApiException import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel +import foundation.e.apps.api.cleanapk.blockedApps.BlockedAppRepository import foundation.e.apps.api.fused.FusedAPIImpl import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp @@ -67,7 +68,8 @@ class MainActivityViewModel @Inject constructor( private val dataStoreModule: DataStoreModule, private val fusedAPIRepository: FusedAPIRepository, private val fusedManagerRepository: FusedManagerRepository, - private val pkgManagerModule: PkgManagerModule + private val pkgManagerModule: PkgManagerModule, + private val blockedAppRepository: BlockedAppRepository ) : ViewModel() { val authDataJson: LiveData = dataStoreModule.authData.asLiveData() @@ -489,4 +491,8 @@ class MainActivityViewModel @Inject constructor( it.status = downloadingItem?.status ?: fusedAPIRepository.getFusedAppInstallationStatus(it) } } + + fun updateAppWarningList() { + blockedAppRepository.fetchUpdateOfAppWarningList() + } } diff --git a/app/src/main/java/foundation/e/apps/api/DownloadManager.kt b/app/src/main/java/foundation/e/apps/api/DownloadManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..f5e8cbeedeb469cd565d888e587f462298f2f624 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/DownloadManager.kt @@ -0,0 +1,112 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2022 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.api + +import android.app.DownloadManager +import android.net.Uri +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import java.io.File +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +@Singleton +class DownloadManager @Inject constructor( + private val downloadManager: DownloadManager, + @Named("cacheDir") private val cacheDir: String, + private val downloadManagerQuery: DownloadManager.Query, +) { + private var isDownloading = false + + fun downloadFileInCache( + url: String, + subDirectoryPath: String = "", + fileName: String, + downloadCompleted: ((Boolean, String) -> Unit)? + ): Long { + val directoryFile = File(cacheDir + subDirectoryPath) + val downloadFile = File("$cacheDir/$fileName") + if (!directoryFile.exists()) { + directoryFile.mkdirs() + } + val request = DownloadManager.Request(Uri.parse(url)) + .setTitle("Downloading...") + .setDestinationUri(Uri.fromFile(downloadFile)) + val downloadId = downloadManager.enqueue(request) + isDownloading = true + tickerFlow(.5.seconds).onEach { + checkDownloadProgress(downloadId, downloadFile.absolutePath, downloadCompleted) + }.launchIn(CoroutineScope(Dispatchers.IO)) + return downloadId + } + + private fun checkDownloadProgress( + downloadId: Long, + filePath: String = "", + downloadCompleted: ((Boolean, String) -> Unit)? + ) { + downloadManager.query(downloadManagerQuery.setFilterById(downloadId)) + .use { cursor -> + if (cursor.moveToFirst()) { + val id = + cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)) + val status = + cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) + val totalSizeBytes = + cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) + val bytesDownloadedSoFar = + cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + Log.d( + "DownloadManager", + "checkDownloadProcess: $filePath=> $bytesDownloadedSoFar/$totalSizeBytes $status" + ) + if (status == DownloadManager.STATUS_FAILED) { + Log.d( + "DownloadManager", + "Download Failed: $filePath=> $bytesDownloadedSoFar/$totalSizeBytes $status" + ) + isDownloading = false + downloadCompleted?.invoke(false, filePath) + } else if (status == DownloadManager.STATUS_SUCCESSFUL) { + Log.d( + "DownloadManager", + "Download Successful: $filePath=> $bytesDownloadedSoFar/$totalSizeBytes $status" + ) + isDownloading = false + downloadCompleted?.invoke(true, filePath) + } + } + } + } + + private fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow { + delay(initialDelay) + while (isDownloading) { + emit(Unit) + delay(period) + } + } +} diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/blockedApps/AppWarningInfo.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/blockedApps/AppWarningInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..5175e1f19b3b6b5444013fabe43de091631457f6 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/blockedApps/AppWarningInfo.kt @@ -0,0 +1,23 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2022 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.api.cleanapk.blockedApps + +import com.google.gson.annotations.SerializedName + +data class AppWarningInfo(@SerializedName("not_working_apps") val notWorkingApps: List) diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/blockedApps/BlockedAppRepository.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/blockedApps/BlockedAppRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ab84a6627a323acc8fb6d44064965b354a6f79f --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/blockedApps/BlockedAppRepository.kt @@ -0,0 +1,70 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2022 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.api.cleanapk.blockedApps + +import com.google.gson.Gson +import foundation.e.apps.api.DownloadManager +import foundation.e.apps.manager.fused.FileManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class BlockedAppRepository @Inject constructor( + private val downloadManager: DownloadManager, + private val gson: Gson, + @Named("cacheDir") private val cacheDir: String, + @Named("ioCoroutineScope") private val coroutineScope: CoroutineScope +) { + + companion object { + private const val APP_WARNING_LIST_FILE_URL = + "https://gitlab.e.foundation/e/os/blocklist-app-lounge/-/raw/main/app-lounge-warning-list.json?inline=false" + private const val WARNING_LIST_FILE_NAME = "app-lounge-warning-list.json" + } + + private var blockedAppInfoList: AppWarningInfo? = null + + fun getBlockedAppPackages(): List { + return blockedAppInfoList?.notWorkingApps ?: listOf() + } + + fun fetchUpdateOfAppWarningList() { + downloadManager.downloadFileInCache( + APP_WARNING_LIST_FILE_URL, + fileName = "app-lounge-warning-list.json" + ) { success, path -> + if (success) { + parseBlockedAppDataFromFile(path) + } + } + } + + private fun parseBlockedAppDataFromFile(path: String) { + coroutineScope.launch { + val outputPath = "$cacheDir/warning_list/" + FileManager.moveFile("$cacheDir/", WARNING_LIST_FILE_NAME, outputPath) + val downloadedFile = File(outputPath + WARNING_LIST_FILE_NAME) + val blockedAppInfoJson = String(downloadedFile.inputStream().readBytes()) + blockedAppInfoList = gson.fromJson(blockedAppInfoJson, AppWarningInfo::class.java) + } + } +} diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index d5f360c6bfd90ad6333e6e8b5eec747370fba894..229cd21867b88804482bd49d25525b34903c09bf 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -256,6 +256,10 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { } } + if (appInfoFetchViewModel.isAppInBlockedList(it)) { + binding.snackbarLayout.visibility = View.VISIBLE + } + observeDownloadStatus(view) fetchAppTracker(it) } @@ -423,15 +427,19 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { } applicationIcon?.let { if (fusedApp.isFree) { - mainActivityViewModel.getApplication(fusedApp, it) + installApplication(fusedApp, it) } else { if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(fusedApp)) { ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, fusedApp.name), - message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + message = getString( + R.string.dialog_paidapp_message, + fusedApp.name, + fusedApp.price + ), positiveButtonText = getString(R.string.dialog_confirm), positiveButtonAction = { - mainActivityViewModel.getApplication(fusedApp, it) + installApplication(fusedApp, it) }, cancelButtonText = getString(R.string.dialog_cancel), ).show(childFragmentManager, "ApplicationFragment") @@ -444,6 +452,24 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { appSize.visibility = View.VISIBLE } + private fun installApplication( + fusedApp: FusedApp, + it: ImageView + ) { + if (appInfoFetchViewModel.isAppInBlockedList(fusedApp)) { + ApplicationDialogFragment( + title = getString(R.string.this_app_may_not_work_properly), + message = getString(R.string.may_not_work_warning_message), + positiveButtonText = getString(R.string.install_anyway), + positiveButtonAction = { + mainActivityViewModel.getApplication(fusedApp, it) + } + ).show(childFragmentManager, "ApplicationFragment") + } else { + mainActivityViewModel.getApplication(fusedApp, it) + } + } + private fun handleUpdatable( installButton: MaterialButton, view: View, diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt index b94d70af43631d0471fe4907f2075b0d05d82311..13aaf9c09954996ac262f1eb544bc7c5b3c077a5 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt @@ -160,34 +160,54 @@ class ApplicationListRVAdapter( } else -> Log.wtf(TAG, "${searchApp.package_name} is from an unknown origin") } - when (searchApp.status) { - Status.INSTALLED -> { - handleInstalled(view, searchApp) - } - Status.UPDATABLE -> { - handleUpdatable(view, searchApp) - } - Status.UNAVAILABLE -> { - handleUnavailable(view, searchApp) - } - Status.QUEUED, Status.AWAITING, Status.DOWNLOADING -> { - handleDownloading(view, searchApp) - } - Status.INSTALLING, Status.UNINSTALLING -> { - handleInstalling(view, holder) - } - Status.BLOCKED -> { - handleBlocked(view) - } - Status.INSTALLATION_ISSUE -> { - handleInstallationIssue(view, searchApp) - } + + if (appInfoFetchViewModel.isAppInBlockedList(searchApp)) { + setupShowMoreButton() + } else { + setupInstallButton(searchApp, view, holder) } showCalculatedPrivacyScoreData(searchApp, view) } } + private fun ApplicationListItemBinding.setupInstallButton( + searchApp: FusedApp, + view: View, + holder: ViewHolder + ) { + installButton.visibility = View.VISIBLE + showMore.visibility = View.GONE + when (searchApp.status) { + Status.INSTALLED -> { + handleInstalled(view, searchApp) + } + Status.UPDATABLE -> { + handleUpdatable(view, searchApp) + } + Status.UNAVAILABLE -> { + handleUnavailable(view, searchApp) + } + Status.QUEUED, Status.AWAITING, Status.DOWNLOADING -> { + handleDownloading(view, searchApp) + } + Status.INSTALLING, Status.UNINSTALLING -> { + handleInstalling(view, holder) + } + Status.BLOCKED -> { + handleBlocked(view) + } + Status.INSTALLATION_ISSUE -> { + handleInstallationIssue(view, searchApp) + } + } + } + + private fun ApplicationListItemBinding.setupShowMoreButton() { + installButton.visibility = View.GONE + showMore.visibility = View.VISIBLE + } + private fun ApplicationListItemBinding.handleInstallationIssue( view: View, searchApp: FusedApp, diff --git a/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt b/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt index a7b5250f1ad27d2db854ede8049cb9b2c0d91f52..e85f97965e20b79bf20961c08c275808e504cd83 100644 --- a/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt +++ b/app/src/main/java/foundation/e/apps/manager/download/DownloadManagerUtils.kt @@ -54,16 +54,21 @@ class DownloadManagerUtils @Inject constructor( mutex.withLock { delay(1500) // Waiting for downloadmanager to publish the progress of last bytes val fusedDownload = fusedManagerRepository.getFusedDownload(downloadId) - fusedDownload.downloadIdMap[downloadId] = true - fusedManagerRepository.updateFusedDownload(fusedDownload) - val downloaded = fusedDownload.downloadIdMap.values.filter { it }.size - Log.d( - TAG, - "===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $downloaded/${fusedDownload.downloadIdMap.size} " - ) - if (downloaded == fusedDownload.downloadIdMap.size) { - fusedManagerRepository.moveOBBFileToOBBDirectory(fusedDownload) - fusedManagerRepository.updateDownloadStatus(fusedDownload, Status.INSTALLING) + if (fusedDownload.id.isNotEmpty()) { + fusedDownload.downloadIdMap[downloadId] = true + fusedManagerRepository.updateFusedDownload(fusedDownload) + val downloaded = fusedDownload.downloadIdMap.values.filter { it }.size + Log.d( + TAG, + "===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $downloaded/${fusedDownload.downloadIdMap.size} " + ) + if (downloaded == fusedDownload.downloadIdMap.size) { + fusedManagerRepository.moveOBBFileToOBBDirectory(fusedDownload) + fusedManagerRepository.updateDownloadStatus( + fusedDownload, + Status.INSTALLING + ) + } } } } diff --git a/app/src/main/java/foundation/e/apps/utils/ViewUtils.kt b/app/src/main/java/foundation/e/apps/utils/ViewUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..a40e4a1d3a3a53500e4e67607bc7102832fad8e1 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/ViewUtils.kt @@ -0,0 +1,11 @@ +package foundation.e.apps.utils + +import android.content.res.Resources + +fun Int.toDP(): Int { + return (this / Resources.getSystem().displayMetrics.density).toInt() +} + +fun Int.toPX(): Int { + return (this * Resources.getSystem().displayMetrics.density).toInt() +} diff --git a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt index e00439dbba2eef0b15ec99764f91f1eda20de02c..8b2b6e000b306e72daf6acd14b18b9736ca4c0b0 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt @@ -34,6 +34,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import okhttp3.Cache import java.lang.reflect.Modifier import javax.inject.Named @@ -153,4 +156,10 @@ object CommonUtilsModule { e.printStackTrace() } } + + @Provides + @Named("ioCoroutineScope") + fun getIOCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.IO) + } } diff --git a/app/src/main/res/drawable/ic_warning_white.xml b/app/src/main/res/drawable/ic_warning_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..e885ef12faad1ee4a31b139a0ef50c6109882319 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning_white.xml @@ -0,0 +1,26 @@ + + + + diff --git a/app/src/main/res/layout/application_list_item.xml b/app/src/main/res/layout/application_list_item.xml index eb8ad1af61459a009d972aed157817dac9e8ef3f..a9d60a107c1decbdedec4f5e23856c6a171a721e 100644 --- a/app/src/main/res/layout/application_list_item.xml +++ b/app/src/main/res/layout/application_list_item.xml @@ -103,6 +103,17 @@ app:layout_constraintBottom_toBottomOf="@+id/app_rating_bar" app:layout_constraintLeft_toRightOf="@+id/app_rating_bar" /> + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e0eb34c5cdfd604ef44b5da4e0f9d42bc20b445..79d6cf1fcad86444dc859e7484ba0a6d79e5e98b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,6 +105,8 @@ Unknown Error! There is not enough space available to download this application! Your application was not found. + Show more + Update All @@ -172,5 +174,9 @@ Unsupported app! The app %s is currently unsupported. This can be because the app is not yet widely released, or there is some other error. + Install anyway + This app may not work properly! + Forcing installation will allow you to download and install it, but it won\'t guarantee that it will work.<br /><br />Attempting to install unsupported apps may cause crashes or slow down the system.<br /><br />We\'re working on improving compatiblity with this application in a near future. +