From ca4f8d939e92c05c2baedc1f14caec913d8332e4 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Tue, 8 Jun 2021 22:22:25 +0300 Subject: [PATCH] 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 --- app/src/main/AndroidManifest.xml | 1 + .../store/data/downloader/DownloadManager.kt | 5 +- .../store/data/downloader/RequestBuilder.kt | 6 +- .../data/downloader/RequestGroupIdBuilder.kt | 55 +++ .../store/data/installer/ServiceInstaller.kt | 54 ++- .../data/receiver/PackageManagerReceiver.kt | 6 +- .../store/data/service/NotificationService.kt | 36 +- .../store/data/service/SelfUpdateService.kt | 7 +- .../store/data/service/UpdateService.kt | 346 ++++++++++++++++++ .../java/com/aurora/store/util/Preferences.kt | 10 +- .../view/ui/details/AppDetailsActivity.kt | 25 +- .../store/view/ui/updates/UpdatesFragment.kt | 93 +++-- .../store/viewmodel/all/UpdatesViewModel.kt | 16 +- 13 files changed, 586 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/data/downloader/RequestGroupIdBuilder.kt create mode 100644 app/src/main/java/com/aurora/store/data/service/UpdateService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 600d84b71..f7a741ad2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -157,6 +157,7 @@ + diff --git a/app/src/main/java/com/aurora/store/data/downloader/DownloadManager.kt b/app/src/main/java/com/aurora/store/data/downloader/DownloadManager.kt index fe08723f5..809b86351 100644 --- a/app/src/main/java/com/aurora/store/data/downloader/DownloadManager.kt +++ b/app/src/main/java/com/aurora/store/data/downloader/DownloadManager.kt @@ -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) } } diff --git a/app/src/main/java/com/aurora/store/data/downloader/RequestBuilder.kt b/app/src/main/java/com/aurora/store/data/downloader/RequestBuilder.kt index c21bdc866..30752d7ea 100644 --- a/app/src/main/java/com/aurora/store/data/downloader/RequestBuilder.kt +++ b/app/src/main/java/com/aurora/store/data/downloader/RequestBuilder.kt @@ -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) } } diff --git a/app/src/main/java/com/aurora/store/data/downloader/RequestGroupIdBuilder.kt b/app/src/main/java/com/aurora/store/data/downloader/RequestGroupIdBuilder.kt new file mode 100644 index 000000000..53f346c78 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/downloader/RequestGroupIdBuilder.kt @@ -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 { + val data = Preferences.getPrefs(context).getString(PREFERENCE_UNIQUE_GROUP_IDS, "")!! + val gson = Gson() + var groupIDMap = HashMap() + if (data.isNotEmpty()) { + val empMapType = object : TypeToken?>() {}.type + groupIDMap = HashMap(gson.fromJson(data, empMapType) ?: HashMap()) + } + val out = mutableListOf() + 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() + if (data.isNotEmpty()) { + val empMapType = object : TypeToken?>() {}.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 +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt index 13d012d68..f996fe90a 100644 --- a/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt @@ -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) { - 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 { diff --git a/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt b/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt index 48293aa37..51f3ad1d0 100644 --- a/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt +++ b/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt @@ -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) { diff --git a/app/src/main/java/com/aurora/store/data/service/NotificationService.kt b/app/src/main/java/com/aurora/store/data/service/NotificationService.kt index 98677e079..205f6ed03 100644 --- a/app/src/main/java/com/aurora/store/data/service/NotificationService.kt +++ b/app/src/main/java/com/aurora/store/data/service/NotificationService.kt @@ -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) } diff --git a/app/src/main/java/com/aurora/store/data/service/SelfUpdateService.kt b/app/src/main/java/com/aurora/store/data/service/SelfUpdateService.kt index 6c714c88d..dcce3457e 100644 --- a/app/src/main/java/com/aurora/store/data/service/SelfUpdateService.kt +++ b/app/src/main/java/com/aurora/store/data/service/SelfUpdateService.kt @@ -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() } diff --git a/app/src/main/java/com/aurora/store/data/service/UpdateService.kt b/app/src/main/java/com/aurora/store/data/service/UpdateService.kt new file mode 100644 index 000000000..216f2dad9 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/service/UpdateService.kt @@ -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 { + 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 = ArrayList() + + private val pendingEvents: MutableMap = mutableMapOf() + + var liveUpdateData: MutableLiveData> = 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?) { + 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) { + updateFileMap.values.forEach { updateApp(it.app) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/util/Preferences.kt b/app/src/main/java/com/aurora/store/util/Preferences.kt index 4628eaf00..53444df8c 100644 --- a/app/src/main/java/com/aurora/store/util/Preferences.kt +++ b/app/src/main/java/com/aurora/store/util/Preferences.kt @@ -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) { diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt index b43855633..a5e1c977f 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt @@ -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) ) } } diff --git a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt index 4383dcbfa..407b482ac 100644 --- a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt @@ -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 = 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 diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt index 4ea77f9fd..ef1e98998 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt @@ -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().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().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().applicationContext, packageName.hashCode()) + groupIDsOfPackageName.forEach { + //Remove from map + updateFileMap.remove(it) + } //Post new update list liveUpdateData.postValue(updateFileMap)