Merge branch 'master' into 'master'

Enabled downloading/updating any/all apps in the background with a service, fixed many issues with download, install, uninstall

See merge request AuroraOSS/AuroraStore!130
This commit is contained in:
Rahul Patel
2021-06-19 15:14:45 +00:00
23 changed files with 1418 additions and 332 deletions

View File

@@ -157,6 +157,7 @@
<service android:name=".data.service.NotificationService" />
<service android:name=".data.installer.InstallerService" />
<service android:name=".data.service.UpdateService" />
<service
android:name="com.novoda.merlin.MerlinService"
android:exported="false" />

View File

@@ -22,6 +22,8 @@ interface IPrivilegedService {
boolean hasPrivilegedPermissions();
boolean isMoreMethodImplemented();
oneway void installPackage(
in Uri packageURI,
in int flags,
@@ -52,6 +54,15 @@ interface IPrivilegedService {
in IPrivilegedCallback callback
);
oneway void installSplitPackageMore(
in String packageName,
in List<Uri> uriList,
in int flags,
in String installerPackageName,
in IPrivilegedCallback callback,
in List<String> fileList
);
oneway void deletePackage(
in String packageName,
in int flags,

View File

@@ -37,6 +37,7 @@ object Constants {
const val NOTIFICATION_CHANNEL_ALERT = "NOTIFICATION_CHANNEL_ALERT"
const val NOTIFICATION_CHANNEL_GENERAL = "NOTIFICATION_CHANNEL_GENERAL"
const val NOTIFICATION_CHANNEL_UPDATER_SERVICE = "NOTIFICATION_CHANNEL_UPDATER_SERVICE"
const val URL_DISPENSER = "http://goolag.store:1337/api/auth"

View File

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

View File

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

View File

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

View File

@@ -26,9 +26,10 @@ import com.aurora.store.R
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
open class AppInstaller constructor(var context: Context) {
open class AppInstaller private constructor(var context: Context) {
companion object {
private var instance: AppInstaller? = null
fun getErrorString(context: Context, status: Int): String {
return when (status) {
PackageInstaller.STATUS_FAILURE_ABORTED -> context.getString(R.string.installer_status_user_action)
@@ -40,22 +41,50 @@ open class AppInstaller constructor(var context: Context) {
else -> context.getString(R.string.installer_status_failure)
}
}
fun getInstance(context: Context): AppInstaller {
if (instance == null) {
instance = AppInstaller(context.applicationContext)
}
return instance!!
}
}
val choiceAndInstaller = HashMap<Int, IInstaller>()
fun getPreferredInstaller(): IInstaller {
val prefValue = Preferences.getInteger(
context,
PREFERENCE_INSTALLER_ID
)
if (choiceAndInstaller.containsKey(prefValue)) {
return choiceAndInstaller[prefValue]!!
}
return when (prefValue) {
1 -> NativeInstaller(context)
2 -> RootInstaller(context)
3 -> ServiceInstaller(context)
1 -> {
val installer = NativeInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
}
2 -> {
val installer = RootInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
}
3 -> {
val installer = ServiceInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
}
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SessionInstaller(context)
val installer = SessionInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
} else {
NativeInstaller(context)
val installer = NativeInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
}
}
}

View File

@@ -25,23 +25,30 @@ import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import android.os.*
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
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
class ServiceInstaller(context: Context) : InstallerBase(context) {
private lateinit var serviceConnection: ServiceConnection
private val executor = ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, LinkedBlockingQueue())
companion object {
const val ACTION_INSTALL_REPLACE_EXISTING = 2
const val PRIVILEGED_EXTENSION_PACKAGE_NAME = "com.aurora.services"
@@ -67,8 +74,17 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
}
}
}
val fileList = files.map {
when (it) {
is File -> it.absolutePath
is String -> File(it).absolutePath
else -> {
throw Exception("Invalid data, expecting listOf() File or String")
}
}
}
xInstall(packageName, uriList)
xInstall(packageName, uriList, fileList)
}
else -> {
postError(
@@ -81,146 +97,256 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
}
override fun uninstall(packageName: String) {
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IPrivilegedService.Stub.asInterface(binder)
if (service.hasPrivilegedPermissions()) {
Log.i(context.getString(R.string.installer_service_available))
val callback = object : IPrivilegedCallback.Stub() {
override fun handleResult(packageName: String, returnCode: Int) {
executor.execute {
val readyWithAction = AtomicBoolean(false)
Handler(Looper.getMainLooper()).post {
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
if (isAlreadyQueued(packageName)) {
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
readyWithAction.set(true)
return
}
AuroraApplication.enqueuedInstalls.add(packageName)
val service = IPrivilegedService.Stub.asInterface(binder)
override fun handleResultX(
packageName: String,
returnCode: Int,
extra: String?
) {
if (service.hasPrivilegedPermissions()) {
Log.i(context.getString(R.string.installer_service_available))
val callback = object : IPrivilegedCallback.Stub() {
override fun handleResult(packageName: String, returnCode: Int) {}
override fun handleResultX(
packageName: String,
returnCode: Int,
extra: String?
) {
removeFromInstallQueue(packageName)
handleCallbackUninstall(packageName, returnCode, extra)
readyWithAction.set(true)
}
}
try {
service.deletePackageX(
packageName,
2,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
Log.e("Failed to connect Aurora Services")
removeFromInstallQueue(packageName)
readyWithAction.set(true)
}
} else {
removeFromInstallQueue(packageName)
handleCallback(packageName, returnCode, extra)
postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
readyWithAction.set(true)
}
}
try {
service.deletePackageX(
packageName,
2,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
Log.e("Failed to connect Aurora Services")
override fun onServiceDisconnected(name: ComponentName) {
removeFromInstallQueue(packageName)
Log.e("Disconnected from Aurora Services")
readyWithAction.set(true)
}
} else {
postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
}
}
override fun onServiceDisconnected(name: ComponentName) {
removeFromInstallQueue(packageName)
Log.e("Disconnected from Aurora Services")
val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
while (!readyWithAction.get()) {
Thread.sleep(1000)
}
}
val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun xInstall(packageName: String, uriList: List<Uri>) {
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IPrivilegedService.Stub.asInterface(binder)
if (service.hasPrivilegedPermissions()) {
Log.i(context.getString(R.string.installer_service_available))
val callback = object : IPrivilegedCallback.Stub() {
override fun handleResult(packageName: String, returnCode: Int) {
private fun xInstall(packageName: String, uriList: List<Uri>, fileList: List<String>) {
executor.execute {
val readyWithAction = AtomicBoolean(false)
Handler(Looper.getMainLooper()).post {
serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
if (isAlreadyQueued(packageName)) {
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
readyWithAction.set(true)
return
}
AuroraApplication.enqueuedInstalls.add(packageName)
val service = IPrivilegedService.Stub.asInterface(binder)
override fun handleResultX(
packageName: String,
returnCode: Int,
extra: String?
) {
if (service.hasPrivilegedPermissions()) {
Log.i(context.getString(R.string.installer_service_available))
val callback = object : IPrivilegedCallback.Stub() {
override fun handleResult(packageName: String, returnCode: Int) {}
override fun handleResultX(
packageName: String,
returnCode: Int,
extra: String?
) {
removeFromInstallQueue(packageName)
handleCallback(packageName, returnCode, extra)
readyWithAction.set(true)
}
}
try {
if (service.isMoreMethodImplemented) {
try {
service.installSplitPackageMore(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback,
fileList
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
readyWithAction.set(true)
}
} else {
throw Exception("New method not implemented")
}
} catch (th: Throwable) {
th.printStackTrace()
try {
service.installSplitPackageX(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
readyWithAction.set(true)
}
}
} else {
removeFromInstallQueue(packageName)
handleCallback(packageName, returnCode, extra)
postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
readyWithAction.set(true)
}
}
try {
service.installSplitPackageX(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
override fun onServiceDisconnected(name: ComponentName) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
readyWithAction.set(true)
Log.e("Disconnected from Aurora Services")
}
} else {
postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
}
val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
while (!readyWithAction.get()) {
Thread.sleep(1000)
}
Log.i("Services Callback : install wait done")
}
}
private fun handleCallbackUninstall(packageName: String, returnCode: Int, extra: String?) {
Log.i("Services Callback : $packageName $returnCode $extra")
try {
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
)
override fun onServiceDisconnected(name: ComponentName) {
removeFromInstallQueue(packageName)
Log.e("Disconnected from Aurora Services")
postError(packageName, error, extra)
}
}
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
} catch (th: Throwable) {
th.printStackTrace()
}
val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
private fun handleCallback(packageName: String, returnCode: Int, extra: String?) {
Log.i("Services Callback : $packageName $returnCode $extra")
when (returnCode) {
PackageInstaller.STATUS_SUCCESS -> {
EventBus.getDefault().post(
InstallerEvent.Success(
packageName,
context.getString(R.string.installer_status_success)
try {
when (returnCode) {
PackageInstaller.STATUS_SUCCESS -> {
EventBus.getDefault().post(
InstallerEvent.Success(
packageName,
context.getString(R.string.installer_status_success)
)
)
}
else -> {
val error = AppInstaller.getErrorString(
context,
returnCode
)
)
}
else -> {
val error = AppInstaller.getErrorString(
context,
returnCode
)
postError(packageName, error, extra)
postError(packageName, error, extra)
}
}
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
} catch (th: Throwable) {
th.printStackTrace()
}
}
override fun postError(packageName: String, error: String?, extra: String?) {
try {
super.postError(packageName, error, extra)
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
} catch (th: Throwable) {
th.printStackTrace()
}
}

View File

@@ -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
@@ -49,7 +50,7 @@ open class PackageManagerReceiver : BroadcastReceiver() {
}
//Clear installation queue
AppInstaller(context)
AppInstaller.getInstance(context)
.getPreferredInstaller()
.removeFromInstallQueue(packageName)
@@ -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) {

View File

@@ -0,0 +1,7 @@
package com.aurora.store.data.service
import com.aurora.gplayapi.data.models.App
interface AppMetadataStatusListener {
fun onAppMetadataStatusError(reason: String, app: App)
}

View File

@@ -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)
@@ -159,6 +161,13 @@ class NotificationService : Service() {
NotificationManager.IMPORTANCE_MIN
)
)
channels.add(
NotificationChannel(
Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE,
getString(R.string.notification_channel_updater_service),
NotificationManager.IMPORTANCE_MIN
)
)
notificationManager.createNotificationChannels(channels)
}
}
@@ -184,6 +193,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 +215,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 +377,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)
}
@@ -379,7 +408,7 @@ class NotificationService : Service() {
@Synchronized
private fun install(packageName: String, files: List<Download>) {
AppInstaller(this)
AppInstaller.getInstance(this)
.getPreferredInstaller()
.install(
packageName,

View File

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

View File

@@ -0,0 +1,702 @@
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.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.exceptions.ApiException
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.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.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.Log
import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock
import com.tonyodev.fetch2core.FetchObserver
import com.tonyodev.fetch2core.Reason
import nl.komponents.kovenant.task
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 org.greenrobot.eventbus.ThreadMode
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.timerTask
class UpdateService: LifecycleService() {
lateinit var fetch: Fetch
lateinit var downloadManager: DownloadManager
private lateinit var fetchListener: FetchGroupListener
private var fetchActiveDownloadObserver = object : FetchObserver<Boolean> {
override fun onChanged(data: Boolean, reason: Reason) {
if (!data && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
Handler(Looper.getMainLooper()).postDelayed ({
if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
stopSelf()
}
}, 5 * 1000)
}
}
}
private var hasActiveDownloadObserver = false
private val fetchListeners: ArrayList<FetchGroupListener> = ArrayList()
private val fetchPendingEvents: MutableMap</*groupId: */Int, AppDownloadStatus> = mutableMapOf()
private val appMetadataListeners: ArrayList<AppMetadataStatusListener> = ArrayList()
private val appMetadataPendingEvents: ArrayList<AppMetadataStatus> = ArrayList()
var liveUpdateData: MutableLiveData<MutableMap<Int, UpdateFile>> = MutableLiveData()
inner class UpdateServiceBinder : Binder() {
fun getUpdateService() : UpdateService {
return this@UpdateService
}
}
// HashMap<packageName, HashSet<downloadFilePath>>
private var downloadsInCompletedGroup = HashMap<String, HashSet<String>>()
private var installing = HashSet<String>()
private var lock = ReentrantLock()
fun putInInstalling(packageName: String?) {
if (packageName == null) {
return
}
if (lock.tryLock()) {
installing.add(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
} else {
Thread {
while (!lock.tryLock()) {
Thread.sleep(50)
}
installing.add(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
}.start()
}
}
fun removeFromInstalling(packageName: String?, runFromCurrentThread: Boolean = false) {
if (packageName == null) {
return
}
if (lock.tryLock()) {
installing.remove(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
} else {
val toRun = Runnable {
while (!lock.tryLock()) {
Thread.sleep(50)
}
installing.remove(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
}
if (runFromCurrentThread) {
toRun.run()
} else {
Thread(toRun).start()
}
}
}
fun isEmptyInstalling(): Boolean {
while (!lock.tryLock()) {
Thread.sleep(50)
}
val out: Boolean = installing.isEmpty()
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
return out
}
fun containsInInstalling(packageName: String?): Boolean {
if (packageName == null) {
return false
}
while (!lock.tryLock()) {
Thread.sleep(50)
}
val out = installing.contains(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
return out
}
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)
data class AppMetadataStatus(val reason: String, val app: App)
@get:RequiresApi(Build.VERSION_CODES.O)
private val notification: Notification
get() {
val notificationBuilder =
NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE)
return getNotification(notificationBuilder)
}
private fun getNotification(builder: NotificationCompat.Builder): Notification {
return builder.setAutoCancel(true)
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
.setContentTitle(getString(R.string.app_updater_service_notif_title))
.setContentText(getString(R.string.app_updater_service_notif_text))
.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_UPDATER_SERVICE
)
)
startForeground(1, notification)
}
EventBus.getDefault().register(this)
authData = AuthProvider.with(this).getAuthData()
purchaseHelper = PurchaseHelper(authData).using(HttpClient.getPreferredClient())
downloadManager = DownloadManager.with(this)
fetch = downloadManager.fetch
fetchListener = object : FetchGroupListener {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onAdded(groupId, download, fetchGroup)
}
if (download.tag != null) {
removeFromInstalling(download.tag, runFromCurrentThread = true)
}
if (!hasActiveDownloadObserver) {
hasActiveDownloadObserver = true
fetch.addActiveDownloadsObserver(true, fetchActiveDownloadObserver)
}
}
override fun onAdded(download: Download) {
fetchListeners.forEach {
it.onAdded(download)
}
}
override fun onProgress(
groupId: Int,
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onProgress(groupId, download, etaInMilliSeconds, downloadedBytesPerSecond, fetchGroup)
}
}
override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) {
fetchListeners.forEach {
it.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond)
}
}
override fun onQueued(groupId: Int, download: Download, waitingNetwork: Boolean, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onQueued(groupId, download, waitingNetwork, fetchGroup)
}
}
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
fetchListeners.forEach {
it.onQueued(download, waitingOnNetwork)
}
}
override fun onRemoved(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onRemoved(groupId, download, fetchGroup)
}
}
override fun onRemoved(download: Download) {
fetchListeners.forEach {
it.onRemoved(download)
}
}
override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onResumed(groupId, download, fetchGroup)
}
}
override fun onResumed(download: Download) {
fetchListeners.forEach {
it.onResumed(download)
}
}
override fun onStarted(
groupId: Int,
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onStarted(
groupId,
download,
downloadBlocks,
totalBlocks,
fetchGroup)
}
}
override fun onStarted(download: Download, downloadBlocks: List<DownloadBlock>, totalBlocks: Int) {
fetchListeners.forEach {
it.onStarted(
download,
downloadBlocks,
totalBlocks)
}
}
override fun onWaitingNetwork(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onWaitingNetwork(groupId, download, fetchGroup)
}
}
override fun onWaitingNetwork(download: Download) {
fetchListeners.forEach {
it.onWaitingNetwork(download)
}
}
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onCompleted(groupId, download, fetchGroup)
}
if (fetchListeners.isEmpty()) {
fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true)
}
var packageDownloadFilesWhichCompleted = downloadsInCompletedGroup[download.tag!!]
if (packageDownloadFilesWhichCompleted == null) {
packageDownloadFilesWhichCompleted = HashSet()
downloadsInCompletedGroup[download.tag!!] = packageDownloadFilesWhichCompleted
}
packageDownloadFilesWhichCompleted.add(download.file)
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.downloads.all { packageDownloadFilesWhichCompleted.contains(it.file) }) {
downloadsInCompletedGroup.remove(download.tag!!)
if (download.tag != null) {
removeFromInstalling(download.tag, runFromCurrentThread = true)
}
Log.d("Group (${download.tag!!}) downloaded and verified all downloaded!")
Handler(Looper.getMainLooper()).post {
try {
install(download.tag!!, fetchGroup.downloads)
} catch (e: Exception) {
Log.e(e.stackTraceToString())
}
}
}
if (fetchGroup.groupDownloadProgress == 100) {
Log.d("Group (${download.tag!!}) downloaded but NOT verified all downloaded!")
} /* else if (fetchGroup.groupDownloadProgress == -1) {
fetch.deleteGroup(fetchGroup.id)
}*/
}
override fun onCompleted(download: Download) {
fetchListeners.forEach {
it.onCompleted(download)
}
}
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onCancelled(groupId, download, fetchGroup)
}
if (fetchListeners.isEmpty()) {
fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true)
}
}
override fun onCancelled(download: Download) {
fetchListeners.forEach {
it.onCancelled(download)
}
}
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onDeleted(groupId, download, fetchGroup)
}
if (fetchListeners.isEmpty()) {
fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true)
}
}
override fun onDeleted(download: Download) {
fetchListeners.forEach {
it.onDeleted(download)
}
}
override fun onDownloadBlockUpdated(
groupId: Int,
download: Download,
downloadBlock: DownloadBlock,
totalBlocks: Int,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onDownloadBlockUpdated(
groupId,
download,
downloadBlock,
totalBlocks,
fetchGroup
)
}
}
override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) {
fetchListeners.forEach {
it.onDownloadBlockUpdated(
download,
downloadBlock,
totalBlocks
)
}
}
override fun onError(
groupId: Int,
download: Download,
error: Error,
throwable: Throwable?,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onError(
groupId,
download,
error,
throwable,
fetchGroup
)
}
}
override fun onError(download: Download, error: Error, throwable: Throwable?) {
fetchListeners.forEach {
it.onError(
download,
error,
throwable
)
}
}
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onPaused(groupId, download, fetchGroup)
}
}
override fun onPaused(download: Download) {
fetchListeners.forEach {
it.onPaused(download)
}
}
}
/*liveUpdateData.observe(this) { updateData ->
}*/
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
fetch.addListener(fetchListener)
}
}
fun updateApp(app: App, removeExisiting: Boolean = false) {
putInInstalling(app.packageName)
task {
val files = purchaseHelper.purchase(
app.packageName,
app.versionCode,
app.offerType
)
files.filter { it.url.isNotEmpty() }
.map { RequestBuilder.buildRequest(this, app, it) }
.toList()
} successUi { requests ->
if (requests.isNotEmpty()) {
if (removeExisiting) {
/*Remove old fetch group if downloaded earlier, mostly in case of updates*/
fetch.deleteGroup(app.getGroupId(this))
}
fetch.enqueue(requests) {
Log.i("Updating ${app.displayName}")
}
} else {
removeFromInstalling(app.packageName)
Log.e("Failed to download : ${app.displayName}")
appMetadataListeners.forEach {
it.onAppMetadataStatusError(getString(R.string.purchase_session_expired), app)
}
if (appMetadataListeners.isEmpty()) {
appMetadataPendingEvents.add(AppMetadataStatus(getString(R.string.purchase_session_expired), app))
}
}
} failUi { failException ->
removeFromInstalling(app.packageName)
var reason = "Unknown"
when (failException) {
is ApiException.AppNotPurchased -> {
reason = getString(R.string.purchase_invalid)
}
is ApiException.AppNotFound -> {
reason = getString(R.string.purchase_not_found)
}
is ApiException.AppNotSupported -> {
reason = getString(R.string.purchase_unsupported)
}
is ApiException.EmptyDownloads -> {
reason = getString(R.string.purchase_no_file)
}
}
appMetadataListeners.forEach {
it.onAppMetadataStatusError(reason, app)
}
if (appMetadataListeners.isEmpty()) {
appMetadataPendingEvents.add(AppMetadataStatus(reason, app))
}
Log.e("Failed to purchase ${app.displayName} : $reason")
}
}
var timer: Timer? = null
val timerTaskRun: Runnable = Runnable {
Handler(Looper.getMainLooper()).post {
if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
fetch.hasActiveDownloads(true) { hasActiveDownloads ->
if (!hasActiveDownloads && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
Handler(Looper.getMainLooper()).post {
stopSelf()
}
}
}
}
}
}
@Synchronized
private fun install(packageName: String, files: List<Download>?) {
if (containsInInstalling(packageName)) {
println("Already installing $packageName!")
return
}
putInInstalling(packageName)
files?.let { downloads ->
var filesExist = true
downloads.forEach { download ->
filesExist = filesExist && FileUtils.getFile(download.file).exists()
}
if (filesExist) {
task {
try {
val installer = AppInstaller.getInstance(this)
.getPreferredInstaller()
installer.install(
packageName,
files
.filter { it.file.endsWith(".apk") }
.map { it.file }.toList()
)
} catch (th: Throwable) {
removeFromInstalling(packageName)
th.printStackTrace()
}
}.fail {
removeFromInstalling(packageName)
Log.e(it.stackTraceToString())
}
} else {
removeFromInstalling(packageName)
}
}
}
var timerLock = Object()
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onEventBackgroundThreadExec(event: Any) {
when (event) {
is InstallerEvent.Success,
is InstallerEvent.Cancelled,
is InstallerEvent.Failed -> {
when (event) {
is InstallerEvent.Success -> event.packageName
is InstallerEvent.Cancelled -> event.packageName
is InstallerEvent.Failed -> event.packageName
else -> null
}?.run {
removeFromInstalling(this)
}
synchronized(timerLock) {
if (timer != null) {
timer!!.cancel()
timer = null
}
if (timer == null) {
timer = Timer()
}
timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000)
}
}
else -> {
}
}
}
fun registerFetchListener(listener: FetchGroupListener) {
fetchListeners.add(listener)
val iterator = fetchPendingEvents.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 registerAppMetadataListener(listener: AppMetadataStatusListener) {
appMetadataListeners.add(listener)
val iterator = appMetadataPendingEvents.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
listener.onAppMetadataStatusError(item.reason, item.app)
iterator.remove()
}
}
fun unregisterAppMetadataListener(listener: AppMetadataStatusListener) {
appMetadataListeners.remove(listener)
}
fun unregisterFetchListener(listener: AbstractFetchGroupListener) {
fetchListeners.remove(listener)
}
private var binder: UpdateServiceBinder = UpdateServiceBinder()
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
fetchListeners.clear()
appMetadataListeners.clear()
synchronized(timerLock) {
if (timer != null) {
timer!!.cancel()
timer = null
}
if (timer == null) {
timer = Timer()
}
timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000)
}
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) }
}
}

View File

@@ -26,7 +26,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File
fun Context.getInternalBaseDirectory(): String {
return filesDir.path
return (getExternalFilesDir(null) ?: filesDir).path
}
object PathUtil {

View File

@@ -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) {

View File

@@ -21,9 +21,12 @@ package com.aurora.store.view.ui.details
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -32,20 +35,19 @@ import com.aurora.Constants
import com.aurora.extensions.*
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.data.models.File
import com.aurora.gplayapi.exceptions.ApiException
import com.aurora.gplayapi.helpers.AppDetailsHelper
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.MainActivity
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
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.service.AppMetadataStatusListener
import com.aurora.store.data.service.UpdateService
import com.aurora.store.databinding.ActivityDetailsBinding
import com.aurora.store.util.*
import com.aurora.store.view.ui.downloads.DownloadActivity
@@ -64,7 +66,6 @@ import org.apache.commons.io.FileUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
class AppDetailsActivity : BaseDetailsActivity() {
@@ -73,9 +74,39 @@ class AppDetailsActivity : BaseDetailsActivity() {
private lateinit var authData: AuthData
private lateinit var app: App
private lateinit var downloadManager: DownloadManager
private lateinit var fetch: Fetch
private var fetch: Fetch? = null
private var downloadManager: DownloadManager? = null
private var attachToServiceCalled = false
private var updateService: UpdateService? = null
private var pendingAddListeners = true
private var serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) {
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
pendingAddListeners = false
}
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
pendingAddListeners = true
}
}
private lateinit var fetchGroupListener: FetchGroupListener
private lateinit var appMetadataListener: AppMetadataStatusListener
private lateinit var completionMarker: java.io.File
private lateinit var inProgressMarker: java.io.File
@@ -83,6 +114,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
private var isNone = false
private var status = Status.NONE
private var isInstalled: Boolean = false
private var isUpdatable: Boolean = false
private var autoDownload: Boolean = false
private var downloadOnly: Boolean = false
@@ -165,9 +197,8 @@ class AppDetailsActivity : BaseDetailsActivity() {
}
override fun onResume() {
if (!isLAndAbove()) {
checkAndSetupInstall()
}
getUpdateServiceInstance()
checkAndSetupInstall()
super.onResume()
}
@@ -206,11 +237,14 @@ class AppDetailsActivity : BaseDetailsActivity() {
}
}
private var uninstallActionEnabled = false
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_details, menu)
if (::app.isInitialized) {
val installed = PackageUtil.isInstalled(this, app.packageName)
menu?.findItem(R.id.action_uninstall)?.isVisible = installed
uninstallActionEnabled = installed
}
return true
}
@@ -305,7 +339,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
} else {
task {
AppInstaller(this)
AppInstaller.getInstance(this)
.getPreferredInstaller()
.install(
app.packageName,
@@ -324,7 +358,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
@Synchronized
private fun uninstallApp() {
task {
AppInstaller(this)
AppInstaller.getInstance(this)
.getPreferredInstaller()
.uninstall(app.packageName)
}
@@ -427,14 +461,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)
}
}
@@ -444,92 +478,25 @@ class AppDetailsActivity : BaseDetailsActivity() {
}
}
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
private fun purchase() {
bottomSheetBehavior.isHideable = false
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
updateActionState(State.PROGRESS)
task {
val authData = AuthProvider
.with(this)
.getAuthData()
PurchaseHelper(authData)
.using(HttpClient.getPreferredClient())
.purchase(app.packageName, app.versionCode, app.offerType)
} successUi { files ->
if (files.isNotEmpty()) {
var hasOBB = false
files.forEach { file ->
if (file.type == File.FileType.OBB || file.type == File.FileType.PATCH) {
hasOBB = true
}
}
if (hasOBB)
enqueueWithStoragePermission(files)
else
enqueue(files)
runWithPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) {
if (updateService == null) {
listOfActionsWhenServiceAttaches.add({
updateService?.updateApp(app, removeExisiting = true)
})
getUpdateServiceInstance()
} else {
Log.e("Failed to download : ${app.displayName}")
updateActionState(State.IDLE)
updateService?.updateApp(app, removeExisiting = true)
}
} failUi {
updateActionState(State.IDLE)
var reason = "Unknown"
when (it) {
is ApiException.AppNotPurchased -> {
reason = getString(R.string.purchase_invalid)
}
is ApiException.AppNotFound -> {
reason = getString(R.string.purchase_not_found)
}
is ApiException.AppNotSupported -> {
reason = getString(R.string.purchase_unsupported)
}
is ApiException.EmptyDownloads -> {
reason = getString(R.string.purchase_no_file)
}
}
expandBottomSheet(reason)
Log.e("Failed to purchase ${app.displayName} : $reason")
}
}
private fun enqueueWithStoragePermission(files: List<File>) = runWithPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) {
enqueue(files)
}
private fun enqueue(files: List<File>) {
val requestList = files
.filter { it.url.isNotEmpty() }
.map {
RequestBuilder.buildRequest(this, app, it)
}
.toList()
if (requestList.isNotEmpty()) {
/*Remove old fetch group if downloaded earlier, mostly in case of updates*/
fetch.deleteGroup(app.id)
/*Enqueue new fetch group*/
fetch.enqueue(
requestList
) {
status = Status.ADDED
Log.i("Downloading Apks : %s", app.displayName)
}
} else {
updateActionState(State.IDLE)
expandBottomSheet(getString(R.string.purchase_session_expired))
}
}
@@ -544,6 +511,10 @@ class AppDetailsActivity : BaseDetailsActivity() {
else
0
if (progress == 100) {
B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
return@runOnUiThread
}
B.layoutDetailsInstall.apply {
txtProgressPercent.text = ("${progress}%")
@@ -585,7 +556,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
B.layoutDetailsInstall.btnDownload.let { btn ->
if (isInstalled) {
val isUpdatable = PackageUtil.isUpdatable(
isUpdatable = PackageUtil.isUpdatable(
this,
app.packageName,
app.versionCode.toLong()
@@ -603,6 +574,9 @@ class AppDetailsActivity : BaseDetailsActivity() {
btn.setText(R.string.action_open)
btn.addOnClickListener { openApp() }
}
if (!uninstallActionEnabled) {
invalidateOptionsMenu()
}
} else {
if (app.isFree) {
btn.setText(R.string.action_install)
@@ -618,6 +592,9 @@ class AppDetailsActivity : BaseDetailsActivity() {
startDownload()
}
}
if (uninstallActionEnabled) {
invalidateOptionsMenu()
}
}
}
}
@@ -635,16 +612,17 @@ class AppDetailsActivity : BaseDetailsActivity() {
}
private fun attachFetch() {
downloadManager = DownloadManager.with(this)
fetch = downloadManager.fetch
fetch.getFetchGroup(app.id) { fetchGroup: FetchGroup ->
if (fetch == null) {
downloadManager = DownloadManager.with(this)
fetch = downloadManager!!.fetch
}
fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { fetchGroup: FetchGroup ->
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
status = Status.COMPLETED
} else if (downloadManager.isDownloading(fetchGroup)) {
} else if (downloadManager?.isDownloading(fetchGroup) == true) {
status = Status.DOWNLOADING
flip(1)
} else if (downloadManager.isCanceled(fetchGroup)) {
} else if (downloadManager?.isCanceled(fetchGroup) == true) {
status = Status.CANCELLED
} else if (fetchGroup.pausedDownloads.isNotEmpty()) {
status = Status.PAUSED
@@ -655,6 +633,12 @@ class AppDetailsActivity : BaseDetailsActivity() {
fetchGroupListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status
}
}
override fun onStarted(
groupId: Int,
download: Download,
@@ -662,7 +646,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 +664,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 +672,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 +685,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,22 +695,21 @@ 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)
inProgressMarker.delete()
completionMarker.createNewFile()
try {
verifyAndInstall(fetchGroup.downloads)
} catch (e: Exception) {
Log.e(e.stackTraceToString())
inProgressMarker.delete()
completionMarker.createNewFile()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
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 +723,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()
@@ -748,13 +731,63 @@ class AppDetailsActivity : BaseDetailsActivity() {
}
}
fetch.addListener(fetchGroupListener)
appMetadataListener = object : AppMetadataStatusListener {
override fun onAppMetadataStatusError(reason: String, app: App) {
if (app.packageName == this@AppDetailsActivity.app.packageName) {
updateActionState(State.IDLE)
expandBottomSheet(reason)
}
}
}
getUpdateServiceInstance()
B.layoutDetailsInstall.imgCancel.setOnClickListener {
fetch.cancelGroup(
app.id
fetch?.cancelGroup(
app.getGroupId(this@AppDetailsActivity)
)
}
if (updateService != null) {
pendingAddListeners = false
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
} else {
pendingAddListeners = true
}
}
fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(this, UpdateService::class.java)
startService(intent)
bindService(
intent,
serviceConnection,
0
)
}
}
override fun onPause() {
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
unbindService(serviceConnection)
}
super.onPause()
}
override fun onDestroy() {
super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
unbindService(serviceConnection)
}
}
private fun attachBottomSheet() {

View File

@@ -28,6 +28,7 @@ import com.aurora.store.R
import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.model.DownloadFile
import com.aurora.store.databinding.ActivityDownloadBinding
import com.aurora.store.util.Preferences
import com.aurora.store.view.epoxy.views.DownloadViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
import com.aurora.store.view.ui.commons.BaseActivity
@@ -156,10 +157,12 @@ class DownloadActivity : BaseActivity() {
}
R.id.action_clear_completed -> {
fetch.removeAllWithStatus(Status.COMPLETED)
Preferences.getPrefs(this).edit().remove(Preferences.PREFERENCE_UNIQUE_GROUP_IDS).apply()
return true
}
R.id.action_force_clear_all -> {
fetch.deleteAll()
Preferences.getPrefs(this).edit().remove(Preferences.PREFERENCE_UNIQUE_GROUP_IDS).apply()
return true
}
}

View File

@@ -113,21 +113,8 @@ class OnboardingActivity : BaseActivity() {
B.viewpager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
runOnUiThread {
B.btnBackward.isEnabled = position != 0
if (position == 4) {
B.btnForward.text = getString(R.string.action_finish)
B.btnForward.setOnClickListener {
save(PREFERENCE_INTRO, true)
open(SplashActivity::class.java, true)
}
} else {
B.btnForward.text = getString(R.string.action_next)
B.btnForward.setOnClickListener {
B.viewpager2.setCurrentItem(
B.viewpager2.currentItem + 1, true
)
}
}
lastPosition = position
refreshButtonState()
}
}
})
@@ -145,6 +132,32 @@ class OnboardingActivity : BaseActivity() {
B.viewpager2.setCurrentItem(B.viewpager2.currentItem - 1, true)
}
var lastPosition = 0
fun refreshButtonState() {
B.btnBackward.isEnabled = lastPosition != 0
if (lastPosition == 4) {
B.btnForward.text = getString(R.string.action_finish)
B.btnForward.setOnClickListener {
save(PREFERENCE_INTRO, true)
open(SplashActivity::class.java, true)
}
for (fragment in supportFragmentManager.fragments) {
if (fragment is PermissionsFragment) {
B.btnForward.isEnabled = fragment.canGoForward()
break
}
}
} else {
B.btnForward.text = getString(R.string.action_next)
B.btnForward.setOnClickListener {
B.viewpager2.setCurrentItem(
B.viewpager2.currentItem + 1, true
)
}
}
}
override fun onBackPressed() {
if (B.viewpager2.currentItem == 0) {
super.onBackPressed()

View File

@@ -91,6 +91,21 @@ class PermissionsFragment : BaseFragment() {
)
B.epoxyRecycler.withModels {
val writeExternalStorage = ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
val storageManager = if (isRAndAbove()) Environment.isExternalStorageManager() else true
val canInstallPackages = if (isOAndAbove()) requireContext().packageManager.canRequestPackageInstalls() else true
canGoForward = writeExternalStorage && storageManager && canInstallPackages
if (canGoForwardInitial == null) {
canGoForwardInitial = canGoForward
}
if (canGoForward && canGoForwardInitial == false) {
if (activity is OnboardingActivity) {
(activity!! as OnboardingActivity).refreshButtonState()
}
}
setFilterDuplicates(true)
installerList.forEach {
add(
@@ -99,12 +114,9 @@ class PermissionsFragment : BaseFragment() {
.permission(it)
.isGranted(
when (it.id) {
0 -> ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
1 -> if (isRAndAbove()) Environment.isExternalStorageManager() else true
2 -> if (isOAndAbove()) requireContext().packageManager.canRequestPackageInstalls() else true
0 -> writeExternalStorage
1 -> storageManager
2 -> canInstallPackages
else -> false
}
)
@@ -164,4 +176,11 @@ class PermissionsFragment : BaseFragment() {
}
}
}
private var canGoForward = false
private var canGoForwardInitial: Boolean? = null
fun canGoForward(): Boolean {
return canGoForward
}
}

View File

@@ -108,7 +108,7 @@ class AppMenuSheet : BaseBottomSheet() {
R.id.action_uninstall -> {
task {
AppInstaller(requireContext())
AppInstaller.getInstance(requireContext())
.getPreferredInstaller().uninstall(app.packageName)
}
}

View File

@@ -19,24 +19,24 @@
package com.aurora.store.view.ui.updates
import android.content.ComponentName
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
import androidx.lifecycle.ViewModelProvider
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.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_
@@ -48,11 +48,8 @@ import com.aurora.store.view.ui.sheets.AppMenuSheet
import com.aurora.store.viewmodel.all.UpdatesViewModel
import com.tonyodev.fetch2.AbstractFetchGroupListener
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchGroup
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.apache.commons.io.FileUtils
class UpdatesFragment : BaseFragment() {
@@ -60,11 +57,28 @@ 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
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
private lateinit var fetchListener: AbstractFetchGroupListener
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()
@@ -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,31 +129,43 @@ 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
attachToServiceCalled = false
requireContext().unbindService(serviceConnection)
}
super.onPause()
}
override fun onDestroy() {
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
fetch.removeListener(fetchListener)
}
super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
requireContext().unbindService(serviceConnection)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
VM.liveUpdateData.observe(viewLifecycleOwner, {
VM.liveUpdateData.observe(viewLifecycleOwner) {
updateFileMap = it
updateController(updateFileMap)
B.swipeRefreshLayout.isRefreshing = false
})
updateService?.liveUpdateData?.postValue(updateFileMap)
}
B.swipeRefreshLayout.setOnRefreshListener {
VM.observe()
@@ -221,44 +239,56 @@ class UpdatesFragment : BaseFragment() {
}
}
private var updateService: UpdateService? = null
fun runInService(runnable: Runnable) {
if (updateService == null) {
listOfActionsWhenServiceAttaches.add(runnable)
getUpdateServiceInstance()
} else {
runnable.run()
}
}
private fun updateSingle(app: App) {
runInService {
VM.updateState(app.getGroupId(requireContext()), State.QUEUED)
VM.updateState(app.id, 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)
runInService {
updateService?.fetch?.cancelGroup(app.getGroupId(requireContext()))
}
}
private fun updateAll() {
updateFileMap.values.forEach { updateSingle(it.app) }
VM.updateAllEnqueued = true
runInService {
updateService?.updateAll(updateFileMap)
VM.updateAllEnqueued = true
}
}
private fun cancelAll() {
VM.updateAllEnqueued = false
updateFileMap.values.forEach { fetch.cancelGroup(it.app.id) }
runInService {
VM.updateAllEnqueued = false
updateFileMap.values.forEach { updateService?.fetch?.cancelGroup(it.app.getGroupId(requireContext())) }
}
}
fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(requireContext(), UpdateService::class.java)
requireContext().startService(intent)
requireContext().bindService(
intent,
serviceConnection,
0
)
}
}
@Synchronized
@@ -272,7 +302,7 @@ class UpdatesFragment : BaseFragment() {
if (filesExist) {
task {
AppInstaller(requireContext())
AppInstaller.getInstance(requireContext())
.getPreferredInstaller()
.install(
packageName,

View File

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

View File

@@ -366,4 +366,7 @@
<string name="toast_aas_token_failed">Could not generate AAS Token</string>
<string name="toast_export_success">Device config exported</string>
<string name="toast_export_failed">Could not export device config</string>
<string name="notification_channel_updater_service">App background download service</string>
<string name="app_updater_service_notif_title">Background app download</string>
<string name="app_updater_service_notif_text">Enables background app download</string>
</resources>