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

This commit is contained in:
Konstantin Tuev
2021-06-14 20:58:58 +03:00
parent 4e111fa9f4
commit b7ebb772dd
13 changed files with 329 additions and 216 deletions

View File

@@ -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<Uri> uriList,
in int flags,
in String installerPackageName,
in IPrivilegedCallback callback,
in List<String> fileList
);
oneway void deletePackage(
in String packageName,
in int flags,

View File

@@ -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"

View File

@@ -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<Uri>) {
private fun xInstall(packageName: String, uriList: List<Uri>, fileList: List<String>) {
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)

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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<Boolean> {
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<FetchGroupListener> = ArrayList()
private val fetchListeners: ArrayList<FetchGroupListener> = ArrayList()
private val pendingEvents: MutableMap</*groupId: */Int, AppDownloadStatus> = mutableMapOf()
private val fetchPendingEvents: MutableMap</*groupId: */Int, AppDownloadStatus> = mutableMapOf()
private val appMetadataListeners: ArrayList<AppMetadataStatusListener> = ArrayList()
private val appMetadataPendingEvents: ArrayList<AppMetadataStatus> = ArrayList()
var liveUpdateData: MutableLiveData<MutableMap<Int, UpdateFile>> = MutableLiveData()
@@ -67,7 +73,7 @@ class UpdateService: LifecycleService() {
}
}
private var installing: AtomicBoolean = AtomicBoolean()
private var installing = CopyOnWriteArrayList<String>()
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<DownloadBlock>, 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<Download>?) {
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
}

View File

@@ -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 {

View File

@@ -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<File>) = runWithPermissions(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) {
enqueue(files)
}
private fun enqueue(files: List<File>) {
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)
}
}

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -366,4 +366,7 @@
<string name="toast_aas_token_failed">Failed to generate AAS Token</string>
<string name="toast_export_success">Device config exported successfully</string>
<string name="toast_export_failed">Failed to export device config</string>
<string name="notification_channel_updater_service">App background download service</string>
<string name="app_updater_service_notif_title">Background app download</string>
<string name="app_updater_service_notif_text">Enables background app download</string>
</resources>