From fb2b11d74f2a9d7e3249b09c8070b778708c3302 Mon Sep 17 00:00:00 2001 From: Saalim Quadri Date: Fri, 31 Oct 2025 13:32:41 +0530 Subject: [PATCH 1/2] feat: Add support for architecture based updates in application Signed-off-by: Saalim Quadri --- .../repositories/CleanApkAppsRepository.kt | 4 ++- .../apps/data/gitlab/models/SystemAppInfo.kt | 36 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt index 8745422db..5307b0782 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt @@ -18,6 +18,7 @@ package foundation.e.apps.data.cleanapk.repositories +import android.os.Build import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.data.Home @@ -106,6 +107,7 @@ class CleanApkAppsRepository @Inject constructor( override suspend fun getDownloadInfo(idOrPackageName: String, versionCode: Any?): Response { val version = versionCode?.let { it as String } - return cleanApkRetrofit.getDownloadInfo(idOrPackageName, version, null) + val arch = Build.SUPPORTED_ABIS.firstOrNull() + return cleanApkRetrofit.getDownloadInfo(idOrPackageName, version, arch) } } diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt index 2008c8bd3..d1b54680b 100644 --- a/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt +++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt @@ -18,6 +18,7 @@ package foundation.e.apps.data.gitlab.models import android.content.Context +import android.os.Build import android.text.format.Formatter import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.squareup.moshi.Json @@ -32,7 +33,8 @@ data class SystemAppInfo( @Json(name = "version_code") val versionCode: Long, @Json(name = "min_sdk") val minSdk: Int, @Json(name = "version_name") val versionName: String, - @Json(name = "url") val downloadUrl: String, + @Json(name = "url") val downloadUrl: String? = null, + @Json(name = "urls") val archDownloadUrls: Map? = null, @Json(name = "size") val size: Long?, @Json(name = "author_name") val authorName: String?, val priority: Boolean?, @@ -44,6 +46,8 @@ private const val RANDOM_SIZE = 1L fun SystemAppInfo.toApplication(context: Context): Application { val apkSize = size ?: RANDOM_SIZE + val selectedUrl = selectUrlForArchitecture() + return Application( _id = UUID.randomUUID().toString(), author = authorName ?: "eFoundation", @@ -54,8 +58,36 @@ fun SystemAppInfo.toApplication(context: Context): Application { package_name = packageName, originalSize = apkSize, appSize = Formatter.formatFileSize(context, apkSize), - url = downloadUrl, + url = selectedUrl, isSystemApp = true, filterLevel = FilterLevel.NONE, ) } + +/** + * Select the appropriate download URL based on device architecture. + * + * This function prioritizes architecture-specific URLs from [archDownloadUrls] based on + * the device's [Build.SUPPORTED_ABIS], falling back to the generic [downloadUrl] if needed. + * + * @return The selected download URL + * @throws IllegalStateException if no suitable URL is found + */ +private fun SystemAppInfo.selectUrlForArchitecture(): String { + if (archDownloadUrls.isNullOrEmpty()) { + return downloadUrl ?: error("No download URL provided for $packageName") + } + + val compatibleUrl = Build.SUPPORTED_ABIS + .asSequence() + .mapNotNull { abi -> archDownloadUrls[abi] } + .firstOrNull { url -> url.isNotBlank() } + + return compatibleUrl + ?: downloadUrl?.takeIf { it.isNotBlank() } + ?: error( + "No compatible URL found for $packageName. " + + "Device supports: ${Build.SUPPORTED_ABIS.joinToString()}, " + + "Available: ${archDownloadUrls.keys.joinToString()}" + ) +} -- GitLab From 4a4b5b73f3236d98a2ab169e24075ee828fb63fa Mon Sep 17 00:00:00 2001 From: Saalim Quadri Date: Tue, 4 Nov 2025 09:50:08 +0530 Subject: [PATCH 2/2] feat: tests: Write a unit test to check archDownloadUrls Signed-off-by: Saalim Quadri --- .../cleanapk/CleanApkAppsRepositoryTest.kt | 61 ++++++ .../e/apps/data/gitlab/SystemAppInfoTest.kt | 192 ++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkAppsRepositoryTest.kt create mode 100644 app/src/test/java/foundation/e/apps/data/gitlab/SystemAppInfoTest.kt diff --git a/app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkAppsRepositoryTest.kt b/app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkAppsRepositoryTest.kt new file mode 100644 index 000000000..2e10feed4 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/cleanapk/CleanApkAppsRepositoryTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright MURENA SAS 2025 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 . + */ + +package foundation.e.apps.data.cleanapk + +import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository +import foundation.e.apps.data.cleanapk.repositories.HomeConverter +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +/** + * Unit tests for CleanApkAppsRepository. + * + * These unit tests verify the repository can be instantiated correctly. + */ +class CleanApkAppsRepositoryTest { + + @Mock + private lateinit var cleanApkRetrofit: CleanApkRetrofit + + @Mock + private lateinit var homeConverter: HomeConverter + + @Mock + private lateinit var cleanApkSearchHelper: CleanApkSearchHelper + + private lateinit var repository: CleanApkAppsRepository + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + + repository = CleanApkAppsRepository( + cleanApkRetrofit, + homeConverter, + cleanApkSearchHelper + ) + } + + @Test + fun `repository can be instantiated`() { + assert(repository is CleanApkAppsRepository) + } +} diff --git a/app/src/test/java/foundation/e/apps/data/gitlab/SystemAppInfoTest.kt b/app/src/test/java/foundation/e/apps/data/gitlab/SystemAppInfoTest.kt new file mode 100644 index 000000000..dd8e19688 --- /dev/null +++ b/app/src/test/java/foundation/e/apps/data/gitlab/SystemAppInfoTest.kt @@ -0,0 +1,192 @@ +/* + * Copyright MURENA SAS 2025 + * Apps Quickly and easily install Android apps onto your device! + * + * 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 . + */ + +package foundation.e.apps.data.gitlab + +import android.content.Context +import android.text.format.Formatter +import foundation.e.apps.data.gitlab.models.SystemAppInfo +import foundation.e.apps.data.gitlab.models.toApplication +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockedStatic +import org.mockito.Mockito +import org.mockito.MockitoAnnotations +import kotlin.test.assertFailsWith + +/** + * Unit tests for SystemAppInfo architecture-based URL selection. + * + */ +class SystemAppInfoTest { + + @Mock + private lateinit var context: Context + + private lateinit var formatterMocked: MockedStatic + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + formatterMocked = Mockito.mockStatic(Formatter::class.java) + formatterMocked.`when` { Formatter.formatFileSize(context, 1000L) } + .thenReturn("1 KB") + } + + @After + fun tearDown() { + formatterMocked.close() + } + + @Test + fun `selectUrlForArchitecture falls back to generic downloadUrl when archDownloadUrls is null`() { + val systemAppInfo = SystemAppInfo( + name = "Example App", + packageName = "com.example.app", + versionCode = 100, + minSdk = 21, + versionName = "1.0.0", + downloadUrl = "https://example.com/app.apk", + archDownloadUrls = null, + size = 1000L, + authorName = "Author", + priority = false, + blockedAndroid = null, + blockedDevices = null + ) + + val app = systemAppInfo.toApplication(context) + + assertEquals("https://example.com/app.apk", app.url) + } + + @Test + fun `selectUrlForArchitecture falls back to generic downloadUrl when archDownloadUrls is empty`() { + val systemAppInfo = SystemAppInfo( + name = "Example App", + packageName = "com.example.app", + versionCode = 100, + minSdk = 21, + versionName = "1.0.0", + downloadUrl = "https://example.com/app-generic.apk", + archDownloadUrls = emptyMap(), + size = 1000L, + authorName = "Author", + priority = false, + blockedAndroid = null, + blockedDevices = null + ) + + val app = systemAppInfo.toApplication(context) + + assertEquals("https://example.com/app-generic.apk", app.url) + } + + @Test + fun `selectUrlForArchitecture throws error when no URLs are available`() { + val systemAppInfo = SystemAppInfo( + name = "Example App", + packageName = "com.example.app", + versionCode = 100, + minSdk = 21, + versionName = "1.0.0", + downloadUrl = null, + archDownloadUrls = null, + size = 1000L, + authorName = "Author", + priority = false, + blockedAndroid = null, + blockedDevices = null + ) + + val exception = assertFailsWith { + systemAppInfo.toApplication(context) + } + + assert(exception.message!!.contains("No download URL provided")) + } + + @Test + fun `toApplication creates valid Application object with generic URL`() { + val systemAppInfo = SystemAppInfo( + name = "Test App", + packageName = "com.example.testapp", + versionCode = 42, + minSdk = 21, + versionName = "2.0.0", + downloadUrl = "https://example.com/app.apk", + archDownloadUrls = null, // No architecture-specific URLs + size = 1000L, + authorName = "Test Author", + priority = true, + blockedAndroid = null, + blockedDevices = null + ) + + val app = systemAppInfo.toApplication(context) + + assertEquals("com.example.testapp", app.package_name) + assertEquals(42L, app.latest_version_code) + assertEquals("2.0.0", app.latest_version_number) + assertEquals("Test Author", app.author) + assertEquals(1000L, app.originalSize) + assertEquals("1 KB", app.appSize) + assertEquals(true, app.isSystemApp) + assertEquals("https://example.com/app.apk", app.url) + assertNotNull(app.url) + } + + @Test + fun `SystemAppInfo data class holds correct values`() { + val systemAppInfo = SystemAppInfo( + name = "My App", + packageName = "com.test.myapp", + versionCode = 123, + minSdk = 28, + versionName = "3.0.0", + downloadUrl = "https://example.com/myapp.apk", + archDownloadUrls = mapOf( + "arm64-v8a" to "https://example.com/myapp-arm64.apk", + "x86_64" to "https://example.com/myapp-x86_64.apk" + ), + size = 5000L, + authorName = "Test Publisher", + priority = true, + blockedAndroid = listOf(27, 28), + blockedDevices = listOf("device1", "device2") + ) + + assertEquals("My App", systemAppInfo.name) + assertEquals("com.test.myapp", systemAppInfo.packageName) + assertEquals(123L, systemAppInfo.versionCode) + assertEquals(28, systemAppInfo.minSdk) + assertEquals("3.0.0", systemAppInfo.versionName) + assertEquals("https://example.com/myapp.apk", systemAppInfo.downloadUrl) + assertNotNull(systemAppInfo.archDownloadUrls) + assertEquals(2, systemAppInfo.archDownloadUrls?.size) + assertEquals(5000L, systemAppInfo.size) + assertEquals("Test Publisher", systemAppInfo.authorName) + assertEquals(true, systemAppInfo.priority) + assertEquals(listOf(27, 28), systemAppInfo.blockedAndroid) + assertEquals(listOf("device1", "device2"), systemAppInfo.blockedDevices) + } +} -- GitLab