Loading app/build.gradle +2 −0 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkSearchHelper.kt +37 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 } } Loading app/src/main/java/foundation/e/apps/data/cleanapk/data/search/Search.kt +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 Loading @@ -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 Loading @@ -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 ) app/src/main/java/foundation/e/apps/data/search/CleanApkSearchPagingRepository.kt 0 → 100644 +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 } } app/src/main/java/foundation/e/apps/data/search/CleanApkSearchPagingSource.kt 0 → 100644 +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
app/build.gradle +2 −0 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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 Loading
app/src/main/java/foundation/e/apps/data/cleanapk/CleanApkSearchHelper.kt +37 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 } } Loading
app/src/main/java/foundation/e/apps/data/cleanapk/data/search/Search.kt +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 Loading @@ -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 Loading @@ -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 )
app/src/main/java/foundation/e/apps/data/search/CleanApkSearchPagingRepository.kt 0 → 100644 +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 } }
app/src/main/java/foundation/e/apps/data/search/CleanApkSearchPagingSource.kt 0 → 100644 +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) } } }