Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ dependencies {
implementation(libs.lifecycle.viewModel)
implementation(libs.recyclerview)
implementation(libs.sqlite.ktx)
implementation(libs.paging)

implementation(libs.image.viewer)
implementation(libs.bundles.coil)
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SHOW_APP_INFO" />
</intent-filter>
Expand Down
126 changes: 53 additions & 73 deletions app/src/main/kotlin/com/looker/droidify/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.looker.droidify
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback
Expand All @@ -12,9 +11,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.looker.droidify.database.CursorOwner
import androidx.recyclerview.widget.RecyclerView
import com.looker.droidify.database.AppListRowViewType
import com.looker.droidify.datastore.SettingsRepository
import com.looker.droidify.datastore.extension.getThemeRes
import com.looker.droidify.datastore.get
Expand All @@ -39,17 +40,15 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.parcelize.Parcelize
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
companion object {
private const val STATE_FRAGMENT_STACK = "fragmentStack"
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"
const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
const val EXTRA_CACHE_FILE_NAME =
Expand All @@ -62,24 +61,17 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var installer: InstallManager

@Parcelize
private class FragmentStackItem(
val className: String, val arguments: Bundle?, val savedState: Fragment.SavedState?,
) : Parcelable

lateinit var cursorOwner: CursorOwner
private set

private var onBackPressedCallback: OnBackPressedCallback? = null

private val fragmentStack = mutableListOf<FragmentStackItem>()

private val currentFragment: Fragment?
get() {
val supportFragmentManager = supportFragmentManager
supportFragmentManager.executePendingTransactions()
return supportFragmentManager.findFragmentById(R.id.main_content)
}

val appListViewPool: RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool().apply {
setMaxRecycledViews(AppListRowViewType.PRODUCT, 30)
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface CustomUserRepositoryInjector {
Expand Down Expand Up @@ -130,63 +122,49 @@ class MainActivity : AppCompatActivity() {
hideKeyboard()
}

if (savedInstanceState == null) {
cursorOwner = CursorOwner()
supportFragmentManager.commit {
add(cursorOwner, CursorOwner::class.java.name)
}
} 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)
val intent = intent
if ((intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
handleIntent(intent)
}
}

if (SdkCheck.isR) {
window.statusBarColor = resources.getColor(android.R.color.transparent, theme)
window.navigationBarColor = resources.getColor(android.R.color.transparent, theme)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
backHandler()
}

override fun onDestroy() {
super.onDestroy()
onBackPressedCallback = null
setupBackHandler()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelableArrayList(STATE_FRAGMENT_STACK, ArrayList(fragmentStack))
}

private fun backHandler() {
if (onBackPressedCallback == null) {
onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
hideKeyboard()
popFragment()
}
private fun setupBackHandler() {
val onBackPressedCallback = object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
hideKeyboard()
popFragment()
}
onBackPressedDispatcher.addCallback(
this,
onBackPressedCallback!!,
)
}
onBackPressedCallback?.isEnabled = fragmentStack.isNotEmpty()

onBackPressedDispatcher.addCallback(
this,
onBackPressedCallback,
)

val supportFragmentManager = supportFragmentManager
onBackPressedCallback.isEnabled = supportFragmentManager.hasBackStackEntry()
supportFragmentManager.addOnBackStackChangedListener {
onBackPressedCallback.isEnabled = supportFragmentManager.hasBackStackEntry()
}
}

private fun replaceFragment(fragment: Fragment, open: Boolean?) {
if (open != null) {
currentFragment?.view?.translationZ =
(if (open) Int.MIN_VALUE else Int.MAX_VALUE).toFloat()
}

supportFragmentManager.commit {
if (open != null) {
setCustomAnimations(
Expand All @@ -196,41 +174,33 @@ class MainActivity : AppCompatActivity() {
}
setReorderingAllowed(true)
replace(R.id.main_content, fragment)

if (fragment !is TabsFragment) {
addToBackStack(null)
}
}
}

private fun pushFragment(fragment: Fragment) {
currentFragment?.let {
fragmentStack.add(
FragmentStackItem(
it::class.java.name,
it.arguments,
supportFragmentManager.saveFragmentInstanceState(it),
),
)
}
replaceFragment(fragment, true)
backHandler()
}

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)
backHandler()
true
val supportFragmentManager = supportFragmentManager
if (supportFragmentManager.hasBackStackEntry()) {
supportFragmentManager.popBackStack()
return true
}

return false
}

private fun hideKeyboard() {
inputManager?.hideSoftInputFromWindow((currentFocus ?: window.decorView).windowToken, 0)
}

internal fun onToolbarCreated(toolbar: Toolbar) {
if (fragmentStack.isNotEmpty()) {
if (supportFragmentManager.hasBackStackEntry()) {
toolbar.navigationIcon = toolbar.context.homeAsUp
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
}
Expand All @@ -247,7 +217,6 @@ class MainActivity : AppCompatActivity() {
navigateToTabsFragment()
val tabsFragment = currentFragment as TabsFragment
tabsFragment.selectUpdates()
backHandler()
}

ACTION_INSTALL -> {
Expand Down Expand Up @@ -289,14 +258,21 @@ class MainActivity : AppCompatActivity() {
navigateProduct(packageName)
}
}

}
}
}

private fun clearFragmentBackStack() {
val supportFragmentManager = supportFragmentManager
while (supportFragmentManager.hasBackStackEntry()) {
supportFragmentManager.popBackStack()
}
}

private fun navigateToTabsFragment() {
clearFragmentBackStack()

if (currentFragment !is TabsFragment) {
fragmentStack.clear()
replaceFragment(TabsFragment(), true)
}
}
Expand All @@ -322,3 +298,7 @@ class MainActivity : AppCompatActivity() {
fun navigateEditRepository(repositoryId: Long) =
pushFragment(EditRepositoryFragment(repositoryId, null))
}

private fun FragmentManager.hasBackStackEntry(): Boolean {
return backStackEntryCount > 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.looker.droidify.database

import android.os.Handler
import androidx.annotation.VisibleForTesting
import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext

@VisibleForTesting
fun interface CursorFactory<CursorType : DbCursor<*>> {
fun create(): CursorType
}

abstract class BaseCursorPagingSource<ItemType : Any, CursorType : DbCursor<*>>(
params: Params,
cursorFactory: CursorFactory<CursorType>
) : PagingSource<Int, ItemType>() {

interface Params {
val mainHandler: Handler
val ioDispatcher: CoroutineDispatcher
}

private val ioDispatcher: CoroutineDispatcher = params.ioDispatcher

protected val cursor: CursorType by lazy {
cursorFactory.create().also { cursor ->
cursor.registerOnInvalidatedCallback(params.mainHandler) {
invalidate()
}
}
}

init {
registerInvalidatedCallback {
cursor.close()
}
}

abstract suspend fun createRows(offset: Int, limit: Int): List<ItemType>

final override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ItemType> =
withContext(ioDispatcher) {
loadImpl(params)
}

protected open suspend fun loadImpl(params: LoadParams<Int>): LoadResult<Int, ItemType> {
if (params is LoadParams.Prepend) {
return LoadResult.Page(
data = emptyList(),
prevKey = null,
nextKey = 0
)
}

val offset = params.key ?: 0
val limit = params.loadSize

val items = if (params is LoadParams.Refresh) {
createRows(0, offset + limit)
} else {
createRows(offset, limit)
}

val nextKey = run {
val n = offset + limit
if (n < cursor.count) n else null
}

return LoadResult.Page(
data = items,
prevKey = if (offset == 0) null else offset - limit,
nextKey = nextKey
)
}

override fun getRefreshKey(state: PagingState<Int, ItemType>): Int? = state.anchorPosition
}
Loading