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/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/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/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 13d012d68..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 @@ -25,23 +25,30 @@ import android.content.Intent import android.content.ServiceConnection import android.content.pm.PackageInstaller import android.net.Uri -import android.os.Build -import android.os.IBinder -import android.os.RemoteException +import android.os.* import androidx.annotation.RequiresApi import androidx.core.content.FileProvider import com.aurora.services.IPrivilegedCallback import com.aurora.services.IPrivilegedService +import com.aurora.store.AuroraApplication import com.aurora.store.BuildConfig import com.aurora.store.R +import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.InstallerEvent import com.aurora.store.util.Log import com.aurora.store.util.PackageUtil import org.greenrobot.eventbus.EventBus import java.io.File +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean class ServiceInstaller(context: Context) : InstallerBase(context) { + private lateinit var serviceConnection: ServiceConnection + private val executor = ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, LinkedBlockingQueue()) + companion object { const val ACTION_INSTALL_REPLACE_EXISTING = 2 const val PRIVILEGED_EXTENSION_PACKAGE_NAME = "com.aurora.services" @@ -67,8 +74,17 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { } } } + val fileList = files.map { + when (it) { + is File -> it.absolutePath + is String -> File(it).absolutePath + else -> { + throw Exception("Invalid data, expecting listOf() File or String") + } + } + } - xInstall(packageName, uriList) + xInstall(packageName, uriList, fileList) } else -> { postError( @@ -81,146 +97,256 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { } override fun uninstall(packageName: String) { - val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, binder: IBinder) { - val service = IPrivilegedService.Stub.asInterface(binder) - - if (service.hasPrivilegedPermissions()) { - Log.i(context.getString(R.string.installer_service_available)) - - val callback = object : IPrivilegedCallback.Stub() { - - override fun handleResult(packageName: String, returnCode: Int) { - + executor.execute { + val readyWithAction = AtomicBoolean(false) + Handler(Looper.getMainLooper()).post { + val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + if (isAlreadyQueued(packageName)) { + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + readyWithAction.set(true) + return } + AuroraApplication.enqueuedInstalls.add(packageName) + val service = IPrivilegedService.Stub.asInterface(binder) - override fun handleResultX( - packageName: String, - returnCode: Int, - extra: String? - ) { + if (service.hasPrivilegedPermissions()) { + Log.i(context.getString(R.string.installer_service_available)) + + val callback = object : IPrivilegedCallback.Stub() { + + override fun handleResult(packageName: String, returnCode: Int) {} + + override fun handleResultX( + packageName: String, + returnCode: Int, + extra: String? + ) { + removeFromInstallQueue(packageName) + handleCallbackUninstall(packageName, returnCode, extra) + readyWithAction.set(true) + } + } + + try { + service.deletePackageX( + packageName, + 2, + BuildConfig.APPLICATION_ID, + callback + ) + } catch (e: RemoteException) { + Log.e("Failed to connect Aurora Services") + removeFromInstallQueue(packageName) + readyWithAction.set(true) + } + } else { removeFromInstallQueue(packageName) - handleCallback(packageName, returnCode, extra) + postError( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_service_misconfigured) + ) + readyWithAction.set(true) } } - try { - service.deletePackageX( - packageName, - 2, - BuildConfig.APPLICATION_ID, - callback - ) - } catch (e: RemoteException) { - Log.e("Failed to connect Aurora Services") + override fun onServiceDisconnected(name: ComponentName) { + removeFromInstallQueue(packageName) + Log.e("Disconnected from Aurora Services") + readyWithAction.set(true) } - } else { - postError( - packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) - ) } - } - override fun onServiceDisconnected(name: ComponentName) { - removeFromInstallQueue(packageName) - Log.e("Disconnected from Aurora Services") + val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) + intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) + + context.bindService( + intent, + serviceConnection, + Context.BIND_AUTO_CREATE + ) + } + while (!readyWithAction.get()) { + Thread.sleep(1000) } } - - val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) - intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) - - context.bindService( - intent, - serviceConnection, - Context.BIND_AUTO_CREATE - ) } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - private fun xInstall(packageName: String, uriList: List) { - val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, binder: IBinder) { - val service = IPrivilegedService.Stub.asInterface(binder) - - if (service.hasPrivilegedPermissions()) { - Log.i(context.getString(R.string.installer_service_available)) - - val callback = object : IPrivilegedCallback.Stub() { - - override fun handleResult(packageName: String, returnCode: Int) { - + private fun xInstall(packageName: String, uriList: List, fileList: List) { + executor.execute { + val readyWithAction = AtomicBoolean(false) + Handler(Looper.getMainLooper()).post { + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + if (isAlreadyQueued(packageName)) { + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + readyWithAction.set(true) + return } + AuroraApplication.enqueuedInstalls.add(packageName) + val service = IPrivilegedService.Stub.asInterface(binder) - override fun handleResultX( - packageName: String, - returnCode: Int, - extra: String? - ) { + if (service.hasPrivilegedPermissions()) { + Log.i(context.getString(R.string.installer_service_available)) + + val callback = object : IPrivilegedCallback.Stub() { + + override fun handleResult(packageName: String, returnCode: Int) {} + + override fun handleResultX( + packageName: String, + returnCode: Int, + extra: String? + ) { + removeFromInstallQueue(packageName) + handleCallback(packageName, returnCode, extra) + readyWithAction.set(true) + } + } + + try { + if (service.isMoreMethodImplemented) { + try { + service.installSplitPackageMore( + packageName, + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback, + fileList + ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + postError(packageName, e.localizedMessage, e.stackTraceToString()) + readyWithAction.set(true) + } + } else { + throw Exception("New method not implemented") + } + } catch (th: Throwable) { + th.printStackTrace() + try { + service.installSplitPackageX( + packageName, + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback + ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + postError(packageName, e.localizedMessage, e.stackTraceToString()) + readyWithAction.set(true) + } + } + } else { removeFromInstallQueue(packageName) - handleCallback(packageName, returnCode, extra) + postError( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_service_misconfigured) + ) + readyWithAction.set(true) } } - try { - service.installSplitPackageX( - packageName, - uriList, - ACTION_INSTALL_REPLACE_EXISTING, - BuildConfig.APPLICATION_ID, - callback - ) - } catch (e: RemoteException) { + override fun onServiceDisconnected(name: ComponentName) { removeFromInstallQueue(packageName) - postError(packageName, e.localizedMessage, e.stackTraceToString()) + readyWithAction.set(true) + Log.e("Disconnected from Aurora Services") } - } else { - postError( - packageName, - context.getString(R.string.installer_status_failure), - context.getString(R.string.installer_service_misconfigured) + } + + val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) + intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) + + context.bindService( + intent, + serviceConnection, + Context.BIND_AUTO_CREATE + ) + } + while (!readyWithAction.get()) { + Thread.sleep(1000) + } + Log.i("Services Callback : install wait done") + } + } + + private fun handleCallbackUninstall(packageName: String, returnCode: Int, extra: String?) { + Log.i("Services Callback : $packageName $returnCode $extra") + + try { + when (returnCode) { + PackageInstaller.STATUS_SUCCESS -> { + EventBus.getDefault().post( + BusEvent.UninstallEvent( + packageName, + context.getString(R.string.installer_status_success) + ) ) } - } + else -> { + val error = AppInstaller.getErrorString( + context, + returnCode + ) - override fun onServiceDisconnected(name: ComponentName) { - removeFromInstallQueue(packageName) - Log.e("Disconnected from Aurora Services") + postError(packageName, error, extra) + } } + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + } catch (th: Throwable) { + th.printStackTrace() } - - val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT) - intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME) - - context.bindService( - intent, - serviceConnection, - Context.BIND_AUTO_CREATE - ) } private fun handleCallback(packageName: String, returnCode: Int, extra: String?) { Log.i("Services Callback : $packageName $returnCode $extra") - when (returnCode) { - PackageInstaller.STATUS_SUCCESS -> { - EventBus.getDefault().post( - InstallerEvent.Success( - packageName, - context.getString(R.string.installer_status_success) + try { + when (returnCode) { + PackageInstaller.STATUS_SUCCESS -> { + EventBus.getDefault().post( + InstallerEvent.Success( + packageName, + context.getString(R.string.installer_status_success) + ) + ) + } + else -> { + val error = AppInstaller.getErrorString( + context, + returnCode ) - ) - } - else -> { - val error = AppInstaller.getErrorString( - context, - returnCode - ) - postError(packageName, error, extra) + postError(packageName, error, extra) + } } + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + } catch (th: Throwable) { + th.printStackTrace() + } + } + + override fun postError(packageName: String, error: String?, extra: String?) { + try { + super.postError(packageName, error, extra) + if (::serviceConnection.isInitialized) { + context.unbindService(serviceConnection) + } + } catch (th: Throwable) { + th.printStackTrace() } } 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..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 @@ -24,6 +24,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.aurora.store.BuildConfig +import com.aurora.store.data.downloader.RequestGroupIdBuilder import com.aurora.store.data.event.BusEvent.InstallEvent import com.aurora.store.data.event.BusEvent.UninstallEvent import com.aurora.store.data.installer.AppInstaller @@ -49,7 +50,7 @@ open class PackageManagerReceiver : BroadcastReceiver() { } //Clear installation queue - AppInstaller(context) + AppInstaller.getInstance(context) .getPreferredInstaller() .removeFromInstallQueue(packageName) @@ -72,7 +73,10 @@ open class PackageManagerReceiver : BroadcastReceiver() { private fun clearNotification(context: Context, packageName: String) { val notificationManager = context.applicationContext .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(packageName, packageName.hashCode()) + val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(context, packageName.hashCode()) + groupIDsOfPackageName.forEach { + notificationManager.cancel(packageName, it) + } } private fun clearDownloads(context: Context, packageName: String) { 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 98677e079..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 @@ -26,6 +26,7 @@ import android.graphics.Color import android.os.Build import android.os.IBinder import android.util.ArrayMap +import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.aurora.Constants @@ -34,6 +35,7 @@ import com.aurora.extensions.isLAndAbove import com.aurora.gplayapi.data.models.App import com.aurora.store.R import com.aurora.store.data.downloader.DownloadManager +import com.aurora.store.data.downloader.RequestGroupIdBuilder import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.receiver.DownloadCancelReceiver @@ -78,10 +80,6 @@ class NotificationService : Service() { return null } - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - return START_NOT_STICKY - } - override fun onCreate() { super.onCreate() @@ -137,6 +135,10 @@ class NotificationService : Service() { override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { showNotification(groupId, download, fetchGroup) } + + override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { + showNotification(groupId, download, fetchGroup) + } } fetch.addListener(fetchListener) @@ -159,6 +161,13 @@ class NotificationService : Service() { NotificationManager.IMPORTANCE_MIN ) ) + channels.add( + NotificationChannel( + Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE, + getString(R.string.notification_channel_updater_service), + NotificationManager.IMPORTANCE_MIN + ) + ) notificationManager.createNotificationChannels(channels) } } @@ -184,6 +193,11 @@ class NotificationService : Service() { if (app == null) return + if (status == Status.DELETED) { + notificationManager.cancel(app.id) + return + } + val builder = NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL) builder.setContentTitle(app.displayName) builder.setSmallIcon(R.drawable.ic_notification_outlined) @@ -201,7 +215,8 @@ class NotificationService : Service() { builder.setContentText(getString(R.string.download_canceled)) builder.color = Color.RED } - Status.FAILED -> { + Status.FAILED -> + { builder.setSmallIcon(R.drawable.ic_download_fail) builder.setContentText(getString(R.string.download_failed)) builder.color = Color.RED @@ -362,12 +377,26 @@ class NotificationService : Service() { fun onEventMainThread(event: Any) { when (event) { is InstallerEvent.Success -> { - val app = appMap[event.packageName.hashCode()] + val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode()) + var app: App? = null + for (item in groupIDsOfPackageName) { + app = appMap[item] + if (app != null) { + break + } + } if (app != null) notifyInstallationStatus(app, event.extra) } is InstallerEvent.Failed -> { - val app = appMap[event.packageName.hashCode()] + val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode()) + var app: App? = null + for (item in groupIDsOfPackageName) { + app = appMap[item] + if (app != null) { + break + } + } if (app != null) notifyInstallationStatus(app, event.error) } @@ -379,7 +408,7 @@ class NotificationService : Service() { @Synchronized private fun install(packageName: String, files: List) { - AppInstaller(this) + AppInstaller.getInstance(this) .getPreferredInstaller() .install( packageName, 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..27dcb8c26 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/service/UpdateService.kt @@ -0,0 +1,702 @@ +package com.aurora.store.data.service + +import android.app.Notification +import android.content.Intent +import android.os.* +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.MutableLiveData +import com.aurora.Constants +import com.aurora.extensions.stackTraceToString +import com.aurora.gplayapi.data.models.App +import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.exceptions.ApiException +import com.aurora.gplayapi.helpers.PurchaseHelper +import com.aurora.store.R +import com.aurora.store.data.downloader.DownloadManager +import com.aurora.store.data.downloader.RequestBuilder +import com.aurora.store.data.downloader.getGroupId +import com.aurora.store.data.event.InstallerEvent +import com.aurora.store.data.installer.AppInstaller +import com.aurora.store.data.model.UpdateFile +import com.aurora.store.data.network.HttpClient +import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.util.Log +import com.tonyodev.fetch2.* +import com.tonyodev.fetch2core.DownloadBlock +import com.tonyodev.fetch2core.FetchObserver +import com.tonyodev.fetch2core.Reason +import nl.komponents.kovenant.task +import nl.komponents.kovenant.ui.failUi +import nl.komponents.kovenant.ui.successUi +import org.apache.commons.io.FileUtils +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.util.* +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.timerTask + +class UpdateService: LifecycleService() { + + lateinit var fetch: Fetch + lateinit var downloadManager: DownloadManager + private lateinit var fetchListener: FetchGroupListener + private var fetchActiveDownloadObserver = object : FetchObserver { + override fun onChanged(data: Boolean, reason: Reason) { + if (!data && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + Handler(Looper.getMainLooper()).postDelayed ({ + if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + stopSelf() + } + }, 5 * 1000) + } + } + } + private var hasActiveDownloadObserver = false + + private val fetchListeners: ArrayList = ArrayList() + + private val fetchPendingEvents: MutableMap = mutableMapOf() + + private val appMetadataListeners: ArrayList = ArrayList() + + private val appMetadataPendingEvents: ArrayList = ArrayList() + + var liveUpdateData: MutableLiveData> = MutableLiveData() + + inner class UpdateServiceBinder : Binder() { + fun getUpdateService() : UpdateService { + return this@UpdateService + } + } + + // 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 + + private lateinit var authData: AuthData + + + data class AppDownloadStatus(val download: Download, val fetchGroup: FetchGroup, + val isCancelled: Boolean = false, + val isComplete: Boolean = false) + + data class AppMetadataStatus(val reason: String, val app: App) + + @get:RequiresApi(Build.VERSION_CODES.O) + private val notification: Notification + get() { + val notificationBuilder = + NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE) + return getNotification(notificationBuilder) + } + + private fun getNotification(builder: NotificationCompat.Builder): Notification { + return builder.setAutoCancel(true) + .setColor(ContextCompat.getColor(this, R.color.colorAccent)) + .setContentTitle(getString(R.string.app_updater_service_notif_title)) + .setContentText(getString(R.string.app_updater_service_notif_text)) + .setOngoing(true) + .setSmallIcon(R.drawable.ic_notification_outlined) + .build() + } + + override fun onCreate() { + super.onCreate() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForeground(1, notification) + } else { + val notification = getNotification( + NotificationCompat.Builder( + this, + Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE + ) + ) + startForeground(1, notification) + } + EventBus.getDefault().register(this) + authData = AuthProvider.with(this).getAuthData() + purchaseHelper = PurchaseHelper(authData).using(HttpClient.getPreferredClient()) + downloadManager = DownloadManager.with(this) + fetch = downloadManager.fetch + fetchListener = object : FetchGroupListener { + + override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onAdded(groupId, download, fetchGroup) + } + if (download.tag != null) { + removeFromInstalling(download.tag, runFromCurrentThread = true) + } + if (!hasActiveDownloadObserver) { + hasActiveDownloadObserver = true + fetch.addActiveDownloadsObserver(true, fetchActiveDownloadObserver) + } + } + + override fun onAdded(download: Download) { + fetchListeners.forEach { + it.onAdded(download) + } + } + + override fun onProgress( + groupId: Int, + download: Download, + etaInMilliSeconds: Long, + downloadedBytesPerSecond: Long, + fetchGroup: FetchGroup + ) { + fetchListeners.forEach { + it.onProgress(groupId, download, etaInMilliSeconds, downloadedBytesPerSecond, fetchGroup) + } + } + + override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) { + fetchListeners.forEach { + it.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond) + } + } + + override fun onQueued(groupId: Int, download: Download, waitingNetwork: Boolean, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onQueued(groupId, download, waitingNetwork, fetchGroup) + } + } + + override fun onQueued(download: Download, waitingOnNetwork: Boolean) { + fetchListeners.forEach { + it.onQueued(download, waitingOnNetwork) + } + } + + override fun onRemoved(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onRemoved(groupId, download, fetchGroup) + } + } + + override fun onRemoved(download: Download) { + fetchListeners.forEach { + it.onRemoved(download) + } + } + + override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onResumed(groupId, download, fetchGroup) + } + } + + override fun onResumed(download: Download) { + fetchListeners.forEach { + it.onResumed(download) + } + } + + override fun onStarted( + groupId: Int, + download: Download, + downloadBlocks: List, + totalBlocks: Int, + fetchGroup: FetchGroup + ) { + fetchListeners.forEach { + it.onStarted( + groupId, + download, + downloadBlocks, + totalBlocks, + fetchGroup) + } + } + + override fun onStarted(download: Download, downloadBlocks: List, totalBlocks: Int) { + fetchListeners.forEach { + it.onStarted( + download, + downloadBlocks, + totalBlocks) + } + } + + override fun onWaitingNetwork(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onWaitingNetwork(groupId, download, fetchGroup) + } + } + + override fun onWaitingNetwork(download: Download) { + fetchListeners.forEach { + it.onWaitingNetwork(download) + } + } + + override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onCompleted(groupId, download, fetchGroup) + } + if (fetchListeners.isEmpty()) { + fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true) + } + var packageDownloadFilesWhichCompleted = downloadsInCompletedGroup[download.tag!!] + if (packageDownloadFilesWhichCompleted == null) { + packageDownloadFilesWhichCompleted = HashSet() + downloadsInCompletedGroup[download.tag!!] = packageDownloadFilesWhichCompleted + } + packageDownloadFilesWhichCompleted.add(download.file) + if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.downloads.all { packageDownloadFilesWhichCompleted.contains(it.file) }) { + downloadsInCompletedGroup.remove(download.tag!!) + if (download.tag != null) { + removeFromInstalling(download.tag, runFromCurrentThread = true) + } + Log.d("Group (${download.tag!!}) downloaded and verified all downloaded!") + Handler(Looper.getMainLooper()).post { + try { + install(download.tag!!, fetchGroup.downloads) + } catch (e: Exception) { + Log.e(e.stackTraceToString()) + } + } + } + if (fetchGroup.groupDownloadProgress == 100) { + Log.d("Group (${download.tag!!}) downloaded but NOT verified all downloaded!") + } /* else if (fetchGroup.groupDownloadProgress == -1) { + fetch.deleteGroup(fetchGroup.id) + }*/ + } + + override fun onCompleted(download: Download) { + fetchListeners.forEach { + it.onCompleted(download) + } + } + + override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onCancelled(groupId, download, fetchGroup) + } + if (fetchListeners.isEmpty()) { + fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) + } + } + + override fun onCancelled(download: Download) { + fetchListeners.forEach { + it.onCancelled(download) + } + } + + override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onDeleted(groupId, download, fetchGroup) + } + if (fetchListeners.isEmpty()) { + fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true) + } + } + + override fun onDeleted(download: Download) { + fetchListeners.forEach { + it.onDeleted(download) + } + } + + override fun onDownloadBlockUpdated( + groupId: Int, + download: Download, + downloadBlock: DownloadBlock, + totalBlocks: Int, + fetchGroup: FetchGroup + ) { + fetchListeners.forEach { + it.onDownloadBlockUpdated( + groupId, + download, + downloadBlock, + totalBlocks, + fetchGroup + ) + } + } + + override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) { + fetchListeners.forEach { + it.onDownloadBlockUpdated( + download, + downloadBlock, + totalBlocks + ) + } + } + + override fun onError( + groupId: Int, + download: Download, + error: Error, + throwable: Throwable?, + fetchGroup: FetchGroup + ) { + fetchListeners.forEach { + it.onError( + groupId, + download, + error, + throwable, + fetchGroup + ) + } + } + + override fun onError(download: Download, error: Error, throwable: Throwable?) { + fetchListeners.forEach { + it.onError( + download, + error, + throwable + ) + } + } + + override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { + fetchListeners.forEach { + it.onPaused(groupId, download, fetchGroup) + } + } + + override fun onPaused(download: Download) { + fetchListeners.forEach { + it.onPaused(download) + } + } + } + /*liveUpdateData.observe(this) { updateData -> + + }*/ + if (::fetch.isInitialized && ::fetchListener.isInitialized) { + fetch.addListener(fetchListener) + } + } + + fun updateApp(app: App, removeExisiting: Boolean = false) { + putInInstalling(app.packageName) + task { + val files = purchaseHelper.purchase( + app.packageName, + app.versionCode, + app.offerType + ) + + files.filter { it.url.isNotEmpty() } + .map { RequestBuilder.buildRequest(this, app, it) } + .toList() + } successUi { requests -> + if (requests.isNotEmpty()) { + if (removeExisiting) { + /*Remove old fetch group if downloaded earlier, mostly in case of updates*/ + fetch.deleteGroup(app.getGroupId(this)) + } + fetch.enqueue(requests) { + Log.i("Updating ${app.displayName}") + } + } else { + removeFromInstalling(app.packageName) + Log.e("Failed to download : ${app.displayName}") + appMetadataListeners.forEach { + it.onAppMetadataStatusError(getString(R.string.purchase_session_expired), app) + } + + if (appMetadataListeners.isEmpty()) { + appMetadataPendingEvents.add(AppMetadataStatus(getString(R.string.purchase_session_expired), app)) + } + } + } failUi { failException -> + removeFromInstalling(app.packageName) + var reason = "Unknown" + + when (failException) { + is ApiException.AppNotPurchased -> { + reason = getString(R.string.purchase_invalid) + } + + is ApiException.AppNotFound -> { + reason = getString(R.string.purchase_not_found) + } + + is ApiException.AppNotSupported -> { + reason = getString(R.string.purchase_unsupported) + } + + is ApiException.EmptyDownloads -> { + reason = getString(R.string.purchase_no_file) + } + } + + appMetadataListeners.forEach { + it.onAppMetadataStatusError(reason, app) + } + + if (appMetadataListeners.isEmpty()) { + appMetadataPendingEvents.add(AppMetadataStatus(reason, app)) + } + + + Log.e("Failed to purchase ${app.displayName} : $reason") + } + } + + var timer: Timer? = null + val timerTaskRun: Runnable = Runnable { + Handler(Looper.getMainLooper()).post { + if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + fetch.hasActiveDownloads(true) { hasActiveDownloads -> + if (!hasActiveDownloads && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) { + Handler(Looper.getMainLooper()).post { + stopSelf() + } + } + } + } + } + } + + @Synchronized + private fun install(packageName: String, files: List?) { + if (containsInInstalling(packageName)) { + println("Already installing $packageName!") + return + } + putInInstalling(packageName) + files?.let { downloads -> + var filesExist = true + + downloads.forEach { download -> + filesExist = filesExist && FileUtils.getFile(download.file).exists() + } + + if (filesExist) { + task { + try { + val installer = AppInstaller.getInstance(this) + .getPreferredInstaller() + installer.install( + packageName, + files + .filter { it.file.endsWith(".apk") } + .map { it.file }.toList() + ) + } catch (th: Throwable) { + removeFromInstalling(packageName) + th.printStackTrace() + } + }.fail { + removeFromInstalling(packageName) + Log.e(it.stackTraceToString()) + } + } else { + removeFromInstalling(packageName) + } + } + } + + var timerLock = Object() + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onEventBackgroundThreadExec(event: Any) { + when (event) { + is InstallerEvent.Success, + is InstallerEvent.Cancelled, + is InstallerEvent.Failed -> { + when (event) { + is InstallerEvent.Success -> event.packageName + is InstallerEvent.Cancelled -> event.packageName + is InstallerEvent.Failed -> event.packageName + else -> null + }?.run { + removeFromInstalling(this) + } + synchronized(timerLock) { + if (timer != null) { + timer!!.cancel() + timer = null + } + if (timer == null) { + timer = Timer() + } + timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000) + } + } + else -> { + + } + } + } + + fun registerFetchListener(listener: FetchGroupListener) { + fetchListeners.add(listener) + val iterator = fetchPendingEvents.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (item.value.isCancelled && !item.value.isComplete) { + listener.onCancelled(item.key, item.value.download, item.value.fetchGroup) + } else if (!item.value.isCancelled && item.value.isComplete) { + listener.onCompleted(item.key, item.value.download, item.value.fetchGroup) + } + iterator.remove() + } + } + + fun registerAppMetadataListener(listener: AppMetadataStatusListener) { + appMetadataListeners.add(listener) + val iterator = appMetadataPendingEvents.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + listener.onAppMetadataStatusError(item.reason, item.app) + iterator.remove() + } + } + + fun unregisterAppMetadataListener(listener: AppMetadataStatusListener) { + appMetadataListeners.remove(listener) + } + + fun unregisterFetchListener(listener: AbstractFetchGroupListener) { + fetchListeners.remove(listener) + } + + private var binder: UpdateServiceBinder = UpdateServiceBinder() + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return binder + } + + override fun onUnbind(intent: Intent?): Boolean { + fetchListeners.clear() + appMetadataListeners.clear() + synchronized(timerLock) { + if (timer != null) { + timer!!.cancel() + timer = null + } + if (timer == null) { + timer = Timer() + } + timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000) + } + return true + } + + override fun onRebind(intent: Intent?) { + super.onRebind(intent) + } + + override fun onDestroy() { + super.onDestroy() + if (hasActiveDownloadObserver) { + hasActiveDownloadObserver = false + fetch.removeActiveDownloadsObserver(fetchActiveDownloadObserver) + } + if (::fetch.isInitialized && ::fetchListener.isInitialized) { + fetch.removeListener(fetchListener) + } + } + + fun updateAll(updateFileMap: MutableMap) { + updateFileMap.values.forEach { updateApp(it.app) } + } +} \ No newline at end of file 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/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..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 @@ -21,9 +21,12 @@ package com.aurora.store.view.ui.details import android.Manifest import android.content.ActivityNotFoundException +import android.content.ComponentName import android.content.Intent +import android.content.ServiceConnection import android.os.Build import android.os.Bundle +import android.os.IBinder import android.view.Menu import android.view.MenuItem import android.view.View @@ -32,20 +35,19 @@ import com.aurora.Constants import com.aurora.extensions.* import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.data.models.File -import com.aurora.gplayapi.exceptions.ApiException import com.aurora.gplayapi.helpers.AppDetailsHelper -import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.store.MainActivity import com.aurora.store.R import com.aurora.store.State import com.aurora.store.data.downloader.DownloadManager -import com.aurora.store.data.downloader.RequestBuilder +import com.aurora.store.data.downloader.getGroupId import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.network.HttpClient import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.data.service.AppMetadataStatusListener +import com.aurora.store.data.service.UpdateService import com.aurora.store.databinding.ActivityDetailsBinding import com.aurora.store.util.* import com.aurora.store.view.ui.downloads.DownloadActivity @@ -64,7 +66,6 @@ import org.apache.commons.io.FileUtils import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import java.util.* class AppDetailsActivity : BaseDetailsActivity() { @@ -73,9 +74,39 @@ class AppDetailsActivity : BaseDetailsActivity() { private lateinit var authData: AuthData private lateinit var app: App - private lateinit var downloadManager: DownloadManager - private lateinit var fetch: Fetch + private var fetch: Fetch? = null + private var downloadManager: DownloadManager? = null + + private var attachToServiceCalled = false + private var updateService: UpdateService? = null + private var pendingAddListeners = true + private var serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService() + if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) { + updateService!!.registerFetchListener(fetchGroupListener) + // appMetadataListener needs to be initialized after the fetchGroupListener + updateService!!.registerAppMetadataListener(appMetadataListener) + pendingAddListeners = false + } + if (listOfActionsWhenServiceAttaches.isNotEmpty()) { + val iterator = listOfActionsWhenServiceAttaches.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + next.run() + iterator.remove() + } + } + } + + override fun onServiceDisconnected(name: ComponentName) { + updateService = null + attachToServiceCalled = false + pendingAddListeners = true + } + } private lateinit var fetchGroupListener: FetchGroupListener + private lateinit var appMetadataListener: AppMetadataStatusListener private lateinit var completionMarker: java.io.File private lateinit var inProgressMarker: java.io.File @@ -83,6 +114,7 @@ class AppDetailsActivity : BaseDetailsActivity() { private var isNone = false private var status = Status.NONE private var isInstalled: Boolean = false + private var isUpdatable: Boolean = false private var autoDownload: Boolean = false private var downloadOnly: Boolean = false @@ -165,9 +197,8 @@ class AppDetailsActivity : BaseDetailsActivity() { } override fun onResume() { - if (!isLAndAbove()) { - checkAndSetupInstall() - } + getUpdateServiceInstance() + checkAndSetupInstall() super.onResume() } @@ -206,11 +237,14 @@ class AppDetailsActivity : BaseDetailsActivity() { } } + private var uninstallActionEnabled = false + override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_details, menu) if (::app.isInitialized) { val installed = PackageUtil.isInstalled(this, app.packageName) menu?.findItem(R.id.action_uninstall)?.isVisible = installed + uninstallActionEnabled = installed } return true } @@ -305,7 +339,7 @@ class AppDetailsActivity : BaseDetailsActivity() { showDialog(R.string.title_installer, R.string.dialog_desc_native_split) } else { task { - AppInstaller(this) + AppInstaller.getInstance(this) .getPreferredInstaller() .install( app.packageName, @@ -324,7 +358,7 @@ class AppDetailsActivity : BaseDetailsActivity() { @Synchronized private fun uninstallApp() { task { - AppInstaller(this) + AppInstaller.getInstance(this) .getPreferredInstaller() .uninstall(app.packageName) } @@ -427,14 +461,14 @@ class AppDetailsActivity : BaseDetailsActivity() { private fun startDownload() { when (status) { Status.PAUSED -> { - fetch.resumeGroup(app.id) + fetch?.resumeGroup(app.getGroupId(this@AppDetailsActivity)) } Status.DOWNLOADING -> { flip(1) toast("Already downloading") } Status.COMPLETED -> { - fetch.getFetchGroup(app.id) { + fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { verifyAndInstall(it.downloads) } } @@ -444,92 +478,25 @@ class AppDetailsActivity : BaseDetailsActivity() { } } + val listOfActionsWhenServiceAttaches = ArrayList() + private fun purchase() { + bottomSheetBehavior.isHideable = false + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED updateActionState(State.PROGRESS) - task { - val authData = AuthProvider - .with(this) - .getAuthData() - - PurchaseHelper(authData) - .using(HttpClient.getPreferredClient()) - .purchase(app.packageName, app.versionCode, app.offerType) - } successUi { files -> - if (files.isNotEmpty()) { - var hasOBB = false - - files.forEach { file -> - if (file.type == File.FileType.OBB || file.type == File.FileType.PATCH) { - hasOBB = true - } - } - - if (hasOBB) - enqueueWithStoragePermission(files) - else - enqueue(files) + runWithPermissions( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) { + if (updateService == null) { + listOfActionsWhenServiceAttaches.add({ + updateService?.updateApp(app, removeExisiting = true) + }) + getUpdateServiceInstance() } else { - Log.e("Failed to download : ${app.displayName}") - updateActionState(State.IDLE) + updateService?.updateApp(app, removeExisiting = true) } - } failUi { - updateActionState(State.IDLE) - var reason = "Unknown" - - when (it) { - is ApiException.AppNotPurchased -> { - reason = getString(R.string.purchase_invalid) - } - - is ApiException.AppNotFound -> { - reason = getString(R.string.purchase_not_found) - } - - is ApiException.AppNotSupported -> { - reason = getString(R.string.purchase_unsupported) - } - - is ApiException.EmptyDownloads -> { - reason = getString(R.string.purchase_no_file) - } - } - - expandBottomSheet(reason) - - Log.e("Failed to purchase ${app.displayName} : $reason") - } - } - - private fun enqueueWithStoragePermission(files: List) = 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*/ - fetch.deleteGroup(app.id) - - /*Enqueue new fetch group*/ - fetch.enqueue( - requestList - ) { - status = Status.ADDED - Log.i("Downloading Apks : %s", app.displayName) - } - } else { - updateActionState(State.IDLE) - expandBottomSheet(getString(R.string.purchase_session_expired)) } } @@ -544,6 +511,10 @@ class AppDetailsActivity : BaseDetailsActivity() { else 0 + if (progress == 100) { + B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) + return@runOnUiThread + } B.layoutDetailsInstall.apply { txtProgressPercent.text = ("${progress}%") @@ -585,7 +556,7 @@ class AppDetailsActivity : BaseDetailsActivity() { B.layoutDetailsInstall.btnDownload.let { btn -> if (isInstalled) { - val isUpdatable = PackageUtil.isUpdatable( + isUpdatable = PackageUtil.isUpdatable( this, app.packageName, app.versionCode.toLong() @@ -603,6 +574,9 @@ class AppDetailsActivity : BaseDetailsActivity() { btn.setText(R.string.action_open) btn.addOnClickListener { openApp() } } + if (!uninstallActionEnabled) { + invalidateOptionsMenu() + } } else { if (app.isFree) { btn.setText(R.string.action_install) @@ -618,6 +592,9 @@ class AppDetailsActivity : BaseDetailsActivity() { startDownload() } } + if (uninstallActionEnabled) { + invalidateOptionsMenu() + } } } } @@ -635,16 +612,17 @@ class AppDetailsActivity : BaseDetailsActivity() { } private fun attachFetch() { - downloadManager = DownloadManager.with(this) - fetch = downloadManager.fetch - - fetch.getFetchGroup(app.id) { fetchGroup: FetchGroup -> + if (fetch == null) { + downloadManager = DownloadManager.with(this) + fetch = downloadManager!!.fetch + } + fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { fetchGroup: FetchGroup -> if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) { status = Status.COMPLETED - } else if (downloadManager.isDownloading(fetchGroup)) { + } else if (downloadManager?.isDownloading(fetchGroup) == true) { status = Status.DOWNLOADING flip(1) - } else if (downloadManager.isCanceled(fetchGroup)) { + } else if (downloadManager?.isCanceled(fetchGroup) == true) { status = Status.CANCELLED } else if (fetchGroup.pausedDownloads.isNotEmpty()) { status = Status.PAUSED @@ -655,6 +633,12 @@ class AppDetailsActivity : BaseDetailsActivity() { fetchGroupListener = object : AbstractFetchGroupListener() { + override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { + status = download.status + } + } + override fun onStarted( groupId: Int, download: Download, @@ -662,7 +646,7 @@ class AppDetailsActivity : BaseDetailsActivity() { totalBlocks: Int, fetchGroup: FetchGroup ) { - if (groupId == app.id) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { status = download.status flip(1) @@ -680,7 +664,7 @@ class AppDetailsActivity : BaseDetailsActivity() { } override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) { - if (groupId == app.id) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { status = download.status flip(1) FileUtils.touch(inProgressMarker) @@ -688,7 +672,7 @@ class AppDetailsActivity : BaseDetailsActivity() { } override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { - if (groupId == app.id) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { status = download.status flip(0) } @@ -701,7 +685,7 @@ class AppDetailsActivity : BaseDetailsActivity() { downloadedBytesPerSecond: Long, fetchGroup: FetchGroup ) { - if (groupId == app.id) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond) Log.i( "${app.displayName} : ${download.file} -> Progress : %d", @@ -711,22 +695,21 @@ class AppDetailsActivity : BaseDetailsActivity() { } override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { - if (groupId == app.id && fetchGroup.groupDownloadProgress == 100) { + if (groupId == app.getGroupId(this@AppDetailsActivity) && fetchGroup.groupDownloadProgress == 100) { status = download.status flip(0) updateProgress(fetchGroup, -1, -1) - inProgressMarker.delete() - completionMarker.createNewFile() try { - verifyAndInstall(fetchGroup.downloads) - } catch (e: Exception) { - Log.e(e.stackTraceToString()) + inProgressMarker.delete() + completionMarker.createNewFile() + } catch (ex: Exception) { + ex.printStackTrace() } } } override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) { - if (groupId == app.id) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { status = download.status flip(0) inProgressMarker.delete() @@ -740,7 +723,7 @@ class AppDetailsActivity : BaseDetailsActivity() { throwable: Throwable?, fetchGroup: FetchGroup ) { - if (groupId == app.id) { + if (groupId == app.getGroupId(this@AppDetailsActivity)) { status = download.status flip(0) inProgressMarker.delete() @@ -748,13 +731,63 @@ class AppDetailsActivity : BaseDetailsActivity() { } } - fetch.addListener(fetchGroupListener) + appMetadataListener = object : AppMetadataStatusListener { + override fun onAppMetadataStatusError(reason: String, app: App) { + if (app.packageName == this@AppDetailsActivity.app.packageName) { + updateActionState(State.IDLE) + expandBottomSheet(reason) + } + } + } + + getUpdateServiceInstance() B.layoutDetailsInstall.imgCancel.setOnClickListener { - fetch.cancelGroup( - app.id + fetch?.cancelGroup( + app.getGroupId(this@AppDetailsActivity) ) } + if (updateService != null) { + pendingAddListeners = false + updateService!!.registerFetchListener(fetchGroupListener) + // appMetadataListener needs to be initialized after the fetchGroupListener + updateService!!.registerAppMetadataListener(appMetadataListener) + } else { + pendingAddListeners = true + } + } + + fun getUpdateServiceInstance() { + if (updateService == null && !attachToServiceCalled) { + attachToServiceCalled = true + val intent = Intent(this, UpdateService::class.java) + startService(intent) + bindService( + intent, + serviceConnection, + 0 + ) + } + } + + override fun onPause() { + if (updateService != null) { + updateService = null + attachToServiceCalled = false + pendingAddListeners = true + unbindService(serviceConnection) + } + super.onPause() + } + + override fun onDestroy() { + super.onDestroy() + if (updateService != null) { + updateService = null + attachToServiceCalled = false + pendingAddListeners = true + unbindService(serviceConnection) + } } private fun attachBottomSheet() { 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/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 4383dcbfa..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 @@ -19,24 +19,24 @@ package com.aurora.store.view.ui.updates +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection import android.os.Bundle +import android.os.IBinder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.lifecycle.ViewModelProvider import com.aurora.Constants import com.aurora.extensions.stackTraceToString -import com.aurora.extensions.toast import com.aurora.gplayapi.data.models.App -import com.aurora.gplayapi.data.models.AuthData -import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.store.R import com.aurora.store.State -import com.aurora.store.data.downloader.DownloadManager -import com.aurora.store.data.downloader.RequestBuilder +import com.aurora.store.data.downloader.getGroupId import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.model.UpdateFile -import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.data.service.UpdateService import com.aurora.store.databinding.FragmentUpdatesBinding import com.aurora.store.util.Log import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_ @@ -48,11 +48,8 @@ import com.aurora.store.view.ui.sheets.AppMenuSheet import com.aurora.store.viewmodel.all.UpdatesViewModel import com.tonyodev.fetch2.AbstractFetchGroupListener import com.tonyodev.fetch2.Download -import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.FetchGroup import nl.komponents.kovenant.task -import nl.komponents.kovenant.ui.failUi -import nl.komponents.kovenant.ui.successUi import org.apache.commons.io.FileUtils class UpdatesFragment : BaseFragment() { @@ -60,11 +57,28 @@ class UpdatesFragment : BaseFragment() { private lateinit var B: FragmentUpdatesBinding private lateinit var VM: UpdatesViewModel - private lateinit var authData: AuthData - private lateinit var purchaseHelper: PurchaseHelper - - private lateinit var fetch: Fetch + val listOfActionsWhenServiceAttaches = ArrayList() private lateinit var fetchListener: AbstractFetchGroupListener + private var attachToServiceCalled = false + private var serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, binder: IBinder) { + updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService() + updateService!!.registerFetchListener(fetchListener) + if (listOfActionsWhenServiceAttaches.isNotEmpty()) { + val iterator = listOfActionsWhenServiceAttaches.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + next.run() + iterator.remove() + } + } + } + + override fun onServiceDisconnected(name: ComponentName) { + updateService = null + attachToServiceCalled = false + } + } private var updateFileMap: MutableMap = mutableMapOf() @@ -83,10 +97,7 @@ class UpdatesFragment : BaseFragment() { VM = ViewModelProvider(requireActivity()).get(UpdatesViewModel::class.java) - authData = AuthProvider.with(requireContext()).getAuthData() - purchaseHelper = PurchaseHelper(authData) - fetch = DownloadManager.with(requireContext()).fetch fetchListener = object : AbstractFetchGroupListener() { override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) { @@ -106,11 +117,6 @@ class UpdatesFragment : BaseFragment() { override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { if (fetchGroup.groupDownloadProgress == 100) { VM.updateDownload(groupId, fetchGroup, isComplete = true) - try { - install(download.tag!!, fetchGroup.downloads) - } catch (e: Exception) { - Log.e(e.stackTraceToString()) - } } } @@ -123,31 +129,43 @@ class UpdatesFragment : BaseFragment() { } } + getUpdateServiceInstance() + return B.root } override fun onResume() { + getUpdateServiceInstance() super.onResume() - if (::fetch.isInitialized && ::fetchListener.isInitialized) { - fetch.addListener(fetchListener) + } + + override fun onPause() { + if (updateService != null) { + updateService = null + attachToServiceCalled = false + requireContext().unbindService(serviceConnection) } + super.onPause() } override fun onDestroy() { - if (::fetch.isInitialized && ::fetchListener.isInitialized) { - fetch.removeListener(fetchListener) - } super.onDestroy() + if (updateService != null) { + updateService = null + attachToServiceCalled = false + requireContext().unbindService(serviceConnection) + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - VM.liveUpdateData.observe(viewLifecycleOwner, { + VM.liveUpdateData.observe(viewLifecycleOwner) { updateFileMap = it updateController(updateFileMap) B.swipeRefreshLayout.isRefreshing = false - }) + updateService?.liveUpdateData?.postValue(updateFileMap) + } B.swipeRefreshLayout.setOnRefreshListener { VM.observe() @@ -221,44 +239,56 @@ class UpdatesFragment : BaseFragment() { } } + private var updateService: UpdateService? = null + + fun runInService(runnable: Runnable) { + if (updateService == null) { + listOfActionsWhenServiceAttaches.add(runnable) + getUpdateServiceInstance() + } else { + runnable.run() + } + } + private fun updateSingle(app: App) { + runInService { + VM.updateState(app.getGroupId(requireContext()), State.QUEUED) - VM.updateState(app.id, State.QUEUED) - - task { - val files = purchaseHelper.purchase( - app.packageName, - app.versionCode, - app.offerType - ) - - files.map { RequestBuilder.buildRequest(requireContext(), app, it) } - } successUi { - val requests = it.filter { request -> request.url.isNotEmpty() }.toList() - if (requests.isNotEmpty()) { - fetch.enqueue(requests) { - Log.i("Updating ${app.displayName}") - } - } else { - requireContext().toast("Failed to update ${app.displayName}") - } - } failUi { - Log.e("Failed to update ${app.displayName}") + updateService?.updateApp(app) } } private fun cancelSingle(app: App) { - fetch.cancelGroup(app.id) + runInService { + updateService?.fetch?.cancelGroup(app.getGroupId(requireContext())) + } } private fun updateAll() { - updateFileMap.values.forEach { updateSingle(it.app) } - VM.updateAllEnqueued = true + runInService { + updateService?.updateAll(updateFileMap) + VM.updateAllEnqueued = true + } } private fun cancelAll() { - VM.updateAllEnqueued = false - updateFileMap.values.forEach { fetch.cancelGroup(it.app.id) } + runInService { + VM.updateAllEnqueued = false + updateFileMap.values.forEach { updateService?.fetch?.cancelGroup(it.app.getGroupId(requireContext())) } + } + } + + fun getUpdateServiceInstance() { + if (updateService == null && !attachToServiceCalled) { + attachToServiceCalled = true + val intent = Intent(requireContext(), UpdateService::class.java) + requireContext().startService(intent) + requireContext().bindService( + intent, + serviceConnection, + 0 + ) + } } @Synchronized @@ -272,7 +302,7 @@ class UpdatesFragment : BaseFragment() { if (filesExist) { task { - AppInstaller(requireContext()) + AppInstaller.getInstance(requireContext()) .getPreferredInstaller() .install( packageName, 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) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c4b10e79..d8fca7342 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -366,4 +366,7 @@ Could not generate AAS Token Device config exported Could not export device config + App background download service + Background app download + Enables background app download \ No newline at end of file