From 294df7aa4155b750e4a288c418948d76adbddf15 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Wed, 10 Jul 2024 12:45:11 +0700 Subject: [PATCH] Switch to an expedited worker to export apps & downloads Signed-off-by: Aayush Gupta --- .../aurora/store/data/work/ExportWorker.kt | 145 ++++++++++++++++++ .../java/com/aurora/store/util/ApkCopier.kt | 93 ----------- .../view/ui/sheets/ManualDownloadSheet.kt | 2 +- .../store/viewmodel/sheets/SheetsViewModel.kt | 16 +- 4 files changed, 150 insertions(+), 106 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/data/work/ExportWorker.kt delete mode 100644 app/src/main/java/com/aurora/store/util/ApkCopier.kt diff --git a/app/src/main/java/com/aurora/store/data/work/ExportWorker.kt b/app/src/main/java/com/aurora/store/data/work/ExportWorker.kt new file mode 100644 index 000000000..00ea2dbde --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/work/ExportWorker.kt @@ -0,0 +1,145 @@ +package com.aurora.store.data.work + +import android.content.Context +import android.content.pm.PackageManager +import android.net.Uri +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.aurora.store.util.PackageUtil.getPackageInfo +import com.aurora.store.util.PathUtil +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * An expedited worker to export downloaded or installed apps. + */ +@HiltWorker +class ExportWorker @AssistedInject constructor( + @Assisted private val appContext: Context, + @Assisted workerParams: WorkerParameters +) : CoroutineWorker(appContext, workerParams) { + + companion object { + private const val TAG = "ExportWorker" + + private const val URI = "URI" + private const val IS_DOWNLOAD = "IS_DOWNLOAD" + private const val PACKAGE_NAME = "PACKAGE_NAME" + private const val VERSION_CODE = "VERSION_CODE" + + /** + * Exports the installed package to the given URI + * @param packageName packageName of the app to export + * @see [ExportWorker] + */ + fun exportInstalledApp(context: Context, packageName: String, uri: Uri) { + val inputData = Data.Builder() + .putBoolean(IS_DOWNLOAD, false) + .putString(URI, uri.toString()) + .putString(PACKAGE_NAME, packageName) + .build() + + val oneTimeWorkRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST) + .build() + + Log.i(TAG, "Exporting $packageName") + WorkManager.getInstance(context).enqueue(oneTimeWorkRequest) + } + + /** + * Exports the given download to the URI + * @param packageName Name of the package to export + * @param versionCode version of the package + * @see [ExportWorker] + */ + fun exportDownloadedApp(context: Context, packageName: String, versionCode: Int, uri: Uri) { + val inputData = Data.Builder() + .putBoolean(IS_DOWNLOAD, true) + .putString(URI, uri.toString()) + .putString(PACKAGE_NAME, packageName) + .putInt(VERSION_CODE, versionCode) + .build() + + val oneTimeWorkRequest = OneTimeWorkRequestBuilder() + .setInputData(inputData) + .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST) + .build() + + Log.i(TAG, "Exporting download for ${packageName}/${versionCode}") + WorkManager.getInstance(context).enqueue(oneTimeWorkRequest) + } + } + + override suspend fun doWork(): Result { + val isDownload = inputData.getBoolean(IS_DOWNLOAD, false) + val uri = Uri.parse(inputData.getString(URI)) + val packageName = inputData.getString(PACKAGE_NAME) + val versionCode = inputData.getInt(VERSION_CODE, -1) + + if (packageName.isNullOrEmpty() || isDownload && versionCode == -1) { + Log.e(TAG, "Input data is corrupt, bailing out!") + return Result.failure() + } + + try { + if (isDownload) { + copyDownloadedApp(packageName, versionCode, uri) + } else { + copyInstalledApp(packageName, uri) + } + } catch (exception: Exception) { + Log.e(TAG, "Failed to export $packageName", exception) + return Result.failure() + } + + return Result.success() + } + + private fun copyInstalledApp(packageName: String, uri: Uri) { + val packageInfo = getPackageInfo(appContext, packageName, PackageManager.GET_META_DATA) + val fileList: MutableList = mutableListOf() + + fileList.add(File(packageInfo.applicationInfo.sourceDir)) + packageInfo.applicationInfo.splitSourceDirs?.let { splits -> + fileList.addAll(splits.map { File(it) }) + } + + bundleAllAPKs(fileList.filterNotNull(), uri) + } + + private fun copyDownloadedApp(packageName: String, versionCode: Int, uri: Uri) { + return bundleAllAPKs( + PathUtil.getAppDownloadDir(appContext, packageName, versionCode).listFiles()!!.toList(), + uri + ) + } + + /** + * Bundles all the given APKs to a zip file + * @param fileList List of APKs to add to the zip + * @param uri [Uri] of the file to write the APKs + */ + private fun bundleAllAPKs(fileList: List, uri: Uri) { + ZipOutputStream(appContext.contentResolver.openOutputStream(uri)).use { zipOutput -> + fileList.forEach { file -> + file.inputStream().use { input -> + val zipEntry = ZipEntry(file.name) + zipOutput.putNextEntry(zipEntry) + input.copyTo(zipOutput) + zipOutput.closeEntry() + } + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/util/ApkCopier.kt b/app/src/main/java/com/aurora/store/util/ApkCopier.kt deleted file mode 100644 index 9e77ba3f0..000000000 --- a/app/src/main/java/com/aurora/store/util/ApkCopier.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Aurora Store - * Copyright (C) 2021, Rahul Kumar Patel - * - * Aurora Store 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 2 of the License, or - * (at your option) any later version. - * - * Aurora Store 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 Aurora Store. If not, see . - * - */ - -package com.aurora.store.util - -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.net.Uri -import android.util.Log -import com.aurora.store.util.PackageUtil.getPackageInfo -import java.io.File -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -object ApkCopier { - - private const val TAG = "ApkCopier" - - fun copyInstalledApp(context: Context, packageName: String, uri: Uri) { - val packageInfo = getPackageInfo(context, packageName, PackageManager.GET_META_DATA) - val baseApk = getBaseApk(packageInfo) - val fileList: MutableList = mutableListOf() - - /*Add base APK*/ - fileList.add(baseApk) - - if (!packageInfo.applicationInfo.splitSourceDirs.isNullOrEmpty()) { - fileList.addAll(getSplitAPKs(packageInfo)) - } - bundleAllAPKs(context, fileList.filterNotNull(), uri) - } - - fun copyDownloadedApp(context: Context, packageName: String, versionCode: Int, uri: Uri) { - return bundleAllAPKs( - context, - PathUtil.getAppDownloadDir(context, packageName, versionCode).listFiles()!!.toList(), - uri - ) - } - - private fun getBaseApk(packageInfo: PackageInfo?): File? { - return if (packageInfo?.applicationInfo != null) { - File(packageInfo.applicationInfo.sourceDir) - } else { - null - } - } - - private fun getSplitAPKs(packageInfo: PackageInfo): MutableList { - val fileList: MutableList = ArrayList() - val splitSourceDirs = packageInfo.applicationInfo.splitSourceDirs - if (splitSourceDirs != null) { - for (fileName in splitSourceDirs) fileList.add(File(fileName)) - } - return fileList - } - - private fun bundleAllAPKs(context: Context, fileList: List, uri: Uri) { - try { - val zipOutputStream = ZipOutputStream(context.contentResolver.openOutputStream(uri)) - - fileList.forEach { file -> - file.inputStream().use { output -> - val zipEntry = ZipEntry(file.name) - zipOutputStream.putNextEntry(zipEntry) - output.copyTo(zipOutputStream) - zipOutputStream.closeEntry() - } - } - - zipOutputStream.close() - } catch (exception: Exception) { - Log.e(TAG, "Failed to copy app bundles", exception) - } - } -} diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/ManualDownloadSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/ManualDownloadSheet.kt index c7f9e7508..ccbcc915e 100644 --- a/app/src/main/java/com/aurora/store/view/ui/sheets/ManualDownloadSheet.kt +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/ManualDownloadSheet.kt @@ -104,7 +104,7 @@ class ManualDownloadSheet : BottomSheetDialogFragment(R.layout.sheet_manual_down if (customVersionString.isEmpty()) binding.versionCodeInp.error = "Enter version code" else { - viewModel.purchase(requireContext(), args.app, customVersionString.toInt()) + viewModel.purchase(args.app, customVersionString.toInt()) } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/sheets/SheetsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/sheets/SheetsViewModel.kt index 6dfdd4d32..a79651267 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/sheets/SheetsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/sheets/SheetsViewModel.kt @@ -9,7 +9,7 @@ import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.store.data.event.BusEvent import com.aurora.store.data.providers.AuthProvider -import com.aurora.store.util.ApkCopier +import com.aurora.store.data.work.ExportWorker import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.Dispatchers @@ -28,7 +28,7 @@ class SheetsViewModel @Inject constructor( private val _purchaseStatus = MutableSharedFlow() val purchaseStatus = _purchaseStatus.asSharedFlow() - fun purchase(context: Context, app: App, customVersion: Int) { + fun purchase(app: App, customVersion: Int) { viewModelScope.launch(Dispatchers.IO) { try { val purchaseHelper = PurchaseHelper(authProvider.authData) @@ -46,18 +46,10 @@ class SheetsViewModel @Inject constructor( } fun copyInstalledApp(context: Context, packageName: String, uri: Uri) { - viewModelScope.launch(Dispatchers.IO) { - ApkCopier.copyInstalledApp(context, packageName, uri) - } + ExportWorker.exportInstalledApp(context, packageName, uri) } fun copyDownloadedApp(context: Context, packageName: String, versionCode: Int, uri: Uri) { - viewModelScope.launch(Dispatchers.IO) { - try { - ApkCopier.copyDownloadedApp(context, packageName, versionCode, uri) - } catch (exception: Exception) { - Log.e(TAG, "Failed to copy downloads", exception) - } - } + ExportWorker.exportDownloadedApp(context, packageName, versionCode, uri) } }