Switch to an expedited worker to export apps & downloads

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2024-07-10 12:45:11 +07:00
parent dff3b74448
commit 294df7aa41
4 changed files with 150 additions and 106 deletions

View File

@@ -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<ExportWorker>()
.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<ExportWorker>()
.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<File?> = 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<File>, 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()
}
}
}
}
}

View File

@@ -1,93 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<File?> = 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<File> {
val fileList: MutableList<File> = 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<File>, 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)
}
}
}

View File

@@ -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())
}
}

View File

@@ -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<Boolean>()
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)
}
}