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)