Remove USE_FAKE_API infrastructure to reduce maintenance overhead

- Remove buildConfigField("Boolean", "USE_FAKE_API") from both debug and release build types
- Remove validateUseFakeApiConfig Gradle task (saved ~80 lines)
- Delete RepositoryConfigProvider.kt
- Delete app/src/main/java/ink/trmnl/android/data/fake/ directory
- Remove fake data conditional logic from TrmnlDisplayRepository
- Remove fake data conditional logic from TrmnlUserRepository
- Remove validation step from .github/workflows/android-release.yml
- Remove usesFakeApiData from AppSettingsScreen state and UI
- Remove FakeApiInfoBanner composable
- Update tests to remove fake data test cases
- Update CLAUDE.md documentation

All tests pass (89 tests), linting clean, build successful.

Co-authored-by: hossain-khan <99822+hossain-khan@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-09 12:26:53 +00:00
co-authored by hossain-khan
parent c861b1fde3
commit d94b70ffba
9 changed files with 0 additions and 344 deletions
-3
View File
@@ -53,9 +53,6 @@ jobs:
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore/trmnl-app-release.keystore
- name: Validate USE_FAKE_API configuration
run: ./gradlew validateUseFakeApiConfig
- name: Build Release APK and AAB
run: ./gradlew assembleRelease bundleRelease
env:
-3
View File
@@ -172,13 +172,10 @@ The project uses optimized Gradle settings based on best practices from the [Now
**Note:** This project uses only build TYPES (debug/release), NOT product flavors.
**Debug:**
- `buildConfigField("Boolean", "USE_FAKE_API", "true")`
- Uses debug keystore with password "android" (included in repo)
- Fake API can be overridden in `RepositoryConfigProvider`
- HttpLoggingInterceptor enabled with BODY level
**Release:**
- `buildConfigField("Boolean", "USE_FAKE_API", "false")`
- Requires production keystore from CI secrets or `secret.properties`
- Keystore env vars: `KEYSTORE_PASSWORD`, `KEY_ALIAS`
- Code shrinking: `isMinifyEnabled = true`, `isShrinkResources = true`
-92
View File
@@ -75,11 +75,6 @@ android {
buildTypes {
release {
// Always force `USE_FAKE_API` to `false` for release builds
// See https://github.com/usetrmnl/trmnl-android/issues/16
// ️ To override during local development, change the value in `RepositoryConfigProvider`
buildConfigField("Boolean", "USE_FAKE_API", "false")
// Enables code shrinking, obfuscation, and optimization
// See https://github.com/usetrmnl/trmnl-android/issues/199
isMinifyEnabled = true
@@ -99,13 +94,6 @@ android {
}
debug {
// Allow developers to configure this value for debug builds
// Use fake API response for local development and testing purposes.
// ️ To override during local development, change this value to `"false"`
// or, you can change the value in the `RepositoryConfigProvider`
// Ideally, this value should always be set to `true` for debug builds.
buildConfigField("Boolean", "USE_FAKE_API", "true")
signingConfig = signingConfigs.getByName("debug")
}
}
@@ -221,83 +209,3 @@ ksp {
tasks.withType<Test> {
jvmArgs("-XX:+EnableDynamicAgentLoading")
}
/**
* Validation task to ensure USE_FAKE_API is correctly configured for release builds.
* This task will fail the build if:
* 1. Release build type has USE_FAKE_API set to anything other than "false"
* 2. RepositoryConfigProvider doesn't return `BuildConfig.USE_FAKE_API`
*
* Run manually: ./gradlew validateUseFakeApiConfig
* Automatically runs before: assembleRelease, bundleRelease
*/
val validateUseFakeApiConfig = tasks.register("validateUseFakeApiConfig") {
group = "verification"
description = "Validates that USE_FAKE_API is correctly configured for release builds"
// Capture file paths at configuration time to avoid configuration cache issues
val buildGradleFile = project.file("build.gradle.kts")
val repositoryConfigFile = project.file("src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt")
doLast {
println("🔍 Validating USE_FAKE_API configuration...")
// Check 1: Validate build.gradle.kts release configuration
val buildGradleContent = buildGradleFile.readText()
// Find the release block and check USE_FAKE_API value
val releaseBuildPattern = Regex(
"""release\s*\{[^}]*buildConfigField\s*\(\s*"Boolean"\s*,\s*"USE_FAKE_API"\s*,\s*"false"\s*\)""",
RegexOption.DOT_MATCHES_ALL
)
if (!releaseBuildPattern.containsMatchIn(buildGradleContent)) {
throw GradleException(
"""
❌ BUILD VALIDATION FAILED!
Release build type MUST have USE_FAKE_API set to "false"
Expected: buildConfigField("Boolean", "USE_FAKE_API", "false")
Location: app/build.gradle.kts (release buildType)
""".trimIndent()
)
}
// Check 2: Validate RepositoryConfigProvider.kt
if (repositoryConfigFile.exists()) {
val repositoryConfigContent = repositoryConfigFile.readText()
// Check that shouldUseFakeData returns BuildConfig.USE_FAKE_API
val correctReturnPattern = Regex(
"""val\s+shouldUseFakeData\s*:\s*Boolean[^}]*return\s+BuildConfig\.USE_FAKE_API""",
RegexOption.DOT_MATCHES_ALL
)
if (!correctReturnPattern.containsMatchIn(repositoryConfigContent)) {
throw GradleException(
"""
❌ BUILD VALIDATION FAILED!
RepositoryConfigProvider.shouldUseFakeData MUST return BuildConfig.USE_FAKE_API
Expected: return BuildConfig.USE_FAKE_API
Location: app/src/main/java/ink/trmnl/android/data/RepositoryConfigProvider.kt
Do NOT hardcode or override this value in committed code!
""".trimIndent()
)
}
} else {
println("⚠️ RepositoryConfigProvider.kt not found, skipping validation")
}
println("✅ USE_FAKE_API configuration is valid")
}
}
// Automatically run validation before release builds (after Android tasks are registered)
afterEvaluate {
tasks.findByName("assembleRelease")?.dependsOn(validateUseFakeApiConfig)
tasks.findByName("bundleRelease")?.dependsOn(validateUseFakeApiConfig)
}
@@ -1,25 +0,0 @@
package ink.trmnl.android.data
import ink.trmnl.android.BuildConfig
import javax.inject.Inject
/**
* Provides configuration for repository data sources. This class helps control whether repositories
* should use fake/mock data instead of real API data, which is useful for development and testing.
*/
class RepositoryConfigProvider
@Inject
constructor() {
/**
* Indicates if the app should use fake data instead of real API responses.
*
* @return Boolean value from [BuildConfig.USE_FAKE_API]
*/
val shouldUseFakeData: Boolean
get() {
// To change this value, update the `buildConfigField` in the app's build.gradle file
// Or, change the value here for local development. Do not commit this change.
// ⚠️ Return value should always be `BuildConfig.USE_FAKE_API`
return BuildConfig.USE_FAKE_API
}
}
@@ -3,9 +3,6 @@ package ink.trmnl.android.data
import com.slack.eithernet.ApiResult
import com.slack.eithernet.exceptionOrNull
import com.squareup.anvil.annotations.optional.SingleIn
import ink.trmnl.android.BuildConfig.USE_FAKE_API
import ink.trmnl.android.data.fake.generateFakeDeviceSetupInfo
import ink.trmnl.android.data.fake.generateFakeTrmnlDisplayInfo
import ink.trmnl.android.di.AppScope
import ink.trmnl.android.model.SupportedDeviceModel
import ink.trmnl.android.model.TrmnlDeviceConfig
@@ -27,12 +24,6 @@ import javax.inject.Inject
/**
* Repository class responsible for fetching and mapping display data.
*
* ⚠️ NOTE: [USE_FAKE_API] is set to `true` in debug builds, meaning it will
* use mock data and avoid network calls. In release builds, it is set to `false`
* to enable real API calls.
*
* You can override this behavior by updating [RepositoryConfigProvider.shouldUseFakeData] for local development.
*/
@SingleIn(AppScope::class)
class TrmnlDisplayRepository
@@ -40,12 +31,10 @@ class TrmnlDisplayRepository
constructor(
private val apiService: TrmnlApiService,
private val imageMetadataStore: ImageMetadataStore,
private val repositoryConfigProvider: RepositoryConfigProvider,
private val androidDeviceInfoProvider: AndroidDeviceInfoProvider,
) {
/**
* Fetches display data for next plugin from the server using the provided access token.
* If the app is in debug mode, it uses mock data instead.
*
* @param trmnlDeviceConfig Device configuration containing the access token and other settings.
* @return A [TrmnlDisplayInfo] object containing the display data.
@@ -53,11 +42,6 @@ class TrmnlDisplayRepository
suspend fun getNextDisplayData(trmnlDeviceConfig: TrmnlDeviceConfig): TrmnlDisplayInfo {
Timber.i("Fetching next playlist item display data from server for device: ${trmnlDeviceConfig.type}")
if (repositoryConfigProvider.shouldUseFakeData) {
// Avoid using real API in debug mode
return generateFakeTrmnlDisplayInfo(imageMetadataStore = imageMetadataStore, apiUsed = "next-image")
}
val result =
apiService
.getNextDisplayData(
@@ -126,7 +110,6 @@ class TrmnlDisplayRepository
/**
* Fetches the current display data from the server using the provided access token.
* If the app is in debug mode, it uses mock data instead.
*
* ⚠️ NOTE: This API is not available on BYOS servers.
* See https://discord.com/channels/1281055965508141100/1331360842809348106/1382863253880963124
@@ -141,11 +124,6 @@ class TrmnlDisplayRepository
Timber.w("Current display image data API is not available for BYOS service.")
}
if (repositoryConfigProvider.shouldUseFakeData) {
// Avoid using real API in debug mode
return generateFakeTrmnlDisplayInfo(imageMetadataStore = imageMetadataStore, apiUsed = "current-image")
}
val result =
apiService
.getCurrentDisplayData(
@@ -197,11 +175,6 @@ class TrmnlDisplayRepository
Timber.w("Device setup is only applicable for BYOS devices.")
}
if (repositoryConfigProvider.shouldUseFakeData) {
// Avoid using real API in debug mode
return generateFakeDeviceSetupInfo()
}
val result =
apiService.setupNewDevice(
fullApiUrl = constructApiUrl(trmnlDeviceConfig.apiBaseUrl, TrmnlApiService.SETUP_API_PATH),
@@ -33,7 +33,6 @@ class TrmnlUserRepository
@Inject
constructor(
private val userApiService: TrmnlUserApiService,
private val repositoryConfigProvider: RepositoryConfigProvider,
private val androidDeviceInfoProvider: AndroidDeviceInfoProvider,
) {
/**
@@ -52,23 +51,6 @@ class TrmnlUserRepository
): Result<TrmnlUser> {
Timber.i("Validating user API token")
if (repositoryConfigProvider.shouldUseFakeData) {
// Return fake user data in debug mode
return Result.success(
TrmnlUser(
id = 42,
name = "Test User",
email = "test@example.com",
firstName = "Test",
lastName = "User",
locale = "en",
timeZone = "Eastern Time (US & Canada)",
timeZoneIana = "America/New_York",
utcOffset = -14400,
),
)
}
val result =
userApiService.getUserInfo(
fullApiUrl = constructApiUrl(apiBaseUrl, USER_INFO_API_PATH),
@@ -1,52 +0,0 @@
package ink.trmnl.android.data.fake
import ink.trmnl.android.data.DeviceSetupInfo
import ink.trmnl.android.data.ImageMetadataStore
import ink.trmnl.android.data.RepositoryConfigProvider
import ink.trmnl.android.data.TrmnlDisplayInfo
import ink.trmnl.android.model.TrmnlDeviceType
import ink.trmnl.android.util.HTTP_200
import timber.log.Timber
// Contains fake implementations of network communication for local development
// and testing without depending on real servers.
/**
* Generates fake display info for debugging purposes without wasting an API request.
*
* ️ This is only used when [RepositoryConfigProvider.shouldUseFakeData] is true.
*/
internal suspend fun generateFakeTrmnlDisplayInfo(
imageMetadataStore: ImageMetadataStore,
apiUsed: String,
): TrmnlDisplayInfo {
Timber.d("DEBUG: Using mock data for display info")
val timestampMin = System.currentTimeMillis() / 60_000 // Changes every minute
val mockImageUrl = "https://picsum.photos/300/200?grayscale&time=$timestampMin&api=$apiUsed"
val mockRefreshRate = 600L
// Save mock data to the data store
imageMetadataStore.saveImageMetadata(mockImageUrl, mockRefreshRate)
return TrmnlDisplayInfo(
status = HTTP_200,
trmnlDeviceType = TrmnlDeviceType.TRMNL,
imageUrl = mockImageUrl,
imageFileName = "mocked-image-" + mockImageUrl.substringAfterLast('?'),
error = null,
refreshIntervalSeconds = mockRefreshRate,
)
}
/**
* Generates fake setup info for debugging purposes without wasting an API request.
*
* ️ This is only used when [RepositoryConfigProvider.shouldUseFakeData] is true.
*/
internal fun generateFakeDeviceSetupInfo(): DeviceSetupInfo =
DeviceSetupInfo(
success = true,
deviceMacId = "A1:B2:C3:D4:E5:F6",
apiKey = "mocked-api-key-${System.currentTimeMillis()}",
message = "Mocked device setup successful",
)
@@ -5,7 +5,6 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -60,7 +59,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@@ -85,7 +83,6 @@ import ink.trmnl.android.R
import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC
import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL
import ink.trmnl.android.data.DeviceSetupInfo
import ink.trmnl.android.data.RepositoryConfigProvider
import ink.trmnl.android.data.TrmnlDeviceConfigDataStore
import ink.trmnl.android.data.TrmnlDisplayRepository
import ink.trmnl.android.di.AppScope
@@ -144,7 +141,6 @@ data class AppSettingsScreen(
val accessToken: String,
val deviceMacId: String,
val isByodMasterDevice: Boolean,
val usesFakeApiData: Boolean,
val isLoading: Boolean = false,
val validationResult: ValidationResult? = null,
val isDeviceSetupLoading: Boolean = false,
@@ -273,7 +269,6 @@ class AppSettingsPresenter
private val deviceConfigStore: TrmnlDeviceConfigDataStore,
private val trmnlWorkScheduler: TrmnlWorkScheduler,
private val trmnlImageUpdateManager: TrmnlImageUpdateManager,
private val repositoryConfigProvider: RepositoryConfigProvider,
) : Presenter<AppSettingsScreen.State> {
@Composable
override fun present(): AppSettingsScreen.State {
@@ -286,7 +281,6 @@ class AppSettingsPresenter
var validationResult by remember { mutableStateOf<ValidationResult?>(null) }
var isDeviceSetupLoading by remember { mutableStateOf(false) }
var deviceSetupMessage by remember { mutableStateOf<String?>(null) }
val usesFakeApiData = repositoryConfigProvider.shouldUseFakeData
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
@@ -354,7 +348,6 @@ class AppSettingsPresenter
accessToken = accessToken,
deviceMacId = deviceMacId,
isByodMasterDevice = isByodMasterDevice,
usesFakeApiData = usesFakeApiData,
isLoading = isLoading,
validationResult = validationResult,
isDeviceSetupLoading = isDeviceSetupLoading,
@@ -668,15 +661,6 @@ fun AppSettingsContent(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
if (state.usesFakeApiData) {
FakeApiInfoBanner(
modifier =
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
)
}
Icon(
painter = painterResource(R.drawable.trmnl_logo_plain),
contentDescription = "TRMNL Logo",
@@ -1298,52 +1282,6 @@ private fun WorkScheduleStatusCard(
* A composable function that displays a banner indicating that the app is in developer mode
* and is using mock data instead of real API calls.
*/
@Composable
private fun FakeApiInfoBanner(modifier: Modifier = Modifier) {
Card(
modifier = modifier,
colors =
CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary),
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
Column {
Text(
text = "Developer Mode - Using Fake API",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold,
)
Text(
text = "This app is currently using mock data instead of real API calls.",
style = MaterialTheme.typography.bodySmall,
)
Text(
text = "Set `BuildConfig.USE_FAKE_API` to `false` using build.gradle to use real API.",
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
)
}
}
}
}
@Preview(name = "App Settings Content - Initial State")
@Composable
private fun PreviewAppSettingsContentInitial() {
@@ -1356,7 +1294,6 @@ private fun PreviewAppSettingsContentInitial() {
accessToken = "",
deviceMacId = "aa:bb:cc:dd:ee:ff",
isByodMasterDevice = true,
usesFakeApiData = true,
isLoading = false,
validationResult = null,
nextRefreshJobInfo = null,
@@ -1378,7 +1315,6 @@ private fun PreviewAppSettingsContentLoading() {
accessToken = "some-token",
deviceMacId = "aa:bb:cc:dd:ee:ff",
isByodMasterDevice = true,
usesFakeApiData = false,
isLoading = true,
validationResult = null,
nextRefreshJobInfo = null,
@@ -1400,7 +1336,6 @@ private fun PreviewAppSettingsContentSuccess() {
accessToken = "valid-token-123",
deviceMacId = "aa:bb:cc:dd:ee:ff",
isByodMasterDevice = true,
usesFakeApiData = false,
isLoading = false,
validationResult =
ValidationResult.Success(
@@ -1426,7 +1361,6 @@ private fun PreviewAppSettingsContentFailure() {
accessToken = "invalid-token",
deviceMacId = "aa:bb:cc:dd:ee:ff",
isByodMasterDevice = true,
usesFakeApiData = false,
isLoading = false,
validationResult =
ValidationResult.Failure(
@@ -1455,7 +1389,6 @@ private fun PreviewAppSettingsContentWithWork() {
accessToken = "valid-token-123",
deviceMacId = "aa:bb:cc:dd:ee:ff",
isByodMasterDevice = true,
usesFakeApiData = false,
isLoading = false,
validationResult = null, // Can also be Success state
nextRefreshJobInfo =
@@ -1487,7 +1420,6 @@ private fun PreviewWorkScheduleStatusCardScheduled() {
accessToken = "some-token",
deviceMacId = "AA:BB:CC:DD:EE:FF",
isByodMasterDevice = true,
usesFakeApiData = false,
nextRefreshJobInfo =
NextImageRefreshDisplayInfo(
workerState = WorkInfo.State.ENQUEUED,
@@ -1513,7 +1445,6 @@ private fun PreviewWorkScheduleStatusCardNoWork() {
accessToken = "some-token",
deviceMacId = "aa:bb:cc:dd:ee:ff",
isByodMasterDevice = true,
usesFakeApiData = false,
nextRefreshJobInfo = null,
eventSink = {},
),
@@ -1521,14 +1452,6 @@ private fun PreviewWorkScheduleStatusCardNoWork() {
}
}
@Preview(name = "Fake API Info Banner")
@Composable
private fun PreviewFakeApiInfoBanner() {
TrmnlDisplayAppTheme {
FakeApiInfoBanner()
}
}
@Preview(name = "App Settings Content - BYOD Selected")
@Composable
private fun PreviewAppSettingsContentByod() {
@@ -1541,7 +1464,6 @@ private fun PreviewAppSettingsContentByod() {
accessToken = "byod-access-token-here",
deviceMacId = "",
isByodMasterDevice = false,
usesFakeApiData = false,
isLoading = false,
validationResult = null,
nextRefreshJobInfo = null,
@@ -1564,7 +1486,6 @@ private fun PreviewAppSettingsContentByos() {
accessToken = "byos-access-token-here",
deviceMacId = "AA:BB:CC:DD:EE:FF",
isByodMasterDevice = true,
usesFakeApiData = false,
isLoading = false,
validationResult = null,
nextRefreshJobInfo = null,
@@ -30,7 +30,6 @@ class TrmnlDisplayRepositoryTest {
private lateinit var repository: TrmnlDisplayRepository
private lateinit var apiService: TrmnlApiService
private lateinit var imageMetadataStore: ImageMetadataStore
private lateinit var repositoryConfigProvider: RepositoryConfigProvider
private lateinit var deviceConfigDataStore: TrmnlDeviceConfigDataStore
private lateinit var androidDeviceInfoProvider: AndroidDeviceInfoProvider
@@ -61,18 +60,14 @@ class TrmnlDisplayRepositoryTest {
@Before
fun setup() {
apiService = mockk()
repositoryConfigProvider = mockk()
deviceConfigDataStore = mockk()
imageMetadataStore = mockk(relaxed = true)
androidDeviceInfoProvider = mockk(relaxed = true)
every { repositoryConfigProvider.shouldUseFakeData } returns false
repository =
TrmnlDisplayRepository(
apiService = apiService,
imageMetadataStore = imageMetadataStore,
repositoryConfigProvider = repositoryConfigProvider,
androidDeviceInfoProvider = androidDeviceInfoProvider,
)
}
@@ -242,46 +237,6 @@ class TrmnlDisplayRepositoryTest {
coVerify(exactly = 0) { imageMetadataStore.saveImageMetadata(any(), any()) }
}
@Test
fun `getNextDisplayData should return fake data when shouldUseFakeData is true`() =
runTest {
// Arrange
every { repositoryConfigProvider.shouldUseFakeData } returns true
// Act
val result = repository.getNextDisplayData(testDeviceConfig)
// Assert
assertThat(result.status).isEqualTo(200)
assertThat(result.imageUrl).contains("picsum.photos")
assertThat(result.imageFileName).contains("mocked-image-grayscale&time")
assertThat(result.refreshIntervalSeconds).isEqualTo(600L)
assertThat(result.error).isNull()
// Verify API was NOT called
coVerify(exactly = 0) { apiService.getNextDisplayData(any(), any()) }
}
@Test
fun `getCurrentDisplayData should return fake data when shouldUseFakeData is true`() =
runTest {
// Arrange
every { repositoryConfigProvider.shouldUseFakeData } returns true
// Act
val result = repository.getCurrentDisplayData(testDeviceConfig)
// Assert
assertThat(result.status).isEqualTo(200)
assertThat(result.imageUrl).contains("picsum.photos")
assertThat(result.imageFileName).contains("mocked-image-grayscale&time")
assertThat(result.refreshIntervalSeconds).isEqualTo(600L)
assertThat(result.error).isNull()
// Verify API was NOT called
coVerify(exactly = 0) { apiService.getCurrentDisplayData(any(), any()) }
}
@Test
fun `constructApiUrl should handle URLs with and without trailing slashes`() =
runTest {