Merge branch 'master' into 'master'

Enabled downloading/updating any/all apps in the background with a service, fixed many issues with download, install, uninstall

See merge request AuroraOSS/AuroraStore!130
This commit is contained in:
Rahul Patel
2021-06-19 15:14:45 +00:00
23 changed files with 1418 additions and 332 deletions

View File

@@ -157,6 +157,7 @@
<service android:name=".data.service.NotificationService" /> <service android:name=".data.service.NotificationService" />
<service android:name=".data.installer.InstallerService" /> <service android:name=".data.installer.InstallerService" />
<service android:name=".data.service.UpdateService" />
<service <service
android:name="com.novoda.merlin.MerlinService" android:name="com.novoda.merlin.MerlinService"
android:exported="false" /> android:exported="false" />

View File

@@ -22,6 +22,8 @@ interface IPrivilegedService {
boolean hasPrivilegedPermissions(); boolean hasPrivilegedPermissions();
boolean isMoreMethodImplemented();
oneway void installPackage( oneway void installPackage(
in Uri packageURI, in Uri packageURI,
in int flags, in int flags,
@@ -52,6 +54,15 @@ interface IPrivilegedService {
in IPrivilegedCallback callback 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( oneway void deletePackage(
in String packageName, in String packageName,
in int flags, in int flags,

View File

@@ -37,6 +37,7 @@ object Constants {
const val NOTIFICATION_CHANNEL_ALERT = "NOTIFICATION_CHANNEL_ALERT" const val NOTIFICATION_CHANNEL_ALERT = "NOTIFICATION_CHANNEL_ALERT"
const val NOTIFICATION_CHANNEL_GENERAL = "NOTIFICATION_CHANNEL_GENERAL" 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" const val URL_DISPENSER = "http://goolag.store:1337/api/auth"

View File

@@ -77,7 +77,10 @@ class DownloadManager private constructor(var context: Context) {
if (packageList.contains(download.tag)) { if (packageList.contains(download.tag)) {
val packageName = download.tag val packageName = download.tag
if (packageName != null) { if (packageName != null) {
fetch.deleteGroup(packageName.hashCode()) val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(context, packageName.hashCode())
groupIDsOfPackageName.forEach {
fetch.deleteGroup(it)
}
packageList.remove(packageName) packageList.remove(packageName)
} }
} }

View File

@@ -31,9 +31,9 @@ import com.tonyodev.fetch2.Request
import com.tonyodev.fetch2core.Extras import com.tonyodev.fetch2core.Extras
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
private inline fun Request.attachMetaData(app: App) { private inline fun Request.attachMetaData(context: Context, app: App) {
apply { apply {
groupId = app.id groupId = app.getGroupId(context)
tag = app.packageName tag = app.packageName
enqueueAction = EnqueueAction.UPDATE_ACCORDINGLY enqueueAction = EnqueueAction.UPDATE_ACCORDINGLY
networkType = NetworkType.ALL networkType = NetworkType.ALL
@@ -61,7 +61,7 @@ object RequestBuilder {
File.FileType.PATCH -> PathUtil.getObbDownloadFile(context, app, file) File.FileType.PATCH -> PathUtil.getObbDownloadFile(context, app, file)
} }
return Request(file.url, fileName).apply { return Request(file.url, fileName).apply {
attachMetaData(app) attachMetaData(context, app)
attachExtra(app) attachExtra(app)
} }
} }

View File

@@ -0,0 +1,55 @@
package com.aurora.store.data.downloader
import android.content.Context
import com.aurora.gplayapi.data.models.App
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_UNIQUE_GROUP_IDS
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.apache.commons.lang3.RandomUtils
class RequestGroupIdBuilder {
data class AppIDnVersion(val id: Int, val versionCode: Int)
companion object {
fun getGroupIDsForApp(context: Context, appID: Int): MutableList<Int> {
val data = Preferences.getPrefs(context).getString(PREFERENCE_UNIQUE_GROUP_IDS, "")!!
val gson = Gson()
var groupIDMap = HashMap<Int, RequestGroupIdBuilder.AppIDnVersion>()
if (data.isNotEmpty()) {
val empMapType = object : TypeToken<Map<Int, RequestGroupIdBuilder.AppIDnVersion>?>() {}.type
groupIDMap = HashMap(gson.fromJson(data, empMapType) ?: HashMap())
}
val out = mutableListOf<Int>()
for (item in groupIDMap.entries) {
if (item.value.id == appID) {
out.add(item.key)
}
}
return out
}
}
}
fun App.getGroupId(context: Context): Int {
val data = Preferences.getPrefs(context).getString(PREFERENCE_UNIQUE_GROUP_IDS, "")!!
val gson = Gson()
var groupIDMap = HashMap<Int, RequestGroupIdBuilder.AppIDnVersion>()
if (data.isNotEmpty()) {
val empMapType = object : TypeToken<Map<Int, RequestGroupIdBuilder.AppIDnVersion>?>() {}.type
groupIDMap = HashMap(gson.fromJson(data, empMapType) ?: HashMap())
}
for (item in groupIDMap.entries) {
if (item.value.id == this.id && item.value.versionCode == this.versionCode) {
return item.key
}
}
var randomGroupID = RandomUtils.nextInt(0, Int.MAX_VALUE)
while (groupIDMap.containsKey(randomGroupID)) {
randomGroupID = RandomUtils.nextInt(0, Int.MAX_VALUE)
}
groupIDMap[randomGroupID] = RequestGroupIdBuilder.AppIDnVersion(this.id, this.versionCode)
Preferences.getPrefs(context).edit().putString(PREFERENCE_UNIQUE_GROUP_IDS, gson.toJson(groupIDMap)).apply()
return randomGroupID
}

View File

@@ -26,9 +26,10 @@ import com.aurora.store.R
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID
open class AppInstaller constructor(var context: Context) { open class AppInstaller private constructor(var context: Context) {
companion object { companion object {
private var instance: AppInstaller? = null
fun getErrorString(context: Context, status: Int): String { fun getErrorString(context: Context, status: Int): String {
return when (status) { return when (status) {
PackageInstaller.STATUS_FAILURE_ABORTED -> context.getString(R.string.installer_status_user_action) PackageInstaller.STATUS_FAILURE_ABORTED -> context.getString(R.string.installer_status_user_action)
@@ -40,22 +41,50 @@ open class AppInstaller constructor(var context: Context) {
else -> context.getString(R.string.installer_status_failure) else -> context.getString(R.string.installer_status_failure)
} }
} }
fun getInstance(context: Context): AppInstaller {
if (instance == null) {
instance = AppInstaller(context.applicationContext)
}
return instance!!
}
} }
val choiceAndInstaller = HashMap<Int, IInstaller>()
fun getPreferredInstaller(): IInstaller { fun getPreferredInstaller(): IInstaller {
val prefValue = Preferences.getInteger( val prefValue = Preferences.getInteger(
context, context,
PREFERENCE_INSTALLER_ID PREFERENCE_INSTALLER_ID
) )
if (choiceAndInstaller.containsKey(prefValue)) {
return choiceAndInstaller[prefValue]!!
}
return when (prefValue) { return when (prefValue) {
1 -> NativeInstaller(context) 1 -> {
2 -> RootInstaller(context) val installer = NativeInstaller(context)
3 -> ServiceInstaller(context) choiceAndInstaller[prefValue] = installer
installer
}
2 -> {
val installer = RootInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
}
3 -> {
val installer = ServiceInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
}
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
SessionInstaller(context) val installer = SessionInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
} else { } else {
NativeInstaller(context) val installer = NativeInstaller(context)
choiceAndInstaller[prefValue] = installer
installer
} }
} }
} }

View File

@@ -25,23 +25,30 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.*
import android.os.IBinder
import android.os.RemoteException
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.aurora.services.IPrivilegedCallback import com.aurora.services.IPrivilegedCallback
import com.aurora.services.IPrivilegedService import com.aurora.services.IPrivilegedService
import com.aurora.store.AuroraApplication
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import java.io.File import java.io.File
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
class ServiceInstaller(context: Context) : InstallerBase(context) { class ServiceInstaller(context: Context) : InstallerBase(context) {
private lateinit var serviceConnection: ServiceConnection
private val executor = ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, LinkedBlockingQueue())
companion object { companion object {
const val ACTION_INSTALL_REPLACE_EXISTING = 2 const val ACTION_INSTALL_REPLACE_EXISTING = 2
const val PRIVILEGED_EXTENSION_PACKAGE_NAME = "com.aurora.services" const val PRIVILEGED_EXTENSION_PACKAGE_NAME = "com.aurora.services"
@@ -67,8 +74,17 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
} }
} }
} }
val fileList = files.map {
when (it) {
is File -> it.absolutePath
is String -> File(it).absolutePath
else -> {
throw Exception("Invalid data, expecting listOf() File or String")
}
}
}
xInstall(packageName, uriList) xInstall(packageName, uriList, fileList)
} }
else -> { else -> {
postError( postError(
@@ -81,146 +97,256 @@ class ServiceInstaller(context: Context) : InstallerBase(context) {
} }
override fun uninstall(packageName: String) { override fun uninstall(packageName: String) {
val serviceConnection = object : ServiceConnection { executor.execute {
override fun onServiceConnected(name: ComponentName, binder: IBinder) { val readyWithAction = AtomicBoolean(false)
val service = IPrivilegedService.Stub.asInterface(binder) Handler(Looper.getMainLooper()).post {
val serviceConnection = object : ServiceConnection {
if (service.hasPrivilegedPermissions()) { override fun onServiceConnected(name: ComponentName, binder: IBinder) {
Log.i(context.getString(R.string.installer_service_available)) if (isAlreadyQueued(packageName)) {
if (::serviceConnection.isInitialized) {
val callback = object : IPrivilegedCallback.Stub() { context.unbindService(serviceConnection)
}
override fun handleResult(packageName: String, returnCode: Int) { readyWithAction.set(true)
return
} }
AuroraApplication.enqueuedInstalls.add(packageName)
val service = IPrivilegedService.Stub.asInterface(binder)
override fun handleResultX( if (service.hasPrivilegedPermissions()) {
packageName: String, Log.i(context.getString(R.string.installer_service_available))
returnCode: Int,
extra: String? val callback = object : IPrivilegedCallback.Stub() {
) {
override fun handleResult(packageName: String, returnCode: Int) {}
override fun handleResultX(
packageName: String,
returnCode: Int,
extra: String?
) {
removeFromInstallQueue(packageName)
handleCallbackUninstall(packageName, returnCode, extra)
readyWithAction.set(true)
}
}
try {
service.deletePackageX(
packageName,
2,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
Log.e("Failed to connect Aurora Services")
removeFromInstallQueue(packageName)
readyWithAction.set(true)
}
} else {
removeFromInstallQueue(packageName) removeFromInstallQueue(packageName)
handleCallback(packageName, returnCode, extra) postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
readyWithAction.set(true)
} }
} }
try { override fun onServiceDisconnected(name: ComponentName) {
service.deletePackageX( removeFromInstallQueue(packageName)
packageName, Log.e("Disconnected from Aurora Services")
2, readyWithAction.set(true)
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
Log.e("Failed to connect Aurora Services")
} }
} else {
postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
} }
}
override fun onServiceDisconnected(name: ComponentName) { val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
removeFromInstallQueue(packageName) intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
Log.e("Disconnected from Aurora Services")
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
while (!readyWithAction.get()) {
Thread.sleep(1000)
} }
} }
val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
} }
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun xInstall(packageName: String, uriList: List<Uri>) { private fun xInstall(packageName: String, uriList: List<Uri>, fileList: List<String>) {
val serviceConnection = object : ServiceConnection { executor.execute {
override fun onServiceConnected(name: ComponentName, binder: IBinder) { val readyWithAction = AtomicBoolean(false)
val service = IPrivilegedService.Stub.asInterface(binder) Handler(Looper.getMainLooper()).post {
serviceConnection = object : ServiceConnection {
if (service.hasPrivilegedPermissions()) { override fun onServiceConnected(name: ComponentName, binder: IBinder) {
Log.i(context.getString(R.string.installer_service_available)) if (isAlreadyQueued(packageName)) {
if (::serviceConnection.isInitialized) {
val callback = object : IPrivilegedCallback.Stub() { context.unbindService(serviceConnection)
}
override fun handleResult(packageName: String, returnCode: Int) { readyWithAction.set(true)
return
} }
AuroraApplication.enqueuedInstalls.add(packageName)
val service = IPrivilegedService.Stub.asInterface(binder)
override fun handleResultX( if (service.hasPrivilegedPermissions()) {
packageName: String, Log.i(context.getString(R.string.installer_service_available))
returnCode: Int,
extra: String? val callback = object : IPrivilegedCallback.Stub() {
) {
override fun handleResult(packageName: String, returnCode: Int) {}
override fun handleResultX(
packageName: String,
returnCode: Int,
extra: String?
) {
removeFromInstallQueue(packageName)
handleCallback(packageName, returnCode, extra)
readyWithAction.set(true)
}
}
try {
if (service.isMoreMethodImplemented) {
try {
service.installSplitPackageMore(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback,
fileList
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
readyWithAction.set(true)
}
} else {
throw Exception("New method not implemented")
}
} catch (th: Throwable) {
th.printStackTrace()
try {
service.installSplitPackageX(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString())
readyWithAction.set(true)
}
}
} else {
removeFromInstallQueue(packageName) removeFromInstallQueue(packageName)
handleCallback(packageName, returnCode, extra) postError(
packageName,
context.getString(R.string.installer_status_failure),
context.getString(R.string.installer_service_misconfigured)
)
readyWithAction.set(true)
} }
} }
try { override fun onServiceDisconnected(name: ComponentName) {
service.installSplitPackageX(
packageName,
uriList,
ACTION_INSTALL_REPLACE_EXISTING,
BuildConfig.APPLICATION_ID,
callback
)
} catch (e: RemoteException) {
removeFromInstallQueue(packageName) removeFromInstallQueue(packageName)
postError(packageName, e.localizedMessage, e.stackTraceToString()) readyWithAction.set(true)
Log.e("Disconnected from Aurora Services")
} }
} else { }
postError(
packageName, val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
context.getString(R.string.installer_status_failure), intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.getString(R.string.installer_service_misconfigured)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
while (!readyWithAction.get()) {
Thread.sleep(1000)
}
Log.i("Services Callback : install wait done")
}
}
private fun handleCallbackUninstall(packageName: String, returnCode: Int, extra: String?) {
Log.i("Services Callback : $packageName $returnCode $extra")
try {
when (returnCode) {
PackageInstaller.STATUS_SUCCESS -> {
EventBus.getDefault().post(
BusEvent.UninstallEvent(
packageName,
context.getString(R.string.installer_status_success)
)
) )
} }
} else -> {
val error = AppInstaller.getErrorString(
context,
returnCode
)
override fun onServiceDisconnected(name: ComponentName) { postError(packageName, error, extra)
removeFromInstallQueue(packageName) }
Log.e("Disconnected from Aurora Services")
} }
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
} catch (th: Throwable) {
th.printStackTrace()
} }
val intent = Intent(PRIVILEGED_EXTENSION_SERVICE_INTENT)
intent.setPackage(PRIVILEGED_EXTENSION_PACKAGE_NAME)
context.bindService(
intent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
} }
private fun handleCallback(packageName: String, returnCode: Int, extra: String?) { private fun handleCallback(packageName: String, returnCode: Int, extra: String?) {
Log.i("Services Callback : $packageName $returnCode $extra") Log.i("Services Callback : $packageName $returnCode $extra")
when (returnCode) { try {
PackageInstaller.STATUS_SUCCESS -> { when (returnCode) {
EventBus.getDefault().post( PackageInstaller.STATUS_SUCCESS -> {
InstallerEvent.Success( EventBus.getDefault().post(
packageName, InstallerEvent.Success(
context.getString(R.string.installer_status_success) packageName,
context.getString(R.string.installer_status_success)
)
)
}
else -> {
val error = AppInstaller.getErrorString(
context,
returnCode
) )
)
}
else -> {
val error = AppInstaller.getErrorString(
context,
returnCode
)
postError(packageName, error, extra) postError(packageName, error, extra)
}
} }
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
} catch (th: Throwable) {
th.printStackTrace()
}
}
override fun postError(packageName: String, error: String?, extra: String?) {
try {
super.postError(packageName, error, extra)
if (::serviceConnection.isInitialized) {
context.unbindService(serviceConnection)
}
} catch (th: Throwable) {
th.printStackTrace()
} }
} }

View File

@@ -24,6 +24,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import com.aurora.store.BuildConfig import com.aurora.store.BuildConfig
import com.aurora.store.data.downloader.RequestGroupIdBuilder
import com.aurora.store.data.event.BusEvent.InstallEvent import com.aurora.store.data.event.BusEvent.InstallEvent
import com.aurora.store.data.event.BusEvent.UninstallEvent import com.aurora.store.data.event.BusEvent.UninstallEvent
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
@@ -49,7 +50,7 @@ open class PackageManagerReceiver : BroadcastReceiver() {
} }
//Clear installation queue //Clear installation queue
AppInstaller(context) AppInstaller.getInstance(context)
.getPreferredInstaller() .getPreferredInstaller()
.removeFromInstallQueue(packageName) .removeFromInstallQueue(packageName)
@@ -72,7 +73,10 @@ open class PackageManagerReceiver : BroadcastReceiver() {
private fun clearNotification(context: Context, packageName: String) { private fun clearNotification(context: Context, packageName: String) {
val notificationManager = context.applicationContext val notificationManager = context.applicationContext
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancel(packageName, packageName.hashCode()) val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(context, packageName.hashCode())
groupIDsOfPackageName.forEach {
notificationManager.cancel(packageName, it)
}
} }
private fun clearDownloads(context: Context, packageName: String) { private fun clearDownloads(context: Context, packageName: String) {

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

@@ -26,6 +26,7 @@ import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.util.ArrayMap import android.util.ArrayMap
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.aurora.Constants import com.aurora.Constants
@@ -34,6 +35,7 @@ import com.aurora.extensions.isLAndAbove
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.downloader.RequestGroupIdBuilder
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.receiver.DownloadCancelReceiver import com.aurora.store.data.receiver.DownloadCancelReceiver
@@ -78,10 +80,6 @@ class NotificationService : Service() {
return null return null
} }
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return START_NOT_STICKY
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -137,6 +135,10 @@ class NotificationService : Service() {
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
showNotification(groupId, download, fetchGroup) showNotification(groupId, download, fetchGroup)
} }
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
showNotification(groupId, download, fetchGroup)
}
} }
fetch.addListener(fetchListener) fetch.addListener(fetchListener)
@@ -159,6 +161,13 @@ class NotificationService : Service() {
NotificationManager.IMPORTANCE_MIN NotificationManager.IMPORTANCE_MIN
) )
) )
channels.add(
NotificationChannel(
Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE,
getString(R.string.notification_channel_updater_service),
NotificationManager.IMPORTANCE_MIN
)
)
notificationManager.createNotificationChannels(channels) notificationManager.createNotificationChannels(channels)
} }
} }
@@ -184,6 +193,11 @@ class NotificationService : Service() {
if (app == null) if (app == null)
return return
if (status == Status.DELETED) {
notificationManager.cancel(app.id)
return
}
val builder = NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL) val builder = NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL)
builder.setContentTitle(app.displayName) builder.setContentTitle(app.displayName)
builder.setSmallIcon(R.drawable.ic_notification_outlined) builder.setSmallIcon(R.drawable.ic_notification_outlined)
@@ -201,7 +215,8 @@ class NotificationService : Service() {
builder.setContentText(getString(R.string.download_canceled)) builder.setContentText(getString(R.string.download_canceled))
builder.color = Color.RED builder.color = Color.RED
} }
Status.FAILED -> { Status.FAILED ->
{
builder.setSmallIcon(R.drawable.ic_download_fail) builder.setSmallIcon(R.drawable.ic_download_fail)
builder.setContentText(getString(R.string.download_failed)) builder.setContentText(getString(R.string.download_failed))
builder.color = Color.RED builder.color = Color.RED
@@ -362,12 +377,26 @@ class NotificationService : Service() {
fun onEventMainThread(event: Any) { fun onEventMainThread(event: Any) {
when (event) { when (event) {
is InstallerEvent.Success -> { is InstallerEvent.Success -> {
val app = appMap[event.packageName.hashCode()] val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode())
var app: App? = null
for (item in groupIDsOfPackageName) {
app = appMap[item]
if (app != null) {
break
}
}
if (app != null) if (app != null)
notifyInstallationStatus(app, event.extra) notifyInstallationStatus(app, event.extra)
} }
is InstallerEvent.Failed -> { is InstallerEvent.Failed -> {
val app = appMap[event.packageName.hashCode()] val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode())
var app: App? = null
for (item in groupIDsOfPackageName) {
app = appMap[item]
if (app != null) {
break
}
}
if (app != null) if (app != null)
notifyInstallationStatus(app, event.error) notifyInstallationStatus(app, event.error)
} }
@@ -379,7 +408,7 @@ class NotificationService : Service() {
@Synchronized @Synchronized
private fun install(packageName: String, files: List<Download>) { private fun install(packageName: String, files: List<Download>) {
AppInstaller(this) AppInstaller.getInstance(this)
.getPreferredInstaller() .getPreferredInstaller()
.install( .install(
packageName, packageName,

View File

@@ -15,6 +15,7 @@ import com.aurora.store.BuildConfig
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.downloader.RequestBuilder.buildRequest import com.aurora.store.data.downloader.RequestBuilder.buildRequest
import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.installer.NativeInstaller import com.aurora.store.data.installer.NativeInstaller
import com.aurora.store.data.model.SelfUpdate import com.aurora.store.data.model.SelfUpdate
import com.aurora.store.util.CertUtil.isFDroidApp import com.aurora.store.util.CertUtil.isFDroidApp
@@ -135,14 +136,14 @@ class SelfUpdateService : Service() {
groupId: Int, download: Download, error: Error, groupId: Int, download: Download, error: Error,
throwable: Throwable?, fetchGroup: FetchGroup throwable: Throwable?, fetchGroup: FetchGroup
) { ) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@SelfUpdateService)) {
Log.e("Error self-updating ${app.displayName}") Log.e("Error self-updating ${app.displayName}")
destroyService() destroyService()
} }
} }
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.id && fetchGroup.groupDownloadProgress == 100) { if (groupId == app.getGroupId(this@SelfUpdateService) && fetchGroup.groupDownloadProgress == 100) {
Log.d("Calling installer ${app.displayName}") Log.d("Calling installer ${app.displayName}")
try { try {
@@ -163,7 +164,7 @@ class SelfUpdateService : Service() {
} }
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@SelfUpdateService)) {
Log.d("Self-update cancelled ${app.displayName}") Log.d("Self-update cancelled ${app.displayName}")
destroyService() destroyService()
} }

View File

@@ -0,0 +1,702 @@
package com.aurora.store.data.service
import android.app.Notification
import android.content.Intent
import android.os.*
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.MutableLiveData
import com.aurora.Constants
import com.aurora.extensions.stackTraceToString
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.exceptions.ApiException
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.R
import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.downloader.RequestBuilder
import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.UpdateFile
import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.util.Log
import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock
import com.tonyodev.fetch2core.FetchObserver
import com.tonyodev.fetch2core.Reason
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.apache.commons.io.FileUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.timerTask
class UpdateService: LifecycleService() {
lateinit var fetch: Fetch
lateinit var downloadManager: DownloadManager
private lateinit var fetchListener: FetchGroupListener
private var fetchActiveDownloadObserver = object : FetchObserver<Boolean> {
override fun onChanged(data: Boolean, reason: Reason) {
if (!data && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
Handler(Looper.getMainLooper()).postDelayed ({
if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
stopSelf()
}
}, 5 * 1000)
}
}
}
private var hasActiveDownloadObserver = false
private val fetchListeners: ArrayList<FetchGroupListener> = ArrayList()
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()
inner class UpdateServiceBinder : Binder() {
fun getUpdateService() : UpdateService {
return this@UpdateService
}
}
// HashMap<packageName, HashSet<downloadFilePath>>
private var downloadsInCompletedGroup = HashMap<String, HashSet<String>>()
private var installing = HashSet<String>()
private var lock = ReentrantLock()
fun putInInstalling(packageName: String?) {
if (packageName == null) {
return
}
if (lock.tryLock()) {
installing.add(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
} else {
Thread {
while (!lock.tryLock()) {
Thread.sleep(50)
}
installing.add(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
}.start()
}
}
fun removeFromInstalling(packageName: String?, runFromCurrentThread: Boolean = false) {
if (packageName == null) {
return
}
if (lock.tryLock()) {
installing.remove(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
} else {
val toRun = Runnable {
while (!lock.tryLock()) {
Thread.sleep(50)
}
installing.remove(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
}
if (runFromCurrentThread) {
toRun.run()
} else {
Thread(toRun).start()
}
}
}
fun isEmptyInstalling(): Boolean {
while (!lock.tryLock()) {
Thread.sleep(50)
}
val out: Boolean = installing.isEmpty()
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
return out
}
fun containsInInstalling(packageName: String?): Boolean {
if (packageName == null) {
return false
}
while (!lock.tryLock()) {
Thread.sleep(50)
}
val out = installing.contains(packageName)
try {
lock.unlock()
} catch (th: Throwable) {
th.printStackTrace()
}
return out
}
private lateinit var purchaseHelper: PurchaseHelper
private lateinit var authData: AuthData
data class AppDownloadStatus(val download: Download, val fetchGroup: FetchGroup,
val isCancelled: Boolean = false,
val isComplete: Boolean = false)
data class AppMetadataStatus(val reason: String, val app: App)
@get:RequiresApi(Build.VERSION_CODES.O)
private val notification: Notification
get() {
val notificationBuilder =
NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE)
return getNotification(notificationBuilder)
}
private fun getNotification(builder: NotificationCompat.Builder): Notification {
return builder.setAutoCancel(true)
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
.setContentTitle(getString(R.string.app_updater_service_notif_title))
.setContentText(getString(R.string.app_updater_service_notif_text))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_notification_outlined)
.build()
}
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(1, notification)
} else {
val notification = getNotification(
NotificationCompat.Builder(
this,
Constants.NOTIFICATION_CHANNEL_UPDATER_SERVICE
)
)
startForeground(1, notification)
}
EventBus.getDefault().register(this)
authData = AuthProvider.with(this).getAuthData()
purchaseHelper = PurchaseHelper(authData).using(HttpClient.getPreferredClient())
downloadManager = DownloadManager.with(this)
fetch = downloadManager.fetch
fetchListener = object : FetchGroupListener {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onAdded(groupId, download, fetchGroup)
}
if (download.tag != null) {
removeFromInstalling(download.tag, runFromCurrentThread = true)
}
if (!hasActiveDownloadObserver) {
hasActiveDownloadObserver = true
fetch.addActiveDownloadsObserver(true, fetchActiveDownloadObserver)
}
}
override fun onAdded(download: Download) {
fetchListeners.forEach {
it.onAdded(download)
}
}
override fun onProgress(
groupId: Int,
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onProgress(groupId, download, etaInMilliSeconds, downloadedBytesPerSecond, fetchGroup)
}
}
override fun onProgress(download: Download, etaInMilliSeconds: Long, downloadedBytesPerSecond: Long) {
fetchListeners.forEach {
it.onProgress(download, etaInMilliSeconds, downloadedBytesPerSecond)
}
}
override fun onQueued(groupId: Int, download: Download, waitingNetwork: Boolean, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onQueued(groupId, download, waitingNetwork, fetchGroup)
}
}
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
fetchListeners.forEach {
it.onQueued(download, waitingOnNetwork)
}
}
override fun onRemoved(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onRemoved(groupId, download, fetchGroup)
}
}
override fun onRemoved(download: Download) {
fetchListeners.forEach {
it.onRemoved(download)
}
}
override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onResumed(groupId, download, fetchGroup)
}
}
override fun onResumed(download: Download) {
fetchListeners.forEach {
it.onResumed(download)
}
}
override fun onStarted(
groupId: Int,
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onStarted(
groupId,
download,
downloadBlocks,
totalBlocks,
fetchGroup)
}
}
override fun onStarted(download: Download, downloadBlocks: List<DownloadBlock>, totalBlocks: Int) {
fetchListeners.forEach {
it.onStarted(
download,
downloadBlocks,
totalBlocks)
}
}
override fun onWaitingNetwork(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onWaitingNetwork(groupId, download, fetchGroup)
}
}
override fun onWaitingNetwork(download: Download) {
fetchListeners.forEach {
it.onWaitingNetwork(download)
}
}
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onCompleted(groupId, download, fetchGroup)
}
if (fetchListeners.isEmpty()) {
fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isComplete = true)
}
var packageDownloadFilesWhichCompleted = downloadsInCompletedGroup[download.tag!!]
if (packageDownloadFilesWhichCompleted == null) {
packageDownloadFilesWhichCompleted = HashSet()
downloadsInCompletedGroup[download.tag!!] = packageDownloadFilesWhichCompleted
}
packageDownloadFilesWhichCompleted.add(download.file)
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.downloads.all { packageDownloadFilesWhichCompleted.contains(it.file) }) {
downloadsInCompletedGroup.remove(download.tag!!)
if (download.tag != null) {
removeFromInstalling(download.tag, runFromCurrentThread = true)
}
Log.d("Group (${download.tag!!}) downloaded and verified all downloaded!")
Handler(Looper.getMainLooper()).post {
try {
install(download.tag!!, fetchGroup.downloads)
} catch (e: Exception) {
Log.e(e.stackTraceToString())
}
}
}
if (fetchGroup.groupDownloadProgress == 100) {
Log.d("Group (${download.tag!!}) downloaded but NOT verified all downloaded!")
} /* else if (fetchGroup.groupDownloadProgress == -1) {
fetch.deleteGroup(fetchGroup.id)
}*/
}
override fun onCompleted(download: Download) {
fetchListeners.forEach {
it.onCompleted(download)
}
}
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onCancelled(groupId, download, fetchGroup)
}
if (fetchListeners.isEmpty()) {
fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true)
}
}
override fun onCancelled(download: Download) {
fetchListeners.forEach {
it.onCancelled(download)
}
}
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onDeleted(groupId, download, fetchGroup)
}
if (fetchListeners.isEmpty()) {
fetchPendingEvents[groupId] = AppDownloadStatus(download, fetchGroup, isCancelled = true)
}
}
override fun onDeleted(download: Download) {
fetchListeners.forEach {
it.onDeleted(download)
}
}
override fun onDownloadBlockUpdated(
groupId: Int,
download: Download,
downloadBlock: DownloadBlock,
totalBlocks: Int,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onDownloadBlockUpdated(
groupId,
download,
downloadBlock,
totalBlocks,
fetchGroup
)
}
}
override fun onDownloadBlockUpdated(download: Download, downloadBlock: DownloadBlock, totalBlocks: Int) {
fetchListeners.forEach {
it.onDownloadBlockUpdated(
download,
downloadBlock,
totalBlocks
)
}
}
override fun onError(
groupId: Int,
download: Download,
error: Error,
throwable: Throwable?,
fetchGroup: FetchGroup
) {
fetchListeners.forEach {
it.onError(
groupId,
download,
error,
throwable,
fetchGroup
)
}
}
override fun onError(download: Download, error: Error, throwable: Throwable?) {
fetchListeners.forEach {
it.onError(
download,
error,
throwable
)
}
}
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
fetchListeners.forEach {
it.onPaused(groupId, download, fetchGroup)
}
}
override fun onPaused(download: Download) {
fetchListeners.forEach {
it.onPaused(download)
}
}
}
/*liveUpdateData.observe(this) { updateData ->
}*/
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
fetch.addListener(fetchListener)
}
}
fun updateApp(app: App, removeExisiting: Boolean = false) {
putInInstalling(app.packageName)
task {
val files = purchaseHelper.purchase(
app.packageName,
app.versionCode,
app.offerType
)
files.filter { it.url.isNotEmpty() }
.map { RequestBuilder.buildRequest(this, app, it) }
.toList()
} successUi { requests ->
if (requests.isNotEmpty()) {
if (removeExisiting) {
/*Remove old fetch group if downloaded earlier, mostly in case of updates*/
fetch.deleteGroup(app.getGroupId(this))
}
fetch.enqueue(requests) {
Log.i("Updating ${app.displayName}")
}
} else {
removeFromInstalling(app.packageName)
Log.e("Failed to download : ${app.displayName}")
appMetadataListeners.forEach {
it.onAppMetadataStatusError(getString(R.string.purchase_session_expired), app)
}
if (appMetadataListeners.isEmpty()) {
appMetadataPendingEvents.add(AppMetadataStatus(getString(R.string.purchase_session_expired), app))
}
}
} failUi { failException ->
removeFromInstalling(app.packageName)
var reason = "Unknown"
when (failException) {
is ApiException.AppNotPurchased -> {
reason = getString(R.string.purchase_invalid)
}
is ApiException.AppNotFound -> {
reason = getString(R.string.purchase_not_found)
}
is ApiException.AppNotSupported -> {
reason = getString(R.string.purchase_unsupported)
}
is ApiException.EmptyDownloads -> {
reason = getString(R.string.purchase_no_file)
}
}
appMetadataListeners.forEach {
it.onAppMetadataStatusError(reason, app)
}
if (appMetadataListeners.isEmpty()) {
appMetadataPendingEvents.add(AppMetadataStatus(reason, app))
}
Log.e("Failed to purchase ${app.displayName} : $reason")
}
}
var timer: Timer? = null
val timerTaskRun: Runnable = Runnable {
Handler(Looper.getMainLooper()).post {
if (isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
fetch.hasActiveDownloads(true) { hasActiveDownloads ->
if (!hasActiveDownloads && isEmptyInstalling() && fetchListeners.isEmpty() && appMetadataListeners.isEmpty()) {
Handler(Looper.getMainLooper()).post {
stopSelf()
}
}
}
}
}
}
@Synchronized
private fun install(packageName: String, files: List<Download>?) {
if (containsInInstalling(packageName)) {
println("Already installing $packageName!")
return
}
putInInstalling(packageName)
files?.let { downloads ->
var filesExist = true
downloads.forEach { download ->
filesExist = filesExist && FileUtils.getFile(download.file).exists()
}
if (filesExist) {
task {
try {
val installer = AppInstaller.getInstance(this)
.getPreferredInstaller()
installer.install(
packageName,
files
.filter { it.file.endsWith(".apk") }
.map { it.file }.toList()
)
} catch (th: Throwable) {
removeFromInstalling(packageName)
th.printStackTrace()
}
}.fail {
removeFromInstalling(packageName)
Log.e(it.stackTraceToString())
}
} else {
removeFromInstalling(packageName)
}
}
}
var timerLock = Object()
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onEventBackgroundThreadExec(event: Any) {
when (event) {
is InstallerEvent.Success,
is InstallerEvent.Cancelled,
is InstallerEvent.Failed -> {
when (event) {
is InstallerEvent.Success -> event.packageName
is InstallerEvent.Cancelled -> event.packageName
is InstallerEvent.Failed -> event.packageName
else -> null
}?.run {
removeFromInstalling(this)
}
synchronized(timerLock) {
if (timer != null) {
timer!!.cancel()
timer = null
}
if (timer == null) {
timer = Timer()
}
timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000)
}
}
else -> {
}
}
}
fun registerFetchListener(listener: FetchGroupListener) {
fetchListeners.add(listener)
val iterator = fetchPendingEvents.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
if (item.value.isCancelled && !item.value.isComplete) {
listener.onCancelled(item.key, item.value.download, item.value.fetchGroup)
} else if (!item.value.isCancelled && item.value.isComplete) {
listener.onCompleted(item.key, item.value.download, item.value.fetchGroup)
}
iterator.remove()
}
}
fun registerAppMetadataListener(listener: AppMetadataStatusListener) {
appMetadataListeners.add(listener)
val iterator = appMetadataPendingEvents.iterator()
while (iterator.hasNext()) {
val item = iterator.next()
listener.onAppMetadataStatusError(item.reason, item.app)
iterator.remove()
}
}
fun unregisterAppMetadataListener(listener: AppMetadataStatusListener) {
appMetadataListeners.remove(listener)
}
fun unregisterFetchListener(listener: AbstractFetchGroupListener) {
fetchListeners.remove(listener)
}
private var binder: UpdateServiceBinder = UpdateServiceBinder()
override fun onBind(intent: Intent): IBinder {
super.onBind(intent)
return binder
}
override fun onUnbind(intent: Intent?): Boolean {
fetchListeners.clear()
appMetadataListeners.clear()
synchronized(timerLock) {
if (timer != null) {
timer!!.cancel()
timer = null
}
if (timer == null) {
timer = Timer()
}
timer!!.schedule(timerTask { timerTaskRun.run() }, 5 * 1000)
}
return true
}
override fun onRebind(intent: Intent?) {
super.onRebind(intent)
}
override fun onDestroy() {
super.onDestroy()
if (hasActiveDownloadObserver) {
hasActiveDownloadObserver = false
fetch.removeActiveDownloadsObserver(fetchActiveDownloadObserver)
}
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
fetch.removeListener(fetchListener)
}
}
fun updateAll(updateFileMap: MutableMap<Int, UpdateFile>) {
updateFileMap.values.forEach { updateApp(it.app) }
}
}

View File

@@ -26,7 +26,7 @@ import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.File import com.aurora.gplayapi.data.models.File
fun Context.getInternalBaseDirectory(): String { fun Context.getInternalBaseDirectory(): String {
return filesDir.path return (getExternalFilesDir(null) ?: filesDir).path
} }
object PathUtil { object PathUtil {

View File

@@ -23,6 +23,8 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.gson.Gson
object Preferences { object Preferences {
@@ -57,9 +59,15 @@ object Preferences {
const val PREFERENCE_UPDATES_EXTENDED = "PREFERENCE_UPDATES_EXTENDED" const val PREFERENCE_UPDATES_EXTENDED = "PREFERENCE_UPDATES_EXTENDED"
const val PREFERENCE_UNIQUE_GROUP_IDS = "PREFERENCE_UNIQUE_GROUP_IDS"
private var prefs: SharedPreferences? = null
fun getPrefs(context: Context): SharedPreferences { fun getPrefs(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context) if (prefs == null) {
prefs = PreferenceManager.getDefaultSharedPreferences(context)
}
return prefs!!
} }
fun putString(context: Context, key: String, value: String) { fun putString(context: Context, key: String, value: String) {

View File

@@ -21,9 +21,12 @@ package com.aurora.store.view.ui.details
import android.Manifest import android.Manifest
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -32,20 +35,19 @@ import com.aurora.Constants
import com.aurora.extensions.* import com.aurora.extensions.*
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.AuthData 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.AppDetailsHelper
import com.aurora.gplayapi.helpers.PurchaseHelper
import com.aurora.store.MainActivity import com.aurora.store.MainActivity
import com.aurora.store.R import com.aurora.store.R
import com.aurora.store.State import com.aurora.store.State
import com.aurora.store.data.downloader.DownloadManager 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.BusEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider 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.databinding.ActivityDetailsBinding
import com.aurora.store.util.* import com.aurora.store.util.*
import com.aurora.store.view.ui.downloads.DownloadActivity import com.aurora.store.view.ui.downloads.DownloadActivity
@@ -64,7 +66,6 @@ import org.apache.commons.io.FileUtils
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.util.*
class AppDetailsActivity : BaseDetailsActivity() { class AppDetailsActivity : BaseDetailsActivity() {
@@ -73,9 +74,39 @@ class AppDetailsActivity : BaseDetailsActivity() {
private lateinit var authData: AuthData private lateinit var authData: AuthData
private lateinit var app: App private lateinit var app: App
private lateinit var downloadManager: DownloadManager private var fetch: Fetch? = null
private lateinit var fetch: Fetch private var downloadManager: DownloadManager? = null
private var attachToServiceCalled = false
private var updateService: UpdateService? = null
private var pendingAddListeners = true
private var serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) {
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
pendingAddListeners = false
}
if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
val iterator = listOfActionsWhenServiceAttaches.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
next.run()
iterator.remove()
}
}
}
override fun onServiceDisconnected(name: ComponentName) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
}
}
private lateinit var fetchGroupListener: FetchGroupListener private lateinit var fetchGroupListener: FetchGroupListener
private lateinit var appMetadataListener: AppMetadataStatusListener
private lateinit var completionMarker: java.io.File private lateinit var completionMarker: java.io.File
private lateinit var inProgressMarker: java.io.File private lateinit var inProgressMarker: java.io.File
@@ -83,6 +114,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
private var isNone = false private var isNone = false
private var status = Status.NONE private var status = Status.NONE
private var isInstalled: Boolean = false private var isInstalled: Boolean = false
private var isUpdatable: Boolean = false
private var autoDownload: Boolean = false private var autoDownload: Boolean = false
private var downloadOnly: Boolean = false private var downloadOnly: Boolean = false
@@ -165,9 +197,8 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
override fun onResume() { override fun onResume() {
if (!isLAndAbove()) { getUpdateServiceInstance()
checkAndSetupInstall() checkAndSetupInstall()
}
super.onResume() super.onResume()
} }
@@ -206,11 +237,14 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
} }
private var uninstallActionEnabled = false
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_details, menu) menuInflater.inflate(R.menu.menu_details, menu)
if (::app.isInitialized) { if (::app.isInitialized) {
val installed = PackageUtil.isInstalled(this, app.packageName) val installed = PackageUtil.isInstalled(this, app.packageName)
menu?.findItem(R.id.action_uninstall)?.isVisible = installed menu?.findItem(R.id.action_uninstall)?.isVisible = installed
uninstallActionEnabled = installed
} }
return true return true
} }
@@ -305,7 +339,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
showDialog(R.string.title_installer, R.string.dialog_desc_native_split) showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
} else { } else {
task { task {
AppInstaller(this) AppInstaller.getInstance(this)
.getPreferredInstaller() .getPreferredInstaller()
.install( .install(
app.packageName, app.packageName,
@@ -324,7 +358,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
@Synchronized @Synchronized
private fun uninstallApp() { private fun uninstallApp() {
task { task {
AppInstaller(this) AppInstaller.getInstance(this)
.getPreferredInstaller() .getPreferredInstaller()
.uninstall(app.packageName) .uninstall(app.packageName)
} }
@@ -427,14 +461,14 @@ class AppDetailsActivity : BaseDetailsActivity() {
private fun startDownload() { private fun startDownload() {
when (status) { when (status) {
Status.PAUSED -> { Status.PAUSED -> {
fetch.resumeGroup(app.id) fetch?.resumeGroup(app.getGroupId(this@AppDetailsActivity))
} }
Status.DOWNLOADING -> { Status.DOWNLOADING -> {
flip(1) flip(1)
toast("Already downloading") toast("Already downloading")
} }
Status.COMPLETED -> { Status.COMPLETED -> {
fetch.getFetchGroup(app.id) { fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) {
verifyAndInstall(it.downloads) verifyAndInstall(it.downloads)
} }
} }
@@ -444,92 +478,25 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
} }
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
private fun purchase() { private fun purchase() {
bottomSheetBehavior.isHideable = false
bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
updateActionState(State.PROGRESS) updateActionState(State.PROGRESS)
task { runWithPermissions(
val authData = AuthProvider Manifest.permission.READ_EXTERNAL_STORAGE,
.with(this) Manifest.permission.WRITE_EXTERNAL_STORAGE
.getAuthData() ) {
if (updateService == null) {
PurchaseHelper(authData) listOfActionsWhenServiceAttaches.add({
.using(HttpClient.getPreferredClient()) updateService?.updateApp(app, removeExisiting = true)
.purchase(app.packageName, app.versionCode, app.offerType) })
} successUi { files -> getUpdateServiceInstance()
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 { } else {
Log.e("Failed to download : ${app.displayName}") updateService?.updateApp(app, removeExisiting = true)
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*/
fetch.deleteGroup(app.id)
/*Enqueue new fetch group*/
fetch.enqueue(
requestList
) {
status = Status.ADDED
Log.i("Downloading Apks : %s", app.displayName)
}
} else {
updateActionState(State.IDLE)
expandBottomSheet(getString(R.string.purchase_session_expired))
} }
} }
@@ -544,6 +511,10 @@ class AppDetailsActivity : BaseDetailsActivity() {
else else
0 0
if (progress == 100) {
B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
return@runOnUiThread
}
B.layoutDetailsInstall.apply { B.layoutDetailsInstall.apply {
txtProgressPercent.text = ("${progress}%") txtProgressPercent.text = ("${progress}%")
@@ -585,7 +556,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
B.layoutDetailsInstall.btnDownload.let { btn -> B.layoutDetailsInstall.btnDownload.let { btn ->
if (isInstalled) { if (isInstalled) {
val isUpdatable = PackageUtil.isUpdatable( isUpdatable = PackageUtil.isUpdatable(
this, this,
app.packageName, app.packageName,
app.versionCode.toLong() app.versionCode.toLong()
@@ -603,6 +574,9 @@ class AppDetailsActivity : BaseDetailsActivity() {
btn.setText(R.string.action_open) btn.setText(R.string.action_open)
btn.addOnClickListener { openApp() } btn.addOnClickListener { openApp() }
} }
if (!uninstallActionEnabled) {
invalidateOptionsMenu()
}
} else { } else {
if (app.isFree) { if (app.isFree) {
btn.setText(R.string.action_install) btn.setText(R.string.action_install)
@@ -618,6 +592,9 @@ class AppDetailsActivity : BaseDetailsActivity() {
startDownload() startDownload()
} }
} }
if (uninstallActionEnabled) {
invalidateOptionsMenu()
}
} }
} }
} }
@@ -635,16 +612,17 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
private fun attachFetch() { private fun attachFetch() {
downloadManager = DownloadManager.with(this) if (fetch == null) {
fetch = downloadManager.fetch downloadManager = DownloadManager.with(this)
fetch = downloadManager!!.fetch
fetch.getFetchGroup(app.id) { fetchGroup: FetchGroup -> }
fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { fetchGroup: FetchGroup ->
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) { if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
status = Status.COMPLETED status = Status.COMPLETED
} else if (downloadManager.isDownloading(fetchGroup)) { } else if (downloadManager?.isDownloading(fetchGroup) == true) {
status = Status.DOWNLOADING status = Status.DOWNLOADING
flip(1) flip(1)
} else if (downloadManager.isCanceled(fetchGroup)) { } else if (downloadManager?.isCanceled(fetchGroup) == true) {
status = Status.CANCELLED status = Status.CANCELLED
} else if (fetchGroup.pausedDownloads.isNotEmpty()) { } else if (fetchGroup.pausedDownloads.isNotEmpty()) {
status = Status.PAUSED status = Status.PAUSED
@@ -655,6 +633,12 @@ class AppDetailsActivity : BaseDetailsActivity() {
fetchGroupListener = object : AbstractFetchGroupListener() { fetchGroupListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status
}
}
override fun onStarted( override fun onStarted(
groupId: Int, groupId: Int,
download: Download, download: Download,
@@ -662,7 +646,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
totalBlocks: Int, totalBlocks: Int,
fetchGroup: FetchGroup fetchGroup: FetchGroup
) { ) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status status = download.status
flip(1) flip(1)
@@ -680,7 +664,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status status = download.status
flip(1) flip(1)
FileUtils.touch(inProgressMarker) FileUtils.touch(inProgressMarker)
@@ -688,7 +672,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status status = download.status
flip(0) flip(0)
} }
@@ -701,7 +685,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
downloadedBytesPerSecond: Long, downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup fetchGroup: FetchGroup
) { ) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@AppDetailsActivity)) {
updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond) updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond)
Log.i( Log.i(
"${app.displayName} : ${download.file} -> Progress : %d", "${app.displayName} : ${download.file} -> Progress : %d",
@@ -711,22 +695,21 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.id && fetchGroup.groupDownloadProgress == 100) { if (groupId == app.getGroupId(this@AppDetailsActivity) && fetchGroup.groupDownloadProgress == 100) {
status = download.status status = download.status
flip(0) flip(0)
updateProgress(fetchGroup, -1, -1) updateProgress(fetchGroup, -1, -1)
inProgressMarker.delete()
completionMarker.createNewFile()
try { try {
verifyAndInstall(fetchGroup.downloads) inProgressMarker.delete()
} catch (e: Exception) { completionMarker.createNewFile()
Log.e(e.stackTraceToString()) } catch (ex: Exception) {
ex.printStackTrace()
} }
} }
} }
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status status = download.status
flip(0) flip(0)
inProgressMarker.delete() inProgressMarker.delete()
@@ -740,7 +723,7 @@ class AppDetailsActivity : BaseDetailsActivity() {
throwable: Throwable?, throwable: Throwable?,
fetchGroup: FetchGroup fetchGroup: FetchGroup
) { ) {
if (groupId == app.id) { if (groupId == app.getGroupId(this@AppDetailsActivity)) {
status = download.status status = download.status
flip(0) flip(0)
inProgressMarker.delete() inProgressMarker.delete()
@@ -748,13 +731,63 @@ class AppDetailsActivity : BaseDetailsActivity() {
} }
} }
fetch.addListener(fetchGroupListener) appMetadataListener = object : AppMetadataStatusListener {
override fun onAppMetadataStatusError(reason: String, app: App) {
if (app.packageName == this@AppDetailsActivity.app.packageName) {
updateActionState(State.IDLE)
expandBottomSheet(reason)
}
}
}
getUpdateServiceInstance()
B.layoutDetailsInstall.imgCancel.setOnClickListener { B.layoutDetailsInstall.imgCancel.setOnClickListener {
fetch.cancelGroup( fetch?.cancelGroup(
app.id app.getGroupId(this@AppDetailsActivity)
) )
} }
if (updateService != null) {
pendingAddListeners = false
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
} else {
pendingAddListeners = true
}
}
fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(this, UpdateService::class.java)
startService(intent)
bindService(
intent,
serviceConnection,
0
)
}
}
override fun onPause() {
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
unbindService(serviceConnection)
}
super.onPause()
}
override fun onDestroy() {
super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
unbindService(serviceConnection)
}
} }
private fun attachBottomSheet() { private fun attachBottomSheet() {

View File

@@ -28,6 +28,7 @@ import com.aurora.store.R
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.DownloadManager
import com.aurora.store.data.model.DownloadFile import com.aurora.store.data.model.DownloadFile
import com.aurora.store.databinding.ActivityDownloadBinding 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.DownloadViewModel_
import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ import com.aurora.store.view.epoxy.views.app.NoAppViewModel_
import com.aurora.store.view.ui.commons.BaseActivity import com.aurora.store.view.ui.commons.BaseActivity
@@ -156,10 +157,12 @@ class DownloadActivity : BaseActivity() {
} }
R.id.action_clear_completed -> { R.id.action_clear_completed -> {
fetch.removeAllWithStatus(Status.COMPLETED) fetch.removeAllWithStatus(Status.COMPLETED)
Preferences.getPrefs(this).edit().remove(Preferences.PREFERENCE_UNIQUE_GROUP_IDS).apply()
return true return true
} }
R.id.action_force_clear_all -> { R.id.action_force_clear_all -> {
fetch.deleteAll() fetch.deleteAll()
Preferences.getPrefs(this).edit().remove(Preferences.PREFERENCE_UNIQUE_GROUP_IDS).apply()
return true return true
} }
} }

View File

@@ -113,21 +113,8 @@ class OnboardingActivity : BaseActivity() {
B.viewpager2.registerOnPageChangeCallback(object : OnPageChangeCallback() { B.viewpager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
runOnUiThread { runOnUiThread {
B.btnBackward.isEnabled = position != 0 lastPosition = position
if (position == 4) { refreshButtonState()
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
)
}
}
} }
} }
}) })
@@ -145,6 +132,32 @@ class OnboardingActivity : BaseActivity() {
B.viewpager2.setCurrentItem(B.viewpager2.currentItem - 1, true) 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() { override fun onBackPressed() {
if (B.viewpager2.currentItem == 0) { if (B.viewpager2.currentItem == 0) {
super.onBackPressed() super.onBackPressed()

View File

@@ -91,6 +91,21 @@ class PermissionsFragment : BaseFragment() {
) )
B.epoxyRecycler.withModels { 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) setFilterDuplicates(true)
installerList.forEach { installerList.forEach {
add( add(
@@ -99,12 +114,9 @@ class PermissionsFragment : BaseFragment() {
.permission(it) .permission(it)
.isGranted( .isGranted(
when (it.id) { when (it.id) {
0 -> ActivityCompat.checkSelfPermission( 0 -> writeExternalStorage
requireContext(), 1 -> storageManager
Manifest.permission.WRITE_EXTERNAL_STORAGE 2 -> canInstallPackages
) == PackageManager.PERMISSION_GRANTED
1 -> if (isRAndAbove()) Environment.isExternalStorageManager() else true
2 -> if (isOAndAbove()) requireContext().packageManager.canRequestPackageInstalls() else true
else -> false 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

@@ -108,7 +108,7 @@ class AppMenuSheet : BaseBottomSheet() {
R.id.action_uninstall -> { R.id.action_uninstall -> {
task { task {
AppInstaller(requireContext()) AppInstaller.getInstance(requireContext())
.getPreferredInstaller().uninstall(app.packageName) .getPreferredInstaller().uninstall(app.packageName)
} }
} }

View File

@@ -19,24 +19,24 @@
package com.aurora.store.view.ui.updates package com.aurora.store.view.ui.updates
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.aurora.Constants import com.aurora.Constants
import com.aurora.extensions.stackTraceToString import com.aurora.extensions.stackTraceToString
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App 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.R
import com.aurora.store.State import com.aurora.store.State
import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.downloader.RequestBuilder
import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.installer.AppInstaller
import com.aurora.store.data.model.UpdateFile 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.databinding.FragmentUpdatesBinding
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_ import com.aurora.store.view.epoxy.views.UpdateHeaderViewModel_
@@ -48,11 +48,8 @@ import com.aurora.store.view.ui.sheets.AppMenuSheet
import com.aurora.store.viewmodel.all.UpdatesViewModel import com.aurora.store.viewmodel.all.UpdatesViewModel
import com.tonyodev.fetch2.AbstractFetchGroupListener import com.tonyodev.fetch2.AbstractFetchGroupListener
import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchGroup import com.tonyodev.fetch2.FetchGroup
import nl.komponents.kovenant.task import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
class UpdatesFragment : BaseFragment() { class UpdatesFragment : BaseFragment() {
@@ -60,11 +57,28 @@ class UpdatesFragment : BaseFragment() {
private lateinit var B: FragmentUpdatesBinding private lateinit var B: FragmentUpdatesBinding
private lateinit var VM: UpdatesViewModel private lateinit var VM: UpdatesViewModel
private lateinit var authData: AuthData val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
private lateinit var purchaseHelper: PurchaseHelper
private lateinit var fetch: Fetch
private lateinit var fetchListener: AbstractFetchGroupListener private lateinit var fetchListener: AbstractFetchGroupListener
private var attachToServiceCalled = false
private var serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
updateService!!.registerFetchListener(fetchListener)
if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
val iterator = listOfActionsWhenServiceAttaches.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
next.run()
iterator.remove()
}
}
}
override fun onServiceDisconnected(name: ComponentName) {
updateService = null
attachToServiceCalled = false
}
}
private var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf() private var updateFileMap: MutableMap<Int, UpdateFile> = mutableMapOf()
@@ -83,10 +97,7 @@ class UpdatesFragment : BaseFragment() {
VM = ViewModelProvider(requireActivity()).get(UpdatesViewModel::class.java) VM = ViewModelProvider(requireActivity()).get(UpdatesViewModel::class.java)
authData = AuthProvider.with(requireContext()).getAuthData()
purchaseHelper = PurchaseHelper(authData)
fetch = DownloadManager.with(requireContext()).fetch
fetchListener = object : AbstractFetchGroupListener() { fetchListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
@@ -106,11 +117,6 @@ class UpdatesFragment : BaseFragment() {
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (fetchGroup.groupDownloadProgress == 100) { if (fetchGroup.groupDownloadProgress == 100) {
VM.updateDownload(groupId, fetchGroup, isComplete = true) VM.updateDownload(groupId, fetchGroup, isComplete = true)
try {
install(download.tag!!, fetchGroup.downloads)
} catch (e: Exception) {
Log.e(e.stackTraceToString())
}
} }
} }
@@ -123,31 +129,43 @@ class UpdatesFragment : BaseFragment() {
} }
} }
getUpdateServiceInstance()
return B.root return B.root
} }
override fun onResume() { override fun onResume() {
getUpdateServiceInstance()
super.onResume() super.onResume()
if (::fetch.isInitialized && ::fetchListener.isInitialized) { }
fetch.addListener(fetchListener)
override fun onPause() {
if (updateService != null) {
updateService = null
attachToServiceCalled = false
requireContext().unbindService(serviceConnection)
} }
super.onPause()
} }
override fun onDestroy() { override fun onDestroy() {
if (::fetch.isInitialized && ::fetchListener.isInitialized) {
fetch.removeListener(fetchListener)
}
super.onDestroy() super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
requireContext().unbindService(serviceConnection)
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
VM.liveUpdateData.observe(viewLifecycleOwner, { VM.liveUpdateData.observe(viewLifecycleOwner) {
updateFileMap = it updateFileMap = it
updateController(updateFileMap) updateController(updateFileMap)
B.swipeRefreshLayout.isRefreshing = false B.swipeRefreshLayout.isRefreshing = false
}) updateService?.liveUpdateData?.postValue(updateFileMap)
}
B.swipeRefreshLayout.setOnRefreshListener { B.swipeRefreshLayout.setOnRefreshListener {
VM.observe() VM.observe()
@@ -221,44 +239,56 @@ class UpdatesFragment : BaseFragment() {
} }
} }
private var updateService: UpdateService? = null
fun runInService(runnable: Runnable) {
if (updateService == null) {
listOfActionsWhenServiceAttaches.add(runnable)
getUpdateServiceInstance()
} else {
runnable.run()
}
}
private fun updateSingle(app: App) { private fun updateSingle(app: App) {
runInService {
VM.updateState(app.getGroupId(requireContext()), State.QUEUED)
VM.updateState(app.id, State.QUEUED) updateService?.updateApp(app)
task {
val files = purchaseHelper.purchase(
app.packageName,
app.versionCode,
app.offerType
)
files.map { RequestBuilder.buildRequest(requireContext(), app, it) }
} successUi {
val requests = it.filter { request -> request.url.isNotEmpty() }.toList()
if (requests.isNotEmpty()) {
fetch.enqueue(requests) {
Log.i("Updating ${app.displayName}")
}
} else {
requireContext().toast("Failed to update ${app.displayName}")
}
} failUi {
Log.e("Failed to update ${app.displayName}")
} }
} }
private fun cancelSingle(app: App) { private fun cancelSingle(app: App) {
fetch.cancelGroup(app.id) runInService {
updateService?.fetch?.cancelGroup(app.getGroupId(requireContext()))
}
} }
private fun updateAll() { private fun updateAll() {
updateFileMap.values.forEach { updateSingle(it.app) } runInService {
VM.updateAllEnqueued = true updateService?.updateAll(updateFileMap)
VM.updateAllEnqueued = true
}
} }
private fun cancelAll() { private fun cancelAll() {
VM.updateAllEnqueued = false runInService {
updateFileMap.values.forEach { fetch.cancelGroup(it.app.id) } VM.updateAllEnqueued = false
updateFileMap.values.forEach { updateService?.fetch?.cancelGroup(it.app.getGroupId(requireContext())) }
}
}
fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(requireContext(), UpdateService::class.java)
requireContext().startService(intent)
requireContext().bindService(
intent,
serviceConnection,
0
)
}
} }
@Synchronized @Synchronized
@@ -272,7 +302,7 @@ class UpdatesFragment : BaseFragment() {
if (filesExist) { if (filesExist) {
task { task {
AppInstaller(requireContext()) AppInstaller.getInstance(requireContext())
.getPreferredInstaller() .getPreferredInstaller()
.install( .install(
packageName, packageName,

View File

@@ -25,6 +25,8 @@ import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.State import com.aurora.store.State
import com.aurora.store.data.RequestState import com.aurora.store.data.RequestState
import com.aurora.store.data.downloader.RequestGroupIdBuilder
import com.aurora.store.data.downloader.getGroupId
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.event.InstallerEvent import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.UpdateFile import com.aurora.store.data.model.UpdateFile
@@ -72,7 +74,7 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
updateFileMap.clear() updateFileMap.clear()
apps.forEach { apps.forEach {
updateFileMap[it.id] = UpdateFile(it) updateFileMap[it.getGroupId(getApplication<Application>().applicationContext)] = UpdateFile(it)
} }
liveUpdateData.postValue(updateFileMap) liveUpdateData.postValue(updateFileMap)
@@ -110,7 +112,10 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
is InstallerEvent.Failed -> { is InstallerEvent.Failed -> {
val packageName = event.packageName val packageName = event.packageName
packageName?.let { packageName?.let {
updateDownload(packageName.hashCode(), null, true) val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
groupIDsOfPackageName.forEach {
updateDownload(it, null, true)
}
} }
} }
} }
@@ -146,8 +151,11 @@ class UpdatesViewModel(application: Application) : BaseAppsViewModel(application
} }
private fun updateListAndPost(packageName: String) { private fun updateListAndPost(packageName: String) {
//Remove from map val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(getApplication<Application>().applicationContext, packageName.hashCode())
updateFileMap.remove(packageName.hashCode()) groupIDsOfPackageName.forEach {
//Remove from map
updateFileMap.remove(it)
}
//Post new update list //Post new update list
liveUpdateData.postValue(updateFileMap) liveUpdateData.postValue(updateFileMap)

View File

@@ -366,4 +366,7 @@
<string name="toast_aas_token_failed">Could not generate AAS Token</string> <string name="toast_aas_token_failed">Could not generate AAS Token</string>
<string name="toast_export_success">Device config exported</string> <string name="toast_export_success">Device config exported</string>
<string name="toast_export_failed">Could not export device config</string> <string name="toast_export_failed">Could not 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> </resources>