AppDetailsFragment: Download apps using DownloadWorker

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-10-28 18:29:17 +05:30
parent cbeda8cc9f
commit f4226ac9ef
2 changed files with 102 additions and 325 deletions

View File

@@ -129,4 +129,8 @@ class ActionButton : RelativeLayout {
fun addOnClickListener(onClickListener: OnClickListener?) { fun addOnClickListener(onClickListener: OnClickListener?) {
B.btn.setOnClickListener(onClickListener) B.btn.setOnClickListener(onClickListener)
} }
fun addOnLongClickListener(onLongClickListener: OnLongClickListener?) {
B.btn.setOnLongClickListener(onLongClickListener)
}
} }

View File

@@ -22,15 +22,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.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.IBinder
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
@@ -44,6 +41,8 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.work.WorkInfo
import androidx.work.WorkManager
import coil.load import coil.load
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.epoxy.EpoxyRecyclerView
@@ -65,15 +64,13 @@ import com.aurora.gplayapi.data.models.StreamCluster
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.ViewState import com.aurora.store.data.ViewState
import com.aurora.store.data.downloader.DownloadManager
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.installer.RootInstaller import com.aurora.store.data.installer.RootInstaller
import com.aurora.store.data.model.DownloadStatus
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.work.DownloadWorker
import com.aurora.store.data.service.UpdateService
import com.aurora.store.databinding.FragmentDetailsBinding import com.aurora.store.databinding.FragmentDetailsBinding
import com.aurora.store.databinding.LayoutDetailsBetaBinding import com.aurora.store.databinding.LayoutDetailsBetaBinding
import com.aurora.store.databinding.LayoutDetailsDescriptionBinding import com.aurora.store.databinding.LayoutDetailsDescriptionBinding
@@ -81,7 +78,6 @@ import com.aurora.store.databinding.LayoutDetailsDevBinding
import com.aurora.store.databinding.LayoutDetailsPermissionsBinding import com.aurora.store.databinding.LayoutDetailsPermissionsBinding
import com.aurora.store.databinding.LayoutDetailsReviewBinding import com.aurora.store.databinding.LayoutDetailsReviewBinding
import com.aurora.store.util.CommonUtil import com.aurora.store.util.CommonUtil
import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.aurora.store.util.PathUtil import com.aurora.store.util.PathUtil
import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences
@@ -98,14 +94,6 @@ import com.aurora.store.viewmodel.details.DetailsClusterViewModel
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.FetchGroupListener
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@@ -113,6 +101,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlin.io.path.pathString
@AndroidEntryPoint @AndroidEntryPoint
class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
@@ -130,64 +119,29 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
private val startForStorageManagerResult = private val startForStorageManagerResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (isRAndAbove() && Environment.isExternalStorageManager()) { if (isRAndAbove() && Environment.isExternalStorageManager()) {
updateApp(app) DownloadWorker.enqueueApp(requireContext(), app)
} else { } else {
toast(R.string.permissions_denied) toast(R.string.permissions_denied)
} }
} }
private val startForPermissions = private val startForPermissions =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) updateApp(app) else toast(R.string.permissions_denied) if (it) {
DownloadWorker.enqueueApp(requireContext(), app)
} else {
toast(R.string.permissions_denied)
}
} }
private lateinit var authData: AuthData private lateinit var authData: AuthData
private lateinit var app: App private lateinit var app: App
private var fetch: Fetch? = null
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 appMetadataListener: AppMetadataStatusListener
private lateinit var completionMarker: File
private lateinit var inProgressMarker: File
private var isExternal = false private var isExternal = false
private var isNone = false private var downloadStatus = DownloadStatus.UNAVAILABLE
private var status = Status.NONE
private var isUpdatable: Boolean = false private var isUpdatable: Boolean = false
private var autoDownload: Boolean = false private var autoDownload: Boolean = false
private var downloadOnly: Boolean = false
private var uninstallActionEnabled = false private var uninstallActionEnabled = false
val listOfActionsWhenServiceAttaches = ArrayList<Runnable>()
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
@@ -283,6 +237,38 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
} }
// Downloads
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
DownloadWorker.cancelDownload(it.context, app)
if (downloadStatus != DownloadStatus.DOWNLOADING) flip(0)
}
val uniqueWorkName = "${DownloadWorker.DOWNLOAD_WORKER}/${app.packageName}"
WorkManager.getInstance(view.context)
.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)
.observe(viewLifecycleOwner) { workList ->
workList.getOrNull(0)?.let {
if (it.state.isFinished) flip(0) else flip(1)
when (it.state) {
WorkInfo.State.ENQUEUED,
WorkInfo.State.RUNNING -> {
downloadStatus = DownloadStatus.DOWNLOADING
updateProgress(
it.progress.getInt(DownloadWorker.DOWNLOAD_PROGRESS, 0),
it.progress.getLong(DownloadWorker.DOWNLOAD_SPEED, -1),
it.progress.getLong(DownloadWorker.DOWNLOAD_TIME, -1)
)
}
WorkInfo.State.FAILED -> downloadStatus = DownloadStatus.FAILED
WorkInfo.State.CANCELLED -> downloadStatus = DownloadStatus.CANCELLED
WorkInfo.State.SUCCEEDED -> downloadStatus = DownloadStatus.COMPLETED
else -> {}
}
}
}
// Reviews // Reviews
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
viewModel.reviews.collect { viewModel.reviews.collect {
@@ -434,7 +420,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
override fun onResume() { override fun onResume() {
getUpdateServiceInstance()
checkAndSetupInstall() checkAndSetupInstall()
super.onResume() super.onResume()
} }
@@ -459,34 +444,24 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
} }
private fun verifyAndInstall(files: List<Download>) {
if (downloadOnly)
return
var filesExist = true
files.forEach { download ->
filesExist = filesExist && File(download.file).exists()
}
if (filesExist)
install(files)
else
purchase()
}
@Synchronized @Synchronized
private fun install(files: List<Download>) { private fun install() {
updateActionState(State.IDLE) val files = File(
PathUtil.getAppDownloadDir(
requireContext(),
app.packageName,
app.versionCode
).pathString
).listFiles() ?: return
val apkFiles = files.filter { it.file.endsWith(".apk") } val apkFiles = files.filter { it.path.endsWith(".apk") }
val preferredInstaller = val preferredInstaller =
Preferences.getInteger(requireContext(), Preferences.PREFERENCE_INSTALLER_ID) Preferences.getInteger(requireContext(), Preferences.PREFERENCE_INSTALLER_ID)
if (apkFiles.size > 1 && preferredInstaller == 1) { if (apkFiles.size > 1 && preferredInstaller == 1) {
showDialog(R.string.title_installer, R.string.dialog_desc_native_split) showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
} else { } else {
viewModel.install(requireContext(), app.packageName, apkFiles.map { it.file }) viewModel.install(requireContext(), app.packageName, apkFiles)
runOnUiThread { runOnUiThread {
binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
@@ -515,16 +490,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
} }
private fun attachWhiteListStatus() {
}
private fun inflatePartialApp() { private fun inflatePartialApp() {
if (::app.isInitialized) { if (::app.isInitialized) {
attachWhiteListStatus()
attachHeader() attachHeader()
attachBottomSheet() attachBottomSheet()
attachFetch()
attachActions() attachActions()
if (autoDownload) { if (autoDownload) {
@@ -592,21 +561,29 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
@Synchronized @Synchronized
private fun startDownload() { private fun startDownload(forceDownload: Boolean = false) {
when (status) { when (downloadStatus) {
Status.PAUSED -> { DownloadStatus.DOWNLOADING -> {
fetch?.resumeGroup(app.getGroupId(requireContext()))
}
Status.DOWNLOADING -> {
flip(1) flip(1)
toast("Already downloading") toast("Already downloading")
} }
Status.COMPLETED -> { DownloadStatus.COMPLETED -> {
fetch?.getFetchGroup(app.getGroupId(requireContext())) { if (forceDownload) {
verifyAndInstall(it.downloads) purchase()
return
} }
// Check if files are still present, else proceed to re-download the app
val files = File(
PathUtil.getAppDownloadDir(
requireContext(),
app.packageName,
app.versionCode
).pathString
).listFiles()
if (files?.isNotEmpty() == true) install() else purchase()
} }
else -> { else -> {
@@ -627,7 +604,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
) )
} else { } else {
updateApp(app) DownloadWorker.enqueueApp(requireContext(), app)
} }
} else { } else {
if (ContextCompat.checkSelfPermission( if (ContextCompat.checkSelfPermission(
@@ -635,42 +612,23 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) { ) {
updateApp(app) DownloadWorker.enqueueApp(requireContext(), app)
} else { } else {
startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
} }
} }
} else { } else {
updateApp(app) DownloadWorker.enqueueApp(requireContext(), app)
} }
} }
private fun updateApp(app: App) { private fun updateProgress(progress: Int, speed: Long = -1, timeRemaining: Long = -1) {
if (updateService == null) {
listOfActionsWhenServiceAttaches.add {
updateService?.updateApp(app, true)
}
getUpdateServiceInstance()
} else {
updateService?.updateApp(app, true)
}
}
private fun updateProgress(
fetchGroup: FetchGroup,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) {
runOnUiThread { runOnUiThread {
val progress = if (fetchGroup.groupDownloadProgress > 0)
fetchGroup.groupDownloadProgress
else
0
if (progress == 100) { if (progress == 100) {
binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing)) binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
return@runOnUiThread return@runOnUiThread
} }
binding.layoutDetailsInstall.apply { binding.layoutDetailsInstall.apply {
txtProgressPercent.text = ("${progress}%") txtProgressPercent.text = ("${progress}%")
@@ -680,31 +638,8 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
progressDownload.progress = progress progressDownload.progress = progress
} }
txtEta.text = CommonUtil.getETAString( txtEta.text = CommonUtil.getETAString(requireContext(), timeRemaining)
requireContext(), txtSpeed.text = CommonUtil.getDownloadSpeedString(requireContext(), speed)
etaInMilliSeconds
)
txtSpeed.text =
CommonUtil.getDownloadSpeedString(
requireContext(),
downloadedBytesPerSecond
)
}
}
}
private fun expandBottomSheet(message: String?) {
runOnUiThread {
bottomSheetBehavior.isHideable = false
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
with(binding.layoutDetailsInstall) {
txtPurchaseError.text = message
btnDownload.updateState(State.IDLE)
if (app.isFree)
btnDownload.setText(R.string.action_install)
else
btnDownload.setText(app.price)
} }
} }
} }
@@ -713,6 +648,8 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName) app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
runOnUiThread { runOnUiThread {
app.isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
binding.layoutDetailsInstall.btnDownload.let { btn -> binding.layoutDetailsInstall.btnDownload.let { btn ->
if (app.isInstalled) { if (app.isInstalled) {
isUpdatable = PackageUtil.isUpdatable( isUpdatable = PackageUtil.isUpdatable(
@@ -729,6 +666,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
("$installedVersion${app.versionName} (${app.versionCode})") ("$installedVersion${app.versionName} (${app.versionCode})")
btn.setText(R.string.action_update) btn.setText(R.string.action_update)
btn.addOnClickListener { startDownload() } btn.addOnClickListener { startDownload() }
btn.addOnLongClickListener {
startDownload(true)
true
}
} else { } else {
binding.layoutDetailsApp.txtLine3.text = installedVersion binding.layoutDetailsApp.txtLine3.text = installedVersion
btn.setText(R.string.action_open) btn.setText(R.string.action_open)
@@ -738,7 +679,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
binding.layoutDetailsToolbar.toolbar.invalidateMenu() binding.layoutDetailsToolbar.toolbar.invalidateMenu()
} }
} else { } else {
if (app.isFree) { val downloading = downloadStatus == DownloadStatus.DOWNLOADING
if (DownloadWorker.isEnqueued(app.packageName) && !downloading) {
flip(1)
} else if (app.isFree) {
btn.setText(R.string.action_install) btn.setText(R.string.action_install)
} else { } else {
btn.setText(app.price) btn.setText(app.price)
@@ -752,6 +696,15 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
startDownload() startDownload()
} }
} }
btn.addOnLongClickListener {
if (authData.isAnonymous && !app.isFree) {
toast(R.string.toast_purchase_blocked)
} else {
btn.setText(R.string.download_metadata)
startDownload(true)
}
true
}
if (uninstallActionEnabled) { if (uninstallActionEnabled) {
binding.layoutDetailsToolbar.toolbar.invalidateMenu() binding.layoutDetailsToolbar.toolbar.invalidateMenu()
} }
@@ -771,186 +724,6 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
} }
} }
private fun attachFetch() {
if (fetch == null) {
downloadManager = DownloadManager.with(requireContext())
fetch = downloadManager!!.fetch
}
fetch?.getFetchGroup(app.getGroupId(requireContext())) { fetchGroup: FetchGroup ->
if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
status = Status.COMPLETED
} else if (downloadManager?.isDownloading(fetchGroup) == true) {
status = Status.DOWNLOADING
flip(1)
} else if (downloadManager?.isCanceled(fetchGroup) == true) {
status = Status.CANCELLED
} else if (fetchGroup.pausedDownloads.isNotEmpty()) {
status = Status.PAUSED
} else {
status = Status.NONE
}
}
fetchGroupListener = object : AbstractFetchGroupListener() {
override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
}
}
override fun onStarted(
groupId: Int,
download: Download,
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int,
fetchGroup: FetchGroup
) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(1)
val pkgDir = PathUtil.getPackageDirectory(requireContext(), app.packageName)
completionMarker =
File("$pkgDir/.${app.versionCode}.download-complete")
inProgressMarker =
File("$pkgDir/.${app.versionCode}.download-in-progress")
if (completionMarker.exists())
completionMarker.delete()
inProgressMarker.createNewFile()
}
}
override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(1)
inProgressMarker.parentFile?.mkdirs()
inProgressMarker.createNewFile()
}
}
override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(0)
}
}
override fun onProgress(
groupId: Int,
download: Download,
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long,
fetchGroup: FetchGroup
) {
if (groupId == app.getGroupId(requireContext())) {
updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond)
Log.i(
"${app.displayName} : ${download.file} -> Progress : %d",
fetchGroup.groupDownloadProgress
)
}
}
override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext()) && fetchGroup.groupDownloadProgress == 100) {
status = download.status
flip(0)
updateProgress(fetchGroup, -1, -1)
try {
inProgressMarker.delete()
completionMarker.createNewFile()
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(0)
inProgressMarker.delete()
}
}
override fun onError(
groupId: Int,
download: Download,
error: Error,
throwable: Throwable?,
fetchGroup: FetchGroup
) {
if (groupId == app.getGroupId(requireContext())) {
status = download.status
flip(0)
inProgressMarker.delete()
}
}
}
appMetadataListener = object : AppMetadataStatusListener {
override fun onAppMetadataStatusError(reason: String, app: App) {
if (app.packageName == this@AppDetailsFragment.app.packageName) {
updateActionState(State.IDLE)
expandBottomSheet(reason)
}
}
}
getUpdateServiceInstance()
binding.layoutDetailsInstall.imgCancel.setOnClickListener {
fetch?.cancelGroup(
app.getGroupId(requireContext())
)
}
if (updateService != null) {
pendingAddListeners = false
updateService!!.registerFetchListener(fetchGroupListener)
// appMetadataListener needs to be initialized after the fetchGroupListener
updateService!!.registerAppMetadataListener(appMetadataListener)
} else {
pendingAddListeners = true
}
}
private fun getUpdateServiceInstance() {
if (updateService == null && !attachToServiceCalled) {
attachToServiceCalled = true
val intent = Intent(requireContext(), UpdateService::class.java)
activity?.startService(intent)
activity?.bindService(
intent,
serviceConnection,
0
)
}
}
override fun onPause() {
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
activity?.unbindService(serviceConnection)
}
super.onPause()
}
override fun onDestroy() {
super.onDestroy()
if (updateService != null) {
updateService = null
attachToServiceCalled = false
pendingAddListeners = true
activity?.unbindService(serviceConnection)
}
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null