From ca4f8d939e92c05c2baedc1f14caec913d8332e4 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Tue, 8 Jun 2021 22:22:25 +0300 Subject: [PATCH 1/5] 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) From 4e111fa9f488f287a9bd37581f010dc4bf914227 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Wed, 9 Jun 2021 23:09:27 +0300 Subject: [PATCH 2/5] Fix AppInstaller spawning useless instances by preserving the original instance of every instantiated installer type (fixes wrongly started or concurrent installs/uninstalls), implement service based install for new downloads (integrated in the UpdateService.kt and the AppDetailsActivity.kt), prevent multiple calls for install by using ThreadPoolExecutor (applied only to ServiceInstaller.kt for now), fix timer issues in the UpdateService.kt --- .../store/data/installer/AppInstaller.kt | 41 ++- .../store/data/installer/ServiceInstaller.kt | 244 ++++++++++-------- .../data/receiver/PackageManagerReceiver.kt | 2 +- .../store/data/service/NotificationService.kt | 2 +- .../store/data/service/UpdateService.kt | 234 ++++++++++++++--- .../view/ui/details/AppDetailsActivity.kt | 105 ++++++-- .../store/view/ui/sheets/AppMenuSheet.kt | 2 +- .../store/view/ui/updates/UpdatesFragment.kt | 2 +- 8 files changed, 459 insertions(+), 173 deletions(-) diff --git a/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt index d185fd14b..99e5ed78a 100644 --- a/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt @@ -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() + 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 } } } 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 f996fe90a..239da7237 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 @@ -25,9 +25,7 @@ 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 @@ -41,10 +39,14 @@ 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 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 @@ -85,134 +87,168 @@ 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()) { - Log.i(context.getString(R.string.installer_service_available)) - - val callback = object : IPrivilegedCallback.Stub() { - - override fun handleResult(packageName: String, returnCode: Int) { - + executor.execute { + var readyWithAction = 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 = 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) + readyWithAction = true + handleCallbackUninstall(packageName, returnCode, extra) + } + } + + try { + service.deletePackageX( + packageName, + 2, + BuildConfig.APPLICATION_ID, + callback + ) + } catch (e: RemoteException) { + Log.e("Failed to connect Aurora Services") + removeFromInstallQueue(packageName) + readyWithAction = true + } + } else { removeFromInstallQueue(packageName) - handleCallbackUninstall(packageName, returnCode, extra) + readyWithAction = true + postError( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_service_misconfigured) + ) } } - 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 = true } - } else { - postError( - packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) - ) - removeFromInstallQueue(packageName) } - } - 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) { + 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) { - 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()) { - Log.i(context.getString(R.string.installer_service_available)) - - val callback = object : IPrivilegedCallback.Stub() { - - override fun handleResult(packageName: String, returnCode: Int) { - + executor.execute { + var readyWithAction = 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 = 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) + readyWithAction = true + handleCallback(packageName, returnCode, extra) + } + } + + try { + service.installSplitPackageX( + packageName, + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback + ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + readyWithAction = true + postError(packageName, e.localizedMessage, e.stackTraceToString()) + } + } else { removeFromInstallQueue(packageName) - handleCallback(packageName, returnCode, extra) + readyWithAction = true + postError( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_service_misconfigured) + ) } } - 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 = true + Log.e("Disconnected from Aurora Services") } - } else { - postError( - packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) - ) - removeFromInstallQueue(packageName) } - } - 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) { + Thread.sleep(1000) + } + Log.i("Services Callback : install wait done") } - - val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) - intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) - - context.bindService( - intent, - serviceConnection, - Context.BIND_AUTO_CREATE - ) } private fun handleCallbackUninstall(packageName: String, returnCode: Int, extra: String?) { 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 51f3ad1d0..5fe1cfdc0 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 @@ -50,7 +50,7 @@ open class PackageManagerReceiver : BroadcastReceiver() { } //Clear installation queue - AppInstaller(context) + AppInstaller.getInstance(context) .getPreferredInstaller() .removeFromInstallQueue(packageName) 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 205f6ed03..e1f8ffd02 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 @@ -401,7 +401,7 @@ class NotificationService : Service() { @Synchronized private fun install(packageName: String, files: List) { - AppInstaller(this) + AppInstaller.getInstance(this) .getPreferredInstaller() .install( packageName, 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 index 216f2dad9..9b9ab47f5 100644 --- a/app/src/main/java/com/aurora/store/data/service/UpdateService.kt +++ b/app/src/main/java/com/aurora/store/data/service/UpdateService.kt @@ -17,32 +17,31 @@ 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.DownloadBlock 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 org.greenrobot.eventbus.ThreadMode 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 + lateinit var downloadManager: DownloadManager + private lateinit var fetchListener: FetchGroupListener private var fetchActiveDownloadObserver = object : FetchObserver { override fun onChanged(data: Boolean, reason: Reason) { if (!data && !installing.get() && listeners.isEmpty()) { @@ -56,7 +55,7 @@ class UpdateService: LifecycleService() { } private var hasActiveDownloadObserver = false - private val listeners: ArrayList = ArrayList() + private val listeners: ArrayList = ArrayList() private val pendingEvents: MutableMap = mutableMapOf() @@ -114,8 +113,9 @@ class UpdateService: LifecycleService() { EventBus.getDefault().register(this) authData = AuthProvider.with(this).getAuthData() purchaseHelper = PurchaseHelper(authData) - fetch = DownloadManager.with(this).fetch - fetchListener = object : AbstractFetchGroupListener() { + downloadManager = DownloadManager.with(this) + fetch = downloadManager.fetch + fetchListener = object : FetchGroupListener { override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) { listeners.forEach { @@ -127,6 +127,12 @@ class UpdateService: LifecycleService() { } } + override fun onAdded(download: Download) { + listeners.forEach { + it.onAdded(download) + } + } + override fun onProgress( groupId: Int, download: Download, @@ -139,6 +145,86 @@ class UpdateService: LifecycleService() { } } + override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { + listeners.forEach { + it.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond) + } + } + + override fun onQueued(groupId: Int, download: Download, waitingNetwork: Boolean, fetchGroup: FetchGroup) { + listeners.forEach { + it.onQueued(groupId, download, waitingNetwork, fetchGroup) + } + } + + override fun onQueued(download: Download, waitingOnNetwork: Boolean) { + listeners.forEach { + it.onQueued(download, waitingOnNetwork) + } + } + + override fun onRemoved(groupId: Int, download: Download, fetchGroup: FetchGroup) { + listeners.forEach { + it.onRemoved(groupId, download, fetchGroup) + } + } + + override fun onRemoved(download: Download) { + listeners.forEach { + it.onRemoved(download) + } + } + + override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) { + listeners.forEach { + it.onResumed(groupId, download, fetchGroup) + } + } + + override fun onResumed(download: Download) { + listeners.forEach { + it.onResumed(download) + } + } + + override fun onStarted( + groupId: Int, + download: Download, + downloadBlocks: List, + totalBlocks: Int, + fetchGroup: FetchGroup + ) { + listeners.forEach { + it.onStarted( + groupId, + download, + downloadBlocks, + totalBlocks, + fetchGroup) + } + } + + override fun onStarted(download: Download, downloadBlocks: List, totalBlocks: Int) { + listeners.forEach { + it.onStarted( + download, + downloadBlocks, + totalBlocks) + } + } + + override fun onWaitingNetwork(groupId: Int, download: Download, fetchGroup: FetchGroup) { + listeners.forEach { + it.onWaitingNetwork(groupId, download, fetchGroup) + } + } + + override fun onWaitingNetwork(download: Download) { + listeners.forEach { + it.onWaitingNetwork(download) + } + } + override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { listeners.forEach { it.onCompleted(groupId, download, fetchGroup) @@ -146,7 +232,7 @@ class UpdateService: LifecycleService() { if (listeners.isEmpty()) { pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true) } - if (fetchGroup.groupDownloadProgress == 100 || fetchGroup.groupDownloadProgress == -1) { + if (fetchGroup.groupDownloadProgress == 100) { Handler(Looper.getMainLooper()).post { try { install(download.tag!!, fetchGroup.downloads) @@ -159,6 +245,12 @@ class UpdateService: LifecycleService() { }*/ } + override fun onCompleted(download: Download) { + listeners.forEach { + it.onCompleted(download) + } + } + override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) { listeners.forEach { it.onCancelled(groupId, download, fetchGroup) @@ -168,6 +260,10 @@ class UpdateService: LifecycleService() { } } + override fun onCancelled(download: Download) { + TODO("Not yet implemented") + } + override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { listeners.forEach { it.onDeleted(groupId, download, fetchGroup) @@ -176,6 +272,80 @@ class UpdateService: LifecycleService() { pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) } } + + override fun onDeleted(download: Download) { + listeners.forEach { + it.onDeleted(download) + } + } + + override fun onDownloadBlockUpdated( + groupId: Int, + download: Download, + downloadBlock: DownloadBlock, + totalBlocks: Int, + fetchGroup: FetchGroup + ) { + listeners.forEach { + it.onDownloadBlockUpdated( + groupId, + download, + downloadBlock, + totalBlocks, + fetchGroup + ) + } + } + + override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { + listeners.forEach { + it.onDownloadBlockUpdated( + download, + downloadBlock, + totalBlocks + ) + } + } + + override fun onError( + groupId: Int, + download: Download, + error: Error, + throwable: Throwable?, + fetchGroup: FetchGroup + ) { + listeners.forEach { + it.onError( + groupId, + download, + error, + throwable, + fetchGroup + ) + } + } + + override fun onError(download: Download, error: Error, throwable: Throwable?) { + listeners.forEach { + it.onError( + download, + error, + throwable + ) + } + } + + override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { + listeners.forEach { + it.onPaused(groupId, download, fetchGroup) + } + } + + override fun onPaused(download: Download) { + listeners.forEach { + it.onPaused(download) + } + } } /*liveUpdateData.observe(this) { updateData -> @@ -210,16 +380,16 @@ class UpdateService: LifecycleService() { } var timer: Timer? = null - val timerTask: TimerTask = timerTask { + val timerTaskRun: Runnable = Runnable { Handler(Looper.getMainLooper()).post { if (!installing.get() && listeners.isEmpty()) { - fetch.hasActiveDownloads(true, { hasActiveDownloads -> + fetch.hasActiveDownloads(true) { hasActiveDownloads -> if (!hasActiveDownloads && !installing.get() && listeners.isEmpty()) { Handler(Looper.getMainLooper()).post { stopSelf() } } - }) + } } } } @@ -237,7 +407,7 @@ class UpdateService: LifecycleService() { if (filesExist) { task { try { - val installer = AppInstaller(this) + val installer = AppInstaller.getInstance(this) .getPreferredInstaller() installer.install( packageName, @@ -255,30 +425,24 @@ class UpdateService: LifecycleService() { } } - @Subscribe() - fun onEventMainThreadExec(event: Any) { + var timerLock = Object() + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onEventBackgroundThreadExec(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.Success, is InstallerEvent.Failed -> { - if (timer != null) { - timer!!.cancel() - timer = null + synchronized(timerLock) { + if (timer != null) { + timer!!.cancel() + timer = null + } + if (timer == null) { + timer = Timer() + } + installing.set(false) + timer!!.schedule(timerTask { timerTaskRun.run() }, 10 * 1000) } - if (timer == null) { - timer = Timer() - } - installing.set(false) - timer!!.schedule(timerTask, 10 * 1000) } else -> { @@ -286,7 +450,7 @@ class UpdateService: LifecycleService() { } } - fun registerListener(listener: AbstractFetchGroupListener) { + fun registerListener(listener: FetchGroupListener) { listeners.add(listener) val iterator = pendingEvents.iterator() while (iterator.hasNext()) { 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 a5e1c977f..3c69194c1 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 @@ -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 @@ -47,6 +50,7 @@ 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.UpdateService import com.aurora.store.databinding.ActivityDetailsBinding import com.aurora.store.util.* import com.aurora.store.view.ui.downloads.DownloadActivity @@ -74,8 +78,22 @@ 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 updateService: UpdateService? = null + private var pendingAddListener = true + private var serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService() + if (::fetchGroupListener.isInitialized) { + updateService!!.registerListener(fetchGroupListener) + pendingAddListener = false + } + } + + override fun onServiceDisconnected(name: ComponentName) { + updateService = null + } + } private lateinit var fetchGroupListener: FetchGroupListener private lateinit var completionMarker: java.io.File private lateinit var inProgressMarker: java.io.File @@ -166,9 +184,8 @@ class AppDetailsActivity : BaseDetailsActivity() { } override fun onResume() { - if (!isLAndAbove()) { - checkAndSetupInstall() - } + getUpdateServiceInstance() + checkAndSetupInstall() super.onResume() } @@ -207,11 +224,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 } @@ -306,7 +326,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, @@ -325,7 +345,7 @@ class AppDetailsActivity : BaseDetailsActivity() { @Synchronized private fun uninstallApp() { task { - AppInstaller(this) + AppInstaller.getInstance(this) .getPreferredInstaller() .uninstall(app.packageName) } @@ -428,14 +448,14 @@ class AppDetailsActivity : BaseDetailsActivity() { private fun startDownload() { when (status) { Status.PAUSED -> { - fetch.resumeGroup(app.getGroupId(this@AppDetailsActivity)) + updateService?.fetch?.resumeGroup(app.getGroupId(this@AppDetailsActivity)) } Status.DOWNLOADING -> { flip(1) toast("Already downloading") } Status.COMPLETED -> { - fetch.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { + updateService?.fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { verifyAndInstall(it.downloads) } } @@ -519,10 +539,10 @@ class AppDetailsActivity : BaseDetailsActivity() { if (requestList.isNotEmpty()) { /*Remove old fetch group if downloaded earlier, mostly in case of updates*/ - fetch.deleteGroup(app.getGroupId(this@AppDetailsActivity)) + updateService?.fetch?.deleteGroup(app.getGroupId(this@AppDetailsActivity)) /*Enqueue new fetch group*/ - fetch.enqueue( + updateService?.fetch?.enqueue( requestList ) { status = Status.ADDED @@ -604,6 +624,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) @@ -619,6 +642,9 @@ class AppDetailsActivity : BaseDetailsActivity() { startDownload() } } + if (uninstallActionEnabled) { + invalidateOptionsMenu() + } } } } @@ -636,16 +662,13 @@ class AppDetailsActivity : BaseDetailsActivity() { } private fun attachFetch() { - downloadManager = DownloadManager.with(this) - fetch = downloadManager.fetch - - fetch.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { fetchGroup: FetchGroup -> + updateService?.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 (updateService?.downloadManager?.isDownloading(fetchGroup) == true) { status = Status.DOWNLOADING flip(1) - } else if (downloadManager.isCanceled(fetchGroup)) { + } else if (updateService?.downloadManager?.isCanceled(fetchGroup) == true) { status = Status.CANCELLED } else if (fetchGroup.pausedDownloads.isNotEmpty()) { status = Status.PAUSED @@ -716,12 +739,14 @@ class AppDetailsActivity : BaseDetailsActivity() { 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() + } + runOnUiThread { + B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) } } } @@ -749,13 +774,45 @@ class AppDetailsActivity : BaseDetailsActivity() { } } - fetch.addListener(fetchGroupListener) + getUpdateServiceInstance() B.layoutDetailsInstall.imgCancel.setOnClickListener { - fetch.cancelGroup( + updateService?.fetch?.cancelGroup( app.getGroupId(this@AppDetailsActivity) ) } + if (pendingAddListener && updateService != null) { + pendingAddListener = false + updateService!!.registerListener(fetchGroupListener) + } + } + + fun getUpdateServiceInstance() { + if (updateService == null) { + val intent = Intent(this, UpdateService::class.java) + startService(intent) + bindService( + intent, + serviceConnection, + 0 + ) + } + } + + override fun onPause() { + if (updateService != null) { + updateService = null + unbindService(serviceConnection) + } + super.onPause() + } + + override fun onDestroy() { + super.onDestroy() + if (updateService != null) { + updateService = null + unbindService(serviceConnection) + } } private fun attachBottomSheet() { diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt index 310e86df9..29a9cf221 100644 --- a/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt @@ -108,7 +108,7 @@ class AppMenuSheet : BaseBottomSheet() { R.id.action_uninstall -> { task { - AppInstaller(requireContext()) + AppInstaller.getInstance(requireContext()) .getPreferredInstaller().uninstall(app.packageName) } } 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 407b482ac..196a6f1fc 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 @@ -285,7 +285,7 @@ class UpdatesFragment : BaseFragment() { if (filesExist) { task { - AppInstaller(requireContext()) + AppInstaller.getInstance(requireContext()) .getPreferredInstaller() .install( packageName, From b7ebb772dde1c8f96c30ab8d15783f8734b17634 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Mon, 14 Jun 2021 20:58:58 +0300 Subject: [PATCH 3/5] Enable fetching of app download urls in the background (AppDetailsActivity.kt), force granting of all permissions in OnboardingActivity.kt by disabling finish until PermissionsFragment.kt notifies successfully granted permissions, handle currently installing apps better by saving them in multi-thread proof list in UpdateService.kt, add own notification group for UpdateService.kt and all required UpdateService.kt strings, add ability to pass file list to service installer for more advanced, non-system service installers, save downloaded files in getExternalFilesDir by default for more useful file list for more advanced, non-system service installers without compromising security because of Android's restrictions on the Android/data directory --- .../aurora/services/IPrivilegedService.aidl | 11 + app/src/main/java/com/aurora/Constants.kt | 1 + .../store/data/installer/ServiceInstaller.kt | 57 +++-- .../data/service/AppMetadataStatusListener.kt | 7 + .../store/data/service/NotificationService.kt | 7 + .../store/data/service/UpdateService.kt | 213 ++++++++++++------ .../java/com/aurora/store/util/PathUtil.kt | 2 +- .../view/ui/details/AppDetailsActivity.kt | 150 ++++-------- .../view/ui/downloads/DownloadActivity.kt | 3 + .../view/ui/onboarding/OnboardingActivity.kt | 43 ++-- .../view/ui/onboarding/PermissionsFragment.kt | 31 ++- .../store/view/ui/updates/UpdatesFragment.kt | 17 +- app/src/main/res/values/strings.xml | 3 + 13 files changed, 329 insertions(+), 216 deletions(-) create mode 100644 app/src/main/java/com/aurora/store/data/service/AppMetadataStatusListener.kt diff --git a/app/src/main/aidl/com/aurora/services/IPrivilegedService.aidl b/app/src/main/aidl/com/aurora/services/IPrivilegedService.aidl index ccb676e8e..40fb20d5a 100644 --- a/app/src/main/aidl/com/aurora/services/IPrivilegedService.aidl +++ b/app/src/main/aidl/com/aurora/services/IPrivilegedService.aidl @@ -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 uriList, + in int flags, + in String installerPackageName, + in IPrivilegedCallback callback, + in List fileList + ); + oneway void deletePackage( in String packageName, in int flags, diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index def97319c..0e8c24be2 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -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" 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 239da7237..053effdff 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 @@ -73,8 +73,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( @@ -168,7 +177,7 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private fun xInstall(packageName: String, uriList: List) { + private fun xInstall(packageName: String, uriList: List, fileList: List) { executor.execute { var readyWithAction = false Handler(Looper.getMainLooper()).post { @@ -205,17 +214,39 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { } try { - service.installSplitPackageX( - packageName, - uriList, - ACTION_INSTALL_REPLACE_EXISTING, - BuildConfig.APPLICATION_ID, - callback - ) - } catch (e: RemoteException) { - removeFromInstallQueue(packageName) - readyWithAction = true - postError(packageName, e.localizedMessage, e.stackTraceToString()) + if (service.isMoreMethodImplemented) { + try { + service.installSplitPackageMore( + packageName, + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback, + fileList + ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + readyWithAction = true + postError(packageName, e.localizedMessage, e.stackTraceToString()) + } + } 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) + readyWithAction = true + postError(packageName, e.localizedMessage, e.stackTraceToString()) + } } } else { removeFromInstallQueue(packageName) diff --git a/app/src/main/java/com/aurora/store/data/service/AppMetadataStatusListener.kt b/app/src/main/java/com/aurora/store/data/service/AppMetadataStatusListener.kt new file mode 100644 index 000000000..fd927502b --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/service/AppMetadataStatusListener.kt @@ -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) +} \ No newline at end of file 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 e1f8ffd02..421f9218d 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 @@ -161,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) } } 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 index 9b9ab47f5..211a711a8 100644 --- a/app/src/main/java/com/aurora/store/data/service/UpdateService.kt +++ b/app/src/main/java/com/aurora/store/data/service/UpdateService.kt @@ -10,16 +10,18 @@ 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.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.* @@ -34,7 +36,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.util.* -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.CopyOnWriteArrayList import kotlin.concurrent.timerTask class UpdateService: LifecycleService() { @@ -44,9 +46,9 @@ class UpdateService: LifecycleService() { private lateinit var fetchListener: FetchGroupListener private var fetchActiveDownloadObserver = object : FetchObserver { override fun onChanged(data: Boolean, reason: Reason) { - if (!data && !installing.get() && listeners.isEmpty()) { + if (!data && installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { Handler(Looper.getMainLooper()).postDelayed ({ - if (!installing.get() && listeners.isEmpty()) { + if (installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { stopSelf() } }, 5 * 1000) @@ -55,9 +57,13 @@ class UpdateService: LifecycleService() { } private var hasActiveDownloadObserver = false - private val listeners: ArrayList = ArrayList() + private val fetchListeners: ArrayList = ArrayList() - private val pendingEvents: MutableMap = mutableMapOf() + private val fetchPendingEvents: MutableMap = mutableMapOf() + + private val appMetadataListeners: ArrayList = ArrayList() + + private val appMetadataPendingEvents: ArrayList = ArrayList() var liveUpdateData: MutableLiveData> = MutableLiveData() @@ -67,7 +73,7 @@ class UpdateService: LifecycleService() { } } - private var installing: AtomicBoolean = AtomicBoolean() + private var installing = CopyOnWriteArrayList() private lateinit var purchaseHelper: PurchaseHelper @@ -79,19 +85,21 @@ class UpdateService: LifecycleService() { 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_GENERAL) + 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("Updating apps") - .setContentText("Updating apps in the background") + .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() @@ -105,22 +113,25 @@ class UpdateService: LifecycleService() { val notification = getNotification( NotificationCompat.Builder( this, - Constants.NOTIFICATION_CHANNEL_GENERAL + Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE ) ) startForeground(1, notification) } EventBus.getDefault().register(this) authData = AuthProvider.with(this).getAuthData() - purchaseHelper = PurchaseHelper(authData) + 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) { - listeners.forEach { + fetchListeners.forEach { it.onAdded(groupId, download, fetchGroup) } + if (download.tag != null) { + installing.remove(download.tag) + } if (!hasActiveDownloadObserver) { hasActiveDownloadObserver = true fetch.addActiveDownloadsObserver(true, fetchActiveDownloadObserver) @@ -128,7 +139,7 @@ class UpdateService: LifecycleService() { } override fun onAdded(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onAdded(download) } } @@ -140,49 +151,49 @@ class UpdateService: LifecycleService() { downloadedBytesPerSecond: Long, fetchGroup: FetchGroup ) { - listeners.forEach { + fetchListeners.forEach { it.onProgress(groupId, download, etaInMilliSeconds, downloadedBytesPerSecond, fetchGroup) } } override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { - listeners.forEach { + fetchListeners.forEach { it.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond) } } override fun onQueued(groupId: Int, download: Download, waitingNetwork: Boolean, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onQueued(groupId, download, waitingNetwork, fetchGroup) } } override fun onQueued(download: Download, waitingOnNetwork: Boolean) { - listeners.forEach { + fetchListeners.forEach { it.onQueued(download, waitingOnNetwork) } } override fun onRemoved(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onRemoved(groupId, download, fetchGroup) } } override fun onRemoved(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onRemoved(download) } } override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onResumed(groupId, download, fetchGroup) } } override fun onResumed(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onResumed(download) } } @@ -194,7 +205,7 @@ class UpdateService: LifecycleService() { totalBlocks: Int, fetchGroup: FetchGroup ) { - listeners.forEach { + fetchListeners.forEach { it.onStarted( groupId, download, @@ -205,7 +216,7 @@ class UpdateService: LifecycleService() { } override fun onStarted(download: Download, downloadBlocks: List, totalBlocks: Int) { - listeners.forEach { + fetchListeners.forEach { it.onStarted( download, downloadBlocks, @@ -214,23 +225,23 @@ class UpdateService: LifecycleService() { } override fun onWaitingNetwork(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onWaitingNetwork(groupId, download, fetchGroup) } } override fun onWaitingNetwork(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onWaitingNetwork(download) } } override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onCompleted(groupId, download, fetchGroup) } - if (listeners.isEmpty()) { - pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true) + if (fetchListeners.isEmpty()) { + fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true) } if (fetchGroup.groupDownloadProgress == 100) { Handler(Looper.getMainLooper()).post { @@ -246,35 +257,37 @@ class UpdateService: LifecycleService() { } override fun onCompleted(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onCompleted(download) } } override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onCancelled(groupId, download, fetchGroup) } - if (listeners.isEmpty()) { - pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) + if (fetchListeners.isEmpty()) { + fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) } } override fun onCancelled(download: Download) { - TODO("Not yet implemented") + fetchListeners.forEach { + it.onCancelled(download) + } } override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onDeleted(groupId, download, fetchGroup) } - if (listeners.isEmpty()) { - pendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) + if (fetchListeners.isEmpty()) { + fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) } } override fun onDeleted(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onDeleted(download) } } @@ -286,7 +299,7 @@ class UpdateService: LifecycleService() { totalBlocks: Int, fetchGroup: FetchGroup ) { - listeners.forEach { + fetchListeners.forEach { it.onDownloadBlockUpdated( groupId, download, @@ -298,7 +311,7 @@ class UpdateService: LifecycleService() { } override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { - listeners.forEach { + fetchListeners.forEach { it.onDownloadBlockUpdated( download, downloadBlock, @@ -314,7 +327,7 @@ class UpdateService: LifecycleService() { throwable: Throwable?, fetchGroup: FetchGroup ) { - listeners.forEach { + fetchListeners.forEach { it.onError( groupId, download, @@ -326,7 +339,7 @@ class UpdateService: LifecycleService() { } override fun onError(download: Download, error: Error, throwable: Throwable?) { - listeners.forEach { + fetchListeners.forEach { it.onError( download, error, @@ -336,13 +349,13 @@ class UpdateService: LifecycleService() { } override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { - listeners.forEach { + fetchListeners.forEach { it.onPaused(groupId, download, fetchGroup) } } override fun onPaused(download: Download) { - listeners.forEach { + fetchListeners.forEach { it.onPaused(download) } } @@ -355,8 +368,8 @@ class UpdateService: LifecycleService() { } } - fun updateApp(app: App) { - installing.set(true) + fun updateApp(app: App, removeExisiting: Boolean = false) { + installing.add(app.packageName) task { val files = purchaseHelper.purchase( app.packageName, @@ -364,27 +377,70 @@ class UpdateService: LifecycleService() { app.offerType ) - files.map { RequestBuilder.buildRequest(this, app, it) } - } successUi { - val requests = it.filter { request -> request.url.isNotEmpty() }.toList() + 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 { - toast("Failed to update ${app.displayName}") + installing.remove(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 { - Log.e("Failed to update ${app.displayName}") + } failUi { failException -> + installing.remove(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 (!installing.get() && listeners.isEmpty()) { + if (installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { fetch.hasActiveDownloads(true) { hasActiveDownloads -> - if (!hasActiveDownloads && !installing.get() && listeners.isEmpty()) { + if (!hasActiveDownloads && installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { Handler(Looper.getMainLooper()).post { stopSelf() } @@ -396,8 +452,8 @@ class UpdateService: LifecycleService() { @Synchronized private fun install(packageName: String, files: List?) { + installing.add(packageName) files?.let { downloads -> - installing.set(true) var filesExist = true downloads.forEach { download -> @@ -431,7 +487,16 @@ class UpdateService: LifecycleService() { 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 { + installing.remove(this) + } synchronized(timerLock) { if (timer != null) { timer!!.cancel() @@ -440,8 +505,7 @@ class UpdateService: LifecycleService() { if (timer == null) { timer = Timer() } - installing.set(false) - timer!!.schedule(timerTask { timerTaskRun.run() }, 10 * 1000) + timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000) } } else -> { @@ -450,9 +514,9 @@ class UpdateService: LifecycleService() { } } - fun registerListener(listener: FetchGroupListener) { - listeners.add(listener) - val iterator = pendingEvents.iterator() + 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) { @@ -464,8 +528,22 @@ class UpdateService: LifecycleService() { } } - fun unregisterListener(listener: AbstractFetchGroupListener) { - listeners.remove(listener) + 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() @@ -476,15 +554,16 @@ class UpdateService: LifecycleService() { } override fun onUnbind(intent: Intent?): Boolean { - listeners.clear() - if (!installing.get()) { - fetch.hasActiveDownloads(true, { hasActiveDownloads -> - if (!hasActiveDownloads && !installing.get() && listeners.isEmpty()) { + fetchListeners.clear() + appMetadataListeners.clear() + if (installing.isEmpty()) { + fetch.hasActiveDownloads(true) { hasActiveDownloads -> + if (!hasActiveDownloads && installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { Handler(Looper.getMainLooper()).post { stopSelf() } } - }) + } } return true } diff --git a/app/src/main/java/com/aurora/store/util/PathUtil.kt b/app/src/main/java/com/aurora/store/util/PathUtil.kt index 12c3b26f1..b5f952ee3 100644 --- a/app/src/main/java/com/aurora/store/util/PathUtil.kt +++ b/app/src/main/java/com/aurora/store/util/PathUtil.kt @@ -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 { 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 3c69194c1..37db9a5ab 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 @@ -35,21 +35,18 @@ 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.* @@ -69,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() { @@ -78,14 +74,18 @@ class AppDetailsActivity : BaseDetailsActivity() { private lateinit var authData: AuthData private lateinit var app: App + private var fetch: Fetch? = null + private var downloadManager: DownloadManager? = null private var updateService: UpdateService? = null private var pendingAddListener = true private var serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, binder: IBinder) { updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService() - if (::fetchGroupListener.isInitialized) { - updateService!!.registerListener(fetchGroupListener) + if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized) { + updateService!!.registerFetchListener(fetchGroupListener) + // appMetadataListener needs to be initialized after the fetchGroupListener + updateService!!.registerAppMetadataListener(appMetadataListener) pendingAddListener = false } } @@ -95,6 +95,7 @@ class AppDetailsActivity : BaseDetailsActivity() { } } private lateinit var fetchGroupListener: FetchGroupListener + private lateinit var appMetadataListener: AppMetadataStatusListener private lateinit var completionMarker: java.io.File private lateinit var inProgressMarker: java.io.File @@ -448,14 +449,14 @@ class AppDetailsActivity : BaseDetailsActivity() { private fun startDownload() { when (status) { Status.PAUSED -> { - updateService?.fetch?.resumeGroup(app.getGroupId(this@AppDetailsActivity)) + fetch?.resumeGroup(app.getGroupId(this@AppDetailsActivity)) } Status.DOWNLOADING -> { flip(1) toast("Already downloading") } Status.COMPLETED -> { - updateService?.fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { + fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { verifyAndInstall(it.downloads) } } @@ -466,91 +467,15 @@ class AppDetailsActivity : BaseDetailsActivity() { } 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) - } else { - Log.e("Failed to download : ${app.displayName}") - updateActionState(State.IDLE) - } - } 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) = runWithPermissions( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) { - enqueue(files) - } - - private fun enqueue(files: List) { - 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*/ - updateService?.fetch?.deleteGroup(app.getGroupId(this@AppDetailsActivity)) - - /*Enqueue new fetch group*/ - updateService?.fetch?.enqueue( - requestList - ) { - status = Status.ADDED - Log.i("Downloading Apks : %s", app.displayName) - } - } else { - updateActionState(State.IDLE) - expandBottomSheet(getString(R.string.purchase_session_expired)) + runWithPermissions( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) { + updateService?.updateApp(app, removeExisiting = true) } } @@ -560,11 +485,18 @@ class AppDetailsActivity : BaseDetailsActivity() { downloadedBytesPerSecond: Long ) { runOnUiThread { + if (isInstalled) { + return@runOnUiThread + } val progress = if (fetchGroup.groupDownloadProgress > 0) fetchGroup.groupDownloadProgress else 0 + if (progress == 100) { + B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) + return@runOnUiThread + } B.layoutDetailsInstall.apply { txtProgressPercent.text = ("${progress}%") @@ -662,13 +594,17 @@ class AppDetailsActivity : BaseDetailsActivity() { } private fun attachFetch() { - updateService?.fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { 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 (updateService?.downloadManager?.isDownloading(fetchGroup) == true) { + } else if (downloadManager?.isDownloading(fetchGroup) == true) { status = Status.DOWNLOADING flip(1) - } else if (updateService?.downloadManager?.isCanceled(fetchGroup) == true) { + } else if (downloadManager?.isCanceled(fetchGroup) == true) { status = Status.CANCELLED } else if (fetchGroup.pausedDownloads.isNotEmpty()) { status = Status.PAUSED @@ -679,6 +615,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, @@ -745,9 +687,6 @@ class AppDetailsActivity : BaseDetailsActivity() { } catch (ex: Exception) { ex.printStackTrace() } - runOnUiThread { - B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) - } } } @@ -774,16 +713,27 @@ class AppDetailsActivity : BaseDetailsActivity() { } } + 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 { - updateService?.fetch?.cancelGroup( + fetch?.cancelGroup( app.getGroupId(this@AppDetailsActivity) ) } if (pendingAddListener && updateService != null) { pendingAddListener = false - updateService!!.registerListener(fetchGroupListener) + updateService!!.registerFetchListener(fetchGroupListener) + // appMetadataListener needs to be initialized after the fetchGroupListener + updateService!!.registerAppMetadataListener(appMetadataListener) } } diff --git a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadActivity.kt b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadActivity.kt index e83953af7..af9ee69dc 100644 --- a/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/downloads/DownloadActivity.kt @@ -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 } } diff --git a/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingActivity.kt b/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingActivity.kt index bfc084249..974fbc853 100644 --- a/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/com/aurora/store/view/ui/onboarding/OnboardingActivity.kt @@ -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() diff --git a/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt index 5f4fee642..d302b4034 100644 --- a/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/onboarding/PermissionsFragment.kt @@ -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 + } } \ No newline at end of file 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 196a6f1fc..cf6c1b6ea 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,9 +19,7 @@ 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 @@ -32,18 +30,12 @@ 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 @@ -56,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() { @@ -72,7 +61,7 @@ class UpdatesFragment : BaseFragment() { private var serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, binder: IBinder) { updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService() - updateService!!.registerListener(fetchListener) + updateService!!.registerFetchListener(fetchListener) } override fun onServiceDisconnected(name: ComponentName) { @@ -158,12 +147,12 @@ class UpdatesFragment : BaseFragment() { 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() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d8b2b9bd..0ba8e1ba8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -366,4 +366,7 @@ Failed to generate AAS Token Device config exported successfully Failed to export device config + App background download service + Background app download + Enables background app download From 014d3478f2a73a7830c1cd8b3d40a1d8925c93b4 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Tue, 15 Jun 2021 14:11:29 +0300 Subject: [PATCH 4/5] Fix issue with installing being called multiple times when a completed download is queue again, fix issues with UpdateService.kt stiopping when it might be needed (extended stopSelf timeout (5 secs) & made all calls to the service stack in a HashSet in case the service was not running when it was needed - AppDetailsActivity.kt & UpdatesFragment.kt), remove useless ThreadPoolExecutor in ServiceInstaller.kt, made the `installing` list in UpdateService.kt a set and made it concurrency-proof with a lock and dynamic thread creation when needed --- .../store/data/installer/ServiceInstaller.kt | 290 ++++++++---------- .../store/data/service/UpdateService.kt | 153 +++++++-- .../view/ui/details/AppDetailsActivity.kt | 47 ++- .../store/view/ui/updates/UpdatesFragment.kt | 44 ++- 4 files changed, 340 insertions(+), 194 deletions(-) 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 053effdff..63b23f1c0 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 @@ -42,11 +42,12 @@ 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 +import java.util.concurrent.atomic.AtomicInteger 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 @@ -96,190 +97,169 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { } override fun uninstall(packageName: String) { - executor.execute { - var readyWithAction = 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 = true - return + val attachedToServiceTimeout = AtomicBoolean(false) + + AuroraApplication.enqueuedInstalls.add(packageName) + + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + attachedToServiceTimeout.set(true) + 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) { + } - AuroraApplication.enqueuedInstalls.add(packageName) - 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) { - - } - - override fun handleResultX( - packageName: String, - returnCode: Int, - extra: String? - ) { - removeFromInstallQueue(packageName) - readyWithAction = true - handleCallbackUninstall(packageName, returnCode, extra) - } - } - - try { - service.deletePackageX( - packageName, - 2, - BuildConfig.APPLICATION_ID, - callback - ) - } catch (e: RemoteException) { - Log.e("Failed to connect Aurora Services") - removeFromInstallQueue(packageName) - readyWithAction = true - } - } else { + override fun handleResultX( + packageName: String, + returnCode: Int, + extra: String? + ) { removeFromInstallQueue(packageName) - readyWithAction = true - postError( - packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) - ) + handleCallbackUninstall(packageName, returnCode, extra) } } - override fun onServiceDisconnected(name: ComponentName) { + try { + service.deletePackageX( + packageName, + 2, + BuildConfig.APPLICATION_ID, + callback + ) + } catch (e: RemoteException) { + Log.e("Failed to connect Aurora Services") removeFromInstallQueue(packageName) - Log.e("Disconnected from Aurora Services") - readyWithAction = true } + } else { + removeFromInstallQueue(packageName) + 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) { - Thread.sleep(1000) + + 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 + ) + + Handler(Looper.getMainLooper()).postDelayed({ + if (!attachedToServiceTimeout.get()) { + removeFromInstallQueue(packageName) + } + }, 25 * 1000) } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun xInstall(packageName: String, uriList: List, fileList: List) { - executor.execute { - var readyWithAction = 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 = true - return + val attachedToServiceTimeout = AtomicBoolean(false) + + AuroraApplication.enqueuedInstalls.add(packageName) + + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + attachedToServiceTimeout.set(true) + 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) { + } - AuroraApplication.enqueuedInstalls.add(packageName) - 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) { - - } - - override fun handleResultX( - packageName: String, - returnCode: Int, - extra: String? - ) { - removeFromInstallQueue(packageName) - readyWithAction = true - handleCallback(packageName, returnCode, extra) - } - } + override fun handleResultX( + packageName: String, + returnCode: Int, + extra: String? + ) { + removeFromInstallQueue(packageName) + handleCallback(packageName, returnCode, extra) + } + } + try { + if (service.isMoreMethodImplemented) { try { - if (service.isMoreMethodImplemented) { - try { - service.installSplitPackageMore( - packageName, - uriList, - ACTION_INSTALL_REPLACE_EXISTING, - BuildConfig.APPLICATION_ID, - callback, - fileList - ) - } catch (e: RemoteException) { - removeFromInstallQueue(packageName) - readyWithAction = true - postError(packageName, e.localizedMessage, e.stackTraceToString()) - } - } 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) - readyWithAction = true - postError(packageName, e.localizedMessage, e.stackTraceToString()) - } + service.installSplitPackageMore( + packageName, + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback, + fileList + ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + postError(packageName, e.localizedMessage, e.stackTraceToString()) } } else { - removeFromInstallQueue(packageName) - readyWithAction = true - postError( + throw Exception("New method not implemented") + } + } catch (th: Throwable) { + th.printStackTrace() + try { + service.installSplitPackageX( packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + postError(packageName, e.localizedMessage, e.stackTraceToString()) } } - - override fun onServiceDisconnected(name: ComponentName) { - removeFromInstallQueue(packageName) - readyWithAction = true - Log.e("Disconnected from Aurora Services") - } + } else { + removeFromInstallQueue(packageName) + 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) { - Thread.sleep(1000) + + override fun onServiceDisconnected(name: ComponentName) { + removeFromInstallQueue(packageName) + Log.e("Disconnected from Aurora Services") } - Log.i("Services Callback : install wait done") } + + val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) + intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) + + context.bindService( + intent, + serviceConnection, + Context.BIND_AUTO_CREATE + ) + Handler(Looper.getMainLooper()).postDelayed({ + if (!attachedToServiceTimeout.get()) { + removeFromInstallQueue(packageName) + } + }, 25 * 1000) } private fun handleCallbackUninstall(packageName: String, returnCode: Int, extra: String?) { 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 index 211a711a8..27dcb8c26 100644 --- a/app/src/main/java/com/aurora/store/data/service/UpdateService.kt +++ b/app/src/main/java/com/aurora/store/data/service/UpdateService.kt @@ -36,7 +36,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.util.* -import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.timerTask class UpdateService: LifecycleService() { @@ -46,9 +46,9 @@ class UpdateService: LifecycleService() { private lateinit var fetchListener: FetchGroupListener private var fetchActiveDownloadObserver = object : FetchObserver { override fun onChanged(data: Boolean, reason: Reason) { - if (!data && installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + if (!data && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { Handler(Looper.getMainLooper()).postDelayed ({ - if (installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { stopSelf() } }, 5 * 1000) @@ -73,7 +73,97 @@ class UpdateService: LifecycleService() { } } - private var installing = CopyOnWriteArrayList() + // HashMap> + private var downloadsInCompletedGroup = HashMap>() + + private var installing = HashSet() + 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 @@ -130,7 +220,7 @@ class UpdateService: LifecycleService() { it.onAdded(groupId, download, fetchGroup) } if (download.tag != null) { - installing.remove(download.tag) + removeFromInstalling(download.tag, runFromCurrentThread = true) } if (!hasActiveDownloadObserver) { hasActiveDownloadObserver = true @@ -243,7 +333,18 @@ class UpdateService: LifecycleService() { if (fetchListeners.isEmpty()) { fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true) } - if (fetchGroup.groupDownloadProgress == 100) { + 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) @@ -251,6 +352,9 @@ class UpdateService: LifecycleService() { 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) }*/ @@ -369,7 +473,7 @@ class UpdateService: LifecycleService() { } fun updateApp(app: App, removeExisiting: Boolean = false) { - installing.add(app.packageName) + putInInstalling(app.packageName) task { val files = purchaseHelper.purchase( app.packageName, @@ -390,7 +494,7 @@ class UpdateService: LifecycleService() { Log.i("Updating ${app.displayName}") } } else { - installing.remove(app.packageName) + removeFromInstalling(app.packageName) Log.e("Failed to download : ${app.displayName}") appMetadataListeners.forEach { it.onAppMetadataStatusError(getString(R.string.purchase_session_expired), app) @@ -401,7 +505,7 @@ class UpdateService: LifecycleService() { } } } failUi { failException -> - installing.remove(app.packageName) + removeFromInstalling(app.packageName) var reason = "Unknown" when (failException) { @@ -438,9 +542,9 @@ class UpdateService: LifecycleService() { var timer: Timer? = null val timerTaskRun: Runnable = Runnable { Handler(Looper.getMainLooper()).post { - if (installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { fetch.hasActiveDownloads(true) { hasActiveDownloads -> - if (!hasActiveDownloads && installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + if (!hasActiveDownloads && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { Handler(Looper.getMainLooper()).post { stopSelf() } @@ -452,7 +556,11 @@ class UpdateService: LifecycleService() { @Synchronized private fun install(packageName: String, files: List?) { - installing.add(packageName) + if (containsInInstalling(packageName)) { + println("Already installing $packageName!") + return + } + putInInstalling(packageName) files?.let { downloads -> var filesExist = true @@ -472,11 +580,15 @@ class UpdateService: LifecycleService() { .map { it.file }.toList() ) } catch (th: Throwable) { + removeFromInstalling(packageName) th.printStackTrace() } }.fail { + removeFromInstalling(packageName) Log.e(it.stackTraceToString()) } + } else { + removeFromInstalling(packageName) } } } @@ -495,7 +607,7 @@ class UpdateService: LifecycleService() { is InstallerEvent.Failed -> event.packageName else -> null }?.run { - installing.remove(this) + removeFromInstalling(this) } synchronized(timerLock) { if (timer != null) { @@ -556,14 +668,15 @@ class UpdateService: LifecycleService() { override fun onUnbind(intent: Intent?): Boolean { fetchListeners.clear() appMetadataListeners.clear() - if (installing.isEmpty()) { - fetch.hasActiveDownloads(true) { hasActiveDownloads -> - if (!hasActiveDownloads && installing.isEmpty() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { - Handler(Looper.getMainLooper()).post { - stopSelf() - } - } + synchronized(timerLock) { + if (timer != null) { + timer!!.cancel() + timer = null } + if (timer == null) { + timer = Timer() + } + timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000) } return true } 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 37db9a5ab..6b604198c 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 @@ -77,21 +77,32 @@ class AppDetailsActivity : BaseDetailsActivity() { private var fetch: Fetch? = null private var downloadManager: DownloadManager? = null + private var attachToServiceCalled = false private var updateService: UpdateService? = null - private var pendingAddListener = true + 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) { + if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) { updateService!!.registerFetchListener(fetchGroupListener) // appMetadataListener needs to be initialized after the fetchGroupListener updateService!!.registerAppMetadataListener(appMetadataListener) - pendingAddListener = false + 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 @@ -103,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 @@ -466,6 +478,8 @@ class AppDetailsActivity : BaseDetailsActivity() { } } + val listOfActionsWhenServiceAttaches = ArrayList() + private fun purchase() { bottomSheetBehavior.isHideable = false bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED @@ -475,7 +489,14 @@ class AppDetailsActivity : BaseDetailsActivity() { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE ) { - updateService?.updateApp(app, removeExisiting = true) + if (updateService == null) { + listOfActionsWhenServiceAttaches.add({ + updateService?.updateApp(app, removeExisiting = true) + }) + getUpdateServiceInstance() + } else { + updateService?.updateApp(app, removeExisiting = true) + } } } @@ -485,9 +506,6 @@ class AppDetailsActivity : BaseDetailsActivity() { downloadedBytesPerSecond: Long ) { runOnUiThread { - if (isInstalled) { - return@runOnUiThread - } val progress = if (fetchGroup.groupDownloadProgress > 0) fetchGroup.groupDownloadProgress else @@ -538,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() @@ -729,16 +747,19 @@ class AppDetailsActivity : BaseDetailsActivity() { app.getGroupId(this@AppDetailsActivity) ) } - if (pendingAddListener && updateService != null) { - pendingAddListener = false + 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) { + if (updateService == null && !attachToServiceCalled) { + attachToServiceCalled = true val intent = Intent(this, UpdateService::class.java) startService(intent) bindService( @@ -752,6 +773,8 @@ class AppDetailsActivity : BaseDetailsActivity() { override fun onPause() { if (updateService != null) { updateService = null + attachToServiceCalled = false + pendingAddListeners = true unbindService(serviceConnection) } super.onPause() @@ -761,6 +784,8 @@ class AppDetailsActivity : BaseDetailsActivity() { super.onDestroy() if (updateService != null) { updateService = null + attachToServiceCalled = false + pendingAddListeners = true unbindService(serviceConnection) } } 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 cf6c1b6ea..630aa4e10 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 @@ -57,15 +57,26 @@ class UpdatesFragment : BaseFragment() { private lateinit var B: FragmentUpdatesBinding private lateinit var VM: UpdatesViewModel + val listOfActionsWhenServiceAttaches = ArrayList() 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 } } @@ -131,6 +142,7 @@ class UpdatesFragment : BaseFragment() { override fun onPause() { if (updateService != null) { updateService = null + attachToServiceCalled = false requireContext().unbindService(serviceConnection) } super.onPause() @@ -140,6 +152,7 @@ class UpdatesFragment : BaseFragment() { super.onDestroy() if (updateService != null) { updateService = null + attachToServiceCalled = false requireContext().unbindService(serviceConnection) } } @@ -228,9 +241,17 @@ class UpdatesFragment : BaseFragment() { private var updateService: UpdateService? = null - private fun updateSingle(app: App) { + fun runInService(runnable: Runnable) { + if (updateService == null) { + listOfActionsWhenServiceAttaches.add(runnable) + getUpdateServiceInstance() + } else { + runnable.run() + } + } - if (updateService != null) { + private fun updateSingle(app: App) { + runInService { VM.updateState(app.getGroupId(requireContext()), State.QUEUED) updateService?.updateApp(app) @@ -238,21 +259,28 @@ class UpdatesFragment : BaseFragment() { } private fun cancelSingle(app: App) { - updateService?.fetch?.cancelGroup(app.getGroupId(requireContext())) + runInService { + updateService?.fetch?.cancelGroup(app.getGroupId(requireContext())) + } } private fun updateAll() { - updateService?.updateAll(updateFileMap) - VM.updateAllEnqueued = true + runInService { + updateService?.updateAll(updateFileMap) + VM.updateAllEnqueued = true + } } private fun cancelAll() { - VM.updateAllEnqueued = false - updateFileMap.values.forEach { updateService?.fetch?.cancelGroup(it.app.getGroupId(requireContext())) } + runInService { + VM.updateAllEnqueued = false + updateFileMap.values.forEach { updateService?.fetch?.cancelGroup(it.app.getGroupId(requireContext())) } + } } fun getUpdateServiceInstance() { - if (updateService == null) { + if (updateService == null && !attachToServiceCalled) { + attachToServiceCalled = true val intent = Intent(requireContext(), UpdateService::class.java) requireContext().startService(intent) requireContext().bindService( From bcdfb2b8f7e8f5cf98e1c7f8fa8038ef8ea24bf4 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Thu, 17 Jun 2021 20:52:27 +0300 Subject: [PATCH 5/5] Revert "remove useless ThreadPoolExecutor in ServiceInstaller.kt" as it is needed for consistent and manageable ServiceConnection --- .../store/data/installer/ServiceInstaller.kt | 373 ++++++++++-------- 1 file changed, 201 insertions(+), 172 deletions(-) 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 63b23f1c0..3666b3b91 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 @@ -43,11 +43,11 @@ import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger 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 @@ -97,227 +97,256 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { } override fun uninstall(packageName: String) { - val attachedToServiceTimeout = AtomicBoolean(false) - - AuroraApplication.enqueuedInstalls.add(packageName) - - serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, binder: IBinder) { - attachedToServiceTimeout.set(true) - 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) - handleCallbackUninstall(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 { - removeFromInstallQueue(packageName) - 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 - ) - - Handler(Looper.getMainLooper()).postDelayed({ - if (!attachedToServiceTimeout.get()) { - removeFromInstallQueue(packageName) - } - }, 25 * 1000) } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun xInstall(packageName: String, uriList: List, fileList: List) { - val attachedToServiceTimeout = AtomicBoolean(false) - - AuroraApplication.enqueuedInstalls.add(packageName) - - serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, binder: IBinder) { - attachedToServiceTimeout.set(true) - 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 { + 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? - ) { - removeFromInstallQueue(packageName) - handleCallback(packageName, returnCode, extra) - } - } + 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()) + 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 { - 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()) + postError( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_service_misconfigured) + ) + readyWithAction.set(true) } } - } else { - removeFromInstallQueue(packageName) - postError( - packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) - ) + + override fun onServiceDisconnected(name: ComponentName) { + removeFromInstallQueue(packageName) + readyWithAction.set(true) + Log.e("Disconnected from Aurora Services") + } } - } - 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) + } + Log.i("Services Callback : install wait done") } - - val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) - intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) - - context.bindService( - intent, - serviceConnection, - Context.BIND_AUTO_CREATE - ) - Handler(Looper.getMainLooper()).postDelayed({ - if (!attachedToServiceTimeout.get()) { - removeFromInstallQueue(packageName) - } - }, 25 * 1000) } 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) + 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 ) - ) - } - else -> { - val error = AppInstaller.getErrorString( - context, - returnCode - ) - postError(packageName, error, extra) + postError(packageName, error, extra) + } } - } - if (::serviceConnection.isInitialized) { - context.unbindService(serviceConnection) + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + } catch (th: Throwable) { + th.printStackTrace() } } 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) + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + } catch (th: Throwable) { + th.printStackTrace() } } override fun postError(packageName: String, error: String?, extra: String?) { - super.postError(packageName, error, extra) - if (::serviceConnection.isInitialized) { - context.unbindService(serviceConnection) + try { + super.postError(packageName, error, extra) + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + } catch (th: Throwable) { + th.printStackTrace() } }