- Screenshots are Expanded by Default

- Reformatted Code
- Added background to Show More Button
- Shortened Tab Indicator...
This commit is contained in:
Mohit 2021-06-08 21:35:46 +05:30
parent 8c0c48236e
commit baf9944dc9
5 changed files with 2719 additions and 2318 deletions

File diff suppressed because it is too large Load Diff

View File

@ -18,26 +18,21 @@ import androidx.fragment.app.DialogFragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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 com.looker.droidify.R
import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.Database
import com.looker.droidify.entity.InstalledItem
import com.looker.droidify.entity.Product
import com.looker.droidify.entity.ProductPreference
import com.looker.droidify.entity.Release
import com.looker.droidify.entity.Repository
import com.looker.droidify.entity.*
import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService
import com.looker.droidify.utility.RxUtils
import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.android.*
import com.looker.droidify.widget.DividerItemDecoration
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
class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
companion object {
private const val EXTRA_PACKAGE_NAME = "packageName"
@ -45,7 +40,7 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
private const val STATE_ADAPTER = "adapter"
}
constructor(packageName: String): this() {
constructor(packageName: String) : this() {
arguments = Bundle().apply {
putString(EXTRA_PACKAGE_NAME, packageName)
}
@ -53,7 +48,11 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
private class Nullable<T>(val value: T?)
private enum class Action(val id: Int, val adapterAction: ProductAdapter.Action, val iconResId: Int) {
private enum class Action(
val id: Int,
val adapterAction: ProductAdapter.Action,
val iconResId: Int
) {
INSTALL(1, ProductAdapter.Action.INSTALL, R.drawable.ic_archive),
UPDATE(2, ProductAdapter.Action.UPDATE, R.drawable.ic_archive),
LAUNCH(3, ProductAdapter.Action.LAUNCH, R.drawable.ic_launch),
@ -61,8 +60,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
UNINSTALL(5, ProductAdapter.Action.UNINSTALL, R.drawable.ic_delete)
}
private class Installed(val installedItem: InstalledItem, val isSystem: Boolean,
val launcherActivities: List<Pair<String, String>>)
private class Installed(
val installedItem: InstalledItem, val isSystem: Boolean,
val launcherActivities: List<Pair<String, String>>
)
val packageName: String
get() = requireArguments().getString(EXTRA_PACKAGE_NAME)!!
@ -87,7 +88,11 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
downloadDisposable = null
})
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment, container, false)
}
@ -122,15 +127,16 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
isVerticalScrollBarEnabled = false
val adapter = ProductAdapter(this@ProductFragment, columns)
this.adapter = adapter
layoutManager.spanSizeLookup = object: GridLayoutManager.SpanSizeLookup() {
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (adapter.requiresGrid(position)) 1 else layoutManager.spanCount
}
}
addOnScrollListener(scrollListener)
addItemDecoration(adapter.gridItemDecoration)
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER)?.let(adapter::restoreState)
// addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER)
?.let(adapter::restoreState)
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
recyclerView = this
}, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
@ -140,22 +146,41 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
.concatWith(Database.observable(Database.Subject.Products))
.observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.ProductAdapter.get(packageName, it) } }
.flatMapSingle { products -> RxUtils
.flatMapSingle { products ->
RxUtils
.querySingle { Database.RepositoryAdapter.getAll(it) }
.map { it.asSequence().map { Pair(it.id, it) }.toMap()
.let { products.mapNotNull { product -> it[product.repositoryId]?.let { Pair(product, it) } } } } }
.flatMapSingle { products -> RxUtils
.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(Database.InstalledAdapter.get(packageName, it)) }
.map { Pair(products, it) } }
.map { Pair(products, it) }
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
.subscribe { it ->
val (products, installedItem) = it
val firstChanged = first
first = false
val productChanged = this.products != products
val installedItemChanged = this.installed?.installedItem != installedItem.value
if (firstChanged || productChanged || installedItemChanged) {
layoutManagerState?.let { recyclerView?.layoutManager!!.onRestoreInstanceState(it) }
layoutManagerState?.let {
recyclerView?.layoutManager!!.onRestoreInstanceState(
it
)
}
layoutManagerState = null
if (firstChanged || productChanged) {
this.products = products
@ -163,19 +188,28 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
if (firstChanged || installedItemChanged) {
installed = installedItem.value?.let {
val isSystem = try {
((requireContext().packageManager.getApplicationInfo(packageName, 0).flags)
((requireContext().packageManager.getApplicationInfo(
packageName,
0
).flags)
and ApplicationInfo.FLAG_SYSTEM) != 0
} catch (e: Exception) {
false
}
val launcherActivities = if (packageName == requireContext().packageName) {
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 { it.activityInfo }.filter { it.packageName == packageName }
.queryIntentActivities(
Intent(Intent.ACTION_MAIN).addCategory(
Intent.CATEGORY_LAUNCHER
), 0
)
.asSequence().mapNotNull { it.activityInfo }
.filter { it.packageName == packageName }
.mapNotNull { activityInfo ->
val label = try {
activityInfo.loadLabel(packageManager).toString()
@ -193,7 +227,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val recyclerView = recyclerView!!
val adapter = recyclerView.adapter as ProductAdapter
if (firstChanged || productChanged || installedItemChanged) {
adapter.setProducts(recyclerView.context, packageName, products, installedItem.value)
adapter.setProducts(
recyclerView.context,
packageName,
products,
installedItem.value
)
}
updateButtons()
}
@ -218,7 +257,8 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val layoutManagerState = layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()
val layoutManagerState =
layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()
layoutManagerState?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) }
val adapterState = (recyclerView?.adapter as? ProductAdapter)?.saveState()
adapterState?.let { outState.putParcelable(STATE_ADAPTER, it) }
@ -234,10 +274,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
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?.installedItem) &&
val canUpdate =
product != null && compatible && product.canUpdate(installed?.installedItem) &&
!preference.shouldIgnoreUpdate(product.versionCode)
val canUninstall = product != null && installed != null && !installed.isSystem
val canLaunch = product != null && installed != null && installed.launcherActivities.isNotEmpty()
val canLaunch =
product != null && installed != null && installed.launcherActivities.isNotEmpty()
val actions = mutableSetOf<Action>()
if (canInstall) {
@ -263,7 +305,8 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
else -> null
}
val adapterAction = if (downloading) ProductAdapter.Action.CANCEL else primaryAction?.adapterAction
val adapterAction =
if (downloading) ProductAdapter.Action.CANCEL else primaryAction?.adapterAction
(recyclerView?.adapter as? ProductAdapter)?.setAction(adapterAction)
val toolbar = toolbar
@ -299,7 +342,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val status = when (state) {
is DownloadService.State.Pending -> ProductAdapter.Status.Pending
is DownloadService.State.Connecting -> ProductAdapter.Status.Connecting
is DownloadService.State.Downloading -> ProductAdapter.Status.Downloading(state.read, state.total)
is DownloadService.State.Downloading -> ProductAdapter.Status.Downloading(
state.read,
state.total
)
is DownloadService.State.Success, is DownloadService.State.Error, is DownloadService.State.Cancel, null -> null
}
val downloading = status != null
@ -314,11 +360,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
}
}
private val scrollListener = object: RecyclerView.OnScrollListener() {
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 position =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastPosition = lastPosition
this.lastPosition = position
if ((lastPosition == 0) != (position == 0)) {
@ -338,36 +385,48 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val release = if (compatibleReleases.size >= 2) {
compatibleReleases
.filter { it.platforms.contains(Android.primaryPlatform) }
.minBy { it.platforms.size }
?: compatibleReleases.minBy { it.platforms.size }
.minByOrNull { it.platforms.size }
?: compatibleReleases.minByOrNull { it.platforms.size }
?: compatibleReleases.firstOrNull()
} else {
compatibleReleases.firstOrNull()
}
val binder = downloadConnection.binder
if (productRepository != null && release != null && binder != null) {
binder.enqueue(packageName, productRepository.first.name, productRepository.second, release)
binder.enqueue(
packageName,
productRepository.first.name,
productRepository.second,
release
)
}
Unit
}
ProductAdapter.Action.LAUNCH -> {
val launcherActivities = installed?.launcherActivities.orEmpty()
if (launcherActivities.size >= 2) {
LaunchDialog(launcherActivities).show(childFragmentManager, LaunchDialog::class.java.name)
LaunchDialog(launcherActivities).show(
childFragmentManager,
LaunchDialog::class.java.name
)
} else {
launcherActivities.firstOrNull()?.let { startLauncherActivity(it.first) }
}
Unit
}
ProductAdapter.Action.DETAILS -> {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:$packageName")))
startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.parse("package:$packageName"))
)
}
ProductAdapter.Action.UNINSTALL -> {
// TODO Handle deprecation
@Suppress("DEPRECATION")
startActivity(Intent(Intent.ACTION_UNINSTALL_PACKAGE)
.setData(Uri.parse("package:$packageName")))
startActivity(
Intent(Intent.ACTION_UNINSTALL_PACKAGE)
.setData(Uri.parse("package:$packageName"))
)
}
ProductAdapter.Action.CANCEL -> {
val binder = downloadConnection.binder
@ -381,10 +440,12 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
private fun startLauncherActivity(name: String) {
try {
startActivity(Intent(Intent.ACTION_MAIN)
startActivity(
Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER)
.setComponent(ComponentName(packageName, name))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} catch (e: Exception) {
e.printStackTrace()
}
@ -395,17 +456,26 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
}
override fun onPermissionsClick(group: String?, permissions: List<String>) {
MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show(childFragmentManager)
MessageDialog(MessageDialog.Message.Permissions(group, permissions)).show(
childFragmentManager
)
}
override fun onScreenshotClick(screenshot: Product.Screenshot) {
val pair = products.asSequence()
.map { Pair(it.second, it.first.screenshots.find { it === screenshot }?.identifier) }
.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)
ScreenshotsFragment(packageName, repository.id, identifier).show(
childFragmentManager
)
}
}
}
@ -414,20 +484,30 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val installedItem = installed?.installedItem
when {
release.incompatibilities.isNotEmpty() -> {
MessageDialog(MessageDialog.Message.ReleaseIncompatible(release.incompatibilities,
release.platforms, release.minSdkVersion, release.maxSdkVersion)).show(childFragmentManager)
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)
MessageDialog(MessageDialog.Message.ReleaseSignatureMismatch).show(
childFragmentManager
)
}
else -> {
val productRepository = products.asSequence().filter { it.first.releases.any { it === release } }.firstOrNull()
val productRepository =
products.asSequence().filter { it -> it.first.releases.any { it === release } }
.firstOrNull()
if (productRepository != null) {
downloadConnection.binder?.enqueue(packageName, productRepository.first.name,
productRepository.second, release)
downloadConnection.binder?.enqueue(
packageName, productRepository.first.name,
productRepository.second, release
)
}
}
}
@ -448,13 +528,13 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
}
}
class LaunchDialog(): DialogFragment() {
class LaunchDialog() : DialogFragment() {
companion object {
private const val EXTRA_NAMES = "names"
private const val EXTRA_LABELS = "labels"
}
constructor(launcherActivities: List<Pair<String, String>>): this() {
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 }))
@ -466,8 +546,10 @@ class ProductFragment(): ScreenFragment(), ProductAdapter.Callbacks {
val labels = requireArguments().getStringArrayList(EXTRA_LABELS)!!
return AlertDialog.Builder(requireContext())
.setTitle(R.string.launch)
.setItems(labels.toTypedArray()) { _, position -> (parentFragment as ProductFragment)
.startLauncherActivity(names[position]) }
.setItems(labels.toTypedArray()) { _, position ->
(parentFragment as ProductFragment)
.startLauncherActivity(names[position])
}
.setNegativeButton(R.string.cancel, null)
.create()
}

View File

@ -18,15 +18,14 @@ import com.looker.droidify.entity.Repository
import com.looker.droidify.network.PicassoDownloader
import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.extension.resources.*
import com.looker.droidify.utility.extension.text.*
import com.looker.droidify.utility.extension.text.nullIfEmpty
import com.looker.droidify.widget.CursorRecyclerAdapter
import com.looker.droidify.widget.DividerItemDecoration
class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
class ProductsAdapter(private val onClick: (ProductItem) -> Unit) :
CursorRecyclerAdapter<ProductsAdapter.ViewType, RecyclerView.ViewHolder>() {
enum class ViewType { PRODUCT, LOADING, EMPTY }
private class ProductViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
private class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name = itemView.findViewById<TextView>(R.id.name)!!
val status = itemView.findViewById<TextView>(R.id.status)!!
val summary = itemView.findViewById<TextView>(R.id.summary)!!
@ -42,18 +41,23 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
}
}
private class LoadingViewHolder(context: Context): RecyclerView.ViewHolder(FrameLayout(context)) {
private class LoadingViewHolder(context: Context) :
RecyclerView.ViewHolder(FrameLayout(context)) {
init {
itemView as FrameLayout
val progressBar = ProgressBar(itemView.context)
itemView.addView(progressBar, FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT).apply { gravity = Gravity.CENTER })
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT)
itemView.addView(progressBar, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply { gravity = Gravity.CENTER })
itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
}
}
private class EmptyViewHolder(context: Context): RecyclerView.ViewHolder(TextView(context)) {
private class EmptyViewHolder(context: Context) : RecyclerView.ViewHolder(TextView(context)) {
val text: TextView
get() = itemView as TextView
@ -64,22 +68,10 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
itemView.typeface = TypefaceExtra.light
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
itemView.setTextSizeScaled(20)
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT)
}
}
fun configureDivider(context: Context, position: Int, configuration: DividerItemDecoration.Configuration) {
val currentItem = if (getItemEnumViewType(position) == ViewType.PRODUCT) getProductItem(position) else null
val nextItem = if (position + 1 < itemCount && getItemEnumViewType(position + 1) == ViewType.PRODUCT)
getProductItem(position + 1) else null
when {
currentItem != null && nextItem != null && currentItem.matchRank != nextItem.matchRank -> {
configuration.set(true, false, 0, 0)
}
else -> {
configuration.set(true, false, context.resources.sizeScaled(72), 0)
}
itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.MATCH_PARENT
)
}
}
@ -120,7 +112,10 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
return Database.ProductAdapter.transformItem(moveTo(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: ViewType
): RecyclerView.ViewHolder {
return when (viewType) {
ViewType.PRODUCT -> ProductViewHolder(parent.inflate(R.layout.product_item)).apply {
itemView.setOnClickListener { onClick(getProductItem(adapterPosition)) }
@ -136,12 +131,18 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
holder as ProductViewHolder
val productItem = getProductItem(position)
holder.name.text = productItem.name
holder.summary.text = if (productItem.name == productItem.summary) "" else productItem.summary
holder.summary.visibility = if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE
holder.summary.text =
if (productItem.name == productItem.summary) "" else productItem.summary
holder.summary.visibility =
if (holder.summary.text.isNotEmpty()) View.VISIBLE else View.GONE
val repository: Repository? = repositories[productItem.repositoryId]
if ((productItem.icon.isNotEmpty() || productItem.metadataIcon.isNotEmpty()) && repository != null) {
holder.icon.load(PicassoDownloader.createIconUri(holder.icon, productItem.packageName,
productItem.icon, productItem.metadataIcon, repository)) {
holder.icon.load(
PicassoDownloader.createIconUri(
holder.icon, productItem.packageName,
productItem.icon, productItem.metadataIcon, repository
)
) {
placeholder(holder.progressIcon)
error(holder.defaultIcon)
}
@ -155,8 +156,12 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
if (background == null) {
resources.sizeScaled(4).let { setPadding(it, 0, it, 0) }
setTextColor(holder.status.context.getColorFromAttr(android.R.attr.colorBackground))
background = GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, null).apply {
color = holder.status.context.getColorFromAttr(android.R.attr.colorAccent)
background = GradientDrawable(
GradientDrawable.Orientation.TOP_BOTTOM,
null
).apply {
color =
holder.status.context.getColorFromAttr(android.R.attr.colorAccent)
cornerRadius = holder.status.resources.sizeScaled(2).toFloat()
}
}
@ -170,7 +175,9 @@ class ProductsAdapter(private val onClick: (ProductItem) -> Unit):
}
}
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
sequenceOf(holder.name, holder.status, holder.summary).forEach { it.isEnabled = enabled }
sequenceOf(holder.name, holder.status, holder.summary).forEach {
it.isEnabled = enabled
}
}
ViewType.LOADING -> {
// Do nothing

View File

@ -17,7 +17,6 @@ import com.looker.droidify.database.CursorOwner
import com.looker.droidify.database.Database
import com.looker.droidify.entity.ProductItem
import com.looker.droidify.utility.RxUtils
import com.looker.droidify.widget.DividerItemDecoration
import com.looker.droidify.widget.RecyclerFastScroller
class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
@ -80,7 +79,6 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
recycledViewPool.setMaxRecycledViews(ProductsAdapter.ViewType.PRODUCT.ordinal, 30)
val adapter = ProductsAdapter { screenActivity.navigateProduct(it.packageName) }
this.adapter = adapter
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
RecyclerFastScroller(this)
recyclerView = this
}

View File

@ -9,28 +9,15 @@ import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.SearchView
import android.widget.TextView
import android.widget.Toolbar
import android.widget.*
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
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 com.looker.droidify.R
import com.looker.droidify.content.Preferences
import com.looker.droidify.database.Database
@ -44,9 +31,13 @@ import com.looker.droidify.utility.extension.resources.*
import com.looker.droidify.widget.DividerItemDecoration
import com.looker.droidify.widget.FocusSearchView
import com.looker.droidify.widget.StableRecyclerAdapter
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 kotlin.math.*
class TabsFragment: ScreenFragment() {
class TabsFragment : ScreenFragment() {
companion object {
private const val STATE_SEARCH_FOCUSED = "searchFocused"
private const val STATE_SEARCH_QUERY = "searchQuery"
@ -75,8 +66,10 @@ class TabsFragment: ScreenFragment() {
if (field != value) {
field = value
val layout = layout
layout?.tabs?.let { (0 until it.childCount)
.forEach { index -> it.getChildAt(index)!!.isEnabled = !value } }
layout?.tabs?.let {
(0 until it.childCount)
.forEach { index -> it.getChildAt(index)!!.isEnabled = !value }
}
layout?.sectionIcon?.scaleY = if (value) -1f else 1f
if ((sectionsList?.parent as? View)?.height ?: 0 > 0) {
animateSectionsList()
@ -106,7 +99,11 @@ class TabsFragment: ScreenFragment() {
get() = if (host == null) emptySequence() else
childFragmentManager.fragments.asSequence().mapNotNull { it as? ProductsFragment }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment, container, false)
}
@ -125,7 +122,7 @@ class TabsFragment: ScreenFragment() {
searchView.allowFocus = savedInstanceState?.getBoolean(STATE_SEARCH_FOCUSED) == true
searchView.maxWidth = Int.MAX_VALUE
searchView.queryHint = getString(R.string.search)
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
searchView.clearFocus()
return true
@ -155,12 +152,14 @@ class TabsFragment: ScreenFragment() {
.let { menu ->
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
val items = Preferences.Key.SortOrder.default.value.values
.map { sortOrder -> menu
.map { sortOrder ->
menu
.add(sortOrder.order.titleResId)
.setOnMenuItemClickListener {
Preferences[Preferences.Key.SortOrder] = sortOrder
true
} }
}
}
menu.setGroupCheckable(0, true, true)
Pair(menu.item, items)
}
@ -193,20 +192,29 @@ class TabsFragment: ScreenFragment() {
val layout = Layout(view)
this.layout = layout
layout.tabs.background = TabsBackgroundDrawable(layout.tabs.context,
layout.tabs.layoutDirection == View.LAYOUT_DIRECTION_RTL)
layout.tabs.background = TabsBackgroundDrawable(
layout.tabs.context,
layout.tabs.layoutDirection == View.LAYOUT_DIRECTION_RTL
)
ProductsFragment.Source.values().forEach {
val tab = TextView(layout.tabs.context)
val selectedColor = tab.context.getColorFromAttr(android.R.attr.textColorPrimary).defaultColor
val normalColor = tab.context.getColorFromAttr(android.R.attr.textColorSecondary).defaultColor
val selectedColor =
tab.context.getColorFromAttr(android.R.attr.textColorPrimary).defaultColor
val normalColor =
tab.context.getColorFromAttr(android.R.attr.textColorSecondary).defaultColor
tab.gravity = Gravity.CENTER
tab.typeface = TypefaceExtra.medium
tab.setTextColor(ColorStateList(arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf()),
intArrayOf(selectedColor, normalColor)))
tab.setTextColor(
ColorStateList(
arrayOf(intArrayOf(android.R.attr.state_selected), intArrayOf()),
intArrayOf(selectedColor, normalColor)
)
)
tab.setTextSizeScaled(14)
tab.isAllCaps = true
tab.text = getString(it.titleResId)
tab.background = tab.context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
tab.background =
tab.context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
tab.setOnClickListener { _ ->
setSelectedTab(it)
viewPager!!.setCurrentItem(it.ordinal, Utils.areAnimationsEnabled(tab.context))
@ -216,10 +224,13 @@ class TabsFragment: ScreenFragment() {
}
showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0
sections = savedInstanceState?.getParcelableArrayList<ProductItem.Section>(STATE_SECTIONS).orEmpty()
sections = savedInstanceState?.getParcelableArrayList<ProductItem.Section>(STATE_SECTIONS)
.orEmpty()
section = savedInstanceState?.getParcelable(STATE_SECTION) ?: ProductItem.Section.All
layout.sectionChange.setOnClickListener { showSections = sections
.any { it !is ProductItem.Section.All } && !showSections }
layout.sectionChange.setOnClickListener {
showSections = sections
.any { it !is ProductItem.Section.All } && !showSections
}
updateOrder()
sortOrderDisposable = Preferences.observable.subscribe {
@ -232,12 +243,18 @@ class TabsFragment: ScreenFragment() {
viewPager = ViewPager2(content.context).apply {
id = R.id.fragment_pager
adapter = object: FragmentStateAdapter(this@TabsFragment) {
adapter = object : FragmentStateAdapter(this@TabsFragment) {
override fun getItemCount(): Int = ProductsFragment.Source.values().size
override fun createFragment(position: Int): Fragment = ProductsFragment(ProductsFragment
.Source.values()[position])
override fun createFragment(position: Int): Fragment = ProductsFragment(
ProductsFragment
.Source.values()[position]
)
}
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
content.addView(
this,
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
registerOnPageChangeCallback(pageChangeCallback)
offscreenPageLimit = 1
}
@ -247,15 +264,21 @@ class TabsFragment: ScreenFragment() {
.observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { setSectionsAndUpdate(it.asSequence().sorted()
.map(ProductItem.Section::Category).toList(), null) }
.subscribe {
setSectionsAndUpdate(
it.asSequence().sorted()
.map(ProductItem.Section::Category).toList(), null
)
}
repositoriesDisposable = Observable.just(Unit)
.concatWith(Database.observable(Database.Subject.Repositories))
.observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
.map { ProductItem.Section.Repository(it.id, it.name) }.toList()) }
.subscribe { it ->
setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
.map { ProductItem.Section.Repository(it.id, it.name) }.toList())
}
updateSection()
val sectionsList = RecyclerView(toolbar.context).apply {
@ -273,7 +296,7 @@ class TabsFragment: ScreenFragment() {
}
this.adapter = adapter
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
setBackgroundColor(context.getColorFromAttr(android.R.attr.colorPrimaryDark).defaultColor)
setBackgroundResource(R.drawable.background_border)
elevation = resources.sizeScaled(4).toFloat()
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, 0)
visibility = View.GONE
@ -368,7 +391,9 @@ class TabsFragment: ScreenFragment() {
private fun setSelectedTab(source: ProductsFragment.Source) {
val layout = layout!!
(0 until layout.tabs.childCount).forEach { layout.tabs.getChildAt(it).isSelected = it == source.ordinal }
(0 until layout.tabs.childCount).forEach {
layout.tabs.getChildAt(it).isSelected = it == source.ordinal
}
}
internal fun selectUpdates() = selectUpdatesInternal(true)
@ -376,7 +401,10 @@ class TabsFragment: ScreenFragment() {
private fun selectUpdatesInternal(allowSmooth: Boolean) {
if (view != null) {
val viewPager = viewPager
viewPager?.setCurrentItem(ProductsFragment.Source.UPDATES.ordinal, allowSmooth && viewPager.isLaidOut)
viewPager?.setCurrentItem(
ProductsFragment.Source.UPDATES.ordinal,
allowSmooth && viewPager.isLaidOut
)
} else {
needSelectUpdates = true
}
@ -397,13 +425,15 @@ class TabsFragment: ScreenFragment() {
productFragments.forEach { it.setOrder(order) }
}
private inline fun <reified T: ProductItem.Section> collectOldSections(list: List<T>?): List<T>? {
private inline fun <reified T : ProductItem.Section> collectOldSections(list: List<T>?): List<T>? {
val oldList = sections.mapNotNull { it as? T }
return if (list == null || oldList == list) oldList else null
}
private fun setSectionsAndUpdate(categories: List<ProductItem.Section.Category>?,
repositories: List<ProductItem.Section.Repository>?) {
private fun setSectionsAndUpdate(
categories: List<ProductItem.Section.Category>?,
repositories: List<ProductItem.Section.Repository>?
) {
val oldCategories = collectOldSections(categories)
val oldRepositories = collectOldSections(repositories)
if (oldCategories == null || oldRepositories == null) {
@ -423,7 +453,8 @@ class TabsFragment: ScreenFragment() {
is ProductItem.Section.Category -> section.name
is ProductItem.Section.Repository -> section.name
}
layout?.sectionIcon?.visibility = if (sections.any { it !is ProductItem.Section.All }) View.VISIBLE else View.GONE
layout?.sectionIcon?.visibility =
if (sections.any { it !is ProductItem.Section.All }) View.VISIBLE else View.GONE
productFragments.forEach { it.setSection(section) }
sectionsList?.adapter?.notifyDataSetChanged()
}
@ -439,7 +470,8 @@ class TabsFragment: ScreenFragment() {
if (value != target) {
sectionsAnimator = ValueAnimator.ofFloat(value, target).apply {
duration = (250 * abs(target - value)).toLong()
interpolator = if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f)
interpolator =
if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f)
addUpdateListener {
val newValue = animatedValue as Float
sectionsList.apply {
@ -462,8 +494,12 @@ class TabsFragment: ScreenFragment() {
}
}
private val pageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
private val pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
val layout = layout!!
val fromSections = ProductsFragment.Source.values()[position].sections
val toSections = if (positionOffset <= 0f) fromSections else
@ -490,8 +526,11 @@ class TabsFragment: ScreenFragment() {
val source = ProductsFragment.Source.values()[position]
updateUpdateNotificationBlocker(source)
sortOrderMenu!!.first.isVisible = source.order
syncRepositoriesMenuItem!!.setShowAsActionFlags(if (!source.order ||
resources.configuration.screenWidthDp >= 400) MenuItem.SHOW_AS_ACTION_ALWAYS else 0)
syncRepositoriesMenuItem!!.setShowAsActionFlags(
if (!source.order ||
resources.configuration.screenWidthDp >= 400
) MenuItem.SHOW_AS_ACTION_ALWAYS else 0
)
setSelectedTab(source)
if (showSections && !source.sections) {
showSections = false
@ -500,7 +539,8 @@ class TabsFragment: ScreenFragment() {
override fun onPageScrollStateChanged(state: Int) {
val source = ProductsFragment.Source.values()[viewPager!!.currentItem]
layout!!.sectionChange.isEnabled = state != ViewPager2.SCROLL_STATE_DRAGGING && source.sections
layout!!.sectionChange.isEnabled =
state != ViewPager2.SCROLL_STATE_DRAGGING && source.sections
if (state == ViewPager2.SCROLL_STATE_IDLE) {
// onPageSelected can be called earlier than fragments created
updateUpdateNotificationBlocker(source)
@ -508,10 +548,10 @@ class TabsFragment: ScreenFragment() {
}
}
private class TabsBackgroundDrawable(context: Context, private val rtl: Boolean): Drawable() {
private class TabsBackgroundDrawable(context: Context, private val rtl: Boolean) : Drawable() {
private val height = context.resources.sizeScaled(2)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = context.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
color = context.getColorFromAttr(android.R.attr.textColor).defaultColor
}
private var position = 0f
@ -529,11 +569,18 @@ class TabsFragment: ScreenFragment() {
val width = bounds.width() / total.toFloat()
val x = width * position
if (rtl) {
canvas.drawRect(bounds.right - width - x, (bounds.bottom - height).toFloat(),
bounds.right - x, bounds.bottom.toFloat(), paint)
canvas.drawRect(
bounds.right - width - x, (bounds.bottom - height).toFloat(),
bounds.right - x, bounds.bottom.toFloat(), paint
)
} else {
canvas.drawRect(bounds.left + x, (bounds.bottom - height).toFloat(),
bounds.left + x + width, bounds.bottom.toFloat(), paint)
canvas.drawRect(
bounds.left + x + width / 4, // + width/4 from start
(bounds.bottom - height).toFloat(),
bounds.left + x + width - width / 4, // - width/4 from end
bounds.bottom.toFloat(),
paint
)
}
}
}
@ -543,12 +590,15 @@ class TabsFragment: ScreenFragment() {
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
}
private class SectionsAdapter(private val sections: () -> List<ProductItem.Section>,
private val onClick: (ProductItem.Section) -> Unit): StableRecyclerAdapter<SectionsAdapter.ViewType,
private class SectionsAdapter(
private val sections: () -> List<ProductItem.Section>,
private val onClick: (ProductItem.Section) -> Unit
) : StableRecyclerAdapter<SectionsAdapter.ViewType,
RecyclerView.ViewHolder>() {
enum class ViewType { SECTION }
private class SectionViewHolder(context: Context): RecyclerView.ViewHolder(TextView(context)) {
private class SectionViewHolder(context: Context) :
RecyclerView.ViewHolder(TextView(context)) {
val title: TextView
get() = itemView as TextView
@ -556,24 +606,41 @@ class TabsFragment: ScreenFragment() {
itemView as TextView
itemView.gravity = Gravity.CENTER_VERTICAL
itemView.resources.sizeScaled(16).let { itemView.setPadding(it, 0, it, 0) }
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary))
itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColor))
itemView.setTextSizeScaled(16)
itemView.background = context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT,
itemView.resources.sizeScaled(48))
itemView.background =
context.getDrawableFromAttr(android.R.attr.selectableItemBackground)
itemView.layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
itemView.resources.sizeScaled(48)
)
}
}
fun configureDivider(context: Context, position: Int, configuration: DividerItemDecoration.Configuration) {
fun configureDivider(
context: Context,
position: Int,
configuration: DividerItemDecoration.Configuration
) {
val currentSection = sections()[position]
val nextSection = sections().getOrNull(position + 1)
when {
nextSection != null && currentSection.javaClass != nextSection.javaClass -> {
val padding = context.resources.sizeScaled(16)
configuration.set(true, false, padding, padding)
configuration.set(
needDivider = true,
toTop = false,
paddingStart = padding,
paddingEnd = padding
)
}
else -> {
configuration.set(false, false, 0, 0)
configuration.set(
needDivider = false,
toTop = false,
paddingStart = 0,
paddingEnd = 0
)
}
}
}
@ -585,7 +652,10 @@ class TabsFragment: ScreenFragment() {
override fun getItemDescriptor(position: Int): String = sections()[position].toString()
override fun getItemEnumViewType(position: Int): ViewType = ViewType.SECTION
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: ViewType
): RecyclerView.ViewHolder {
return SectionViewHolder(parent.context).apply {
itemView.setOnClickListener { onClick(sections()[adapterPosition]) }
}
@ -599,9 +669,11 @@ class TabsFragment: ScreenFragment() {
val margin = holder.itemView.resources.sizeScaled(8)
val layoutParams = holder.itemView.layoutParams as RecyclerView.LayoutParams
layoutParams.topMargin = if (previousSection == null ||
section.javaClass != previousSection.javaClass) margin else 0
section.javaClass != previousSection.javaClass
) margin else 0
layoutParams.bottomMargin = if (nextSection == null ||
section.javaClass != nextSection.javaClass) margin else 0
section.javaClass != nextSection.javaClass
) margin else 0
holder.title.text = when (section) {
is ProductItem.Section.All -> holder.itemView.resources.getString(R.string.all_applications)
is ProductItem.Section.Category -> section.name