From b7ebb772dde1c8f96c30ab8d15783f8734b17634 Mon Sep 17 00:00:00 2001 From: Konstantin Tuev Date: Mon, 14 Jun 2021 20:58:58 +0300 Subject: [PATCH] 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