Switch to an expedited worker to export apps & downloads
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
145
app/src/main/java/com/aurora/store/data/work/ExportWorker.kt
Normal file
145
app/src/main/java/com/aurora/store/data/work/ExportWorker.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user