Remove: AppDetailFragment

This commit is contained in:
machiav3lli 2022-05-13 11:10:19 +02:00
parent 1477fba932
commit c1e3d857f3

View File

@ -1,604 +0,0 @@
package com.looker.droidify.ui.fragments
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.looker.droidify.R
import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.entity.Product
import com.looker.droidify.database.entity.Release
import com.looker.droidify.database.entity.Repository
import com.looker.droidify.entity.ProductPreference
import com.looker.droidify.entity.Screenshot
import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.screen.MessageDialog
import com.looker.droidify.screen.ScreenFragment
import com.looker.droidify.screen.ScreenshotsFragment
import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService
import com.looker.droidify.ui.activities.MainActivityX
import com.looker.droidify.ui.adapters.AppDetailAdapter
import com.looker.droidify.utility.RxUtils
import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.Utils.rootInstallerEnabled
import com.looker.droidify.utility.Utils.startUpdate
import com.looker.droidify.utility.extension.android.Android
import com.looker.droidify.utility.extension.text.trimAfter
import com.looker.droidify.utility.findSuggestedProduct
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
private val screenActivity: MainActivityX
get() = requireActivity() as MainActivityX
companion object {
private const val EXTRA_PACKAGE_NAME = "packageName"
private const val STATE_LAYOUT_MANAGER = "layoutManager"
private const val STATE_ADAPTER = "adapter"
}
constructor(packageName: String) : this() {
arguments = Bundle().apply {
putString(EXTRA_PACKAGE_NAME, packageName)
}
}
private class Nullable<T>(val value: T?)
private enum class Action(
val id: Int,
val adapterAction: AppDetailAdapter.Action,
) {
INSTALL(1, AppDetailAdapter.Action.INSTALL),
UPDATE(2, AppDetailAdapter.Action.UPDATE),
LAUNCH(3, AppDetailAdapter.Action.LAUNCH),
DETAILS(4, AppDetailAdapter.Action.DETAILS),
UNINSTALL(5, AppDetailAdapter.Action.UNINSTALL),
SHARE(6, AppDetailAdapter.Action.SHARE)
}
private class Installed(
val data: com.looker.droidify.database.entity.Installed, val isSystem: Boolean,
val launcherActivities: List<Pair<String, String>>,
)
val packageName: String
get() = requireArguments().getString(EXTRA_PACKAGE_NAME)!!
private var layoutManagerState: LinearLayoutManager.SavedState? = null
private var actions = Pair(emptySet<Action>(), null as Action?)
private var products = emptyList<Pair<Product, Repository>>()
private var installed: Installed? = null
private var downloading = false
private var recyclerView: RecyclerView? = null
private var productDisposable: Disposable? = null
private val downloadConnection = Connection(DownloadService::class.java, onBind = { _, binder ->
binder.stateSubject
.filter { it.packageName == packageName }
.flowOn(Dispatchers.Default)
.onEach { updateDownloadState(it) }
.flowOn(Dispatchers.Main)
.launchIn(lifecycleScope)
})
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.menu.apply {
for (action in Action.values()) {
add(0, action.id, 0, action.adapterAction.titleResId)
.setIcon(Utils.getToolbarIcon(toolbar.context, action.adapterAction.iconResId))
.setVisible(false)
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
.setOnMenuItemClickListener {
onActionClick(action.adapterAction)
true
}
}
}
val content = fragmentBinding.fragmentContent
content.addView(RecyclerView(content.context).apply {
id = android.R.id.list
this.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
isMotionEventSplittingEnabled = false
isVerticalScrollBarEnabled = false
val adapter = AppDetailAdapter(this@AppDetailFragment)
this.adapter = adapter
addOnScrollListener(scrollListener)
savedInstanceState?.getParcelable<AppDetailAdapter.SavedState>(STATE_ADAPTER)
?.let(adapter::restoreState)
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
recyclerView = this
})
var first = true
productDisposable = Observable.just(Unit)
//.concatWith(Database.observable(Database.Subject.Products)) // TODO have to be replaced like whole rxJava
.observeOn(Schedulers.io())
.flatMapSingle {
RxUtils.querySingle {
screenActivity.db.productDao.get(packageName).filterNotNull()
}
}
.flatMapSingle { products ->
RxUtils
.querySingle { screenActivity.db.repositoryDao.all }
.map { it ->
it.asSequence().map { Pair(it.id, it) }.toMap()
.let {
products.mapNotNull { product ->
it[product.repositoryId]?.let {
Pair(
product,
it
)
}
}
}
}
}
.flatMapSingle { products ->
RxUtils
.querySingle { Nullable(screenActivity.db.installedDao.get(packageName)) }
.map { Pair(products, it) }
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { it ->
val (products, installedItem) = it
val firstChanged = first
first = false
val productChanged = this.products != products
val installedItemChanged = this.installed?.data != installedItem.value
if (firstChanged || productChanged || installedItemChanged) {
layoutManagerState?.let {
recyclerView?.layoutManager!!.onRestoreInstanceState(
it
)
}
layoutManagerState = null
if (firstChanged || productChanged) {
this.products = products
}
if (firstChanged || installedItemChanged) {
installed = installedItem.value?.let {
val isSystem = try {
((requireContext().packageManager.getApplicationInfo(
packageName,
0
).flags)
and ApplicationInfo.FLAG_SYSTEM) != 0
} catch (e: Exception) {
false
}
val launcherActivities =
if (packageName == requireContext().packageName) {
// Don't allow to launch self
emptyList()
} else {
val packageManager = requireContext().packageManager
packageManager
.queryIntentActivities(
Intent(Intent.ACTION_MAIN).addCategory(
Intent.CATEGORY_LAUNCHER
), 0
)
.asSequence()
.mapNotNull { resolveInfo -> resolveInfo.activityInfo }
.filter { activityInfo -> activityInfo.packageName == packageName }
.mapNotNull { activityInfo ->
val label = try {
activityInfo.loadLabel(packageManager).toString()
} catch (e: Exception) {
e.printStackTrace()
null
}
label?.let { labelName ->
Pair(
activityInfo.name,
labelName
)
}
}
.toList()
}
Installed(it, isSystem, launcherActivities)
}
}
val recyclerView = recyclerView!!
val adapter = recyclerView.adapter as AppDetailAdapter
if (firstChanged || productChanged || installedItemChanged) {
adapter.setProducts(
recyclerView.context,
packageName,
products,
installedItem.value
)
}
lifecycleScope.launch { updateButtons() }
}
}
downloadConnection.bind(requireContext())
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView = null
productDisposable?.dispose()
productDisposable = null
downloadConnection.unbind(requireContext())
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val layoutManagerState =
layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()
layoutManagerState?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) }
val adapterState = (recyclerView?.adapter as? AppDetailAdapter)?.saveState()
adapterState?.let { outState.putParcelable(STATE_ADAPTER, it) }
}
private suspend fun updateButtons() {
updateButtons(ProductPreferences[packageName])
}
private suspend fun updateButtons(preference: ProductPreference) =
withContext(Dispatchers.Default) {
val installed = installed
val product = findSuggestedProduct(products, installed?.data) { it.first }?.first
val compatible = product != null && product.selectedReleases.firstOrNull()
.let { it != null && it.incompatibilities.isEmpty() }
val canInstall = product != null && installed == null && compatible
val canUpdate =
product != null && compatible && product.canUpdate(installed?.data) &&
!preference.shouldIgnoreUpdate(product.versionCode)
val canUninstall = product != null && installed != null && !installed.isSystem
val canLaunch =
product != null && installed != null && installed.launcherActivities.isNotEmpty()
val canShare = product != null && products[0].second.name == "F-Droid"
val actions = mutableSetOf<Action>()
launch {
if (canInstall) {
actions += Action.INSTALL
}
}
launch {
if (canUpdate) {
actions += Action.UPDATE
}
}
launch {
if (canLaunch) {
actions += Action.LAUNCH
}
}
launch {
if (installed != null) {
actions += Action.DETAILS
}
}
launch {
if (canUninstall) {
actions += Action.UNINSTALL
}
}
launch {
if (canShare) {
actions += Action.SHARE
}
}
val primaryAction = when {
canUpdate -> Action.UPDATE
canLaunch -> Action.LAUNCH
canInstall -> Action.INSTALL
installed != null -> Action.DETAILS
canShare -> Action.SHARE
else -> null
}
launch(Dispatchers.Main) {
val adapterAction =
if (downloading) AppDetailAdapter.Action.CANCEL else primaryAction?.adapterAction
(recyclerView?.adapter as? AppDetailAdapter)?.setAction(adapterAction)
for (action in sequenceOf(
Action.INSTALL,
Action.SHARE,
Action.UPDATE,
Action.UNINSTALL
)) {
toolbar.menu.findItem(action.id).isEnabled = !downloading
}
}
launch { this@AppDetailFragment.actions = Pair(actions, primaryAction) }
launch(Dispatchers.Main) { updateToolbarButtons() }
}
private suspend fun updateToolbarTitle() {
withContext(Dispatchers.Main) {
val showPackageName = recyclerView
?.let { (it.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() != 0 } == true
launch {
collapsingToolbar.title =
if (showPackageName) products[0].first.label.trimAfter(' ', 2)
else getString(R.string.application)
}
}
}
private suspend fun updateToolbarButtons() {
withContext(Dispatchers.Default) {
val (actions, primaryAction) = actions
val showPrimaryAction = recyclerView
?.let { (it.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() != 0 } == true
val displayActions = actions.toMutableSet()
launch {
if (!showPrimaryAction && primaryAction != null) {
displayActions -= primaryAction
}
}
launch {
if (displayActions.size >= 4 && resources.configuration.screenWidthDp < 400) {
displayActions -= Action.DETAILS
}
}
launch(Dispatchers.Main) {
for (action in Action.values())
toolbar.menu.findItem(action.id).isVisible = action in displayActions
}
}
}
private suspend fun updateDownloadState(state: DownloadService.State?) {
val status = when (state) {
is DownloadService.State.Pending -> AppDetailAdapter.Status.Pending
is DownloadService.State.Connecting -> AppDetailAdapter.Status.Connecting
is DownloadService.State.Downloading -> AppDetailAdapter.Status.Downloading(
state.read,
state.total
)
is DownloadService.State.Success, is DownloadService.State.Error, is DownloadService.State.Cancel, null -> null
}
val downloading = status != null
if (this.downloading != downloading) {
this.downloading = downloading
updateButtons()
}
(recyclerView?.adapter as? AppDetailAdapter)?.setStatus(status)
if (state is DownloadService.State.Success && isResumed && !rootInstallerEnabled) {
withContext(Dispatchers.Default) {
AppInstaller.getInstance(context)?.defaultInstaller?.install(state.release.cacheFileName)
}
}
}
private val scrollListener = object : RecyclerView.OnScrollListener() {
private var lastPosition = -1
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val position =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastPosition = lastPosition
this.lastPosition = position
if ((lastPosition == 0) != (position == 0)) {
lifecycleScope.launch {
launch { updateToolbarTitle() }
launch { updateToolbarButtons() }
}
}
}
}
override fun onActionClick(action: AppDetailAdapter.Action) {
when (action) {
AppDetailAdapter.Action.INSTALL,
AppDetailAdapter.Action.UPDATE,
-> {
val installedItem = installed?.data
lifecycleScope.launch {
startUpdate(
packageName,
installedItem,
products,
downloadConnection
)
}
Unit
}
AppDetailAdapter.Action.LAUNCH -> {
val launcherActivities = installed?.launcherActivities.orEmpty()
if (launcherActivities.size >= 2) {
LaunchDialog(launcherActivities).show(
childFragmentManager,
LaunchDialog::class.java.name
)
} else {
launcherActivities.firstOrNull()?.let { startLauncherActivity(it.first) }
}
Unit
}
AppDetailAdapter.Action.DETAILS -> {
startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:$packageName"))
)
}
AppDetailAdapter.Action.UNINSTALL -> {
lifecycleScope.launch {
AppInstaller.getInstance(context)?.defaultInstaller?.uninstall(packageName)
}
Unit
}
AppDetailAdapter.Action.CANCEL -> {
val binder = downloadConnection.binder
if (downloading && binder != null) {
binder.cancel(packageName)
} else Unit
}
AppDetailAdapter.Action.SHARE -> {
shareIntent(packageName, products[0].first.label)
}
}::class
}
private fun startLauncherActivity(name: String) {
try {
startActivity(
Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(ComponentName(packageName, name))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun shareIntent(packageName: String, appName: String) {
val shareIntent = Intent(Intent.ACTION_SEND)
val extraText = if (Android.sdk(24)) {
"https://www.f-droid.org/${resources.configuration.locales[0].language}/packages/${packageName}/"
} else "https://www.f-droid.org/${resources.configuration.locale.language}/packages/${packageName}/"
shareIntent.type = "text/plain"
shareIntent.putExtra(Intent.EXTRA_TITLE, appName)
shareIntent.putExtra(Intent.EXTRA_SUBJECT, appName)
shareIntent.putExtra(Intent.EXTRA_TEXT, extraText)
startActivity(Intent.createChooser(shareIntent, "Where to Send?"))
}
override fun onPreferenceChanged(preference: ProductPreference) {
lifecycleScope.launch { updateButtons(preference) }
}
override fun onPermissionsClick(group: String?, permissions: List<String>) {
MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show(
childFragmentManager
)
}
override fun onScreenshotClick(screenshot: Screenshot) {
val pair = products.asSequence()
.map { it ->
Pair(
it.second,
it.first.screenshots.find { it === screenshot }?.identifier
)
}
.filter { it.second != null }.firstOrNull()
if (pair != null) {
val (repository, identifier) = pair
if (identifier != null) {
ScreenshotsFragment(packageName, repository.id, identifier).show(
childFragmentManager
)
}
}
}
override fun onReleaseClick(release: Release) {
val installedItem = installed?.data
when {
release.incompatibilities.isNotEmpty() -> {
MessageDialog(
MessageDialog.Message.ReleaseIncompatible(
release.incompatibilities,
release.platforms, release.minSdkVersion, release.maxSdkVersion
)
).show(childFragmentManager)
}
installedItem != null && installedItem.versionCode > release.versionCode -> {
MessageDialog(MessageDialog.Message.ReleaseOlder).show(childFragmentManager)
}
installedItem != null && installedItem.signature != release.signature -> {
MessageDialog(MessageDialog.Message.ReleaseSignatureMismatch).show(
childFragmentManager
)
}
else -> {
val productRepository =
products.asSequence().filter { it -> it.first.releases.any { it === release } }
.firstOrNull()
if (productRepository != null) {
downloadConnection.binder?.enqueue(
packageName, productRepository.first.label,
productRepository.second, release
)
}
}
}
}
override fun onUriClick(uri: Uri, shouldConfirm: Boolean): Boolean {
return if (shouldConfirm && (uri.scheme == "http" || uri.scheme == "https")) {
MessageDialog(MessageDialog.Message.Link(uri)).show(childFragmentManager)
true
} else {
try {
startActivity(Intent(Intent.ACTION_VIEW, uri))
true
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
false
}
}
}
class LaunchDialog() : DialogFragment() {
companion object {
private const val EXTRA_NAMES = "names"
private const val EXTRA_LABELS = "labels"
}
constructor(launcherActivities: List<Pair<String, String>>) : this() {
arguments = Bundle().apply {
putStringArrayList(EXTRA_NAMES, ArrayList(launcherActivities.map { it.first }))
putStringArrayList(EXTRA_LABELS, ArrayList(launcherActivities.map { it.second }))
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): AlertDialog {
val names = requireArguments().getStringArrayList(EXTRA_NAMES)!!
val labels = requireArguments().getStringArrayList(EXTRA_LABELS)!!
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.launch)
.setItems(labels.toTypedArray()) { _, position ->
(parentFragment as AppDetailFragment)
.startLauncherActivity(names[position])
}
.setNegativeButton(R.string.cancel, null)
.create()
}
}
}