Updating apps is done in a separate foreground service so that AuroraStore doesn't need to be open for updating to work, every app downloaded is fetched with a unique random but saved and constant groupID based on appID and versionCode (prevents install issues with updated apps while keeping history), fix double install calls by finishing the implementation of the install queue (ServiceInstaller only for now), fix issues with ServiceInstaller (uninstall calls wrong bus event, service connection was not detached properly) and more + more to come
This commit is contained in:
@@ -77,7 +77,10 @@ class DownloadManager private constructor(var context: Context) {
|
||||
if (packageList.contains(download.tag)) {
|
||||
val packageName = download.tag
|
||||
if (packageName != null) {
|
||||
fetch.deleteGroup(packageName.hashCode())
|
||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(context, packageName.hashCode())
|
||||
groupIDsOfPackageName.forEach {
|
||||
fetch.deleteGroup(it)
|
||||
}
|
||||
packageList.remove(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ import com.tonyodev.fetch2.Request
|
||||
import com.tonyodev.fetch2core.Extras
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
private inline fun Request.attachMetaData(app: App) {
|
||||
private inline fun Request.attachMetaData(context: Context, app: App) {
|
||||
apply {
|
||||
groupId = app.id
|
||||
groupId = app.getGroupId(context)
|
||||
tag = app.packageName
|
||||
enqueueAction = EnqueueAction.UPDATE_ACCORDINGLY
|
||||
networkType = NetworkType.ALL
|
||||
@@ -61,7 +61,7 @@ object RequestBuilder {
|
||||
File.FileType.PATCH -> PathUtil.getObbDownloadFile(context, app, file)
|
||||
}
|
||||
return Request(file.url, fileName).apply {
|
||||
attachMetaData(app)
|
||||
attachMetaData(context, app)
|
||||
attachExtra(app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.aurora.store.data.downloader
|
||||
|
||||
import android.content.Context
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.util.Preferences
|
||||
import com.aurora.store.util.Preferences.PREFERENCE_UNIQUE_GROUP_IDS
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import org.apache.commons.lang3.RandomUtils
|
||||
|
||||
class RequestGroupIdBuilder {
|
||||
|
||||
data class AppIDnVersion(val id: Int, val versionCode: Int)
|
||||
|
||||
companion object {
|
||||
fun getGroupIDsForApp(context: Context, appID: Int): MutableList<Int> {
|
||||
val data = Preferences.getPrefs(context).getString(PREFERENCE_UNIQUE_GROUP_IDS, "")!!
|
||||
val gson = Gson()
|
||||
var groupIDMap = HashMap<Int, RequestGroupIdBuilder.AppIDnVersion>()
|
||||
if (data.isNotEmpty()) {
|
||||
val empMapType = object : TypeToken<Map<Int, RequestGroupIdBuilder.AppIDnVersion>?>() {}.type
|
||||
groupIDMap = HashMap(gson.fromJson(data, empMapType) ?: HashMap())
|
||||
}
|
||||
val out = mutableListOf<Int>()
|
||||
for (item in groupIDMap.entries) {
|
||||
if (item.value.id == appID) {
|
||||
out.add(item.key)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun App.getGroupId(context: Context): Int {
|
||||
val data = Preferences.getPrefs(context).getString(PREFERENCE_UNIQUE_GROUP_IDS, "")!!
|
||||
val gson = Gson()
|
||||
var groupIDMap = HashMap<Int, RequestGroupIdBuilder.AppIDnVersion>()
|
||||
if (data.isNotEmpty()) {
|
||||
val empMapType = object : TypeToken<Map<Int, RequestGroupIdBuilder.AppIDnVersion>?>() {}.type
|
||||
groupIDMap = HashMap(gson.fromJson(data, empMapType) ?: HashMap())
|
||||
}
|
||||
for (item in groupIDMap.entries) {
|
||||
if (item.value.id == this.id && item.value.versionCode == this.versionCode) {
|
||||
return item.key
|
||||
}
|
||||
}
|
||||
var randomGroupID = RandomUtils.nextInt(0, Int.MAX_VALUE)
|
||||
while (groupIDMap.containsKey(randomGroupID)) {
|
||||
randomGroupID = RandomUtils.nextInt(0, Int.MAX_VALUE)
|
||||
}
|
||||
groupIDMap[randomGroupID] = RequestGroupIdBuilder.AppIDnVersion(this.id, this.versionCode)
|
||||
Preferences.getPrefs(context).edit().putString(PREFERENCE_UNIQUE_GROUP_IDS, gson.toJson(groupIDMap)).apply()
|
||||
return randomGroupID
|
||||
}
|
||||
@@ -32,8 +32,10 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.FileProvider
|
||||
import com.aurora.services.IPrivilegedCallback
|
||||
import com.aurora.services.IPrivilegedService
|
||||
import com.aurora.store.AuroraApplication
|
||||
import com.aurora.store.BuildConfig
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.data.event.BusEvent
|
||||
import com.aurora.store.data.event.InstallerEvent
|
||||
import com.aurora.store.util.Log
|
||||
import com.aurora.store.util.PackageUtil
|
||||
@@ -42,6 +44,8 @@ import java.io.File
|
||||
|
||||
class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
|
||||
private lateinit var serviceConnection: ServiceConnection
|
||||
|
||||
companion object {
|
||||
const val ACTION_INSTALL_REPLACE_EXISTING = 2
|
||||
const val PRIVILEGED_EXTENSION_PACKAGE_NAME = "com.aurora.services"
|
||||
@@ -83,6 +87,7 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
override fun uninstall(packageName: String) {
|
||||
val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
AuroraApplication.enqueuedInstalls.add(packageName)
|
||||
val service = IPrivilegedService.Stub.asInterface(binder)
|
||||
|
||||
if (service.hasPrivilegedPermissions()) {
|
||||
@@ -100,7 +105,7 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
extra: String?
|
||||
) {
|
||||
removeFromInstallQueue(packageName)
|
||||
handleCallback(packageName, returnCode, extra)
|
||||
handleCallbackUninstall(packageName, returnCode, extra)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +125,7 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
context.getString(R.string.installer_status_failure),
|
||||
context.getString(R.string.installer_service_misconfigured)
|
||||
)
|
||||
removeFromInstallQueue(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +147,15 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun xInstall(packageName: String, uriList: List<Uri>) {
|
||||
val serviceConnection = object : ServiceConnection {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
if (isAlreadyQueued(packageName)) {
|
||||
if (::serviceConnection.isInitialized) {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
return
|
||||
}
|
||||
AuroraApplication.enqueuedInstalls.add(packageName)
|
||||
val service = IPrivilegedService.Stub.asInterface(binder)
|
||||
|
||||
if (service.hasPrivilegedPermissions()) {
|
||||
@@ -182,6 +195,7 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
context.getString(R.string.installer_status_failure),
|
||||
context.getString(R.string.installer_service_misconfigured)
|
||||
)
|
||||
removeFromInstallQueue(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +215,32 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleCallbackUninstall(packageName: String, returnCode: Int, extra: String?) {
|
||||
Log.i("Services Callback : $packageName $returnCode $extra")
|
||||
|
||||
when (returnCode) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
EventBus.getDefault().post(
|
||||
BusEvent.UninstallEvent(
|
||||
packageName,
|
||||
context.getString(R.string.installer_status_success)
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val error = AppInstaller.getErrorString(
|
||||
context,
|
||||
returnCode
|
||||
)
|
||||
|
||||
postError(packageName, error, extra)
|
||||
}
|
||||
}
|
||||
if (::serviceConnection.isInitialized) {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCallback(packageName: String, returnCode: Int, extra: String?) {
|
||||
Log.i("Services Callback : $packageName $returnCode $extra")
|
||||
|
||||
@@ -222,6 +262,16 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
|
||||
postError(packageName, error, extra)
|
||||
}
|
||||
}
|
||||
if (::serviceConnection.isInitialized) {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
override fun postError(packageName: String, error: String?, extra: String?) {
|
||||
super.postError(packageName, error, extra)
|
||||
if (::serviceConnection.isInitialized) {
|
||||
context.unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUri(file: File): Uri {
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.aurora.store.BuildConfig
|
||||
import com.aurora.store.data.downloader.RequestGroupIdBuilder
|
||||
import com.aurora.store.data.event.BusEvent.InstallEvent
|
||||
import com.aurora.store.data.event.BusEvent.UninstallEvent
|
||||
import com.aurora.store.data.installer.AppInstaller
|
||||
@@ -72,7 +73,10 @@ open class PackageManagerReceiver : BroadcastReceiver() {
|
||||
private fun clearNotification(context: Context, packageName: String) {
|
||||
val notificationManager = context.applicationContext
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(packageName, packageName.hashCode())
|
||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(context, packageName.hashCode())
|
||||
groupIDsOfPackageName.forEach {
|
||||
notificationManager.cancel(packageName, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDownloads(context: Context, packageName: String) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.ArrayMap
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.aurora.Constants
|
||||
@@ -34,6 +35,7 @@ import com.aurora.extensions.isLAndAbove
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.data.downloader.DownloadManager
|
||||
import com.aurora.store.data.downloader.RequestGroupIdBuilder
|
||||
import com.aurora.store.data.event.InstallerEvent
|
||||
import com.aurora.store.data.installer.AppInstaller
|
||||
import com.aurora.store.data.receiver.DownloadCancelReceiver
|
||||
@@ -78,10 +80,6 @@ class NotificationService : Service() {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@@ -137,6 +135,10 @@ class NotificationService : Service() {
|
||||
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
showNotification(groupId, download, fetchGroup)
|
||||
}
|
||||
|
||||
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
showNotification(groupId, download, fetchGroup)
|
||||
}
|
||||
}
|
||||
|
||||
fetch.addListener(fetchListener)
|
||||
@@ -184,6 +186,11 @@ class NotificationService : Service() {
|
||||
if (app == null)
|
||||
return
|
||||
|
||||
if (status == Status.DELETED) {
|
||||
notificationManager.cancel(app.id)
|
||||
return
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
||||
builder.setContentTitle(app.displayName)
|
||||
builder.setSmallIcon(R.drawable.ic_notification_outlined)
|
||||
@@ -201,7 +208,8 @@ class NotificationService : Service() {
|
||||
builder.setContentText(getString(R.string.download_canceled))
|
||||
builder.color = Color.RED
|
||||
}
|
||||
Status.FAILED -> {
|
||||
Status.FAILED ->
|
||||
{
|
||||
builder.setSmallIcon(R.drawable.ic_download_fail)
|
||||
builder.setContentText(getString(R.string.download_failed))
|
||||
builder.color = Color.RED
|
||||
@@ -362,12 +370,26 @@ class NotificationService : Service() {
|
||||
fun onEventMainThread(event: Any) {
|
||||
when (event) {
|
||||
is InstallerEvent.Success -> {
|
||||
val app = appMap[event.packageName.hashCode()]
|
||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode())
|
||||
var app: App? = null
|
||||
for (item in groupIDsOfPackageName) {
|
||||
app = appMap[item]
|
||||
if (app != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (app != null)
|
||||
notifyInstallationStatus(app, event.extra)
|
||||
}
|
||||
is InstallerEvent.Failed -> {
|
||||
val app = appMap[event.packageName.hashCode()]
|
||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode())
|
||||
var app: App? = null
|
||||
for (item in groupIDsOfPackageName) {
|
||||
app = appMap[item]
|
||||
if (app != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (app != null)
|
||||
notifyInstallationStatus(app, event.error)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.aurora.store.BuildConfig
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.data.downloader.DownloadManager
|
||||
import com.aurora.store.data.downloader.RequestBuilder.buildRequest
|
||||
import com.aurora.store.data.downloader.getGroupId
|
||||
import com.aurora.store.data.installer.NativeInstaller
|
||||
import com.aurora.store.data.model.SelfUpdate
|
||||
import com.aurora.store.util.CertUtil.isFDroidApp
|
||||
@@ -135,14 +136,14 @@ class SelfUpdateService : Service() {
|
||||
groupId: Int, download: Download, error: Error,
|
||||
throwable: Throwable?, fetchGroup: FetchGroup
|
||||
) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@SelfUpdateService)) {
|
||||
Log.e("Error self-updating ${app.displayName}")
|
||||
destroyService()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (groupId == app.id && fetchGroup.groupDownloadProgress == 100) {
|
||||
if (groupId == app.getGroupId(this@SelfUpdateService) && fetchGroup.groupDownloadProgress == 100) {
|
||||
Log.d("Calling installer ${app.displayName}")
|
||||
|
||||
try {
|
||||
@@ -163,7 +164,7 @@ class SelfUpdateService : Service() {
|
||||
}
|
||||
|
||||
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@SelfUpdateService)) {
|
||||
Log.d("Self-update cancelled ${app.displayName}")
|
||||
destroyService()
|
||||
}
|
||||
|
||||
346
app/src/main/java/com/aurora/store/data/service/UpdateService.kt
Normal file
346
app/src/main/java/com/aurora/store/data/service/UpdateService.kt
Normal file
@@ -0,0 +1,346 @@
|
||||
package com.aurora.store.data.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.aurora.Constants
|
||||
import com.aurora.extensions.stackTraceToString
|
||||
import com.aurora.extensions.toast
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.gplayapi.data.models.AuthData
|
||||
import com.aurora.gplayapi.helpers.PurchaseHelper
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.data.downloader.DownloadManager
|
||||
import com.aurora.store.data.downloader.RequestBuilder
|
||||
import com.aurora.store.data.downloader.RequestGroupIdBuilder
|
||||
import com.aurora.store.data.downloader.getGroupId
|
||||
import com.aurora.store.data.event.InstallerEvent
|
||||
import com.aurora.store.data.installer.AppInstaller
|
||||
import com.aurora.store.data.model.UpdateFile
|
||||
import com.aurora.store.data.providers.AuthProvider
|
||||
import com.aurora.store.util.Log
|
||||
import com.tonyodev.fetch2.*
|
||||
import com.tonyodev.fetch2core.FetchObserver
|
||||
import com.tonyodev.fetch2core.Reason
|
||||
import nl.komponents.kovenant.task
|
||||
import nl.komponents.kovenant.then
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.timerTask
|
||||
|
||||
class UpdateService: LifecycleService() {
|
||||
|
||||
lateinit var fetch: Fetch
|
||||
private lateinit var fetchListener: AbstractFetchGroupListener
|
||||
private var fetchActiveDownloadObserver = object : FetchObserver<Boolean> {
|
||||
override fun onChanged(data: Boolean, reason: Reason) {
|
||||
if (!data && !installing.get() && listeners.isEmpty()) {
|
||||
Handler(Looper.getMainLooper()).postDelayed ({
|
||||
if (!installing.get() && listeners.isEmpty()) {
|
||||
stopSelf()
|
||||
}
|
||||
}, 5 * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
private var hasActiveDownloadObserver = false
|
||||
|
||||
private val listeners: ArrayList<AbstractFetchGroupListener> = ArrayList()
|
||||
|
||||
private val pendingEvents: MutableMap</*groupId: */Int, AppDownloadStatus> = mutableMapOf()
|
||||
|
||||
var liveUpdateData: MutableLiveData<MutableMap<Int, UpdateFile>> = MutableLiveData()
|
||||
|
||||
inner class UpdateServiceBinder : Binder() {
|
||||
fun getUpdateService() : UpdateService {
|
||||
return this@UpdateService
|
||||
}
|
||||
}
|
||||
|
||||
private var installing: AtomicBoolean = AtomicBoolean()
|
||||
|
||||
|
||||
private lateinit var purchaseHelper: PurchaseHelper
|
||||
|
||||
private lateinit var authData: AuthData
|
||||
|
||||
|
||||
data class AppDownloadStatus(val download: Download, val fetchGroup: FetchGroup,
|
||||
val isCancelled: Boolean = false,
|
||||
val isComplete: Boolean = false)
|
||||
|
||||
@get:RequiresApi(Build.VERSION_CODES.O)
|
||||
private val notification: Notification
|
||||
get() {
|
||||
val notificationBuilder =
|
||||
NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
||||
return getNotification(notificationBuilder)
|
||||
}
|
||||
|
||||
private fun getNotification(builder: NotificationCompat.Builder): Notification {
|
||||
return builder.setAutoCancel(true)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||
.setContentTitle("Updating apps")
|
||||
.setContentText("Updating apps in the background")
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_notification_outlined)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForeground(1, notification)
|
||||
} else {
|
||||
val notification = getNotification(
|
||||
NotificationCompat.Builder(
|
||||
this,
|
||||
Constants.NOTIFICATION_CHANNEL_GENERAL
|
||||
)
|
||||
)
|
||||
startForeground(1, notification)
|
||||
}
|
||||
EventBus.getDefault().register(this)
|
||||
authData = AuthProvider.with(this).getAuthData()
|
||||
purchaseHelper = PurchaseHelper(authData)
|
||||
fetch = DownloadManager.with(this).fetch
|
||||
fetchListener = object : AbstractFetchGroupListener() {
|
||||
|
||||
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
listeners.forEach {
|
||||
it.onAdded(groupId, download, fetchGroup)
|
||||
}
|
||||
if (!hasActiveDownloadObserver) {
|
||||
hasActiveDownloadObserver = true
|
||||
fetch.addActiveDownloadsObserver(true, fetchActiveDownloadObserver)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProgress(
|
||||
groupId: Int,
|
||||
download: Download,
|
||||
etaInMilliSeconds: Long,
|
||||
downloadedBytesPerSecond: Long,
|
||||
fetchGroup: FetchGroup
|
||||
) {
|
||||
listeners.forEach {
|
||||
it.onProgress(groupId, download, etaInMilliSeconds, downloadedBytesPerSecond, fetchGroup)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
listeners.forEach {
|
||||
it.onCompleted(groupId, download, fetchGroup)
|
||||
}
|
||||
if (listeners.isEmpty()) {
|
||||
pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true)
|
||||
}
|
||||
if (fetchGroup.groupDownloadProgress == 100 || fetchGroup.groupDownloadProgress == -1) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
try {
|
||||
install(download.tag!!, fetchGroup.downloads)
|
||||
} catch (e: Exception) {
|
||||
Log.e(e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
} /* else if (fetchGroup.groupDownloadProgress == -1) {
|
||||
fetch.deleteGroup(fetchGroup.id)
|
||||
}*/
|
||||
}
|
||||
|
||||
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
listeners.forEach {
|
||||
it.onCancelled(groupId, download, fetchGroup)
|
||||
}
|
||||
if (listeners.isEmpty()) {
|
||||
pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
listeners.forEach {
|
||||
it.onDeleted(groupId, download, fetchGroup)
|
||||
}
|
||||
if (listeners.isEmpty()) {
|
||||
pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
/*liveUpdateData.observe(this) { updateData ->
|
||||
|
||||
}*/
|
||||
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
|
||||
fetch.addListener(fetchListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateApp(app: App) {
|
||||
installing.set(true)
|
||||
task {
|
||||
val files = purchaseHelper.purchase(
|
||||
app.packageName,
|
||||
app.versionCode,
|
||||
app.offerType
|
||||
)
|
||||
|
||||
files.map { RequestBuilder.buildRequest(this, app, it) }
|
||||
} successUi {
|
||||
val requests = it.filter { request -> request.url.isNotEmpty() }.toList()
|
||||
if (requests.isNotEmpty()) {
|
||||
fetch.enqueue(requests) {
|
||||
Log.i("Updating ${app.displayName}")
|
||||
}
|
||||
} else {
|
||||
toast("Failed to update ${app.displayName}")
|
||||
}
|
||||
} failUi {
|
||||
Log.e("Failed to update ${app.displayName}")
|
||||
}
|
||||
}
|
||||
|
||||
var timer: Timer? = null
|
||||
val timerTask: TimerTask = timerTask {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (!installing.get() && listeners.isEmpty()) {
|
||||
fetch.hasActiveDownloads(true, { hasActiveDownloads ->
|
||||
if (!hasActiveDownloads && !installing.get() && listeners.isEmpty()) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun install(packageName: String, files: List<Download>?) {
|
||||
files?.let { downloads ->
|
||||
installing.set(true)
|
||||
var filesExist = true
|
||||
|
||||
downloads.forEach { download ->
|
||||
filesExist = filesExist && FileUtils.getFile(download.file).exists()
|
||||
}
|
||||
|
||||
if (filesExist) {
|
||||
task {
|
||||
try {
|
||||
val installer = AppInstaller(this)
|
||||
.getPreferredInstaller()
|
||||
installer.install(
|
||||
packageName,
|
||||
files
|
||||
.filter { it.file.endsWith(".apk") }
|
||||
.map { it.file }.toList()
|
||||
)
|
||||
} catch (th: Throwable) {
|
||||
th.printStackTrace()
|
||||
}
|
||||
}.fail {
|
||||
Log.e(it.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe()
|
||||
fun onEventMainThreadExec(event: Any) {
|
||||
when (event) {
|
||||
is InstallerEvent.Success -> {
|
||||
if (timer != null) {
|
||||
timer!!.cancel()
|
||||
timer = null
|
||||
}
|
||||
if (timer == null) {
|
||||
timer = Timer()
|
||||
}
|
||||
installing.set(false)
|
||||
timer!!.schedule(timerTask, 10 * 1000)
|
||||
}
|
||||
is InstallerEvent.Failed -> {
|
||||
if (timer != null) {
|
||||
timer!!.cancel()
|
||||
timer = null
|
||||
}
|
||||
if (timer == null) {
|
||||
timer = Timer()
|
||||
}
|
||||
installing.set(false)
|
||||
timer!!.schedule(timerTask, 10 * 1000)
|
||||
}
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun registerListener(listener: AbstractFetchGroupListener) {
|
||||
listeners.add(listener)
|
||||
val iterator = pendingEvents.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val item = iterator.next()
|
||||
if (item.value.isCancelled && !item.value.isComplete) {
|
||||
listener.onCancelled(item.key, item.value.download, item.value.fetchGroup)
|
||||
} else if (!item.value.isCancelled && item.value.isComplete) {
|
||||
listener.onCompleted(item.key, item.value.download, item.value.fetchGroup)
|
||||
}
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterListener(listener: AbstractFetchGroupListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private var binder: UpdateServiceBinder = UpdateServiceBinder()
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
super.onBind(intent)
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
listeners.clear()
|
||||
if (!installing.get()) {
|
||||
fetch.hasActiveDownloads(true, { hasActiveDownloads ->
|
||||
if (!hasActiveDownloads && !installing.get() && listeners.isEmpty()) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onRebind(intent: Intent?) {
|
||||
super.onRebind(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (hasActiveDownloadObserver) {
|
||||
hasActiveDownloadObserver = false
|
||||
fetch.removeActiveDownloadsObserver(fetchActiveDownloadObserver)
|
||||
}
|
||||
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
|
||||
fetch.removeListener(fetchListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAll(updateFileMap: MutableMap<Int, UpdateFile>) {
|
||||
updateFileMap.values.forEach { updateApp(it.app) }
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.gson.Gson
|
||||
|
||||
|
||||
object Preferences {
|
||||
|
||||
@@ -57,9 +59,15 @@ object Preferences {
|
||||
|
||||
const val PREFERENCE_UPDATES_EXTENDED = "PREFERENCE_UPDATES_EXTENDED"
|
||||
|
||||
const val PREFERENCE_UNIQUE_GROUP_IDS = "PREFERENCE_UNIQUE_GROUP_IDS"
|
||||
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
fun getPrefs(context: Context): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (prefs == null) {
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
return prefs!!
|
||||
}
|
||||
|
||||
fun putString(context: Context, key: String, value: String) {
|
||||
|
||||
@@ -41,6 +41,7 @@ import com.aurora.store.R
|
||||
import com.aurora.store.State
|
||||
import com.aurora.store.data.downloader.DownloadManager
|
||||
import com.aurora.store.data.downloader.RequestBuilder
|
||||
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
|
||||
@@ -427,14 +428,14 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
private fun startDownload() {
|
||||
when (status) {
|
||||
Status.PAUSED -> {
|
||||
fetch.resumeGroup(app.id)
|
||||
fetch.resumeGroup(app.getGroupId(this@AppDetailsActivity))
|
||||
}
|
||||
Status.DOWNLOADING -> {
|
||||
flip(1)
|
||||
toast("Already downloading")
|
||||
}
|
||||
Status.COMPLETED -> {
|
||||
fetch.getFetchGroup(app.id) {
|
||||
fetch.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) {
|
||||
verifyAndInstall(it.downloads)
|
||||
}
|
||||
}
|
||||
@@ -518,7 +519,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
|
||||
if (requestList.isNotEmpty()) {
|
||||
/*Remove old fetch group if downloaded earlier, mostly in case of updates*/
|
||||
fetch.deleteGroup(app.id)
|
||||
fetch.deleteGroup(app.getGroupId(this@AppDetailsActivity))
|
||||
|
||||
/*Enqueue new fetch group*/
|
||||
fetch.enqueue(
|
||||
@@ -638,7 +639,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
downloadManager = DownloadManager.with(this)
|
||||
fetch = downloadManager.fetch
|
||||
|
||||
fetch.getFetchGroup(app.id) { fetchGroup: FetchGroup ->
|
||||
fetch.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { fetchGroup: FetchGroup ->
|
||||
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
|
||||
status = Status.COMPLETED
|
||||
} else if (downloadManager.isDownloading(fetchGroup)) {
|
||||
@@ -662,7 +663,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
totalBlocks: Int,
|
||||
fetchGroup: FetchGroup
|
||||
) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
|
||||
status = download.status
|
||||
flip(1)
|
||||
|
||||
@@ -680,7 +681,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
}
|
||||
|
||||
override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
|
||||
status = download.status
|
||||
flip(1)
|
||||
FileUtils.touch(inProgressMarker)
|
||||
@@ -688,7 +689,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
}
|
||||
|
||||
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
|
||||
status = download.status
|
||||
flip(0)
|
||||
}
|
||||
@@ -701,7 +702,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
downloadedBytesPerSecond: Long,
|
||||
fetchGroup: FetchGroup
|
||||
) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
|
||||
updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond)
|
||||
Log.i(
|
||||
"${app.displayName} : ${download.file} -> Progress : %d",
|
||||
@@ -711,7 +712,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
}
|
||||
|
||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (groupId == app.id && fetchGroup.groupDownloadProgress == 100) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity) && fetchGroup.groupDownloadProgress == 100) {
|
||||
status = download.status
|
||||
flip(0)
|
||||
updateProgress(fetchGroup, -1, -1)
|
||||
@@ -726,7 +727,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
}
|
||||
|
||||
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
|
||||
status = download.status
|
||||
flip(0)
|
||||
inProgressMarker.delete()
|
||||
@@ -740,7 +741,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
throwable: Throwable?,
|
||||
fetchGroup: FetchGroup
|
||||
) {
|
||||
if (groupId == app.id) {
|
||||
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
|
||||
status = download.status
|
||||
flip(0)
|
||||
inProgressMarker.delete()
|
||||
@@ -752,7 +753,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
|
||||
|
||||
B.layoutDetailsInstall.imgCancel.setOnClickListener {
|
||||
fetch.cancelGroup(
|
||||
app.id
|
||||
app.getGroupId(this@AppDetailsActivity)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,13 @@
|
||||
|
||||
package com.aurora.store.view.ui.updates
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -34,9 +40,11 @@ import com.aurora.store.R
|
||||
import com.aurora.store.State
|
||||
import com.aurora.store.data.downloader.DownloadManager
|
||||
import com.aurora.store.data.downloader.RequestBuilder
|
||||
import com.aurora.store.data.downloader.getGroupId
|
||||
import com.aurora.store.data.installer.AppInstaller
|
||||
import com.aurora.store.data.model.UpdateFile
|
||||
import com.aurora.store.data.providers.AuthProvider
|
||||
import com.aurora.store.data.service.UpdateService
|
||||
import com.aurora.store.databinding.FragmentUpdatesBinding
|
||||
import com.aurora.store.util.Log
|
||||
import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_
|
||||
@@ -60,11 +68,17 @@ class UpdatesFragment : BaseFragment() {
|
||||
private lateinit var B: FragmentUpdatesBinding
|
||||
private lateinit var VM: UpdatesViewModel
|
||||
|
||||
private lateinit var authData: AuthData
|
||||
private lateinit var purchaseHelper: PurchaseHelper
|
||||
|
||||
private lateinit var fetch: Fetch
|
||||
private lateinit var fetchListener: AbstractFetchGroupListener
|
||||
private var serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
|
||||
updateService!!.registerListener(fetchListener)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
updateService = null
|
||||
}
|
||||
}
|
||||
|
||||
private var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
|
||||
|
||||
@@ -83,10 +97,7 @@ class UpdatesFragment : BaseFragment() {
|
||||
|
||||
VM = ViewModelProvider(requireActivity()).get(UpdatesViewModel::class.java)
|
||||
|
||||
authData = AuthProvider.with(requireContext()).getAuthData()
|
||||
purchaseHelper = PurchaseHelper(authData)
|
||||
|
||||
fetch = DownloadManager.with(requireContext()).fetch
|
||||
fetchListener = object : AbstractFetchGroupListener() {
|
||||
|
||||
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
@@ -106,11 +117,6 @@ class UpdatesFragment : BaseFragment() {
|
||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
||||
if (fetchGroup.groupDownloadProgress == 100) {
|
||||
VM.updateDownload(groupId, fetchGroup, isComplete = true)
|
||||
try {
|
||||
install(download.tag!!, fetchGroup.downloads)
|
||||
} catch (e: Exception) {
|
||||
Log.e(e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,21 +129,30 @@ class UpdatesFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
getUpdateServiceInstance()
|
||||
|
||||
return B.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
getUpdateServiceInstance()
|
||||
super.onResume()
|
||||
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
|
||||
fetch.addListener(fetchListener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
if (updateService != null) {
|
||||
updateService = null
|
||||
requireContext().unbindService(serviceConnection)
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
|
||||
fetch.removeListener(fetchListener)
|
||||
}
|
||||
super.onDestroy()
|
||||
if (updateService != null) {
|
||||
updateService = null
|
||||
requireContext().unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -147,6 +162,7 @@ class UpdatesFragment : BaseFragment() {
|
||||
updateFileMap = it
|
||||
updateController(updateFileMap)
|
||||
B.swipeRefreshLayout.isRefreshing = false
|
||||
updateService?.liveUpdateData?.postValue(updateFileMap)
|
||||
})
|
||||
|
||||
B.swipeRefreshLayout.setOnRefreshListener {
|
||||
@@ -221,44 +237,41 @@ class UpdatesFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private var updateService: UpdateService? = null
|
||||
|
||||
private fun updateSingle(app: App) {
|
||||
|
||||
VM.updateState(app.id, State.QUEUED)
|
||||
if (updateService != null) {
|
||||
VM.updateState(app.getGroupId(requireContext()), State.QUEUED)
|
||||
|
||||
task {
|
||||
val files = purchaseHelper.purchase(
|
||||
app.packageName,
|
||||
app.versionCode,
|
||||
app.offerType
|
||||
)
|
||||
|
||||
files.map { RequestBuilder.buildRequest(requireContext(), app, it) }
|
||||
} successUi {
|
||||
val requests = it.filter { request -> request.url.isNotEmpty() }.toList()
|
||||
if (requests.isNotEmpty()) {
|
||||
fetch.enqueue(requests) {
|
||||
Log.i("Updating ${app.displayName}")
|
||||
}
|
||||
} else {
|
||||
requireContext().toast("Failed to update ${app.displayName}")
|
||||
}
|
||||
} failUi {
|
||||
Log.e("Failed to update ${app.displayName}")
|
||||
updateService?.updateApp(app)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelSingle(app: App) {
|
||||
fetch.cancelGroup(app.id)
|
||||
updateService?.fetch?.cancelGroup(app.getGroupId(requireContext()))
|
||||
}
|
||||
|
||||
private fun updateAll() {
|
||||
updateFileMap.values.forEach { updateSingle(it.app) }
|
||||
updateService?.updateAll(updateFileMap)
|
||||
VM.updateAllEnqueued = true
|
||||
}
|
||||
|
||||
private fun cancelAll() {
|
||||
VM.updateAllEnqueued = false
|
||||
updateFileMap.values.forEach { fetch.cancelGroup(it.app.id) }
|
||||
updateFileMap.values.forEach { updateService?.fetch?.cancelGroup(it.app.getGroupId(requireContext())) }
|
||||
}
|
||||
|
||||
fun getUpdateServiceInstance() {
|
||||
if (updateService == null) {
|
||||
val intent = Intent(requireContext(), UpdateService::class.java)
|
||||
requireContext().startService(intent)
|
||||
requireContext().bindService(
|
||||
intent,
|
||||
serviceConnection,
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
||||
@@ -25,6 +25,8 @@ 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.model.UpdateFile
|
||||
@@ -72,7 +74,7 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
|
||||
updateFileMap.clear()
|
||||
|
||||
apps.forEach {
|
||||
updateFileMap[it.id] = UpdateFile(it)
|
||||
updateFileMap[it.getGroupId(getApplication<Application>().applicationContext)] = UpdateFile(it)
|
||||
}
|
||||
|
||||
liveUpdateData.postValue(updateFileMap)
|
||||
@@ -110,7 +112,10 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
|
||||
is InstallerEvent.Failed -> {
|
||||
val packageName = event.packageName
|
||||
packageName?.let {
|
||||
updateDownload(packageName.hashCode(), null, true)
|
||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
|
||||
groupIDsOfPackageName.forEach {
|
||||
updateDownload(it, null, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +151,11 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
|
||||
}
|
||||
|
||||
private fun updateListAndPost(packageName: String) {
|
||||
//Remove from map
|
||||
updateFileMap.remove(packageName.hashCode())
|
||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
|
||||
groupIDsOfPackageName.forEach {
|
||||
//Remove from map
|
||||
updateFileMap.remove(it)
|
||||
}
|
||||
|
||||
//Post new update list
|
||||
liveUpdateData.postValue(updateFileMap)
|
||||
|
||||
Reference in New Issue
Block a user