Drop dependency upon Fetch2 library
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
@@ -156,9 +156,6 @@ dependencies {
|
|||||||
//HTTP Clients
|
//HTTP Clients
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
|
||||||
//Fetch - Downloader
|
|
||||||
implementation("androidx.tonyodev.fetch2:xfetch2:3.1.6")
|
|
||||||
|
|
||||||
//EventBus
|
//EventBus
|
||||||
implementation("org.greenrobot:eventbus:3.3.1")
|
implementation("org.greenrobot:eventbus:3.3.1")
|
||||||
|
|
||||||
|
|||||||
@@ -97,11 +97,6 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
|
|
||||||
<service android:name=".data.service.NotificationService" />
|
|
||||||
<service android:name=".data.installer.InstallerService" />
|
|
||||||
<service android:name=".data.service.UpdateService" />
|
|
||||||
<service android:name=".data.service.SelfUpdateService" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
@@ -139,9 +134,6 @@
|
|||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<receiver android:name=".data.receiver.DownloadResumeReceiver" />
|
|
||||||
<receiver android:name=".data.receiver.DownloadPauseReceiver" />
|
|
||||||
<receiver android:name=".data.receiver.DownloadCancelReceiver" />
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.receiver.InstallReceiver"
|
android:name=".data.receiver.InstallReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|||||||
@@ -25,14 +25,11 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.hilt.work.HiltWorkerFactory
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import com.aurora.extensions.isPAndAbove
|
import com.aurora.extensions.isPAndAbove
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import com.aurora.store.data.receiver.PackageManagerReceiver
|
import com.aurora.store.data.receiver.PackageManagerReceiver
|
||||||
import com.aurora.store.data.service.NotificationService
|
|
||||||
import com.aurora.store.util.CommonUtil
|
import com.aurora.store.util.CommonUtil
|
||||||
import com.aurora.store.util.DownloadWorkerUtil
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
import com.aurora.store.util.NotificationUtil
|
import com.aurora.store.util.NotificationUtil
|
||||||
import com.aurora.store.util.PackageUtil
|
import com.aurora.store.util.PackageUtil
|
||||||
import com.tonyodev.fetch2.Fetch
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||||
@@ -52,8 +49,6 @@ class AuroraApplication : Application(), Configuration.Provider {
|
|||||||
.setWorkerFactory(workerFactory)
|
.setWorkerFactory(workerFactory)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private lateinit var fetch: Fetch
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
|
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
|
||||||
}
|
}
|
||||||
@@ -69,9 +64,6 @@ class AuroraApplication : Application(), Configuration.Provider {
|
|||||||
|
|
||||||
//Create Notification Channels : General & Alert
|
//Create Notification Channels : General & Alert
|
||||||
NotificationUtil.createNotificationChannel(this)
|
NotificationUtil.createNotificationChannel(this)
|
||||||
NotificationService.startService(this)
|
|
||||||
|
|
||||||
fetch = DownloadManager.with(this).fetch
|
|
||||||
|
|
||||||
// Initialize DownloadWorker to observe and trigger downloads
|
// Initialize DownloadWorker to observe and trigger downloads
|
||||||
downloadWorkerUtil.init()
|
downloadWorkerUtil.init()
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.data.downloader
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.aurora.store.data.SingletonHolder
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.tonyodev.fetch2.BuildConfig
|
|
||||||
import com.tonyodev.fetch2.Download
|
|
||||||
import com.tonyodev.fetch2.Fetch
|
|
||||||
import com.tonyodev.fetch2.FetchConfiguration
|
|
||||||
import com.tonyodev.fetch2.FetchGroup
|
|
||||||
import com.tonyodev.fetch2.FetchListener
|
|
||||||
import com.tonyodev.fetch2core.DefaultStorageResolver
|
|
||||||
import com.tonyodev.fetch2core.getFileTempDir
|
|
||||||
|
|
||||||
class DownloadManager private constructor(var context: Context) {
|
|
||||||
|
|
||||||
companion object : SingletonHolder<DownloadManager, Context>(::DownloadManager)
|
|
||||||
|
|
||||||
var fetch: Fetch
|
|
||||||
|
|
||||||
init {
|
|
||||||
fetch = Fetch.getInstance(getFetchConfiguration(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFetchInstance(): Fetch {
|
|
||||||
return fetch
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFetchConfiguration(context: Context): FetchConfiguration {
|
|
||||||
return FetchConfiguration.Builder(context)
|
|
||||||
.enableLogging(BuildConfig.DEBUG)
|
|
||||||
.enableHashCheck(true)
|
|
||||||
.enableFileExistChecks(true)
|
|
||||||
.enableRetryOnNetworkGain(true)
|
|
||||||
.enableAutoStart(true)
|
|
||||||
.setAutoRetryMaxAttempts(3)
|
|
||||||
.setProgressReportingInterval(3000)
|
|
||||||
.setNamespace(BuildConfig.APPLICATION_ID)
|
|
||||||
.setStorageResolver(DefaultStorageResolver(context, getFileTempDir(context)))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDownloading(fetchGroup: FetchGroup): Boolean {
|
|
||||||
return fetchGroup.downloadingDownloads.isNotEmpty()
|
|
||||||
|| fetchGroup.queuedDownloads.isNotEmpty()
|
|
||||||
|| fetchGroup.addedDownloads.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isCanceled(fetchGroup: FetchGroup): Boolean {
|
|
||||||
return fetchGroup.cancelledDownloads.isNotEmpty()
|
|
||||||
|| fetchGroup.removedDownloads.isNotEmpty()
|
|
||||||
|| fetchGroup.deletedDownloads.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateOngoingDownloads(
|
|
||||||
fetch: Fetch, packageList: MutableList<String?>, download: Download,
|
|
||||||
fetchListener: FetchListener?
|
|
||||||
) {
|
|
||||||
if (packageList.contains(download.tag)) {
|
|
||||||
val packageName = download.tag
|
|
||||||
if (packageName != null) {
|
|
||||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(context, packageName.hashCode())
|
|
||||||
groupIDsOfPackageName.forEach {
|
|
||||||
fetch.deleteGroup(it)
|
|
||||||
}
|
|
||||||
packageList.remove(packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (packageList.size == 0) {
|
|
||||||
fetch.removeListener(fetchListener!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.data.downloader
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.aurora.Constants
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
import com.aurora.gplayapi.data.models.File
|
|
||||||
import com.aurora.store.util.PathUtil
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.tonyodev.fetch2.EnqueueAction
|
|
||||||
import com.tonyodev.fetch2.NetworkType
|
|
||||||
import com.tonyodev.fetch2.Request
|
|
||||||
import com.tonyodev.fetch2core.Extras
|
|
||||||
import java.lang.reflect.Modifier
|
|
||||||
|
|
||||||
private fun Request.attachMetaData(context: Context, app: App) {
|
|
||||||
val isWifiOnly = Preferences.getBoolean(context, Preferences.PREFERENCE_DOWNLOAD_WIFI_ONLY)
|
|
||||||
|
|
||||||
apply {
|
|
||||||
groupId = app.getGroupId(context)
|
|
||||||
tag = app.packageName
|
|
||||||
enqueueAction = EnqueueAction.UPDATE_ACCORDINGLY
|
|
||||||
networkType = if (isWifiOnly) NetworkType.WIFI_ONLY else NetworkType.ALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Request.attachExtra(app: App) {
|
|
||||||
val stringMap: MutableMap<String, String> = mutableMapOf()
|
|
||||||
val gson = GsonBuilder()
|
|
||||||
.excludeFieldsWithModifiers(Modifier.TRANSIENT)
|
|
||||||
.create()
|
|
||||||
stringMap[Constants.STRING_EXTRA] = gson.toJson(app)
|
|
||||||
apply {
|
|
||||||
extras = Extras(stringMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object RequestBuilder {
|
|
||||||
|
|
||||||
fun buildRequest(context: Context, app: App, file: File): Request {
|
|
||||||
val fileName = when (file.type) {
|
|
||||||
File.FileType.BASE,
|
|
||||||
File.FileType.SPLIT -> PathUtil.getApkDownloadFile(context, app.packageName, app.versionCode, file)
|
|
||||||
File.FileType.OBB,
|
|
||||||
File.FileType.PATCH -> PathUtil.getObbDownloadFile(app.packageName, file)
|
|
||||||
}
|
|
||||||
return Request(file.url, fileName).apply {
|
|
||||||
attachMetaData(context, app)
|
|
||||||
attachExtra(app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
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 = (0 until Int.MAX_VALUE).random()
|
|
||||||
while (groupIDMap.containsKey(randomGroupID)) {
|
|
||||||
randomGroupID = (0 until Int.MAX_VALUE).random()
|
|
||||||
}
|
|
||||||
groupIDMap[randomGroupID] = RequestGroupIdBuilder.AppIDnVersion(this.id, this.versionCode)
|
|
||||||
Preferences.getPrefs(context).edit().putString(PREFERENCE_UNIQUE_GROUP_IDS, gson.toJson(groupIDMap)).apply()
|
|
||||||
return randomGroupID
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.data.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.tonyodev.fetch2.Download
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class DownloadFile(val download: Download) : Parcelable {
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return download.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
return when (other) {
|
|
||||||
is DownloadFile -> other.download.status == download.status && other.download.progress == download.progress
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.data.model
|
|
||||||
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
import com.aurora.store.State
|
|
||||||
import com.tonyodev.fetch2.FetchGroup
|
|
||||||
|
|
||||||
data class UpdateFile(val app: App) {
|
|
||||||
|
|
||||||
var group: FetchGroup? = null
|
|
||||||
var state: State = State.IDLE
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return app.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
return when (other) {
|
|
||||||
is UpdateFile -> other.app.id == app.id
|
|
||||||
&& other.state == state
|
|
||||||
&& isGroupSame(group, other.group)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isGroupSame(oldGroup: FetchGroup?, newGroup: FetchGroup?): Boolean {
|
|
||||||
if (oldGroup != null && newGroup == null)
|
|
||||||
return true
|
|
||||||
return oldGroup?.groupDownloadProgress != newGroup?.groupDownloadProgress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.aurora.store.data.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.aurora.Constants.FETCH_GROUP_ID
|
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DownloadCancelReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val extras = intent.extras
|
|
||||||
if (extras != null) {
|
|
||||||
val groupId = extras.getInt(FETCH_GROUP_ID, -1)
|
|
||||||
DownloadManager
|
|
||||||
.with(context)
|
|
||||||
.getFetchInstance()
|
|
||||||
.cancelGroup(groupId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.aurora.store.data.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.aurora.Constants.FETCH_GROUP_ID
|
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DownloadPauseReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val extras = intent.extras
|
|
||||||
if (extras != null) {
|
|
||||||
val groupId: Int = extras.getInt(FETCH_GROUP_ID, -1)
|
|
||||||
DownloadManager
|
|
||||||
.with(context)
|
|
||||||
.getFetchInstance()
|
|
||||||
.pauseGroup(groupId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.aurora.store.data.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.aurora.Constants.FETCH_GROUP_ID
|
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class DownloadResumeReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val extras = intent.extras
|
|
||||||
if (extras != null) {
|
|
||||||
val groupId: Int = extras.getInt(FETCH_GROUP_ID, -1)
|
|
||||||
DownloadManager
|
|
||||||
.with(context)
|
|
||||||
.getFetchInstance()
|
|
||||||
.resumeGroup(groupId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,12 +19,10 @@
|
|||||||
|
|
||||||
package com.aurora.store.data.receiver
|
package com.aurora.store.data.receiver
|
||||||
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.BroadcastReceiver
|
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
|
||||||
@@ -56,8 +54,6 @@ open class PackageManagerReceiver : BroadcastReceiver() {
|
|||||||
.getPreferredInstaller()
|
.getPreferredInstaller()
|
||||||
.removeFromInstallQueue(packageName)
|
.removeFromInstallQueue(packageName)
|
||||||
|
|
||||||
//clearNotification(context, packageName)
|
|
||||||
|
|
||||||
val isAutoDeleteAPKEnabled = Preferences.getBoolean(
|
val isAutoDeleteAPKEnabled = Preferences.getBoolean(
|
||||||
context,
|
context,
|
||||||
Preferences.PREFERENCE_AUTO_DELETE
|
Preferences.PREFERENCE_AUTO_DELETE
|
||||||
@@ -72,15 +68,6 @@ open class PackageManagerReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearNotification(context: Context, packageName: String) {
|
|
||||||
val notificationManager = context.applicationContext
|
|
||||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
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) {
|
||||||
try {
|
try {
|
||||||
val rootDirPath = PathUtil.getPackageDirectory(context, packageName)
|
val rootDirPath = PathUtil.getPackageDirectory(context, packageName)
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.aurora.store.data.service
|
|
||||||
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
|
|
||||||
interface AppMetadataStatusListener {
|
|
||||||
fun onAppMetadataStatusError(reason: String, app: App)
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
/*
|
|
||||||
* Aurora Store
|
|
||||||
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
|
|
||||||
* Copyright (C) 2022, The Calyx Institute
|
|
||||||
*
|
|
||||||
* Aurora Store is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 2 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Aurora Store is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.data.service
|
|
||||||
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.ArrayMap
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import com.aurora.Constants
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import com.aurora.store.data.downloader.RequestGroupIdBuilder
|
|
||||||
import com.aurora.store.data.event.InstallerEvent
|
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
|
||||||
import com.aurora.store.util.Log
|
|
||||||
import com.aurora.store.util.NotificationUtil
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.tonyodev.fetch2.AbstractFetchGroupListener
|
|
||||||
import com.tonyodev.fetch2.Download
|
|
||||||
import com.tonyodev.fetch2.Error
|
|
||||||
import com.tonyodev.fetch2.Fetch
|
|
||||||
import com.tonyodev.fetch2.FetchGroup
|
|
||||||
import com.tonyodev.fetch2.Status
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import java.lang.reflect.Modifier
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import org.greenrobot.eventbus.Subscribe
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class NotificationService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun startService(context: Context) {
|
|
||||||
try {
|
|
||||||
context.startService(Intent(context, NotificationService::class.java))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Failed to start notification service : %s", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var fetch: Fetch
|
|
||||||
private lateinit var fetchListener: AbstractFetchGroupListener
|
|
||||||
private lateinit var notificationManager: NotificationManager
|
|
||||||
|
|
||||||
private val appMap = ArrayMap<Int, App>()
|
|
||||||
private val gson: Gson = GsonBuilder()
|
|
||||||
.excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT)
|
|
||||||
.create()
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
Log.i("Notification Service Started")
|
|
||||||
|
|
||||||
EventBus.getDefault().register(this)
|
|
||||||
|
|
||||||
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
|
|
||||||
fetch = DownloadManager.with(this).fetch
|
|
||||||
|
|
||||||
fetchListener = object : AbstractFetchGroupListener() {
|
|
||||||
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(
|
|
||||||
groupId: Int,
|
|
||||||
download: Download,
|
|
||||||
error: Error,
|
|
||||||
throwable: Throwable?,
|
|
||||||
fetchGroup: FetchGroup
|
|
||||||
) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgress(
|
|
||||||
groupId: Int,
|
|
||||||
download: Download,
|
|
||||||
etaInMilliSeconds: Long,
|
|
||||||
downloadedBytesPerSecond: Long,
|
|
||||||
fetchGroup: FetchGroup
|
|
||||||
) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueued(
|
|
||||||
groupId: Int,
|
|
||||||
download: Download,
|
|
||||||
waitingNetwork: Boolean,
|
|
||||||
fetchGroup: FetchGroup
|
|
||||||
) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
showNotification(groupId, download, fetchGroup)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch.addListener(fetchListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showNotification(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
//Ignore notifications for completion of sub-parts of a bundled apk
|
|
||||||
if (download.status == Status.COMPLETED && fetchGroup.groupDownloadProgress < 100)
|
|
||||||
return
|
|
||||||
|
|
||||||
//synchronized(appMap) {
|
|
||||||
var app: App? = appMap[groupId]
|
|
||||||
|
|
||||||
if (app == null) {
|
|
||||||
app = gson.fromJson(
|
|
||||||
download.extras.getString(Constants.STRING_EXTRA, "{}"),
|
|
||||||
App::class.java
|
|
||||||
)
|
|
||||||
appMap[groupId] = app
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app == null)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (download.status == Status.DELETED) {
|
|
||||||
notificationManager.cancel(app.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NotificationManagerCompat.from(this).areNotificationsEnabled()) {
|
|
||||||
notificationManager.notify(
|
|
||||||
app.packageName,
|
|
||||||
app.id,
|
|
||||||
NotificationUtil.getDownloadNotification(this, app, groupId, download, fetchGroup)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe()
|
|
||||||
fun onEventMainThread(event: Any) {
|
|
||||||
when (event) {
|
|
||||||
is InstallerEvent.Success -> {
|
|
||||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode())
|
|
||||||
var app: App? = null
|
|
||||||
for (item in groupIDsOfPackageName) {
|
|
||||||
app = appMap[item]
|
|
||||||
if (app != null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (app != null)
|
|
||||||
notifyInstallationStatus(app, event.extra)
|
|
||||||
}
|
|
||||||
is InstallerEvent.Failed -> {
|
|
||||||
val groupIDsOfPackageName = RequestGroupIdBuilder.getGroupIDsForApp(this, event.packageName.hashCode())
|
|
||||||
var app: App? = null
|
|
||||||
for (item in groupIDsOfPackageName) {
|
|
||||||
app = appMap[item]
|
|
||||||
if (app != null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (app != null)
|
|
||||||
notifyInstallationStatus(app, event.error)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun install(packageName: String, files: List<Download>) {
|
|
||||||
AppInstaller.getInstance(this)
|
|
||||||
.getPreferredInstaller()
|
|
||||||
.install(
|
|
||||||
packageName,
|
|
||||||
files
|
|
||||||
.filter { it.file.endsWith(".apk") }
|
|
||||||
.map {
|
|
||||||
it.file
|
|
||||||
}.toList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyInstallationStatus(app: App, status: String?) {
|
|
||||||
if (NotificationManagerCompat.from(this).areNotificationsEnabled()) {
|
|
||||||
notificationManager.notify(
|
|
||||||
app.packageName,
|
|
||||||
app.id,
|
|
||||||
NotificationUtil.getInstallNotification(this, app, status)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
Log.i("Notification Service Stopped")
|
|
||||||
fetch.removeListener(fetchListener)
|
|
||||||
EventBus.getDefault().unregister(this);
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
package com.aurora.store.data.service
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.ServiceCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
import com.aurora.Constants
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
import com.aurora.gplayapi.data.models.File
|
|
||||||
import com.aurora.store.BuildConfig
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import com.aurora.store.data.downloader.RequestBuilder.buildRequest
|
|
||||||
import com.aurora.store.data.downloader.getGroupId
|
|
||||||
import com.aurora.store.data.installer.NativeInstaller
|
|
||||||
import com.aurora.store.data.model.SelfUpdate
|
|
||||||
import com.aurora.store.util.CertUtil.isFDroidApp
|
|
||||||
import com.aurora.store.util.Log
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.tonyodev.fetch2.*
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.lang.reflect.Modifier
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class SelfUpdateService : LifecycleService() {
|
|
||||||
private lateinit var app: App
|
|
||||||
private lateinit var fetch: Fetch
|
|
||||||
private lateinit var fetchListener: FetchListener
|
|
||||||
|
|
||||||
private var gson: Gson = GsonBuilder()
|
|
||||||
.excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT)
|
|
||||||
.create()
|
|
||||||
|
|
||||||
private val hashCode = BuildConfig.APPLICATION_ID.hashCode()
|
|
||||||
|
|
||||||
// Coroutine
|
|
||||||
private val job = SupervisorJob()
|
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
super.onBind(intent)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
super.onStartCommand(intent, flags, startId)
|
|
||||||
val rawSelfUpdate = intent?.getStringExtra(Constants.STRING_EXTRA)
|
|
||||||
if (rawSelfUpdate?.isNotBlank() == true) {
|
|
||||||
val selfUpdate = gson.fromJson(rawSelfUpdate, SelfUpdate::class.java)
|
|
||||||
selfUpdate?.let {
|
|
||||||
downloadAndUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
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_GENERAL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
startForeground(1, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
job.cancel()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun destroyService() {
|
|
||||||
Log.d("Self-update service destroyed")
|
|
||||||
fetch.removeListener(fetchListener)
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadAndUpdate(update: SelfUpdate) {
|
|
||||||
app = App(BuildConfig.APPLICATION_ID)
|
|
||||||
app.id = hashCode
|
|
||||||
app.packageName = BuildConfig.APPLICATION_ID
|
|
||||||
app.displayName = getString(R.string.app_name)
|
|
||||||
app.versionName = update.versionName
|
|
||||||
app.versionCode = update.versionCode
|
|
||||||
|
|
||||||
val file = File().apply {
|
|
||||||
name = "AuroraStore.apk"
|
|
||||||
url = if (isFDroidVariant)
|
|
||||||
update.fdroidBuild
|
|
||||||
else
|
|
||||||
update.auroraBuild
|
|
||||||
type = File.FileType.BASE
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = buildRequest(this, app, file)
|
|
||||||
request.enqueueAction = EnqueueAction.REPLACE_EXISTING
|
|
||||||
|
|
||||||
val requestList: MutableList<Request> = ArrayList()
|
|
||||||
requestList.add(request)
|
|
||||||
|
|
||||||
fetch = DownloadManager.with(this).fetch
|
|
||||||
fetch.enqueue(requestList) {
|
|
||||||
Log.i("Downloading latest update")
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchListener = getFetchListener()
|
|
||||||
fetch.addListener(fetchListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isFDroidVariant: Boolean
|
|
||||||
get() = isFDroidApp(this, BuildConfig.APPLICATION_ID)
|
|
||||||
|
|
||||||
@get:RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private val notification: Notification
|
|
||||||
get() {
|
|
||||||
val notificationBuilder =
|
|
||||||
NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
|
||||||
return getNotification(notificationBuilder)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNotification(builder: NotificationCompat.Builder): Notification {
|
|
||||||
return builder.setAutoCancel(true)
|
|
||||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
|
||||||
.setContentTitle("Self update")
|
|
||||||
.setContentText("Updating Aurora Store in background")
|
|
||||||
.setOngoing(true)
|
|
||||||
.setSmallIcon(R.drawable.ic_notification_outlined)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFetchListener(): FetchListener {
|
|
||||||
return object : AbstractFetchGroupListener() {
|
|
||||||
override fun onError(
|
|
||||||
groupId: Int, download: Download, error: Error,
|
|
||||||
throwable: Throwable?, fetchGroup: FetchGroup
|
|
||||||
) {
|
|
||||||
if (groupId == app.getGroupId(this@SelfUpdateService)) {
|
|
||||||
Log.e("Error self-updating ${app.displayName}")
|
|
||||||
destroyService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
if (groupId == app.getGroupId(this@SelfUpdateService) && fetchGroup.groupDownloadProgress == 100) {
|
|
||||||
Log.d("Calling installer ${app.displayName}")
|
|
||||||
|
|
||||||
try {
|
|
||||||
NativeInstaller(this@SelfUpdateService).install(
|
|
||||||
app.packageName,
|
|
||||||
fetchGroup.downloads.map { it.file }
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("Self update : ${e.stackTraceToString()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceScope.launch {
|
|
||||||
delay(10000)
|
|
||||||
destroyService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
|
|
||||||
if (groupId == app.getGroupId(this@SelfUpdateService)) {
|
|
||||||
Log.d("Self-update cancelled ${app.displayName}")
|
|
||||||
destroyService()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,760 +0,0 @@
|
|||||||
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.app.ServiceCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.aurora.Constants
|
|
||||||
import com.aurora.extensions.isOAndAbove
|
|
||||||
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.aurora.store.util.PackageUtil.isSharedLibraryInstalled
|
|
||||||
import com.aurora.store.util.PathUtil
|
|
||||||
import com.aurora.store.util.Preferences
|
|
||||||
import com.tonyodev.fetch2.*
|
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
|
||||||
import com.tonyodev.fetch2core.FetchObserver
|
|
||||||
import com.tonyodev.fetch2core.Reason
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
|
||||||
import org.greenrobot.eventbus.Subscribe
|
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
|
||||||
import kotlin.concurrent.timerTask
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
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()) {
|
|
||||||
ServiceCompat.stopForeground(
|
|
||||||
this@UpdateService,
|
|
||||||
ServiceCompat.STOP_FOREGROUND_REMOVE
|
|
||||||
)
|
|
||||||
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()
|
|
||||||
|
|
||||||
// Coroutine
|
|
||||||
private val job = SupervisorJob()
|
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + job)
|
|
||||||
|
|
||||||
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(this))
|
|
||||||
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)
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
val files = purchaseHelper.purchase(
|
|
||||||
app.packageName,
|
|
||||||
app.versionCode,
|
|
||||||
app.offerType
|
|
||||||
)
|
|
||||||
|
|
||||||
if (app.dependencies.dependentLibraries.isNotEmpty() && isOAndAbove()) {
|
|
||||||
app.dependencies.dependentLibraries.forEach {
|
|
||||||
if (!isSharedLibraryInstalled(
|
|
||||||
this@UpdateService,
|
|
||||||
it.packageName,
|
|
||||||
it.versionCode
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
it.displayName = getString(R.string.downloading_dep, app.displayName)
|
|
||||||
it.iconArtwork = app.iconArtwork
|
|
||||||
|
|
||||||
updateApp(it, removeExisiting)
|
|
||||||
while (containsInInstalling(it.packageName) ||
|
|
||||||
!isSharedLibraryInstalled(
|
|
||||||
this@UpdateService,
|
|
||||||
it.packageName,
|
|
||||||
it.versionCode
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear library downloads
|
|
||||||
clearDownloadsIfRequired(it.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val requests = files.filter { it.url.isNotEmpty() }
|
|
||||||
.map { RequestBuilder.buildRequest(this@UpdateService, app, it) }
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
if (requests.isNotEmpty()) {
|
|
||||||
if (removeExisiting) {
|
|
||||||
/*Remove old fetch group if downloaded earlier, mostly in case of updates*/
|
|
||||||
fetch.deleteGroup(app.getGroupId(this@UpdateService))
|
|
||||||
}
|
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
removeFromInstalling(app.packageName)
|
|
||||||
var reason = "Unknown"
|
|
||||||
|
|
||||||
when (exception) {
|
|
||||||
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 {
|
|
||||||
ServiceCompat.stopForeground(
|
|
||||||
this@UpdateService,
|
|
||||||
ServiceCompat.STOP_FOREGROUND_REMOVE
|
|
||||||
)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun install(packageName: String, files: List<Download>?) {
|
|
||||||
if (containsInInstalling(packageName)) {
|
|
||||||
println("Already installing $packageName!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
putInInstalling(packageName)
|
|
||||||
files?.let { downloads ->
|
|
||||||
if (downloads.all { File(it.file).exists() }) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
AppInstaller.getInstance(this@UpdateService).getPreferredInstaller()
|
|
||||||
.install(
|
|
||||||
packageName,
|
|
||||||
files.filter { it.file.endsWith(".apk") }.map { it.file }.toList()
|
|
||||||
)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
removeFromInstalling(packageName)
|
|
||||||
Log.e(exception.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) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearDownloadsIfRequired(packageName: String) {
|
|
||||||
if (Preferences.getBoolean(this, Preferences.PREFERENCE_AUTO_DELETE)) {
|
|
||||||
try {
|
|
||||||
val rootDirPath = PathUtil.getPackageDirectory(this, packageName)
|
|
||||||
val rootDir = File(rootDirPath)
|
|
||||||
if (rootDir.exists()) rootDir.deleteRecursively()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d("Failed to clear downloads!", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,12 +25,6 @@ import com.aurora.store.data.room.download.Download as AuroraDownload
|
|||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.data.activity.InstallActivity
|
import com.aurora.store.data.activity.InstallActivity
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
import com.aurora.store.data.receiver.DownloadCancelReceiver
|
|
||||||
import com.aurora.store.data.receiver.DownloadPauseReceiver
|
|
||||||
import com.aurora.store.data.receiver.DownloadResumeReceiver
|
|
||||||
import com.tonyodev.fetch2.Download
|
|
||||||
import com.tonyodev.fetch2.FetchGroup
|
|
||||||
import com.tonyodev.fetch2.Status
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@@ -89,131 +83,6 @@ object NotificationUtil {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadNotification(
|
|
||||||
context: Context,
|
|
||||||
app: App,
|
|
||||||
groupId: Int,
|
|
||||||
download: Download,
|
|
||||||
fetchGroup: FetchGroup
|
|
||||||
): Notification {
|
|
||||||
val status = download.status
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_GENERAL)
|
|
||||||
builder.setContentTitle(app.displayName)
|
|
||||||
builder.setSmallIcon(R.drawable.ic_notification_outlined)
|
|
||||||
builder.color = ContextCompat.getColor(context, R.color.colorAccent)
|
|
||||||
builder.setWhen(download.created)
|
|
||||||
builder.setContentIntent(getContentIntentForDownloads(context))
|
|
||||||
|
|
||||||
when (status) {
|
|
||||||
Status.PAUSED -> {
|
|
||||||
builder.setSmallIcon(R.drawable.ic_download_pause)
|
|
||||||
builder.setContentText(context.getString(R.string.download_paused))
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.CANCELLED -> {
|
|
||||||
builder.setSmallIcon(R.drawable.ic_download_cancel)
|
|
||||||
builder.setContentText(context.getString(R.string.download_canceled))
|
|
||||||
builder.color = Color.RED
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.FAILED -> {
|
|
||||||
builder.setSmallIcon(R.drawable.ic_download_fail)
|
|
||||||
builder.setContentText(context.getString(R.string.download_failed))
|
|
||||||
builder.color = Color.RED
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.COMPLETED -> if (fetchGroup.groupDownloadProgress == 100) {
|
|
||||||
builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
builder.setContentText(context.getString(R.string.download_completed))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
builder.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
||||||
builder.setContentText(context.getString(R.string.download_metadata))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val progress = fetchGroup.groupDownloadProgress
|
|
||||||
val progressBigText = NotificationCompat.BigTextStyle()
|
|
||||||
|
|
||||||
when (status) {
|
|
||||||
Status.QUEUED -> {
|
|
||||||
builder.setProgress(100, 0, true)
|
|
||||||
progressBigText.bigText(context.getString(R.string.download_queued))
|
|
||||||
builder.setStyle(progressBigText)
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.DOWNLOADING -> {
|
|
||||||
val speedString: String =
|
|
||||||
CommonUtil.humanReadableByteSpeed(download.downloadedBytesPerSecond, true)
|
|
||||||
progressBigText.bigText(
|
|
||||||
context.getString(
|
|
||||||
R.string.download_progress,
|
|
||||||
fetchGroup.completedDownloads.size + 1,
|
|
||||||
fetchGroup.downloads.size,
|
|
||||||
speedString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
builder.setStyle(progressBigText)
|
|
||||||
builder.addAction(
|
|
||||||
NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_download_pause,
|
|
||||||
context.getString(R.string.action_pause),
|
|
||||||
getPauseIntent(context, groupId)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
builder.addAction(
|
|
||||||
NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_download_cancel,
|
|
||||||
context.getString(R.string.action_cancel),
|
|
||||||
getCancelIntent(context, groupId)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
if (progress < 0) builder.setProgress(
|
|
||||||
100,
|
|
||||||
0,
|
|
||||||
true
|
|
||||||
) else builder.setProgress(100, progress, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.PAUSED -> {
|
|
||||||
progressBigText.bigText(
|
|
||||||
context.getString(
|
|
||||||
R.string.download_paused,
|
|
||||||
fetchGroup.completedDownloads.size,
|
|
||||||
fetchGroup.downloads.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
builder.setStyle(progressBigText)
|
|
||||||
builder.addAction(
|
|
||||||
NotificationCompat.Action.Builder(
|
|
||||||
R.drawable.ic_download_pause,
|
|
||||||
context.getString(R.string.action_resume),
|
|
||||||
getResumeIntent(context, groupId)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.COMPLETED -> if (fetchGroup.groupDownloadProgress == 100) {
|
|
||||||
builder.setAutoCancel(true)
|
|
||||||
builder.setContentIntent(getContentIntentForDetails(context, app.packageName))
|
|
||||||
builder.setStyle(progressBigText)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when (status) {
|
|
||||||
Status.DOWNLOADING -> builder.setCategory(Notification.CATEGORY_PROGRESS)
|
|
||||||
Status.FAILED, Status.CANCELLED -> builder.setCategory(Notification.CATEGORY_ERROR)
|
|
||||||
else -> builder.setCategory(Notification.CATEGORY_STATUS)
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDownloadNotification(
|
fun getDownloadNotification(
|
||||||
context: Context,
|
context: Context,
|
||||||
download: AuroraDownload,
|
download: AuroraDownload,
|
||||||
@@ -307,33 +176,6 @@ object NotificationUtil {
|
|||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPauseIntent(context: Context, groupId: Int): PendingIntent {
|
|
||||||
val intent = Intent(context, DownloadPauseReceiver::class.java)
|
|
||||||
intent.putExtra(Constants.FETCH_GROUP_ID, groupId)
|
|
||||||
val flags = if (isMAndAbove())
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
else PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
return PendingIntent.getBroadcast(context, groupId, intent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getResumeIntent(context: Context, groupId: Int): PendingIntent {
|
|
||||||
val intent = Intent(context, DownloadResumeReceiver::class.java)
|
|
||||||
intent.putExtra(Constants.FETCH_GROUP_ID, groupId)
|
|
||||||
val flags = if (isMAndAbove())
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
else PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
return PendingIntent.getBroadcast(context, groupId, intent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCancelIntent(context: Context, groupId: Int): PendingIntent {
|
|
||||||
val intent = Intent(context, DownloadCancelReceiver::class.java)
|
|
||||||
intent.putExtra(Constants.FETCH_GROUP_ID, groupId)
|
|
||||||
val flags = if (isMAndAbove())
|
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
else PendingIntent.FLAG_CANCEL_CURRENT
|
|
||||||
return PendingIntent.getBroadcast(context, groupId, intent, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getContentIntentForDetails(context: Context, packageName: String): PendingIntent {
|
private fun getContentIntentForDetails(context: Context, packageName: String): PendingIntent {
|
||||||
return NavDeepLinkBuilder(context)
|
return NavDeepLinkBuilder(context)
|
||||||
.setGraph(R.navigation.mobile_navigation)
|
.setGraph(R.navigation.mobile_navigation)
|
||||||
|
|||||||
@@ -29,17 +29,12 @@ import androidx.navigation.fragment.navArgs
|
|||||||
import com.aurora.extensions.copyToClipBoard
|
import com.aurora.extensions.copyToClipBoard
|
||||||
import com.aurora.extensions.toast
|
import com.aurora.extensions.toast
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.data.downloader.DownloadManager
|
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
|
||||||
import com.aurora.store.databinding.SheetDownloadMenuBinding
|
import com.aurora.store.databinding.SheetDownloadMenuBinding
|
||||||
import com.aurora.store.util.DownloadWorkerUtil
|
import com.aurora.store.util.DownloadWorkerUtil
|
||||||
import com.tonyodev.fetch2.Fetch
|
|
||||||
import com.tonyodev.fetch2.Status
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DownloadMenuSheet : BaseBottomSheet() {
|
class DownloadMenuSheet : BaseBottomSheet() {
|
||||||
|
|||||||
Reference in New Issue
Block a user