mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-06-08 16:59:55 +00:00
Remove: The old-already-migrated hierarchy (+ BIG CLEAN UP)
This commit is contained in:
parent
7694f3cade
commit
3f454c5199
@ -1,135 +0,0 @@
|
|||||||
package com.looker.droidify.database
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.loader.app.LoaderManager
|
|
||||||
import androidx.loader.content.Loader
|
|
||||||
import com.looker.droidify.entity.Order
|
|
||||||
import com.looker.droidify.entity.Section
|
|
||||||
|
|
||||||
class CursorOwner : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
|
|
||||||
sealed class Request {
|
|
||||||
internal abstract val id: Int
|
|
||||||
|
|
||||||
data class ProductsAvailable(
|
|
||||||
val searchQuery: String, val section: Section,
|
|
||||||
val order: Order,
|
|
||||||
) : Request() {
|
|
||||||
override val id: Int
|
|
||||||
get() = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ProductsInstalled(
|
|
||||||
val searchQuery: String, val section: Section,
|
|
||||||
val order: Order,
|
|
||||||
) : Request() {
|
|
||||||
override val id: Int
|
|
||||||
get() = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ProductsUpdates(
|
|
||||||
val searchQuery: String, val section: Section,
|
|
||||||
val order: Order,
|
|
||||||
) : Request() {
|
|
||||||
override val id: Int
|
|
||||||
get() = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
object Repositories : Request() {
|
|
||||||
override val id: Int
|
|
||||||
get() = 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun onCursorData(request: Request, cursor: Cursor?)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ActiveRequest(
|
|
||||||
val request: Request,
|
|
||||||
val callback: Callback?,
|
|
||||||
val cursor: Cursor?,
|
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
|
||||||
retainInstance = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val activeRequests = mutableMapOf<Int, ActiveRequest>()
|
|
||||||
|
|
||||||
fun attach(callback: Callback, request: Request) {
|
|
||||||
val oldActiveRequest = activeRequests[request.id]
|
|
||||||
if (oldActiveRequest?.callback != null &&
|
|
||||||
oldActiveRequest.callback != callback && oldActiveRequest.cursor != null
|
|
||||||
) {
|
|
||||||
oldActiveRequest.callback.onCursorData(oldActiveRequest.request, null)
|
|
||||||
}
|
|
||||||
val cursor = if (oldActiveRequest?.request == request && oldActiveRequest.cursor != null) {
|
|
||||||
callback.onCursorData(request, oldActiveRequest.cursor)
|
|
||||||
oldActiveRequest.cursor
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
activeRequests[request.id] = ActiveRequest(request, callback, cursor)
|
|
||||||
if (cursor == null) {
|
|
||||||
LoaderManager.getInstance(this).restartLoader(request.id, null, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun detach(callback: Callback) {
|
|
||||||
for (id in activeRequests.keys) {
|
|
||||||
val activeRequest = activeRequests[id]!!
|
|
||||||
if (activeRequest.callback == callback) {
|
|
||||||
activeRequests[id] = activeRequest.copy(callback = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
|
|
||||||
val request = activeRequests[id]!!.request
|
|
||||||
val db = DatabaseX.getInstance(requireContext())
|
|
||||||
return QueryLoader(requireContext()) {
|
|
||||||
when (request) {
|
|
||||||
is Request.ProductsAvailable -> db.productDao
|
|
||||||
.query(
|
|
||||||
installed = false,
|
|
||||||
updates = false,
|
|
||||||
searchQuery = request.searchQuery,
|
|
||||||
section = request.section,
|
|
||||||
order = request.order,
|
|
||||||
signal = it
|
|
||||||
)
|
|
||||||
is Request.ProductsInstalled -> db.productDao
|
|
||||||
.query(
|
|
||||||
installed = true,
|
|
||||||
updates = false,
|
|
||||||
searchQuery = request.searchQuery,
|
|
||||||
section = request.section,
|
|
||||||
order = request.order,
|
|
||||||
signal = it
|
|
||||||
)
|
|
||||||
is Request.ProductsUpdates -> db.productDao
|
|
||||||
.query(
|
|
||||||
installed = true,
|
|
||||||
updates = true,
|
|
||||||
searchQuery = request.searchQuery,
|
|
||||||
section = request.section,
|
|
||||||
order = request.order,
|
|
||||||
signal = it
|
|
||||||
)
|
|
||||||
is Request.Repositories -> db.repositoryDao.allCursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadFinished(loader: Loader<Cursor>, data: Cursor?) {
|
|
||||||
val activeRequest = activeRequests[loader.id]
|
|
||||||
if (activeRequest != null) {
|
|
||||||
activeRequests[loader.id] = activeRequest.copy(cursor = data)
|
|
||||||
activeRequest.callback?.onCursorData(activeRequest.request, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoaderReset(loader: Loader<Cursor>) = onLoadFinished(loader, null)
|
|
||||||
}
|
|
@ -1,7 +1,5 @@
|
|||||||
package com.looker.droidify.database
|
package com.looker.droidify.database
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.DataSource
|
import androidx.paging.DataSource
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
@ -13,7 +11,6 @@ import com.looker.droidify.entity.Order
|
|||||||
import com.looker.droidify.entity.Section
|
import com.looker.droidify.entity.Section
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
|
||||||
|
|
||||||
interface BaseDao<T> {
|
interface BaseDao<T> {
|
||||||
@Insert
|
@Insert
|
||||||
fun insert(vararg product: T)
|
fun insert(vararg product: T)
|
||||||
@ -49,9 +46,6 @@ interface RepositoryDao : BaseDao<Repository> {
|
|||||||
@Query("SELECT * FROM repository WHERE _id = :id")
|
@Query("SELECT * FROM repository WHERE _id = :id")
|
||||||
fun getLive(id: Long): LiveData<Repository?>
|
fun getLive(id: Long): LiveData<Repository?>
|
||||||
|
|
||||||
@get:Query("SELECT * FROM repository ORDER BY _id ASC")
|
|
||||||
val allCursor: Cursor
|
|
||||||
|
|
||||||
@get:Query("SELECT * FROM repository ORDER BY _id ASC")
|
@get:Query("SELECT * FROM repository ORDER BY _id ASC")
|
||||||
val all: List<Repository>
|
val all: List<Repository>
|
||||||
|
|
||||||
@ -86,95 +80,6 @@ interface ProductDao : BaseDao<Product> {
|
|||||||
@Query("DELETE FROM product WHERE repository_id = :id")
|
@Query("DELETE FROM product WHERE repository_id = :id")
|
||||||
fun deleteById(id: Long): Int
|
fun deleteById(id: Long): Int
|
||||||
|
|
||||||
@RawQuery
|
|
||||||
fun query(
|
|
||||||
query: SupportSQLiteQuery
|
|
||||||
): Cursor
|
|
||||||
|
|
||||||
// TODO Remove
|
|
||||||
@Transaction
|
|
||||||
fun query(
|
|
||||||
installed: Boolean, updates: Boolean, searchQuery: String,
|
|
||||||
section: Section, order: Order, signal: CancellationSignal?
|
|
||||||
): Cursor {
|
|
||||||
val builder = QueryBuilder()
|
|
||||||
|
|
||||||
val signatureMatches = """installed.${ROW_SIGNATURE} IS NOT NULL AND
|
|
||||||
product.${ROW_SIGNATURES} LIKE ('%.' || installed.${ROW_SIGNATURE} || '.%') AND
|
|
||||||
product.${ROW_SIGNATURES} != ''"""
|
|
||||||
|
|
||||||
builder += """SELECT product.rowid AS _id, product.${ROW_REPOSITORY_ID},
|
|
||||||
product.${ROW_PACKAGE_NAME}, product.${ROW_NAME},
|
|
||||||
product.${ROW_SUMMARY}, installed.${ROW_VERSION},
|
|
||||||
(COALESCE(lock.${ROW_VERSION_CODE}, -1) NOT IN (0, product.${ROW_VERSION_CODE}) AND
|
|
||||||
product.${ROW_COMPATIBLE} != 0 AND product.${ROW_VERSION_CODE} >
|
|
||||||
COALESCE(installed.${ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches)
|
|
||||||
AS ${ROW_CAN_UPDATE}, product.${ROW_COMPATIBLE},"""
|
|
||||||
|
|
||||||
if (searchQuery.isNotEmpty()) {
|
|
||||||
builder += """(((product.${ROW_NAME} LIKE ? OR
|
|
||||||
product.${ROW_SUMMARY} LIKE ?) * 7) |
|
|
||||||
((product.${ROW_PACKAGE_NAME} LIKE ?) * 3) |
|
|
||||||
(product.${ROW_DESCRIPTION} LIKE ?)) AS ${ROW_MATCH_RANK},"""
|
|
||||||
builder %= List(4) { "%$searchQuery%" }
|
|
||||||
} else {
|
|
||||||
builder += "0 AS ${ROW_MATCH_RANK},"
|
|
||||||
}
|
|
||||||
|
|
||||||
builder += """MAX((product.${ROW_COMPATIBLE} AND
|
|
||||||
(installed.${ROW_SIGNATURE} IS NULL OR $signatureMatches)) ||
|
|
||||||
PRINTF('%016X', product.${ROW_VERSION_CODE})) FROM $ROW_PRODUCT_NAME AS product"""
|
|
||||||
builder += """JOIN $ROW_REPOSITORY_NAME AS repository
|
|
||||||
ON product.${ROW_REPOSITORY_ID} = repository.${ROW_ID}"""
|
|
||||||
builder += """LEFT JOIN $ROW_LOCK_NAME AS lock
|
|
||||||
ON product.${ROW_PACKAGE_NAME} = lock.${ROW_PACKAGE_NAME}"""
|
|
||||||
|
|
||||||
if (!installed && !updates) {
|
|
||||||
builder += "LEFT"
|
|
||||||
}
|
|
||||||
builder += """JOIN $ROW_INSTALLED_NAME AS installed
|
|
||||||
ON product.${ROW_PACKAGE_NAME} = installed.${ROW_PACKAGE_NAME}"""
|
|
||||||
|
|
||||||
if (section is Section.Category) {
|
|
||||||
builder += """JOIN $ROW_CATEGORY_NAME AS category
|
|
||||||
ON product.${ROW_PACKAGE_NAME} = category.${ROW_PACKAGE_NAME}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
builder += """WHERE repository.${ROW_ENABLED} != 0"""
|
|
||||||
|
|
||||||
if (section is Section.Category) {
|
|
||||||
builder += "AND category.${ROW_NAME} = ?"
|
|
||||||
builder %= section.name
|
|
||||||
} else if (section is Section.Repository) {
|
|
||||||
builder += "AND product.${ROW_REPOSITORY_ID} = ?"
|
|
||||||
builder %= section.id.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchQuery.isNotEmpty()) {
|
|
||||||
builder += """AND $ROW_MATCH_RANK > 0"""
|
|
||||||
}
|
|
||||||
|
|
||||||
builder += "GROUP BY product.${ROW_PACKAGE_NAME} HAVING 1"
|
|
||||||
|
|
||||||
if (updates) {
|
|
||||||
builder += "AND $ROW_CAN_UPDATE"
|
|
||||||
}
|
|
||||||
builder += "ORDER BY"
|
|
||||||
|
|
||||||
if (searchQuery.isNotEmpty()) {
|
|
||||||
builder += """$ROW_MATCH_RANK DESC,"""
|
|
||||||
}
|
|
||||||
|
|
||||||
when (order) {
|
|
||||||
Order.NAME -> Unit
|
|
||||||
Order.DATE_ADDED -> builder += "product.${ROW_ADDED} DESC,"
|
|
||||||
Order.LAST_UPDATE -> builder += "product.${ROW_UPDATED} DESC,"
|
|
||||||
}::class
|
|
||||||
builder += "product.${ROW_NAME} COLLATE LOCALIZED ASC"
|
|
||||||
|
|
||||||
return query(SimpleSQLiteQuery(builder.build()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@RawQuery
|
@RawQuery
|
||||||
fun queryObject(query: SupportSQLiteQuery): List<Product>
|
fun queryObject(query: SupportSQLiteQuery): List<Product>
|
||||||
|
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
package com.looker.droidify.entity
|
package com.looker.droidify.entity
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
|
||||||
import com.looker.droidify.utility.extension.json.forEachKey
|
|
||||||
|
|
||||||
data class ProductItem(
|
data class ProductItem(
|
||||||
var repositoryId: Long,
|
var repositoryId: Long,
|
||||||
var packageName: String,
|
var packageName: String,
|
||||||
@ -16,55 +12,4 @@ data class ProductItem(
|
|||||||
var compatible: Boolean,
|
var compatible: Boolean,
|
||||||
var canUpdate: Boolean,
|
var canUpdate: Boolean,
|
||||||
var matchRank: Int,
|
var matchRank: Int,
|
||||||
) {
|
|
||||||
fun serialize(generator: JsonGenerator) {
|
|
||||||
generator.writeNumberField("serialVersion", 1)
|
|
||||||
generator.writeNumberField("repositoryId", repositoryId)
|
|
||||||
generator.writeStringField("packageName", packageName)
|
|
||||||
generator.writeStringField("name", name)
|
|
||||||
generator.writeStringField("summary", summary)
|
|
||||||
generator.writeStringField("icon", icon)
|
|
||||||
generator.writeStringField("metadataIcon", metadataIcon)
|
|
||||||
generator.writeStringField("version", version)
|
|
||||||
generator.writeStringField("installedVersion", installedVersion)
|
|
||||||
generator.writeBooleanField("compatible", compatible)
|
|
||||||
generator.writeBooleanField("canUpdate", canUpdate)
|
|
||||||
generator.writeNumberField("matchRank", matchRank)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun deserialize(parser: JsonParser): ProductItem {
|
|
||||||
var repositoryId = 0L
|
|
||||||
var packageName = ""
|
|
||||||
var name = ""
|
|
||||||
var summary = ""
|
|
||||||
var icon = ""
|
|
||||||
var metadataIcon = ""
|
|
||||||
var version = ""
|
|
||||||
var installedVersion = ""
|
|
||||||
var compatible = false
|
|
||||||
var canUpdate = false
|
|
||||||
var matchRank = 0
|
|
||||||
parser.forEachKey {
|
|
||||||
when {
|
|
||||||
it.number("repositoryId") -> repositoryId = valueAsLong
|
|
||||||
it.string("packageName") -> packageName = valueAsString
|
|
||||||
it.string("name") -> name = valueAsString
|
|
||||||
it.string("summary") -> summary = valueAsString
|
|
||||||
it.string("icon") -> icon = valueAsString
|
|
||||||
it.string("metadataIcon") -> metadataIcon = valueAsString
|
|
||||||
it.string("version") -> version = valueAsString
|
|
||||||
it.string("installedVersion") -> installedVersion = valueAsString
|
|
||||||
it.boolean("compatible") -> compatible = valueAsBoolean
|
|
||||||
it.boolean("canUpdate") -> canUpdate = valueAsBoolean
|
|
||||||
it.number("matchRank") -> matchRank = valueAsInt
|
|
||||||
else -> skipChildren()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ProductItem(
|
|
||||||
repositoryId, packageName, name, summary, icon, metadataIcon,
|
|
||||||
version, installedVersion, compatible, canUpdate, matchRank
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package com.looker.droidify.screen
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
|
|
||||||
open class BaseFragment : Fragment() {
|
|
||||||
val screenActivity: ScreenActivity
|
|
||||||
get() = requireActivity() as ScreenActivity
|
|
||||||
|
|
||||||
open fun onBackPressed(): Boolean = false
|
|
||||||
}
|
|
@ -1,264 +0,0 @@
|
|||||||
package com.looker.droidify.screen
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Parcel
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.material.circularreveal.CircularRevealFrameLayout
|
|
||||||
import com.looker.droidify.MainApplication
|
|
||||||
import com.looker.droidify.R
|
|
||||||
import com.looker.droidify.content.Preferences
|
|
||||||
import com.looker.droidify.database.CursorOwner
|
|
||||||
import com.looker.droidify.installer.InstallerService
|
|
||||||
import com.looker.droidify.ui.fragments.AppDetailFragment
|
|
||||||
import com.looker.droidify.utility.KParcelable
|
|
||||||
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
|
||||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
abstract class ScreenActivity : AppCompatActivity() {
|
|
||||||
companion object {
|
|
||||||
private const val STATE_FRAGMENT_STACK = "fragmentStack"
|
|
||||||
}
|
|
||||||
|
|
||||||
val db
|
|
||||||
get() = (application as MainApplication).db
|
|
||||||
|
|
||||||
sealed class SpecialIntent {
|
|
||||||
object Updates : SpecialIntent()
|
|
||||||
class Install(val packageName: String?, val status: Int?, val promptIntent: Intent?) :
|
|
||||||
SpecialIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FragmentStackItem(
|
|
||||||
val className: String, val arguments: Bundle?,
|
|
||||||
val savedState: Fragment.SavedState?,
|
|
||||||
) : KParcelable {
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
|
||||||
dest.writeString(className)
|
|
||||||
dest.writeByte(if (arguments != null) 1 else 0)
|
|
||||||
arguments?.writeToParcel(dest, flags)
|
|
||||||
dest.writeByte(if (savedState != null) 1 else 0)
|
|
||||||
savedState?.writeToParcel(dest, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Suppress("unused")
|
|
||||||
@JvmField
|
|
||||||
val CREATOR = KParcelable.creator {
|
|
||||||
val className = it.readString()!!
|
|
||||||
val arguments =
|
|
||||||
if (it.readByte().toInt() == 0) null else Bundle.CREATOR.createFromParcel(it)
|
|
||||||
arguments?.classLoader = ScreenActivity::class.java.classLoader
|
|
||||||
val savedState = if (it.readByte()
|
|
||||||
.toInt() == 0
|
|
||||||
) null else Fragment.SavedState.CREATOR.createFromParcel(it)
|
|
||||||
FragmentStackItem(className, arguments, savedState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lateinit var cursorOwner: CursorOwner
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val fragmentStack = mutableListOf<FragmentStackItem>()
|
|
||||||
|
|
||||||
private val currentFragment: Fragment?
|
|
||||||
get() {
|
|
||||||
supportFragmentManager.executePendingTransactions()
|
|
||||||
return supportFragmentManager.findFragmentById(R.id.main_content)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
setTheme(Preferences[Preferences.Key.Theme].getResId(resources.configuration))
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
addContentView(
|
|
||||||
CircularRevealFrameLayout(this).apply { id = R.id.main_content },
|
|
||||||
ViewGroup.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
cursorOwner = CursorOwner()
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.add(cursorOwner, CursorOwner::class.java.name)
|
|
||||||
.commit()
|
|
||||||
} else {
|
|
||||||
cursorOwner = supportFragmentManager
|
|
||||||
.findFragmentByTag(CursorOwner::class.java.name) as CursorOwner
|
|
||||||
}
|
|
||||||
|
|
||||||
savedInstanceState?.getParcelableArrayList<FragmentStackItem>(STATE_FRAGMENT_STACK)
|
|
||||||
?.let { fragmentStack += it }
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
replaceFragment(TabsFragment(), null)
|
|
||||||
if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
|
|
||||||
handleIntent(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
val currentFragment = currentFragment
|
|
||||||
if (!(currentFragment is ScreenFragment && currentFragment.onBackPressed())) {
|
|
||||||
hideKeyboard()
|
|
||||||
if (!popFragment()) {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun replaceFragment(fragment: Fragment, open: Boolean?) {
|
|
||||||
if (open != null) {
|
|
||||||
currentFragment?.view?.translationZ =
|
|
||||||
(if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
|
|
||||||
}
|
|
||||||
supportFragmentManager
|
|
||||||
.beginTransaction()
|
|
||||||
.apply {
|
|
||||||
if (open != null) {
|
|
||||||
setCustomAnimations(
|
|
||||||
if (open) R.animator.slide_in else 0,
|
|
||||||
if (open) R.animator.slide_in_keep else R.animator.slide_out
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.replace(R.id.main_content, fragment)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pushFragment(fragment: Fragment) {
|
|
||||||
currentFragment?.let {
|
|
||||||
fragmentStack.add(
|
|
||||||
FragmentStackItem(
|
|
||||||
it::class.java.name, it.arguments,
|
|
||||||
supportFragmentManager.saveFragmentInstanceState(it)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
replaceFragment(fragment, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun popFragment(): Boolean {
|
|
||||||
return fragmentStack.isNotEmpty() && run {
|
|
||||||
val stackItem = fragmentStack.removeAt(fragmentStack.size - 1)
|
|
||||||
val fragment = Class.forName(stackItem.className).newInstance() as Fragment
|
|
||||||
stackItem.arguments?.let(fragment::setArguments)
|
|
||||||
stackItem.savedState?.let(fragment::setInitialSavedState)
|
|
||||||
replaceFragment(fragment, false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideKeyboard() {
|
|
||||||
(getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
|
|
||||||
?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachFragment(fragment: Fragment) {
|
|
||||||
super.onAttachFragment(fragment)
|
|
||||||
hideKeyboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun onToolbarCreated(toolbar: Toolbar) {
|
|
||||||
if (fragmentStack.isNotEmpty()) {
|
|
||||||
toolbar.navigationIcon =
|
|
||||||
toolbar.context.getDrawableFromAttr(android.R.attr.homeAsUpIndicator)
|
|
||||||
toolbar.setNavigationOnClickListener { onBackPressed() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
|
||||||
super.onNewIntent(intent)
|
|
||||||
handleIntent(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected val Intent.packageName: String?
|
|
||||||
get() {
|
|
||||||
val uri = data
|
|
||||||
return when {
|
|
||||||
uri?.scheme == "package" || uri?.scheme == "fdroid.app" -> {
|
|
||||||
uri.schemeSpecificPart?.nullIfEmpty()
|
|
||||||
}
|
|
||||||
uri?.scheme == "market" && uri.host == "details" -> {
|
|
||||||
uri.getQueryParameter("id")?.nullIfEmpty()
|
|
||||||
}
|
|
||||||
uri != null && uri.scheme in setOf("http", "https") -> {
|
|
||||||
val host = uri.host.orEmpty()
|
|
||||||
if (host == "f-droid.org" || host.endsWith(".f-droid.org")) {
|
|
||||||
uri.lastPathSegment?.nullIfEmpty()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun handleSpecialIntent(specialIntent: SpecialIntent) {
|
|
||||||
when (specialIntent) {
|
|
||||||
is SpecialIntent.Updates -> {
|
|
||||||
if (currentFragment !is TabsFragment) {
|
|
||||||
fragmentStack.clear()
|
|
||||||
replaceFragment(TabsFragment(), true)
|
|
||||||
}
|
|
||||||
val tabsFragment = currentFragment as TabsFragment
|
|
||||||
tabsFragment.selectUpdates()
|
|
||||||
}
|
|
||||||
is SpecialIntent.Install -> {
|
|
||||||
val packageName = specialIntent.packageName
|
|
||||||
val status = specialIntent.status
|
|
||||||
val promptIntent = specialIntent.promptIntent
|
|
||||||
if (!packageName.isNullOrEmpty() && status != null && promptIntent != null) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
startService(
|
|
||||||
Intent(baseContext, InstallerService::class.java)
|
|
||||||
.putExtra(PackageInstaller.EXTRA_STATUS, status)
|
|
||||||
.putExtra(
|
|
||||||
PackageInstaller.EXTRA_PACKAGE_NAME,
|
|
||||||
packageName
|
|
||||||
)
|
|
||||||
.putExtra(Intent.EXTRA_INTENT, promptIntent)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException("Missing parameters needed to relaunch InstallerService and trigger prompt.")
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
}::class
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun handleIntent(intent: Intent?) {
|
|
||||||
when (intent?.action) {
|
|
||||||
Intent.ACTION_VIEW -> {
|
|
||||||
val packageName = intent.packageName
|
|
||||||
if (!packageName.isNullOrEmpty()) {
|
|
||||||
val fragment = currentFragment
|
|
||||||
if (fragment !is AppDetailFragment || fragment.packageName != packageName) {
|
|
||||||
navigateProduct(packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun navigateProduct(packageName: String) = pushFragment(AppDetailFragment(packageName))
|
|
||||||
internal fun navigatePreferences() = pushFragment(SettingsFragment())
|
|
||||||
}
|
|
@ -4,11 +4,12 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.looker.droidify.databinding.FragmentBinding
|
import com.looker.droidify.databinding.FragmentBinding
|
||||||
|
|
||||||
open class ScreenFragment : BaseFragment() {
|
open class ScreenFragment : Fragment() {
|
||||||
private var _fragmentBinding: FragmentBinding? = null
|
private var _fragmentBinding: FragmentBinding? = null
|
||||||
val fragmentBinding get() = _fragmentBinding!!
|
val fragmentBinding get() = _fragmentBinding!!
|
||||||
|
|
||||||
|
@ -1,433 +0,0 @@
|
|||||||
package com.looker.droidify.screen
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.InputFilter
|
|
||||||
import android.text.InputType
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.widget.LinearLayoutCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.material.circularreveal.CircularRevealFrameLayout
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import com.looker.droidify.BuildConfig
|
|
||||||
import com.looker.droidify.R
|
|
||||||
import com.looker.droidify.content.Preferences
|
|
||||||
import com.looker.droidify.databinding.PreferenceItemBinding
|
|
||||||
import com.looker.droidify.utility.Utils.getLocaleOfCode
|
|
||||||
import com.looker.droidify.utility.Utils.languagesList
|
|
||||||
import com.looker.droidify.utility.Utils.translateLocale
|
|
||||||
import com.looker.droidify.utility.extension.resources.*
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SettingsFragment : ScreenFragment() {
|
|
||||||
|
|
||||||
private var preferenceBinding: PreferenceItemBinding? = null
|
|
||||||
private val preferences = mutableMapOf<Preferences.Key<*>, Preference<*>>()
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
preferences.forEach { (_, preference) -> preference.update() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
preferenceBinding = PreferenceItemBinding.inflate(layoutInflater)
|
|
||||||
screenActivity.onToolbarCreated(toolbar)
|
|
||||||
collapsingToolbar.title = getString(R.string.settings)
|
|
||||||
|
|
||||||
val content = fragmentBinding.fragmentContent
|
|
||||||
val scroll = NestedScrollView(content.context)
|
|
||||||
scroll.id = R.id.preferences_list
|
|
||||||
scroll.isFillViewport = true
|
|
||||||
content.addView(
|
|
||||||
scroll,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
val scrollLayout = CircularRevealFrameLayout(content.context)
|
|
||||||
scroll.addView(
|
|
||||||
scrollLayout,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
val preferences = LinearLayoutCompat(scrollLayout.context)
|
|
||||||
preferences.orientation = LinearLayoutCompat.VERTICAL
|
|
||||||
scrollLayout.addView(
|
|
||||||
preferences,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
|
|
||||||
preferences.addCategory(requireContext().getString(R.string.prefs_personalization)) {
|
|
||||||
addList(
|
|
||||||
Preferences.Key.Language,
|
|
||||||
context.getString(R.string.prefs_language_title),
|
|
||||||
languagesList
|
|
||||||
) { translateLocale(context.getLocaleOfCode(it)) }
|
|
||||||
addEnumeration(Preferences.Key.Theme, getString(R.string.theme)) {
|
|
||||||
when (it) {
|
|
||||||
is Preferences.Theme.System -> getString(R.string.system)
|
|
||||||
is Preferences.Theme.AmoledSystem -> getString(R.string.system) + " " + getString(
|
|
||||||
R.string.amoled
|
|
||||||
)
|
|
||||||
is Preferences.Theme.Light -> getString(R.string.light)
|
|
||||||
is Preferences.Theme.Dark -> getString(R.string.dark)
|
|
||||||
is Preferences.Theme.Amoled -> getString(R.string.amoled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addSwitch(
|
|
||||||
Preferences.Key.ListAnimation, getString(R.string.list_animation),
|
|
||||||
getString(R.string.list_animation_description)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
preferences.addCategory(getString(R.string.updates)) {
|
|
||||||
addEnumeration(
|
|
||||||
Preferences.Key.AutoSync,
|
|
||||||
getString(R.string.sync_repositories_automatically)
|
|
||||||
) {
|
|
||||||
when (it) {
|
|
||||||
Preferences.AutoSync.Never -> getString(R.string.never)
|
|
||||||
Preferences.AutoSync.Wifi -> getString(R.string.only_on_wifi)
|
|
||||||
Preferences.AutoSync.WifiBattery -> getString(R.string.only_on_wifi_and_battery)
|
|
||||||
Preferences.AutoSync.Always -> getString(R.string.always)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addSwitch(
|
|
||||||
Preferences.Key.InstallAfterSync, getString(R.string.install_after_sync),
|
|
||||||
getString(R.string.install_after_sync_summary)
|
|
||||||
)
|
|
||||||
addSwitch(
|
|
||||||
Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates),
|
|
||||||
getString(R.string.notify_about_updates_summary)
|
|
||||||
)
|
|
||||||
addSwitch(
|
|
||||||
Preferences.Key.UpdateUnstable, getString(R.string.unstable_updates),
|
|
||||||
getString(R.string.unstable_updates_summary)
|
|
||||||
)
|
|
||||||
addSwitch(
|
|
||||||
Preferences.Key.IncompatibleVersions, getString(R.string.incompatible_versions),
|
|
||||||
getString(R.string.incompatible_versions_summary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
preferences.addCategory(getString(R.string.proxy)) {
|
|
||||||
addEnumeration(Preferences.Key.ProxyType, getString(R.string.proxy_type)) {
|
|
||||||
when (it) {
|
|
||||||
is Preferences.ProxyType.Direct -> getString(R.string.no_proxy)
|
|
||||||
is Preferences.ProxyType.Http -> getString(R.string.http_proxy)
|
|
||||||
is Preferences.ProxyType.Socks -> getString(R.string.socks_proxy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addEditString(Preferences.Key.ProxyHost, getString(R.string.proxy_host))
|
|
||||||
addEditInt(Preferences.Key.ProxyPort, getString(R.string.proxy_port), 1..65535)
|
|
||||||
}
|
|
||||||
preferences.addCategory(getString(R.string.install_types)) {
|
|
||||||
addSwitch(
|
|
||||||
Preferences.Key.RootPermission, getString(R.string.root_permission),
|
|
||||||
getString(R.string.root_permission_description)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
preferences.addCategory(getString(R.string.credits)) {
|
|
||||||
addText(
|
|
||||||
title = "Based on Foxy-Droid",
|
|
||||||
summary = "FoxyDroid",
|
|
||||||
url = "https://github.com/kitsunyan/foxy-droid/"
|
|
||||||
)
|
|
||||||
addText(
|
|
||||||
title = getString(R.string.application_name),
|
|
||||||
summary = "v ${BuildConfig.VERSION_NAME}",
|
|
||||||
url = "https://github.com/iamlooker/Droid-ify/"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
Preferences.subject
|
|
||||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
|
||||||
.collect { updatePreference(it) }
|
|
||||||
}
|
|
||||||
updatePreference(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LinearLayoutCompat.addText(title: String, summary: String, url: String) {
|
|
||||||
val text = MaterialTextView(context)
|
|
||||||
val subText = MaterialTextView(context)
|
|
||||||
text.text = title
|
|
||||||
subText.text = summary
|
|
||||||
text.setTextSizeScaled(16)
|
|
||||||
subText.setTextSizeScaled(14)
|
|
||||||
resources.sizeScaled(16).let {
|
|
||||||
text.setPadding(it, it, 5, 5)
|
|
||||||
subText.setPadding(it, 5, 5, 25)
|
|
||||||
}
|
|
||||||
addView(
|
|
||||||
text,
|
|
||||||
LinearLayoutCompat.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
addView(
|
|
||||||
subText,
|
|
||||||
LinearLayoutCompat.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
text.setOnClickListener { openURI(url.toUri()) }
|
|
||||||
subText.setOnClickListener { openURI(url.toUri()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openURI(url: Uri) {
|
|
||||||
startActivity(Intent(Intent.ACTION_VIEW, url))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
preferences.clear()
|
|
||||||
preferenceBinding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePreference(key: Preferences.Key<*>?) {
|
|
||||||
if (key != null) {
|
|
||||||
preferences[key]?.update()
|
|
||||||
}
|
|
||||||
if (key == null || key == Preferences.Key.ProxyType) {
|
|
||||||
val enabled = when (Preferences[Preferences.Key.ProxyType]) {
|
|
||||||
is Preferences.ProxyType.Direct -> false
|
|
||||||
is Preferences.ProxyType.Http, is Preferences.ProxyType.Socks -> true
|
|
||||||
}
|
|
||||||
preferences[Preferences.Key.ProxyHost]?.setEnabled(enabled)
|
|
||||||
preferences[Preferences.Key.ProxyPort]?.setEnabled(enabled)
|
|
||||||
}
|
|
||||||
if (key == Preferences.Key.RootPermission) {
|
|
||||||
preferences[Preferences.Key.RootPermission]?.setEnabled(
|
|
||||||
Shell.getCachedShell()?.isRoot
|
|
||||||
?: Shell.getShell().isRoot
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (key == Preferences.Key.Theme) {
|
|
||||||
requireActivity().recreate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun LinearLayoutCompat.addCategory(
|
|
||||||
title: String,
|
|
||||||
callback: LinearLayoutCompat.() -> Unit,
|
|
||||||
) {
|
|
||||||
val text = MaterialTextView(context)
|
|
||||||
text.typeface = TypefaceExtra.medium
|
|
||||||
text.setTextSizeScaled(14)
|
|
||||||
text.setTextColor(text.context.getColorFromAttr(R.attr.colorPrimary))
|
|
||||||
text.text = title
|
|
||||||
resources.sizeScaled(16).let { text.setPadding(it, it, it, 0) }
|
|
||||||
addView(
|
|
||||||
text,
|
|
||||||
LinearLayoutCompat.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayoutCompat.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> LinearLayoutCompat.addPreference(
|
|
||||||
key: Preferences.Key<T>, title: String,
|
|
||||||
summaryProvider: () -> String, dialogProvider: ((Context) -> AlertDialog)?,
|
|
||||||
): Preference<T> {
|
|
||||||
val preference =
|
|
||||||
Preference(key, this@SettingsFragment, this, title, summaryProvider, dialogProvider)
|
|
||||||
preferences[key] = preference
|
|
||||||
return preference
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LinearLayoutCompat.addSwitch(
|
|
||||||
key: Preferences.Key<Boolean>,
|
|
||||||
title: String,
|
|
||||||
summary: String,
|
|
||||||
) {
|
|
||||||
val preference = addPreference(key, title, { summary }, null)
|
|
||||||
preference.check.visibility = View.VISIBLE
|
|
||||||
preference.view.setOnClickListener { Preferences[key] = !Preferences[key] }
|
|
||||||
preference.setCallback { preference.check.isChecked = Preferences[key] }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> LinearLayoutCompat.addEdit(
|
|
||||||
key: Preferences.Key<T>, title: String, valueToString: (T) -> String,
|
|
||||||
stringToValue: (String) -> T?, configureEdit: (TextInputEditText) -> Unit,
|
|
||||||
) {
|
|
||||||
addPreference(key, title, { valueToString(Preferences[key]) }) { it ->
|
|
||||||
val scroll = NestedScrollView(it)
|
|
||||||
scroll.resources.sizeScaled(20).let { scroll.setPadding(it, 0, it, 0) }
|
|
||||||
val edit = TextInputEditText(it)
|
|
||||||
configureEdit(edit)
|
|
||||||
edit.id = android.R.id.edit
|
|
||||||
edit.resources.sizeScaled(16)
|
|
||||||
.let { edit.setPadding(edit.paddingLeft, it, edit.paddingRight, it) }
|
|
||||||
edit.setText(valueToString(Preferences[key]))
|
|
||||||
edit.hint = edit.text.toString()
|
|
||||||
edit.text?.let { editable -> edit.setSelection(editable.length) }
|
|
||||||
edit.requestFocus()
|
|
||||||
scroll.addView(
|
|
||||||
edit,
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
MaterialAlertDialogBuilder(it)
|
|
||||||
.setTitle(title)
|
|
||||||
.setView(scroll)
|
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
|
||||||
val value = stringToValue(edit.text.toString()) ?: key.default.value
|
|
||||||
post { Preferences[key] = value }
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
.apply {
|
|
||||||
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LinearLayoutCompat.addEditString(key: Preferences.Key<String>, title: String) {
|
|
||||||
addEdit(key, title, { it }, { it }, { })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun LinearLayoutCompat.addEditInt(
|
|
||||||
key: Preferences.Key<Int>,
|
|
||||||
title: String,
|
|
||||||
range: IntRange?,
|
|
||||||
) {
|
|
||||||
addEdit(key, title, { it.toString() }, { it.toIntOrNull() }) {
|
|
||||||
it.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
||||||
if (range != null) it.filters =
|
|
||||||
arrayOf(InputFilter { source, start, end, dest, dstart, dend ->
|
|
||||||
val value = (dest.substring(0, dstart) + source.substring(start, end) +
|
|
||||||
dest.substring(dend, dest.length)).toIntOrNull()
|
|
||||||
if (value != null && value in range) null else ""
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : Preferences.Enumeration<T>> LinearLayoutCompat.addEnumeration(
|
|
||||||
key: Preferences.Key<T>,
|
|
||||||
title: String,
|
|
||||||
valueToString: (T) -> String,
|
|
||||||
) {
|
|
||||||
addPreference(key, title, { valueToString(Preferences[key]) }) {
|
|
||||||
val values = key.default.value.values
|
|
||||||
MaterialAlertDialogBuilder(it)
|
|
||||||
.setTitle(title)
|
|
||||||
.setSingleChoiceItems(
|
|
||||||
values.map(valueToString).toTypedArray(),
|
|
||||||
values.indexOf(Preferences[key])
|
|
||||||
) { dialog, which ->
|
|
||||||
dialog.dismiss()
|
|
||||||
post { Preferences[key] = values[which] }
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> LinearLayoutCompat.addList(
|
|
||||||
key: Preferences.Key<T>,
|
|
||||||
title: String,
|
|
||||||
values: List<T>,
|
|
||||||
valueToString: (T) -> String,
|
|
||||||
) {
|
|
||||||
addPreference(key, title, { valueToString(Preferences[key]) }) {
|
|
||||||
MaterialAlertDialogBuilder(it)
|
|
||||||
.setTitle(title)
|
|
||||||
.setSingleChoiceItems(
|
|
||||||
values.map(valueToString).toTypedArray(),
|
|
||||||
values.indexOf(Preferences[key])
|
|
||||||
) { dialog, which ->
|
|
||||||
dialog.dismiss()
|
|
||||||
post { Preferences[key] = values[which] }
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Preference<T>(
|
|
||||||
private val key: Preferences.Key<T>,
|
|
||||||
fragment: Fragment,
|
|
||||||
parent: ViewGroup,
|
|
||||||
titleText: String,
|
|
||||||
private val summaryProvider: () -> String,
|
|
||||||
private val dialogProvider: ((Context) -> AlertDialog)?,
|
|
||||||
) {
|
|
||||||
val view = parent.inflate(R.layout.preference_item)
|
|
||||||
val title = view.findViewById<MaterialTextView>(R.id.title)!!
|
|
||||||
val summary = view.findViewById<MaterialTextView>(R.id.summary)!!
|
|
||||||
val check = view.findViewById<SwitchMaterial>(R.id.check)!!
|
|
||||||
|
|
||||||
private var callback: (() -> Unit)? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
title.text = titleText
|
|
||||||
parent.addView(view)
|
|
||||||
if (dialogProvider != null) {
|
|
||||||
view.setOnClickListener {
|
|
||||||
PreferenceDialog(key.name)
|
|
||||||
.show(
|
|
||||||
fragment.childFragmentManager,
|
|
||||||
"${PreferenceDialog::class.java.name}.${key.name}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setCallback(callback: () -> Unit) {
|
|
||||||
this.callback = callback
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEnabled(enabled: Boolean) {
|
|
||||||
view.isEnabled = enabled
|
|
||||||
title.isEnabled = enabled
|
|
||||||
summary.isEnabled = enabled
|
|
||||||
check.isEnabled = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update() {
|
|
||||||
summary.text = summaryProvider()
|
|
||||||
summary.visibility = if (summary.text.isNotEmpty()) View.VISIBLE else View.GONE
|
|
||||||
callback?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createDialog(context: Context): AlertDialog {
|
|
||||||
return dialogProvider!!(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PreferenceDialog() : DialogFragment() {
|
|
||||||
companion object {
|
|
||||||
private const val EXTRA_KEY = "key"
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(key: String) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putString(EXTRA_KEY, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val preferences = (parentFragment as SettingsFragment).preferences
|
|
||||||
val key = requireArguments().getString(EXTRA_KEY)!!
|
|
||||||
.let { name -> preferences.keys.find { it.name == name }!! }
|
|
||||||
val preference = preferences[key]!!
|
|
||||||
return preference.createDialog(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,598 +0,0 @@
|
|||||||
package com.looker.droidify.screen
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AccelerateInterpolator
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import com.looker.droidify.R
|
|
||||||
import com.looker.droidify.content.Preferences
|
|
||||||
import com.looker.droidify.databinding.TabsToolbarBinding
|
|
||||||
import com.looker.droidify.entity.Section
|
|
||||||
import com.looker.droidify.service.Connection
|
|
||||||
import com.looker.droidify.service.SyncService
|
|
||||||
import com.looker.droidify.ui.fragments.AppListFragment
|
|
||||||
import com.looker.droidify.utility.RxUtils
|
|
||||||
import com.looker.droidify.utility.Utils
|
|
||||||
import com.looker.droidify.utility.extension.android.Android
|
|
||||||
import com.looker.droidify.utility.extension.resources.getDrawableCompat
|
|
||||||
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
|
||||||
import com.looker.droidify.utility.extension.resources.sizeScaled
|
|
||||||
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 kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class TabsFragment : ScreenFragment() {
|
|
||||||
|
|
||||||
private var _tabsBinding: TabsToolbarBinding? = null
|
|
||||||
private val tabsBinding get() = _tabsBinding!!
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val STATE_SEARCH_FOCUSED = "searchFocused"
|
|
||||||
private const val STATE_SEARCH_QUERY = "searchQuery"
|
|
||||||
private const val STATE_SHOW_SECTIONS = "showSections"
|
|
||||||
private const val STATE_SECTIONS = "sections"
|
|
||||||
private const val STATE_SECTION = "section"
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Layout(view: TabsToolbarBinding) {
|
|
||||||
val tabs = view.tabs
|
|
||||||
val sectionLayout = view.sectionLayout
|
|
||||||
val sectionChange = view.sectionChange
|
|
||||||
val sectionName = view.sectionName
|
|
||||||
val sectionIcon = view.sectionIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
private var searchMenuItem: MenuItem? = null
|
|
||||||
private var sortOrderMenu: Pair<MenuItem, List<MenuItem>>? = null
|
|
||||||
private var syncRepositoriesMenuItem: MenuItem? = null
|
|
||||||
private var layout: Layout? = null
|
|
||||||
private var sectionsList: RecyclerView? = null
|
|
||||||
private var viewPager: ViewPager2? = null
|
|
||||||
|
|
||||||
private var showSections = false
|
|
||||||
set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
field = value
|
|
||||||
val layout = layout
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var searchQuery = ""
|
|
||||||
private var sections = listOf<Section>(Section.All)
|
|
||||||
private var section: Section = Section.All
|
|
||||||
|
|
||||||
private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ ->
|
|
||||||
viewPager?.let {
|
|
||||||
val source = AppListFragment.Source.values()[it.currentItem]
|
|
||||||
updateUpdateNotificationBlocker(source)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
private var categoriesDisposable: Disposable? = null
|
|
||||||
private var repositoriesDisposable: Disposable? = null
|
|
||||||
private var sectionsAnimator: ValueAnimator? = null
|
|
||||||
|
|
||||||
private var needSelectUpdates = false
|
|
||||||
|
|
||||||
private val productFragments: Sequence<AppListFragment>
|
|
||||||
get() = if (host == null) emptySequence() else
|
|
||||||
childFragmentManager.fragments.asSequence().mapNotNull { it as? AppListFragment }
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
_tabsBinding = TabsToolbarBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
syncConnection.bind(requireContext())
|
|
||||||
|
|
||||||
screenActivity.onToolbarCreated(toolbar)
|
|
||||||
collapsingToolbar.title = getString(R.string.application_name)
|
|
||||||
// Move focus from SearchView to Toolbar
|
|
||||||
toolbar.isFocusableInTouchMode = true
|
|
||||||
|
|
||||||
val searchView = FocusSearchView(toolbar.context).apply {
|
|
||||||
maxWidth = Int.MAX_VALUE
|
|
||||||
queryHint = getString(R.string.search)
|
|
||||||
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
clearFocus()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
if (isResumed) {
|
|
||||||
searchQuery = newText.orEmpty()
|
|
||||||
productFragments.forEach { it.setSearchQuery(newText.orEmpty()) }
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setOnSearchClickListener { fragmentBinding.appbarLayout.setExpanded(false, true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
toolbar.menu.apply {
|
|
||||||
if (Android.sdk(28) && !Android.Device.isHuaweiEmui) {
|
|
||||||
setGroupDividerEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchMenuItem = add(0, R.id.toolbar_search, 0, R.string.search)
|
|
||||||
.setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_search))
|
|
||||||
.setActionView(searchView)
|
|
||||||
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
|
|
||||||
|
|
||||||
sortOrderMenu = addSubMenu(0, 0, 0, R.string.sorting_order)
|
|
||||||
.setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_sort))
|
|
||||||
.let { menu ->
|
|
||||||
menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
|
||||||
val items = Preferences.Key.SortOrder.default.value.values
|
|
||||||
.map { sortOrder ->
|
|
||||||
menu
|
|
||||||
.add(sortOrder.order.titleResId)
|
|
||||||
.setOnMenuItemClickListener {
|
|
||||||
Preferences[Preferences.Key.SortOrder] = sortOrder
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menu.setGroupCheckable(0, true, true)
|
|
||||||
Pair(menu.item, items)
|
|
||||||
}
|
|
||||||
|
|
||||||
syncRepositoriesMenuItem = add(0, 0, 0, R.string.sync_repositories)
|
|
||||||
.setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_sync))
|
|
||||||
.setOnMenuItemClickListener {
|
|
||||||
syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
add(1, 0, 0, R.string.settings)
|
|
||||||
.setOnMenuItemClickListener {
|
|
||||||
view.post { screenActivity.navigatePreferences() }
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchQuery = savedInstanceState?.getString(STATE_SEARCH_QUERY).orEmpty()
|
|
||||||
productFragments.forEach { it.setSearchQuery(searchQuery) }
|
|
||||||
|
|
||||||
val toolbarExtra = fragmentBinding.toolbarExtra
|
|
||||||
toolbarExtra.addView(tabsBinding.root)
|
|
||||||
val layout = Layout(tabsBinding)
|
|
||||||
this.layout = layout
|
|
||||||
|
|
||||||
showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0
|
|
||||||
sections = savedInstanceState?.getParcelableArrayList<Section>(STATE_SECTIONS)
|
|
||||||
.orEmpty()
|
|
||||||
section = savedInstanceState?.getParcelable(STATE_SECTION) ?: Section.All
|
|
||||||
layout.sectionChange.setOnClickListener {
|
|
||||||
showSections = sections
|
|
||||||
.any { it !is Section.All } && !showSections
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOrder()
|
|
||||||
lifecycleScope.launch {
|
|
||||||
Preferences.subject
|
|
||||||
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
|
|
||||||
.collect {
|
|
||||||
if (it == Preferences.Key.SortOrder) updateOrder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val content = fragmentBinding.fragmentContent
|
|
||||||
|
|
||||||
viewPager = ViewPager2(content.context).apply {
|
|
||||||
id = R.id.fragment_pager
|
|
||||||
adapter = object : FragmentStateAdapter(this@TabsFragment) {
|
|
||||||
override fun getItemCount(): Int = AppListFragment.Source.values().size
|
|
||||||
override fun createFragment(position: Int): Fragment = AppListFragment(
|
|
||||||
AppListFragment
|
|
||||||
.Source.values()[position]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
content.addView(this)
|
|
||||||
registerOnPageChangeCallback(pageChangeCallback)
|
|
||||||
offscreenPageLimit = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
viewPager?.let {
|
|
||||||
TabLayoutMediator(layout.tabs, it) { tab, position ->
|
|
||||||
tab.text = getString(AppListFragment.Source.values()[position].titleResId)
|
|
||||||
}.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
categoriesDisposable = 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.categoryDao.allNames } }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe {
|
|
||||||
setSectionsAndUpdate(
|
|
||||||
it.asSequence().sorted()
|
|
||||||
.map(Section::Category).toList(), null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
|
||||||
//.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all } }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { it ->
|
|
||||||
setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
|
|
||||||
.map { Section.Repository(it.id, it.name) }.toList())
|
|
||||||
}
|
|
||||||
updateSection()
|
|
||||||
|
|
||||||
val sectionsList = RecyclerView(toolbar.context).apply {
|
|
||||||
id = R.id.sections_list
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
isMotionEventSplittingEnabled = false
|
|
||||||
isVerticalScrollBarEnabled = false
|
|
||||||
setHasFixedSize(true)
|
|
||||||
val adapter = SectionsAdapter({ sections }) {
|
|
||||||
if (showSections) {
|
|
||||||
showSections = false
|
|
||||||
section = it
|
|
||||||
updateSection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.adapter = adapter
|
|
||||||
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
|
|
||||||
background = context.getDrawableCompat(R.drawable.background_border)
|
|
||||||
elevation = resources.sizeScaled(4).toFloat()
|
|
||||||
content.addView(this)
|
|
||||||
val margins = resources.sizeScaled(8)
|
|
||||||
(layoutParams as ViewGroup.MarginLayoutParams).setMargins(margins, margins, margins, 0)
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
this.sectionsList = sectionsList
|
|
||||||
|
|
||||||
var lastContentHeight = -1
|
|
||||||
content.viewTreeObserver.addOnGlobalLayoutListener {
|
|
||||||
if (this.view != null) {
|
|
||||||
val initial = lastContentHeight <= 0
|
|
||||||
val contentHeight = content.height
|
|
||||||
if (lastContentHeight != contentHeight) {
|
|
||||||
lastContentHeight = contentHeight
|
|
||||||
if (initial) {
|
|
||||||
sectionsList.layoutParams.height = if (showSections) contentHeight else 0
|
|
||||||
sectionsList.visibility = if (showSections) View.VISIBLE else View.GONE
|
|
||||||
sectionsList.requestLayout()
|
|
||||||
} else {
|
|
||||||
animateSectionsList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
|
|
||||||
searchMenuItem = null
|
|
||||||
sortOrderMenu = null
|
|
||||||
syncRepositoriesMenuItem = null
|
|
||||||
layout = null
|
|
||||||
sectionsList = null
|
|
||||||
viewPager = null
|
|
||||||
|
|
||||||
syncConnection.unbind(requireContext())
|
|
||||||
categoriesDisposable?.dispose()
|
|
||||||
categoriesDisposable = null
|
|
||||||
repositoriesDisposable?.dispose()
|
|
||||||
repositoriesDisposable = null
|
|
||||||
sectionsAnimator?.cancel()
|
|
||||||
sectionsAnimator = null
|
|
||||||
|
|
||||||
_tabsBinding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
|
|
||||||
outState.putBoolean(STATE_SEARCH_FOCUSED, searchMenuItem?.actionView?.hasFocus() == true)
|
|
||||||
outState.putString(STATE_SEARCH_QUERY, searchQuery)
|
|
||||||
outState.putByte(STATE_SHOW_SECTIONS, if (showSections) 1 else 0)
|
|
||||||
outState.putParcelableArrayList(STATE_SECTIONS, ArrayList(sections))
|
|
||||||
outState.putParcelable(STATE_SECTION, section)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
|
||||||
super.onViewStateRestored(savedInstanceState)
|
|
||||||
|
|
||||||
(searchMenuItem?.actionView as FocusSearchView).allowFocus = true
|
|
||||||
if (needSelectUpdates) {
|
|
||||||
needSelectUpdates = false
|
|
||||||
selectUpdatesInternal(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
|
||||||
return when {
|
|
||||||
searchMenuItem?.isActionViewExpanded == true -> {
|
|
||||||
searchMenuItem?.collapseActionView()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
showSections -> {
|
|
||||||
showSections = false
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun selectUpdates() = selectUpdatesInternal(true)
|
|
||||||
|
|
||||||
private fun selectUpdatesInternal(allowSmooth: Boolean) {
|
|
||||||
if (view != null) {
|
|
||||||
val viewPager = viewPager
|
|
||||||
viewPager?.setCurrentItem(
|
|
||||||
AppListFragment.Source.UPDATES.ordinal,
|
|
||||||
allowSmooth && viewPager.isLaidOut
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
needSelectUpdates = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateUpdateNotificationBlocker(activeSource: AppListFragment.Source) {
|
|
||||||
val blockerFragment = if (activeSource == AppListFragment.Source.UPDATES) {
|
|
||||||
productFragments.find { it.source == activeSource }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
syncConnection.binder?.setUpdateNotificationBlocker(blockerFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateOrder() {
|
|
||||||
val order = Preferences[Preferences.Key.SortOrder].order
|
|
||||||
sortOrderMenu!!.second[order.ordinal].isChecked = true
|
|
||||||
productFragments.forEach { it.setOrder(order) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T : 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<Section.Category>?,
|
|
||||||
repositories: List<Section.Repository>?,
|
|
||||||
) {
|
|
||||||
val oldCategories = collectOldSections(categories)
|
|
||||||
val oldRepositories = collectOldSections(repositories)
|
|
||||||
if (oldCategories == null || oldRepositories == null) {
|
|
||||||
sections = listOf(Section.All) +
|
|
||||||
(categories ?: oldCategories).orEmpty() +
|
|
||||||
(repositories ?: oldRepositories).orEmpty()
|
|
||||||
updateSection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSection() {
|
|
||||||
if (section !in sections) {
|
|
||||||
section = Section.All
|
|
||||||
}
|
|
||||||
layout?.sectionName?.text = when (val section = section) {
|
|
||||||
is Section.All -> getString(R.string.all_applications)
|
|
||||||
is Section.Category -> section.name
|
|
||||||
is Section.Repository -> section.name
|
|
||||||
}
|
|
||||||
layout?.sectionIcon?.visibility =
|
|
||||||
if (sections.any { it !is Section.All }) View.VISIBLE else View.GONE
|
|
||||||
productFragments.forEach { it.setSection(section) }
|
|
||||||
sectionsList?.adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateSectionsList() {
|
|
||||||
val sectionsList = sectionsList!!
|
|
||||||
val value = if (sectionsList.visibility != View.VISIBLE) 0f else
|
|
||||||
sectionsList.height.toFloat() / (sectionsList.parent as View).height
|
|
||||||
val target = if (showSections) 0.98f else 0f
|
|
||||||
sectionsAnimator?.cancel()
|
|
||||||
sectionsAnimator = null
|
|
||||||
|
|
||||||
if (value != target) {
|
|
||||||
sectionsAnimator = ValueAnimator.ofFloat(value, target).apply {
|
|
||||||
duration = (250 * abs(target - value)).toLong()
|
|
||||||
interpolator =
|
|
||||||
if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f)
|
|
||||||
addUpdateListener {
|
|
||||||
val newValue = animatedValue as Float
|
|
||||||
sectionsList.apply {
|
|
||||||
val height = ((parent as View).height * newValue).toInt()
|
|
||||||
val visible = height > 0
|
|
||||||
if ((visibility == View.VISIBLE) != visible) {
|
|
||||||
visibility = if (visible) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
if (layoutParams.height != height) {
|
|
||||||
layoutParams.height = height
|
|
||||||
requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (target <= 0f && newValue <= 0f || target >= 1f && newValue >= 1f) {
|
|
||||||
sectionsAnimator = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageScrolled(
|
|
||||||
position: Int,
|
|
||||||
positionOffset: Float,
|
|
||||||
positionOffsetPixels: Int,
|
|
||||||
) {
|
|
||||||
val layout = layout!!
|
|
||||||
val fromSections = AppListFragment.Source.values()[position].sections
|
|
||||||
val toSections = if (positionOffset <= 0f) fromSections else
|
|
||||||
AppListFragment.Source.values()[position + 1].sections
|
|
||||||
val offset = if (fromSections != toSections) {
|
|
||||||
if (fromSections) 1f - positionOffset else positionOffset
|
|
||||||
} else {
|
|
||||||
if (fromSections) 1f else 0f
|
|
||||||
}
|
|
||||||
assert(layout.sectionLayout.childCount == 1)
|
|
||||||
val child = layout.sectionLayout.getChildAt(0)
|
|
||||||
val height = child.layoutParams.height
|
|
||||||
assert(height > 0)
|
|
||||||
val currentHeight = (offset * height).roundToInt()
|
|
||||||
if (layout.sectionLayout.layoutParams.height != currentHeight) {
|
|
||||||
layout.sectionLayout.layoutParams.height = currentHeight
|
|
||||||
layout.sectionLayout.requestLayout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
val source = AppListFragment.Source.values()[position]
|
|
||||||
updateUpdateNotificationBlocker(source)
|
|
||||||
sortOrderMenu!!.first.apply {
|
|
||||||
isVisible = source.order
|
|
||||||
setShowAsActionFlags(
|
|
||||||
if (!source.order ||
|
|
||||||
resources.configuration.screenWidthDp >= 400
|
|
||||||
) MenuItem.SHOW_AS_ACTION_ALWAYS else 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
syncRepositoriesMenuItem!!.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS)
|
|
||||||
if (showSections && !source.sections) {
|
|
||||||
showSections = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageScrollStateChanged(state: Int) {
|
|
||||||
val source = AppListFragment.Source.values()[viewPager!!.currentItem]
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SectionsAdapter(
|
|
||||||
private val sections: () -> List<Section>,
|
|
||||||
private val onClick: (Section) -> Unit,
|
|
||||||
) : StableRecyclerAdapter<SectionsAdapter.ViewType,
|
|
||||||
RecyclerView.ViewHolder>() {
|
|
||||||
enum class ViewType { SECTION }
|
|
||||||
|
|
||||||
private class SectionViewHolder(context: Context) :
|
|
||||||
RecyclerView.ViewHolder(MaterialTextView(context)) {
|
|
||||||
val title: MaterialTextView
|
|
||||||
get() = itemView as MaterialTextView
|
|
||||||
|
|
||||||
init {
|
|
||||||
itemView as MaterialTextView
|
|
||||||
itemView.gravity = Gravity.CENTER_VERTICAL
|
|
||||||
itemView.resources.sizeScaled(16).let { itemView.setPadding(it, 0, it, 0) }
|
|
||||||
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,
|
|
||||||
) {
|
|
||||||
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(
|
|
||||||
needDivider = true,
|
|
||||||
toTop = false,
|
|
||||||
paddingStart = padding,
|
|
||||||
paddingEnd = padding
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
configuration.set(
|
|
||||||
needDivider = false,
|
|
||||||
toTop = false,
|
|
||||||
paddingStart = 0,
|
|
||||||
paddingEnd = 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val viewTypeClass: Class<ViewType>
|
|
||||||
get() = ViewType::class.java
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = sections().size
|
|
||||||
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 {
|
|
||||||
return SectionViewHolder(parent.context).apply {
|
|
||||||
itemView.setOnClickListener { onClick(sections()[adapterPosition]) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
holder as SectionViewHolder
|
|
||||||
val section = sections()[position]
|
|
||||||
val previousSection = sections().getOrNull(position - 1)
|
|
||||||
val nextSection = sections().getOrNull(position + 1)
|
|
||||||
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
|
|
||||||
layoutParams.bottomMargin = if (nextSection == null ||
|
|
||||||
section.javaClass != nextSection.javaClass
|
|
||||||
) margin else 0
|
|
||||||
holder.title.text = when (section) {
|
|
||||||
is Section.All -> holder.itemView.resources.getString(R.string.all_applications)
|
|
||||||
is Section.Category -> section.name
|
|
||||||
is Section.Repository -> section.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
package com.looker.droidify.ui.adapters
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.AnimationUtils
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.load
|
|
||||||
import coil.transform.RoundedCornersTransformation
|
|
||||||
import com.google.android.material.circularreveal.CircularRevealFrameLayout
|
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
|
||||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import com.looker.droidify.R
|
|
||||||
import com.looker.droidify.content.Preferences
|
|
||||||
import com.looker.droidify.database.entity.Repository
|
|
||||||
import com.looker.droidify.entity.ProductItem
|
|
||||||
import com.looker.droidify.network.CoilDownloader
|
|
||||||
import com.looker.droidify.utility.Utils
|
|
||||||
import com.looker.droidify.utility.extension.resources.*
|
|
||||||
import com.looker.droidify.utility.extension.text.nullIfEmpty
|
|
||||||
import com.looker.droidify.utility.getProduct
|
|
||||||
import com.looker.droidify.widget.CursorRecyclerAdapter
|
|
||||||
|
|
||||||
class AppListAdapter(private val onClick: (ProductItem) -> Unit) :
|
|
||||||
CursorRecyclerAdapter<AppListAdapter.ViewType, RecyclerView.ViewHolder>() {
|
|
||||||
private var lastPosition = 0
|
|
||||||
|
|
||||||
enum class ViewType { PRODUCT, LOADING, EMPTY }
|
|
||||||
|
|
||||||
private class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
val name = itemView.findViewById<MaterialTextView>(R.id.name)!!
|
|
||||||
val status = itemView.findViewById<MaterialTextView>(R.id.status)!!
|
|
||||||
val summary = itemView.findViewById<MaterialTextView>(R.id.summary)!!
|
|
||||||
val icon = itemView.findViewById<ShapeableImageView>(R.id.icon)!!
|
|
||||||
|
|
||||||
val progressIcon: Drawable
|
|
||||||
val defaultIcon: Drawable
|
|
||||||
|
|
||||||
init {
|
|
||||||
val (progressIcon, defaultIcon) = Utils.getDefaultApplicationIcons(icon.context)
|
|
||||||
this.progressIcon = progressIcon
|
|
||||||
this.defaultIcon = defaultIcon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoadingViewHolder(context: Context) :
|
|
||||||
RecyclerView.ViewHolder(CircularRevealFrameLayout(context)) {
|
|
||||||
init {
|
|
||||||
itemView as CircularRevealFrameLayout
|
|
||||||
val progressBar = CircularProgressIndicator(itemView.context)
|
|
||||||
itemView.addView(progressBar)
|
|
||||||
itemView.layoutParams = RecyclerView.LayoutParams(
|
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EmptyViewHolder(context: Context) :
|
|
||||||
RecyclerView.ViewHolder(MaterialTextView(context)) {
|
|
||||||
val text: MaterialTextView
|
|
||||||
get() = itemView as MaterialTextView
|
|
||||||
|
|
||||||
init {
|
|
||||||
itemView as MaterialTextView
|
|
||||||
itemView.gravity = Gravity.CENTER
|
|
||||||
itemView.resources.sizeScaled(20).let { itemView.setPadding(it, it, it, it) }
|
|
||||||
itemView.typeface = TypefaceExtra.light
|
|
||||||
itemView.setTextColor(context.getColorFromAttr(android.R.attr.colorPrimary))
|
|
||||||
itemView.setTextSizeScaled(20)
|
|
||||||
itemView.layoutParams = RecyclerView.LayoutParams(
|
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT,
|
|
||||||
RecyclerView.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var repositories: Map<Long, Repository> = emptyMap()
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
var emptyText: String = ""
|
|
||||||
set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
field = value
|
|
||||||
if (isEmpty) {
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val viewTypeClass: Class<ViewType>
|
|
||||||
get() = ViewType::class.java
|
|
||||||
|
|
||||||
private val isEmpty: Boolean
|
|
||||||
get() = super.getItemCount() == 0
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = if (isEmpty) 1 else super.getItemCount()
|
|
||||||
override fun getItemId(position: Int): Long = if (isEmpty) -1 else super.getItemId(position)
|
|
||||||
|
|
||||||
override fun getItemEnumViewType(position: Int): ViewType {
|
|
||||||
return when {
|
|
||||||
!isEmpty -> ViewType.PRODUCT
|
|
||||||
cursor == null -> ViewType.LOADING
|
|
||||||
else -> ViewType.EMPTY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getProductItem(position: Int): ProductItem {
|
|
||||||
return moveTo(position).getProduct().item()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)) }
|
|
||||||
}
|
|
||||||
ViewType.LOADING -> LoadingViewHolder(parent.context)
|
|
||||||
ViewType.EMPTY -> EmptyViewHolder(parent.context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
|
||||||
super.onViewDetachedFromWindow(holder)
|
|
||||||
if (Preferences[Preferences.Key.ListAnimation]) {
|
|
||||||
holder.itemView.clearAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (getItemEnumViewType(position)) {
|
|
||||||
ViewType.PRODUCT -> {
|
|
||||||
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
|
|
||||||
val repository: Repository? = repositories[productItem.repositoryId]
|
|
||||||
holder.icon.load(
|
|
||||||
repository?.let {
|
|
||||||
CoilDownloader.createIconUri(
|
|
||||||
holder.icon, productItem.packageName,
|
|
||||||
productItem.icon, productItem.metadataIcon, it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
transformations(RoundedCornersTransformation(4.toPx))
|
|
||||||
placeholder(holder.progressIcon)
|
|
||||||
error(holder.defaultIcon)
|
|
||||||
}
|
|
||||||
holder.status.apply {
|
|
||||||
if (productItem.canUpdate) {
|
|
||||||
text = productItem.version
|
|
||||||
if (background == null) {
|
|
||||||
background =
|
|
||||||
ResourcesCompat.getDrawable(
|
|
||||||
holder.itemView.resources,
|
|
||||||
R.drawable.background_border,
|
|
||||||
context.theme
|
|
||||||
)
|
|
||||||
resources.sizeScaled(6).let { setPadding(it, it, it, it) }
|
|
||||||
backgroundTintList =
|
|
||||||
context.getColorFromAttr(R.attr.colorSecondaryContainer)
|
|
||||||
setTextColor(context.getColorFromAttr(R.attr.colorSecondary))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
text = productItem.installedVersion.nullIfEmpty() ?: productItem.version
|
|
||||||
if (background != null) {
|
|
||||||
setPadding(0, 0, 0, 0)
|
|
||||||
setTextColor(holder.status.context.getColorFromAttr(android.R.attr.colorControlNormal))
|
|
||||||
background = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val enabled = productItem.compatible || productItem.installedVersion.isNotEmpty()
|
|
||||||
sequenceOf(holder.name, holder.status, holder.summary).forEach {
|
|
||||||
it.isEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ViewType.LOADING -> {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
ViewType.EMPTY -> {
|
|
||||||
holder as EmptyViewHolder
|
|
||||||
holder.text.text = emptyText
|
|
||||||
}
|
|
||||||
}::class
|
|
||||||
if (Preferences[Preferences.Key.ListAnimation]) {
|
|
||||||
setAnimation(holder.itemView, holder.adapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAnimation(itemView: View, position: Int) {
|
|
||||||
val animation = AnimationUtils.loadAnimation(
|
|
||||||
itemView.context,
|
|
||||||
if (position > lastPosition) R.anim.slide_up else R.anim.slide_down
|
|
||||||
)
|
|
||||||
itemView.startAnimation(animation)
|
|
||||||
lastPosition = position
|
|
||||||
}
|
|
||||||
}
|
|
@ -27,6 +27,7 @@ import com.looker.droidify.screen.ScreenFragment
|
|||||||
import com.looker.droidify.screen.ScreenshotsFragment
|
import com.looker.droidify.screen.ScreenshotsFragment
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.DownloadService
|
import com.looker.droidify.service.DownloadService
|
||||||
|
import com.looker.droidify.ui.activities.MainActivityX
|
||||||
import com.looker.droidify.ui.adapters.AppDetailAdapter
|
import com.looker.droidify.ui.adapters.AppDetailAdapter
|
||||||
import com.looker.droidify.utility.RxUtils
|
import com.looker.droidify.utility.RxUtils
|
||||||
import com.looker.droidify.utility.Utils
|
import com.looker.droidify.utility.Utils
|
||||||
@ -47,6 +48,9 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
||||||
|
private val screenActivity: MainActivityX
|
||||||
|
get() = requireActivity() as MainActivityX
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_PACKAGE_NAME = "packageName"
|
private const val EXTRA_PACKAGE_NAME = "packageName"
|
||||||
private const val STATE_LAYOUT_MANAGER = "layoutManager"
|
private const val STATE_LAYOUT_MANAGER = "layoutManager"
|
||||||
@ -103,7 +107,6 @@ class AppDetailFragment() : ScreenFragment(), AppDetailAdapter.Callbacks {
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
screenActivity.onToolbarCreated(toolbar)
|
|
||||||
toolbar.menu.apply {
|
toolbar.menu.apply {
|
||||||
for (action in Action.values()) {
|
for (action in Action.values()) {
|
||||||
add(0, action.id, 0, action.adapterAction.titleResId)
|
add(0, action.id, 0, action.adapterAction.titleResId)
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
package com.looker.droidify.ui.fragments
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.looker.droidify.R
|
|
||||||
import com.looker.droidify.database.CursorOwner
|
|
||||||
import com.looker.droidify.entity.Order
|
|
||||||
import com.looker.droidify.entity.Section
|
|
||||||
import com.looker.droidify.screen.BaseFragment
|
|
||||||
import com.looker.droidify.ui.adapters.AppListAdapter
|
|
||||||
import com.looker.droidify.ui.viewmodels.AppListViewModel
|
|
||||||
import com.looker.droidify.utility.RxUtils
|
|
||||||
import com.looker.droidify.utility.extension.resources.getDrawableCompat
|
|
||||||
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.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
|
||||||
|
|
||||||
class AppListFragment() : BaseFragment(), CursorOwner.Callback {
|
|
||||||
|
|
||||||
private val viewModel: AppListViewModel by viewModels()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val EXTRA_SOURCE = "source"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean) {
|
|
||||||
AVAILABLE(R.string.available, true, true),
|
|
||||||
INSTALLED(R.string.installed, false, true),
|
|
||||||
UPDATES(R.string.updates, false, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(source: Source) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putString(EXTRA_SOURCE, source.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val source: Source
|
|
||||||
get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf)
|
|
||||||
|
|
||||||
private var recyclerView: RecyclerView? = null
|
|
||||||
|
|
||||||
private var repositoriesDisposable: Disposable? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?,
|
|
||||||
): View {
|
|
||||||
return RecyclerView(requireContext()).apply {
|
|
||||||
id = android.R.id.list
|
|
||||||
layoutManager = LinearLayoutManager(context)
|
|
||||||
isMotionEventSplittingEnabled = false
|
|
||||||
isVerticalScrollBarEnabled = false
|
|
||||||
setHasFixedSize(true)
|
|
||||||
recycledViewPool.setMaxRecycledViews(AppListAdapter.ViewType.PRODUCT.ordinal, 30)
|
|
||||||
val adapter = AppListAdapter { screenActivity.navigateProduct(it.packageName) }
|
|
||||||
this.adapter = adapter
|
|
||||||
FastScrollerBuilder(this)
|
|
||||||
.useMd2Style()
|
|
||||||
.setThumbDrawable(this.context.getDrawableCompat(R.drawable.scrollbar_thumb))
|
|
||||||
.build()
|
|
||||||
recyclerView = this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
|
||||||
repositoriesDisposable = Observable.just(Unit)
|
|
||||||
//.concatWith(Database.observable(Database.Subject.Repositories)) // TODO have to be replaced like whole rxJava
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.flatMapSingle { RxUtils.querySingle { screenActivity.db.repositoryDao.all } }
|
|
||||||
.map { it.asSequence().map { Pair(it.id, it) }.toMap() }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { (recyclerView?.adapter as? AppListAdapter)?.repositories = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
|
|
||||||
recyclerView = null
|
|
||||||
|
|
||||||
screenActivity.cursorOwner.detach(this)
|
|
||||||
repositoriesDisposable?.dispose()
|
|
||||||
repositoriesDisposable = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) {
|
|
||||||
(recyclerView?.adapter as? AppListAdapter)?.apply {
|
|
||||||
this.cursor = cursor
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
emptyText = when {
|
|
||||||
cursor == null -> ""
|
|
||||||
viewModel.searchQuery.first()
|
|
||||||
.isNotEmpty() -> getString(R.string.no_matching_applications_found)
|
|
||||||
else -> when (source) {
|
|
||||||
Source.AVAILABLE -> getString(R.string.no_applications_available)
|
|
||||||
Source.INSTALLED -> getString(R.string.no_applications_installed)
|
|
||||||
Source.UPDATES -> getString(R.string.all_applications_up_to_date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun setSearchQuery(searchQuery: String) {
|
|
||||||
viewModel.setSearchQuery(searchQuery) {
|
|
||||||
if (view != null) {
|
|
||||||
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun setSection(section: Section) {
|
|
||||||
viewModel.setSection(section) {
|
|
||||||
if (view != null) {
|
|
||||||
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun setOrder(order: Order) {
|
|
||||||
viewModel.setOrder(order) {
|
|
||||||
if (view != null) {
|
|
||||||
screenActivity.cursorOwner.attach(this, viewModel.request(source))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package com.looker.droidify.ui.viewmodels
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.looker.droidify.content.Preferences
|
|
||||||
import com.looker.droidify.database.CursorOwner
|
|
||||||
import com.looker.droidify.entity.Order
|
|
||||||
import com.looker.droidify.entity.Section
|
|
||||||
import com.looker.droidify.ui.fragments.AppListFragment
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class AppListViewModel : ViewModel() {
|
|
||||||
|
|
||||||
private val _order = MutableStateFlow(Preferences[Preferences.Key.SortOrder].order)
|
|
||||||
private val _sections = MutableStateFlow<Section>(Section.All)
|
|
||||||
private val _searchQuery = MutableStateFlow("")
|
|
||||||
|
|
||||||
val order: StateFlow<Order> = _order.stateIn(
|
|
||||||
initialValue = Order.LAST_UPDATE,
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5000)
|
|
||||||
)
|
|
||||||
|
|
||||||
val sections: StateFlow<Section> = _sections.stateIn(
|
|
||||||
initialValue = Section.All,
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5000)
|
|
||||||
)
|
|
||||||
val searchQuery: StateFlow<String> = _searchQuery.stateIn(
|
|
||||||
initialValue = "",
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5000)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun request(source: AppListFragment.Source): CursorOwner.Request {
|
|
||||||
var mSearchQuery = ""
|
|
||||||
var mSections: Section = Section.All
|
|
||||||
var mOrder: Order = Order.NAME
|
|
||||||
viewModelScope.launch {
|
|
||||||
launch { searchQuery.collect { if (source.sections) mSearchQuery = it } }
|
|
||||||
launch { sections.collect { if (source.sections) mSections = it } }
|
|
||||||
launch { order.collect { if (source.order) mOrder = it } }
|
|
||||||
}
|
|
||||||
return when (source) {
|
|
||||||
AppListFragment.Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(
|
|
||||||
mSearchQuery,
|
|
||||||
mSections,
|
|
||||||
mOrder
|
|
||||||
)
|
|
||||||
AppListFragment.Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(
|
|
||||||
mSearchQuery,
|
|
||||||
mSections,
|
|
||||||
mOrder
|
|
||||||
)
|
|
||||||
AppListFragment.Source.UPDATES -> CursorOwner.Request.ProductsUpdates(
|
|
||||||
mSearchQuery,
|
|
||||||
mSections,
|
|
||||||
mOrder
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSection(newSection: Section, perform: () -> Unit) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (newSection != sections.value) {
|
|
||||||
_sections.emit(newSection)
|
|
||||||
launch(Dispatchers.Main) { perform() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setOrder(newOrder: Order, perform: () -> Unit) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (newOrder != order.value) {
|
|
||||||
_order.emit(newOrder)
|
|
||||||
launch(Dispatchers.Main) { perform() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchQuery(newSearchQuery: String, perform: () -> Unit) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
if (newSearchQuery != searchQuery.value) {
|
|
||||||
_searchQuery.emit(newSearchQuery)
|
|
||||||
launch(Dispatchers.Main) { perform() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,18 +7,18 @@ import android.content.Context
|
|||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.Signature
|
import android.content.pm.Signature
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParser
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
import com.looker.droidify.*
|
import com.looker.droidify.BuildConfig
|
||||||
|
import com.looker.droidify.PREFS_LANGUAGE_DEFAULT
|
||||||
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.entity.Installed
|
import com.looker.droidify.database.entity.Installed
|
||||||
import com.looker.droidify.database.entity.Repository
|
import com.looker.droidify.database.entity.Repository
|
||||||
import com.looker.droidify.entity.Product
|
import com.looker.droidify.entity.Product
|
||||||
import com.looker.droidify.entity.ProductItem
|
|
||||||
import com.looker.droidify.service.Connection
|
import com.looker.droidify.service.Connection
|
||||||
import com.looker.droidify.service.DownloadService
|
import com.looker.droidify.service.DownloadService
|
||||||
import com.looker.droidify.utility.extension.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
@ -184,39 +184,6 @@ object Utils {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Remove
|
|
||||||
fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA))
|
|
||||||
.jsonParse {
|
|
||||||
Product.deserialize(it).apply {
|
|
||||||
this.repositoryId = getLong(getColumnIndex(ROW_REPOSITORY_ID))
|
|
||||||
this.description = getString(getColumnIndex(ROW_DESCRIPTION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Remove
|
|
||||||
fun Cursor.getProductItem(): ProductItem = getBlob(getColumnIndex(ROW_DATA_ITEM))
|
|
||||||
.jsonParse {
|
|
||||||
ProductItem.deserialize(it).apply {
|
|
||||||
this.repositoryId = getLong(getColumnIndex(ROW_REPOSITORY_ID))
|
|
||||||
this.packageName = getString(getColumnIndex(ROW_PACKAGE_NAME))
|
|
||||||
this.name = getString(getColumnIndex(ROW_NAME))
|
|
||||||
this.summary = getString(getColumnIndex(ROW_SUMMARY))
|
|
||||||
this.installedVersion = getString(getColumnIndex(ROW_VERSION))
|
|
||||||
.orEmpty()
|
|
||||||
this.compatible = getInt(getColumnIndex(ROW_COMPATIBLE)) != 0
|
|
||||||
this.canUpdate = getInt(getColumnIndex(ROW_CAN_UPDATE)) != 0
|
|
||||||
this.matchRank = getInt(getColumnIndex(ROW_MATCH_RANK))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Remove
|
|
||||||
fun Cursor.getRepository(): Repository = getBlob(getColumnIndex(ROW_DATA))
|
|
||||||
.jsonParse {
|
|
||||||
Repository.deserialize(it).apply {
|
|
||||||
this.id = getLong(getColumnIndex(ROW_ID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> ByteArray.jsonParse(callback: (JsonParser) -> T): T {
|
fun <T> ByteArray.jsonParse(callback: (JsonParser) -> T): T {
|
||||||
return Json.factory.createParser(this).use { it.parseDictionary(callback) }
|
return Json.factory.createParser(this).use { it.parseDictionary(callback) }
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user