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