From 4af47e8de0349c502267f80fc7c958d874bd5a86 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sun, 24 Oct 2021 13:13:03 -0700 Subject: [PATCH 1/2] Remove lifecycleOwner parameter from DataModel --- .../adapter/ExpandableAppListAdapter.kt | 6 +- .../com/vanced/manager/model/ButtonTag.kt | 9 ++- .../com/vanced/manager/model/DataModel.kt | 77 ++++++------------- .../com/vanced/manager/model/RootDataModel.kt | 3 +- 4 files changed, 33 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt b/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt index 7a4a9545..0495a011 100644 --- a/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt +++ b/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt @@ -88,6 +88,7 @@ class ExpandableAppListAdapter( apps[position] ) } + appDownload.setIconResource(buttonTag.image) contentDescription = activity.getString( when (buttonTag) { ButtonTag.UPDATE -> R.string.accessibility_update @@ -108,11 +109,6 @@ class ExpandableAppListAdapter( dataModel?.installedVersionName?.observe(activity) { appVersionInstalled.text = it } - dataModel?.buttonImage?.observe(activity) { - if (it != null) { - appDownload.icon = it - } - } } } } diff --git a/app/src/main/java/com/vanced/manager/model/ButtonTag.kt b/app/src/main/java/com/vanced/manager/model/ButtonTag.kt index 602c5088..dd6b503c 100644 --- a/app/src/main/java/com/vanced/manager/model/ButtonTag.kt +++ b/app/src/main/java/com/vanced/manager/model/ButtonTag.kt @@ -1,5 +1,10 @@ package com.vanced.manager.model -enum class ButtonTag { - INSTALL, UPDATE, REINSTALL +import androidx.annotation.DrawableRes +import com.vanced.manager.R + +enum class ButtonTag(@DrawableRes val image: Int) { + INSTALL(R.drawable.ic_app_download), + UPDATE(R.drawable.ic_app_update), + REINSTALL(R.drawable.ic_app_reinstall) } \ No newline at end of file diff --git a/app/src/main/java/com/vanced/manager/model/DataModel.kt b/app/src/main/java/com/vanced/manager/model/DataModel.kt index 64018dc7..c36b1413 100644 --- a/app/src/main/java/com/vanced/manager/model/DataModel.kt +++ b/app/src/main/java/com/vanced/manager/model/DataModel.kt @@ -5,63 +5,46 @@ import android.graphics.drawable.Drawable import android.os.Build import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.* import com.beust.klaxon.JsonObject import com.vanced.manager.R +import com.vanced.manager.core.CombinedLiveData import com.vanced.manager.utils.PackageHelper.isPackageInstalled open class DataModel( - private val jsonObject: LiveData, - private val context: Context, - lifecycleOwner: LifecycleOwner, + jsonObject: LiveData, + context: Context, val appPkg: String, val appName: String, val appDescription: String, @DrawableRes val appIcon: Int ) { - private val versionCode = MutableLiveData() - private val installedVersionCode = MutableLiveData() + val isAppInstalled = Transformations.map(jsonObject) { isAppInstalled(appPkg) } + + private val versionCode = Transformations.map(jsonObject) { jobj -> + jobj?.int("versionCode") ?: 0 + } + private val installedVersionCode = Transformations.map(isAppInstalled) { + getPkgVersionCode(appPkg, it) + } private val unavailable = context.getString(R.string.unavailable) private val pm = context.packageManager - val isAppInstalled = MutableLiveData() - val versionName = MutableLiveData() - val installedVersionName = MutableLiveData() - val buttonTag = MutableLiveData() - val buttonImage = MutableLiveData() - val changelog = MutableLiveData() - - private fun fetch() { - val jobj = jsonObject.value - isAppInstalled.value = isAppInstalled(appPkg) - versionCode.value = jobj?.int("versionCode") ?: 0 - versionName.value = jobj?.string("version") ?: unavailable - changelog.value = jobj?.string("changelog") ?: unavailable + val versionName = Transformations.map(jsonObject) { jobj -> + jobj?.string("version") ?: unavailable + } + val changelog = Transformations.map(jsonObject) { jobj -> + jobj?.string("changelog") ?: unavailable + } + val installedVersionName = Transformations.map(isAppInstalled) { + getPkgVersionName(appPkg, it) + } + val buttonTag = CombinedLiveData(versionCode, installedVersionCode) { versionCode, installedVersionCode -> + compareInt(installedVersionCode, versionCode) } - init { - fetch() - with(lifecycleOwner) { - jsonObject.observe(this) { - fetch() - } - isAppInstalled.observe(this) { - installedVersionCode.value = getPkgVersionCode(appPkg, it) - installedVersionName.value = getPkgVersionName(appPkg, it) - } - versionCode.observe(this) { versionCode -> - installedVersionCode.observe(this) { installedVersionCode -> - buttonTag.value = compareInt(installedVersionCode, versionCode) - buttonImage.value = compareIntDrawable(installedVersionCode, versionCode) - } - } - } - } - - open fun isAppInstalled(pkg: String): Boolean = isPackageInstalled(pkg, context.packageManager) + open fun isAppInstalled(pkg: String): Boolean = isPackageInstalled(pkg, pm) private fun getPkgVersionName(pkg: String, isAppInstalled: Boolean): String { return if (isAppInstalled) { @@ -95,16 +78,4 @@ open class DataModel( } return ButtonTag.INSTALL } - - private fun compareIntDrawable(int1: Int?, int2: Int?): Drawable { - if (int2 != null && int1 != null) { - return when { - int1 == 0 -> ContextCompat.getDrawable(context, R.drawable.ic_app_download)!! - int2 > int1 -> ContextCompat.getDrawable(context, R.drawable.ic_app_update)!! - int1 >= int2 -> ContextCompat.getDrawable(context, R.drawable.ic_app_reinstall)!! - else -> ContextCompat.getDrawable(context, R.drawable.ic_app_download)!! - } - } - return ContextCompat.getDrawable(context, R.drawable.ic_app_download)!! - } } \ No newline at end of file diff --git a/app/src/main/java/com/vanced/manager/model/RootDataModel.kt b/app/src/main/java/com/vanced/manager/model/RootDataModel.kt index fc2cc4e8..c4127cb7 100644 --- a/app/src/main/java/com/vanced/manager/model/RootDataModel.kt +++ b/app/src/main/java/com/vanced/manager/model/RootDataModel.kt @@ -10,7 +10,6 @@ import com.vanced.manager.utils.PackageHelper class RootDataModel( jsonObject: LiveData, context: Context, - lifecycleOwner: LifecycleOwner, appPkg: String, appName: String, appDescription: String, @@ -22,7 +21,7 @@ class RootDataModel( //Ironic, isn't it? private val scriptName: String? ) : DataModel( - jsonObject, context, lifecycleOwner, appPkg, appName, appDescription, appIcon + jsonObject, context, appPkg, appName, appDescription, appIcon ) { override fun isAppInstalled(pkg: String): Boolean { From 368808de0c79bee1291854eab08d7cfe4ad2430e Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Sun, 24 Oct 2021 21:37:31 -0700 Subject: [PATCH 2/2] Remove viewmodel memory leak --- .../adapter/ExpandableAppListAdapter.kt | 1 + .../com/vanced/manager/adapter/LinkAdapter.kt | 2 +- .../vanced/manager/adapter/SponsorAdapter.kt | 2 +- .../vanced/manager/core/CombinedLiveData.kt | 37 ++++++++ .../manager/ui/fragments/HomeFragment.kt | 5 +- .../manager/ui/viewmodels/HomeViewModel.kt | 90 +++++++++---------- .../ui/viewmodels/HomeViewModelFactory.kt | 14 --- .../com/vanced/manager/utils/Extensions.kt | 9 +- 8 files changed, 90 insertions(+), 70 deletions(-) create mode 100644 app/src/main/java/com/vanced/manager/core/CombinedLiveData.kt delete mode 100644 app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModelFactory.kt diff --git a/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt b/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt index 0495a011..9b82dc01 100644 --- a/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt +++ b/app/src/main/java/com/vanced/manager/adapter/ExpandableAppListAdapter.kt @@ -84,6 +84,7 @@ class ExpandableAppListAdapter( appDownload.apply { setOnClickListener { viewModel.openInstallDialog( + activity.supportFragmentManager, buttonTag, apps[position] ) diff --git a/app/src/main/java/com/vanced/manager/adapter/LinkAdapter.kt b/app/src/main/java/com/vanced/manager/adapter/LinkAdapter.kt index a123965d..21253b1e 100644 --- a/app/src/main/java/com/vanced/manager/adapter/LinkAdapter.kt +++ b/app/src/main/java/com/vanced/manager/adapter/LinkAdapter.kt @@ -64,7 +64,7 @@ class LinkAdapter( fun bind(position: Int) { binding.linkBg.setOnClickListener { - viewModel.openUrl(links[position].linkUrl) + viewModel.openUrl(context, links[position].linkUrl) } } } diff --git a/app/src/main/java/com/vanced/manager/adapter/SponsorAdapter.kt b/app/src/main/java/com/vanced/manager/adapter/SponsorAdapter.kt index b93bc5f8..1d16dd10 100644 --- a/app/src/main/java/com/vanced/manager/adapter/SponsorAdapter.kt +++ b/app/src/main/java/com/vanced/manager/adapter/SponsorAdapter.kt @@ -42,7 +42,7 @@ class SponsorAdapter( with(binding) { sponsorName.text = sponsors[position].name cardSponsor.setOnClickListener { - viewModel.openUrl(sponsors[position].url) + viewModel.openUrl(context, sponsors[position].url) } } } diff --git a/app/src/main/java/com/vanced/manager/core/CombinedLiveData.kt b/app/src/main/java/com/vanced/manager/core/CombinedLiveData.kt new file mode 100644 index 00000000..8eea46cc --- /dev/null +++ b/app/src/main/java/com/vanced/manager/core/CombinedLiveData.kt @@ -0,0 +1,37 @@ +package com.vanced.manager.core + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData + +/** + * CombinedLiveData is a helper class to combine results from two LiveData sources. + * @param combine Function reference that will be used to combine all LiveData data. + * @param R The type of data returned after combining all LiveData data. + * Usage: + * CombinedLiveData( + * getLiveData1(), + * getLiveData2() + * ) { data1, data2 -> + * // Use datas[0], datas[1], ..., datas[N] to return a value + * } + */ +class CombinedLiveData( + liveDataA: LiveData, + liveDataB: LiveData, + private val combine: (a: A?, b: B?) -> R +) : MediatorLiveData() { + + private var a: A? = null + private var b: B? = null + + init { + addSource(liveDataA) { + a = it + value = combine(a, b) + } + addSource(liveDataB) { + b = it + value = combine(a, b) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vanced/manager/ui/fragments/HomeFragment.kt b/app/src/main/java/com/vanced/manager/ui/fragments/HomeFragment.kt index b434c083..66e55237 100644 --- a/app/src/main/java/com/vanced/manager/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/vanced/manager/ui/fragments/HomeFragment.kt @@ -26,7 +26,6 @@ import com.vanced.manager.databinding.FragmentHomeBinding import com.vanced.manager.ui.dialogs.AppInfoDialog import com.vanced.manager.ui.dialogs.DialogContainer.installAlertBuilder import com.vanced.manager.ui.viewmodels.HomeViewModel -import com.vanced.manager.ui.viewmodels.HomeViewModelFactory import com.vanced.manager.utils.isFetching import com.vanced.manager.utils.manager @@ -37,9 +36,7 @@ class HomeFragment : BindingFragment() { const val REFRESH_HOME = "REFRESH_HOME" } - private val viewModel: HomeViewModel by viewModels { - HomeViewModelFactory(requireActivity()) - } + private val viewModel: HomeViewModel by viewModels() private val localBroadcastManager by lazy { LocalBroadcastManager.getInstance(requireActivity()) } diff --git a/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt index 63fd3b6f..1ce27d98 100644 --- a/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt @@ -1,13 +1,14 @@ package com.vanced.manager.ui.viewmodels -import android.annotation.SuppressLint +import android.app.Application import android.content.ActivityNotFoundException import android.content.ComponentName +import android.content.Context import android.content.Intent import android.widget.Toast -import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager.getDefaultSharedPreferences import com.vanced.manager.R @@ -38,12 +39,11 @@ import com.vanced.manager.utils.PackageHelper.uninstallRootApk import com.vanced.manager.utils.PackageHelper.vancedInstallFilesExist import kotlinx.coroutines.launch -//TODO fix leak -@SuppressLint("StaticFieldLeak") -class HomeViewModel(private val activity: FragmentActivity) : ViewModel() { +class HomeViewModel(application: Application) : AndroidViewModel(application) { - private val prefs = getDefaultSharedPreferences(activity) + private val prefs = getDefaultSharedPreferences(context) private val variant get() = prefs.getString("vanced_variant", "nonroot") + private val context: Context get() = getApplication() val vancedModel = MutableLiveData() val vancedRootModel = MutableLiveData() @@ -54,13 +54,13 @@ class HomeViewModel(private val activity: FragmentActivity) : ViewModel() { fun fetchData() { viewModelScope.launch { - loadJson(activity) + loadJson(context) } } - private val microgToast = Toast.makeText(activity, R.string.no_microg, Toast.LENGTH_LONG) + private val microgToast = Toast.makeText(context, R.string.no_microg, Toast.LENGTH_LONG) - fun openUrl(url: String) { + fun openUrl(context: Context, url: String) { val color: Int = when (url) { DISCORD -> R.color.Discord @@ -71,82 +71,82 @@ class HomeViewModel(private val activity: FragmentActivity) : ViewModel() { else -> R.color.Vanced } - openUrl(url, color, activity) + openUrl(url, color, context) } fun launchApp(app: String, isRoot: Boolean) { val componentName = when (app) { - activity.getString(R.string.vanced) -> if (isRoot) ComponentName( + context.getString(R.string.vanced) -> if (isRoot) ComponentName( vancedRootPkg, "$vancedRootPkg.HomeActivity" ) else ComponentName(vancedPkg, "$vancedRootPkg.HomeActivity") - activity.getString(R.string.music) -> if (isRoot) ComponentName( + context.getString(R.string.music) -> if (isRoot) ComponentName( musicRootPkg, "$musicRootPkg.activities.MusicActivity" ) else ComponentName(musicPkg, "$musicRootPkg.activities.MusicActivity") - activity.getString(R.string.microg) -> ComponentName( + context.getString(R.string.microg) -> ComponentName( microgPkg, "org.microg.gms.ui.SettingsActivity" ) else -> throw IllegalArgumentException("Can't open this app") } try { - activity.startActivity(Intent().setComponent(componentName)) + context.startActivity(Intent().setComponent(componentName)) } catch (e: ActivityNotFoundException) { log("VMHMV", e.toString()) } } - fun openInstallDialog(buttonTag: ButtonTag?, app: String) { - if (variant == "nonroot" && app != activity.getString(R.string.microg) && !microgModel.value?.isAppInstalled?.value!!) { + fun openInstallDialog(fragmentManager: FragmentManager, buttonTag: ButtonTag?, app: String) { + if (variant == "nonroot" && app != context.getString(R.string.microg) && !microgModel.value?.isAppInstalled?.value!!) { microgToast.show() return } if (buttonTag == ButtonTag.UPDATE) { when (app) { - activity.getString(R.string.vanced) -> VancedPreferencesDialog().show(activity) - activity.getString(R.string.music) -> MusicPreferencesDialog().show(activity) - else -> AppDownloadDialog.newInstance(app).show(activity) + context.getString(R.string.vanced) -> VancedPreferencesDialog().show(fragmentManager) + context.getString(R.string.music) -> MusicPreferencesDialog().show(fragmentManager) + else -> AppDownloadDialog.newInstance(app).show(fragmentManager) } return } when (app) { - activity.getString(R.string.vanced) -> { + context.getString(R.string.vanced) -> { when (variant) { "nonroot" -> { - if (vancedInstallFilesExist(activity)) { - InstallationFilesDetectedDialog.newInstance(app).show(activity) + if (vancedInstallFilesExist(context)) { + InstallationFilesDetectedDialog.newInstance(app).show(fragmentManager) } else { - VancedPreferencesDialog().show(activity) + VancedPreferencesDialog().show(fragmentManager) } } "root" -> { - VancedPreferencesDialog().show(activity) + VancedPreferencesDialog().show(fragmentManager) } } } - activity.getString(R.string.music) -> { + context.getString(R.string.music) -> { when (variant) { "nonroot" -> { - if (musicApkExists(activity)) { - InstallationFilesDetectedDialog.newInstance(app).show(activity) + if (musicApkExists(context)) { + InstallationFilesDetectedDialog.newInstance(app).show(fragmentManager) } else { - MusicPreferencesDialog().show(activity) + MusicPreferencesDialog().show(fragmentManager) } } "root" -> { - MusicPreferencesDialog().show(activity) + MusicPreferencesDialog().show(fragmentManager) } } } - activity.getString(R.string.microg) -> { - if (apkExist(activity, "microg.apk")) { - InstallationFilesDetectedDialog.newInstance(app).show(activity) + context.getString(R.string.microg) -> { + if (apkExist(context, "microg.apk")) { + InstallationFilesDetectedDialog.newInstance(app).show(fragmentManager) } else { - AppDownloadDialog.newInstance(app).show(activity) + AppDownloadDialog.newInstance(app).show(fragmentManager) } } } @@ -155,32 +155,30 @@ class HomeViewModel(private val activity: FragmentActivity) : ViewModel() { fun uninstallPackage(pkg: String) { if (variant == "root" && uninstallRootApk(pkg)) { - viewModelScope.launch { loadJson(activity) } + viewModelScope.launch { loadJson(context) } } else { - uninstallApk(pkg, activity) + uninstallApk(pkg, context) } } init { - with(activity) { + with(context) { if (variant == "root") { vancedRootModel.value = RootDataModel( vanced, this, - this, vancedRootPkg, this.getString(R.string.vanced), - activity.getString(R.string.description_vanced), + this.getString(R.string.description_vanced), R.drawable.ic_vanced, "vanced" ) musicRootModel.value = RootDataModel( music, this, - this, musicRootPkg, this.getString(R.string.music), - activity.getString(R.string.description_vanced_music), + this.getString(R.string.description_vanced_music), R.drawable.ic_music, "music" ) @@ -188,35 +186,31 @@ class HomeViewModel(private val activity: FragmentActivity) : ViewModel() { vancedModel.value = DataModel( vanced, this, - this, vancedPkg, this.getString(R.string.vanced), - activity.getString(R.string.description_vanced), + this.getString(R.string.description_vanced), R.drawable.ic_vanced ) musicModel.value = DataModel( music, this, - this, musicPkg, this.getString(R.string.music), - activity.getString(R.string.description_vanced_music), + this.getString(R.string.description_vanced_music), R.drawable.ic_music ) microgModel.value = DataModel( microg, this, - this, microgPkg, this.getString(R.string.microg), - activity.getString(R.string.description_microg), + this.getString(R.string.description_microg), R.drawable.ic_microg ) } managerModel.value = DataModel( manager, this, - this, managerPkg, this.getString(R.string.app_name), "Just manager meh", diff --git a/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModelFactory.kt b/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModelFactory.kt deleted file mode 100644 index ef7e8d16..00000000 --- a/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModelFactory.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.vanced.manager.ui.viewmodels - -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -class HomeViewModelFactory(private val activity: FragmentActivity) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return HomeViewModel(activity) as T - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/vanced/manager/utils/Extensions.kt b/app/src/main/java/com/vanced/manager/utils/Extensions.kt index 4d46d33e..ca6e5ac8 100644 --- a/app/src/main/java/com/vanced/manager/utils/Extensions.kt +++ b/app/src/main/java/com/vanced/manager/utils/Extensions.kt @@ -7,6 +7,7 @@ import android.widget.RadioGroup import androidx.core.graphics.ColorUtils import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.progressindicator.LinearProgressIndicator @@ -22,14 +23,18 @@ val RadioGroup.checkedButtonTag: String? checkedRadioButtonId )?.tag?.toString() -fun DialogFragment.show(activity: FragmentActivity) { +fun DialogFragment.show(fragmentManager: FragmentManager) { try { - show(activity.supportFragmentManager, "") + show(fragmentManager, "") } catch (e: Exception) { log("VMUI", e.stackTraceToString()) } } +fun DialogFragment.show(activity: FragmentActivity) { + show(activity.supportFragmentManager) +} + fun List.convertToAppVersions(): List = listOf("latest") + reversed() fun String.formatVersion(context: Context): String =