UpdatesFragment: Refactor to work with new downloads logic

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-12-13 22:17:49 +05:30
parent bd55b157e6
commit 63c55b8858
5 changed files with 193 additions and 397 deletions

View File

@@ -26,6 +26,7 @@ import com.aurora.extensions.getString
import com.aurora.extensions.runOnUiThread
import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.databinding.ViewUpdateButtonBinding
class UpdateButton : RelativeLayout {
@@ -72,12 +73,12 @@ class UpdateButton : RelativeLayout {
B.btnPositive.text = getString(text)
}
fun updateState(state: State) {
val displayChild = when (state) {
State.IDLE, State.CANCELED -> 0
State.QUEUED -> 1
State.PROGRESS -> 2
State.COMPLETE -> 3
fun updateState(downloadStatus: DownloadStatus) {
val displayChild = when (downloadStatus) {
DownloadStatus.QUEUED,
DownloadStatus.DOWNLOADING -> 2
DownloadStatus.COMPLETED -> 3
else -> 0
}
if (B.viewFlipper.displayedChild != displayChild) {

View File

@@ -34,13 +34,17 @@ import com.airbnb.epoxy.ModelView
import com.airbnb.epoxy.OnViewRecycled
import com.aurora.extensions.invisible
import com.aurora.extensions.px
import com.aurora.extensions.show
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.model.UpdateFile
import com.aurora.store.data.model.DownloadStatus
import com.aurora.store.data.room.download.Download
import com.aurora.store.databinding.ViewAppUpdateBinding
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.PathUtil
import com.aurora.store.view.epoxy.views.BaseView
import java.io.File
import kotlin.io.path.pathString
@ModelView(
autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT,
@@ -72,76 +76,77 @@ class AppUpdateView : RelativeLayout {
}
@ModelProp
fun updateFile(updateFile: UpdateFile?) {
if (updateFile != null) {
/*Inflate App details*/
with(updateFile.app) {
B.txtLine1.text = displayName
B.imgIcon.load(iconArtwork.url) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(8.px.toFloat()))
}
B.txtLine2.text = developerName
B.txtLine3.text = ("${CommonUtil.addSiPrefix(size)}$updatedOn")
B.txtLine4.text = ("$versionName (${versionCode})")
B.txtChangelog.text = if (changes.isNotEmpty())
HtmlCompat.fromHtml(
changes,
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
)
else
context.getString(R.string.details_changelog_unavailable)
B.headerIndicator.setOnClickListener {
if (B.txtChangelog.isVisible) {
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down)
B.txtChangelog.visibility = View.GONE
} else {
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up)
B.txtChangelog.visibility = View.VISIBLE
}
}
fun app(app: App) {
/*Inflate App details*/
with(app) {
B.txtLine1.text = displayName
B.imgIcon.load(iconArtwork.url) {
placeholder(R.drawable.bg_placeholder)
transformations(RoundedCornersTransformation(8.px.toFloat()))
}
/*Inflate Download details*/
updateFile.group?.let {
when (updateFile.state) {
State.QUEUED -> {
B.progressDownload.progress = 0
B.progressDownload.show()
B.btnAction.updateState(State.QUEUED)
}
State.IDLE, State.CANCELED -> {
B.progressDownload.progress = 0
B.progressDownload.invisible()
B.btnAction.updateState(State.IDLE)
}
State.PROGRESS -> {
val progress = it.groupDownloadProgress
if (progress > 0) {
if (progress == 100) {
B.progressDownload.invisible()
} else {
B.progressDownload.progress = progress
B.progressDownload.show()
}
}
}
State.COMPLETE -> {
B.progressDownload.invisible()
B.btnAction.updateState(State.COMPLETE)
}
B.txtLine2.text = developerName
B.txtLine3.text = ("${CommonUtil.addSiPrefix(size)}$updatedOn")
B.txtLine4.text = ("$versionName (${versionCode})")
B.txtChangelog.text = if (changes.isNotEmpty())
HtmlCompat.fromHtml(
changes,
HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
)
else
context.getString(R.string.details_changelog_unavailable)
B.headerIndicator.setOnClickListener {
if (B.txtChangelog.isVisible) {
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down)
B.txtChangelog.visibility = View.GONE
} else {
B.headerIndicator.icon = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up)
B.txtChangelog.visibility = View.VISIBLE
}
}
}
}
@ModelProp
fun state(state: State?) {
state?.let {
B.btnAction.updateState(it)
fun download(download: Download?) {
if (download != null) {
/*Inflate Download details*/
B.btnAction.updateState(download.status)
when (download.status) {
DownloadStatus.QUEUED -> {
B.progressDownload.progress = 0
B.progressDownload.show()
}
DownloadStatus.DOWNLOADING -> {
if (download.progress > 0) {
if (download.progress == 100) {
B.progressDownload.invisible()
} else {
B.progressDownload.progress = download.progress
B.progressDownload.show()
}
}
}
DownloadStatus.COMPLETED -> {
B.progressDownload.invisible()
// It is possible that while the app was downloaded in past, files have been removed
// Check once again to reflect appropriate status
val files = File(
PathUtil.getAppDownloadDir(
context,
download.packageName,
download.versionCode
).pathString
).listFiles()
if (files.isNullOrEmpty()) B.btnAction.updateState(DownloadStatus.UNAVAILABLE)
}
else -> {
B.progressDownload.progress = 0
B.progressDownload.invisible()
}
}
}
}

View File

@@ -20,26 +20,21 @@
package com.aurora.store.view.ui.updates
import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Environment
import android.os.IBinder
import android.provider.Settings
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.aurora.extensions.isRAndAbove
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.R
import com.aurora.store.State
import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.model.UpdateFile
import com.aurora.store.data.service.UpdateService
import com.aurora.store.data.room.download.Download
import com.aurora.store.databinding.FragmentUpdatesBinding
import com.aurora.store.util.PathUtil
import com.aurora.store.util.isExternalStorageEnable
@@ -49,10 +44,12 @@ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.UpdatesViewModel
import com.tonyodev.fetch2.AbstractFetchGroupListener
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.FetchGroup
import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlin.io.path.pathString
@AndroidEntryPoint
class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
@@ -67,113 +64,30 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
private val startForStorageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isRAndAbove() && Environment.isExternalStorageManager()) {
updateSingle(app)
viewModel.download(app)
} else {
toast(R.string.permissions_denied)
}
}
private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { perm ->
if (perm) updateSingle(app) else toast(R.string.permissions_denied)
if (perm) viewModel.download(app) else toast(R.string.permissions_denied)
}
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
private lateinit var fetchListener: AbstractFetchGroupListener
private var updateService: UpdateService? = null
private var attachToServiceCalled = false
private var serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
updateService!!.registerFetchListener(fetchListener)
if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
val iterator = listOfActionsWhenServiceAttaches.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
next.run()
iterator.remove()
}
}
}
override fun onServiceDisconnected(name: ComponentName) {
updateService = null
attachToServiceCalled = false
}
}
private var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
override fun onResume() {
getUpdateServiceInstance()
super.onResume()
}
override fun onPause() {
if (updateService != null) {
updateService = null
attachToServiceCalled = false
requireContext().unbindService(serviceConnection)
}
super.onPause()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDestroy() {
super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
requireContext().unbindService(serviceConnection)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentUpdatesBinding.bind(view)
fetchListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
viewModel.updateDownload(groupId, fetchGroup)
}
override fun onProgress(
groupId: Int,
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup
) {
viewModel.updateDownload(groupId, fetchGroup)
}
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (fetchGroup.groupDownloadProgress == 100) {
viewModel.updateDownload(groupId, fetchGroup, isComplete = true)
viewModel.observe()
viewLifecycleOwner.lifecycleScope.launch {
viewModel.updates.combine(viewModel.downloadsList) { uList, dList ->
uList?.associateWith { a ->
dList.find { it.packageName == a.packageName && it.versionCode == a.versionCode }
}
}.collectLatest {
updateController(it)
binding.swipeRefreshLayout.isRefreshing = false
}
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
viewModel.updateDownload(groupId, fetchGroup, isCancelled = true)
}
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
viewModel.updateDownload(groupId, fetchGroup, isCancelled = true)
}
}
getUpdateServiceInstance()
viewModel.liveUpdateData.observe(viewLifecycleOwner) {
updateFileMap = it
updateController(updateFileMap)
binding.swipeRefreshLayout.isRefreshing = false
updateService?.liveUpdateData?.postValue(updateFileMap)
}
binding.swipeRefreshLayout.setOnRefreshListener {
@@ -183,10 +97,15 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
updateController(null)
}
private fun updateController(updateFileMap: MutableMap<Int, UpdateFile>?) {
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun updateController(appList: Map<App, Download?>?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (updateFileMap == null) {
if (appList == null) {
for (i in 1..10) {
add(
AppListViewShimmerModel_()
@@ -194,7 +113,7 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
)
}
} else {
if (updateFileMap.isEmpty()) {
if (appList.isEmpty()) {
add(
NoAppViewModel_()
.id("no_update")
@@ -206,8 +125,8 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
UpdateHeaderViewModel_()
.id("header_all")
.title(
"${updateFileMap.size} " +
if (updateFileMap.size == 1)
"${appList.size} " +
if (appList.size == 1)
getString(R.string.update_available)
else
getString(R.string.updates_available)
@@ -219,42 +138,46 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
getString(R.string.action_update_all)
)
.click { _ ->
if (viewModel.updateAllEnqueued)
if (viewModel.updateAllEnqueued) {
cancelAll()
else
updateFileMap.values.forEach { updateSingle(it.app, true) }
} else {
appList.keys.forEach { updateSingle(it, true) }
}
requestModelBuild()
}
)
updateFileMap.values.forEach { updateFile ->
for ((app, download) in appList) {
add(
AppUpdateViewModel_()
.id(updateFile.hashCode())
.updateFile(updateFile)
.click { _ ->
openDetailsFragment(
updateFile.app.packageName,
updateFile.app
)
}
.id(app.packageName)
.app(app)
.download(download)
.click { _ -> openDetailsFragment(app.packageName, app) }
.longClick { _ ->
openAppMenuSheet(updateFile.app)
openAppMenuSheet(app)
false
}
.positiveAction { _ -> updateSingle(updateFile.app) }
.negativeAction { _ -> cancelSingle(updateFile.app) }
.positiveAction { _ -> updateSingle(app) }
.negativeAction { _ -> cancelSingle(app) }
.installAction { _ ->
updateFile.group?.downloads?.let {
viewModel.install(
val files = File(
PathUtil.getAppDownloadDir(
requireContext(),
updateFile.app.packageName,
it
)
app.packageName,
app.versionCode
).pathString
).listFiles()
// Downloaded files are missing, trigger re-download
if (files.isNullOrEmpty()) {
updateSingle(app)
return@installAction
}
val apkFiles = files.filter { it.path.endsWith(".apk") }
viewModel.install(requireContext(), app.packageName, apkFiles)
}
.state(updateFile.state)
)
}
}
@@ -262,78 +185,42 @@ class UpdatesFragment : BaseFragment(R.layout.fragment_updates) {
}
}
private fun runInService(runnable: Runnable) {
if (updateService == null) {
listOfActionsWhenServiceAttaches.add(runnable)
getUpdateServiceInstance()
} else {
runnable.run()
}
}
private fun updateSingle(app: App, updateAll: Boolean = false) {
this.app = app
runInService {
viewModel.updateState(app.getGroupId(requireContext()), State.QUEUED)
viewModel.updateAllEnqueued = updateAll
viewModel.updateAllEnqueued = updateAll
if (PathUtil.needsStorageManagerPerm(app.fileList) ||
requireContext().isExternalStorageEnable()
) {
if (isRAndAbove()) {
if (!Environment.isExternalStorageManager()) {
startForStorageManagerResult.launch(
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
)
} else {
updateService?.updateApp(app)
}
if (PathUtil.needsStorageManagerPerm(app.fileList) ||
requireContext().isExternalStorageEnable()
) {
if (isRAndAbove()) {
if (!Environment.isExternalStorageManager()) {
startForStorageManagerResult.launch(
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
)
} else {
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
updateService?.updateApp(app)
} else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
viewModel.download(app)
}
} else {
updateService?.updateApp(app)
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
viewModel.download(app)
} else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
} else {
viewModel.download(app)
}
}
private fun cancelSingle(app: App) {
runInService {
updateService?.fetch?.cancelGroup(app.getGroupId(requireContext()))
}
viewModel.cancelDownload(app)
}
private fun cancelAll() {
runInService {
viewModel.updateAllEnqueued = false
updateFileMap.values.forEach {
updateService?.fetch?.cancelGroup(
it.app.getGroupId(
requireContext()
)
)
}
}
}
private fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(requireContext(), UpdateService::class.java)
requireContext().startService(intent)
requireContext().bindService(
intent,
serviceConnection,
0
)
}
viewModel.cancelAll()
}
}

View File

@@ -23,172 +23,74 @@ import android.app.Application
import android.content.Context
import android.util.Log
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.store.State
import com.aurora.store.data.RequestState
import com.aurora.store.data.downloader.RequestGroupIdBuilder
import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.UpdateFile
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.FetchGroup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import com.aurora.store.util.DownloadWorkerUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.File
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
class UpdatesViewModel(application: Application) : BaseAppsViewModel(application) {
@HiltViewModel
class UpdatesViewModel @Inject constructor(
application: Application,
private val downloadWorkerUtil: DownloadWorkerUtil
) : BaseAppsViewModel(application) {
private val TAG = UpdatesViewModel::class.java.simpleName
var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
var liveUpdateData: MutableLiveData<MutableMap<Int, UpdateFile>> = MutableLiveData()
var updateAllEnqueued: Boolean = false
init {
EventBus.getDefault().register(this)
private val _updates = MutableSharedFlow<List<App>?>()
val updates = _updates.asSharedFlow()
requestState = RequestState.Init
observe()
}
val downloadsList get() = downloadWorkerUtil.downloadsList
override fun observe() {
viewModelScope.launch(Dispatchers.IO) {
try {
val marketApps = getFilteredApps()
checkUpdate(marketApps)
} catch (e: Exception) {
requestState = RequestState.Pending
}
}
}
private fun checkUpdate(subAppList: List<App>) {
subAppList.filter {
val packageInfo = packageInfoMap[it.packageName]
if (packageInfo != null) {
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
} else {
false
}
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps ->
updateFileMap.clear()
apps.forEach {
updateFileMap[it.getGroupId(getApplication<Application>().applicationContext)] = UpdateFile(it)
}
liveUpdateData.postValue(updateFileMap)
requestState = RequestState.Complete
}
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onBusEvent(event: BusEvent) {
when (event) {
is BusEvent.InstallEvent -> {
updateListAndPost(event.packageName)
}
is BusEvent.UninstallEvent -> {
updateListAndPost(event.packageName)
}
is BusEvent.Blacklisted -> {
updateListAndPost(event.packageName)
}
else -> {
}
}
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onInstallerEvent(event: InstallerEvent) {
when (event) {
is InstallerEvent.Success -> {
}
is InstallerEvent.Cancelled -> {
}
is InstallerEvent.Failed -> {
val packageName = event.packageName
packageName?.let {
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
groupIDsOfPackageName.forEach {
updateDownload(it, null, true)
getFilteredApps().filter {
val packageInfo = packageInfoMap[it.packageName]
if (packageInfo != null) {
it.versionCode.toLong() > PackageInfoCompat.getLongVersionCode(packageInfo)
} else {
false
}
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps ->
_updates.emit(apps)
}
} catch (exception: Exception) {
Log.d(TAG, "Failed to get updates", exception)
}
}
}
fun updateState(id: Int, state: State) {
updateFileMap[id]?.state = state
liveUpdateData.postValue(updateFileMap)
}
fun updateDownload(
id: Int,
group: FetchGroup?,
isCancelled: Boolean = false,
isComplete: Boolean = false
) {
when {
isCancelled -> {
updateFileMap[id]?.state = State.IDLE
updateFileMap[id]?.group = null
}
isComplete -> {
updateFileMap[id]?.state = State.COMPLETE
updateFileMap[id]?.group = group
}
else -> {
updateFileMap[id]?.state = State.PROGRESS
updateFileMap[id]?.group = group
}
}
liveUpdateData.postValue(updateFileMap)
}
@Synchronized
fun install(context: Context, packageName: String, files: List<Download>) {
if (files.all { File(it.file).exists() }) {
fun install(context: Context, packageName: String, files: List<File>) {
try {
viewModelScope.launch(Dispatchers.IO) {
try {
AppInstaller.getInstance(context).getPreferredInstaller().install(
packageName,
files.filter { it.file.endsWith(".apk") }.map { it.file }.toList()
)
} catch (exception: Exception) {
Log.e(TAG, "Failed to install $packageName", exception)
}
AppInstaller.getInstance(context).getPreferredInstaller()
.install(packageName, files)
}
} else {
Log.e(TAG, "Given files doesn't exists!")
} catch (exception: Exception) {
Log.e(TAG, "Failed to install app", exception)
}
}
private fun updateListAndPost(packageName: String) {
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
groupIDsOfPackageName.forEach {
//Remove from map
updateFileMap.remove(it)
}
//Post new update list
liveUpdateData.postValue(updateFileMap)
fun download(app: App) {
viewModelScope.launch { downloadWorkerUtil.enqueueApp(app) }
}
override fun onCleared() {
EventBus.getDefault().unregister(this)
super.onCleared()
fun cancelDownload(app: App) {
viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) }
}
fun cancelAll() {
viewModelScope.launch { downloadWorkerUtil.cancelAll() }
}
}

View File

@@ -17,6 +17,7 @@ import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.DownloadWorkerUtil
import com.google.gson.GsonBuilder
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
@@ -146,7 +147,7 @@ class AppDetailsViewModel @Inject constructor(
}
@Synchronized
fun install(context: Context, packageName: String, files: List<Any>) {
fun install(context: Context, packageName: String, files: List<File>) {
try {
viewModelScope.launch(Dispatchers.IO) {
AppInstaller.getInstance(context).getPreferredInstaller()