Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit b56d46ca authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

feat: implement pagination for CleanAPK search results

parent 1f9aebce
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -268,6 +268,7 @@ dependencies {
    implementation(libs.navigation.fragment.ktx)
    implementation(libs.navigation.ui.ktx)
    implementation(libs.activity.ktx)
    implementation(libs.paging.runtime.ktx)

    // Material Design
    implementation(libs.material)
@@ -358,6 +359,7 @@ dependencies {
    implementation libs.activity.compose
    implementation libs.lifecycle.viewmodel.compose
    implementation libs.runtime.livedata
    implementation libs.paging.compose

    // Android Studio Preview support for Compose
    implementation libs.compose.ui.tooling.preview
+37 −5
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@
package foundation.e.apps.data.cleanapk

import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.data.search.Search
import foundation.e.apps.data.cleanapk.repositories.NUMBER_OF_ITEMS
import foundation.e.apps.data.cleanapk.repositories.NUMBER_OF_PAGES
import foundation.e.apps.data.enums.Source
@@ -36,16 +37,47 @@ class CleanApkSearchHelper @Inject constructor(
        appType: String
    ): List<Application> {
        return withContext(Dispatchers.IO) {
            val searchResult = cleanApkRetrofit.searchApps(
            getSearchResultPage(
                keyword = keyword,
                appSource = appSource,
                appType = appType,
                page = NUMBER_OF_PAGES,
                pageSize = NUMBER_OF_ITEMS,
            ).apps
        }
    }

    suspend fun getSearchResultPage(
        keyword: String,
        appSource: String,
        appType: String,
        page: Int,
        pageSize: Int,
    ): Search {
        return withContext(Dispatchers.IO) {
            val response = cleanApkRetrofit.searchApps(
                keyword = keyword,
                source = appSource,
                type = appType,
                nres = NUMBER_OF_ITEMS,
                page = NUMBER_OF_PAGES,
                nres = pageSize,
                page = page,
                architectures = SystemInfoProvider.getSupportedArchitectureList(),
            )
            searchResult.body()?.apps.orEmpty()
                .map { it.apply { source = mapSource(it) } }

            check(response.isSuccessful) {
                "CleanAPK search failed: HTTP ${response.code()}"
            }

            val body = checkNotNull(response.body()) {
                "CleanAPK search failed: empty body"
            }

            check(body.success) {
                "CleanAPK search failed: success=false"
            }

            body.apps.forEach { app -> app.source = mapSource(app) }
            body
        }
    }

+3 −2
Original line number Diff line number Diff line
/*
 * Apps  Quickly and easily install Android apps onto your device!
 * Copyright (C) 2021  E FOUNDATION
 * Copyright (C) 2021-2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
@@ -14,6 +13,7 @@
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.cleanapk.data.search
@@ -23,5 +23,6 @@ import foundation.e.apps.data.application.data.Application
data class Search(
    val apps: List<Application> = emptyList(),
    val numberOfResults: Int = -1,
    val pages: Int = 0,
    val success: Boolean = false
)
+48 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.search

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.CleanApkSearchHelper
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class CleanApkSearchPagingRepository @Inject constructor(
    private val cleanApkSearchHelper: CleanApkSearchHelper,
) : SearchPagingRepository {

    override fun cleanApkSearch(params: CleanApkSearchParams): Flow<PagingData<Application>> {
        return Pager(
            config = PagingConfig(
                pageSize = params.pageSize,
                enablePlaceholders = false,
                prefetchDistance = 2
            ),
            pagingSourceFactory = {
                CleanApkSearchPagingSource(
                    cleanApkSearchHelper = cleanApkSearchHelper,
                    params = params
                )
            }
        ).flow
    }
}
+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.search

import androidx.paging.PagingSource
import androidx.paging.PagingState
import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.cleanapk.CleanApkSearchHelper
import retrofit2.HttpException
import java.io.IOException

private const val INITIAL_PAGE = 1

class CleanApkSearchPagingSource(
    private val cleanApkSearchHelper: CleanApkSearchHelper,
    private val params: CleanApkSearchParams,
) : PagingSource<Int, Application>() {

    override fun getRefreshKey(state: PagingState<Int, Application>): Int? {
        val anchor = state.anchorPosition ?: return null
        val anchorPage = state.closestPageToPosition(anchor)
        val prev = anchorPage?.prevKey
        val next = anchorPage?.nextKey
        return when {
            prev != null -> prev + 1
            next != null -> next - 1
            else -> null
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Application> {
        return try {
            val page = params.key ?: INITIAL_PAGE
            val response = cleanApkSearchHelper.getSearchResultPage(
                keyword = this.params.keyword,
                appSource = this.params.appSource,
                appType = this.params.appType,
                page = page,
                pageSize = this.params.pageSize,
            )

            val totalPages = response.pages
            val data = response.apps

            val nextKey = if (page < totalPages) page + 1 else null
            val prevKey = if (page > INITIAL_PAGE) page - 1 else null

            LoadResult.Page(
                data = data,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (exception: IOException) {
            LoadResult.Error(exception)
        } catch (exception: HttpException) {
            LoadResult.Error(exception)
        } catch (exception: IllegalStateException) {
            LoadResult.Error(exception)
        }
    }
}
Loading