Skip to content
Closed

tes #1564

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
42 changes: 42 additions & 0 deletions .github/workflows/android-debug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Android Debug APK

on:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: android-debug-${{ github.ref }}
cancel-in-progress: true

env:
CI_CORE_ONLY: "false"

jobs:
assemble-debug:
name: Assemble debug APK
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Assemble debug build
run: ./gradlew :ide-android:assembleDebug --console=plain --stacktrace

- name: Upload debug APK artifact
uses: actions/upload-artifact@v4
with:
name: ide-android-debug-apk
path: |
ide-android/build/outputs/apk/debug/**/*.apk
if-no-files-found: error
retention-days: 14
7 changes: 5 additions & 2 deletions ide-android/src/main/kotlin/dev/ide/android/AndroidAdHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ class AndroidAdHost(
/** Identity of the installed build (the package's `lastUpdateTime`) — see [AdHost.installStamp]. */
override val installStamp: String? = null,
) : AdHost {
override val available: Boolean = true
override val available: Boolean = !BuildConfig.DEBUG

override val privacyOptionsRequired: Boolean get() = privacyOptionsRequiredProvider()
override val privacyOptionsRequired: Boolean get() = available && privacyOptionsRequiredProvider()

override fun showPrivacyOptions() = onShowPrivacyOptions()

Expand All @@ -93,6 +93,7 @@ class AndroidAdHost(
private val mainHandler = Handler(Looper.getMainLooper())

override fun preloadInterstitial() {
if (!available) return
// AdMob load must run on the main thread; hop there defensively if a caller ever invokes us off it.
mainHandler.post {
if (!webViewAvailable || interstitial != null || loadingInterstitial) return@post
Expand Down Expand Up @@ -133,6 +134,7 @@ class AndroidAdHost(
}

override fun showInterstitial(): Boolean {
if (!available) return false
// Callers invoke on the Main dispatcher, and AdMob requires show() on the main thread. If somehow
// off-main, skip this round (return false) rather than risk an off-thread show.
if (Looper.myLooper() != Looper.getMainLooper()) return false
Expand All @@ -147,6 +149,7 @@ class AndroidAdHost(

@Composable
override fun NativeAd(placement: AdPlacement, modifier: Modifier) {
if (!available) return
val context = LocalContext.current
var ad by remember(placement) { mutableStateOf<NativeAd?>(null) }

Expand Down
4 changes: 4 additions & 0 deletions ide-android/src/main/kotlin/dev/ide/android/AndroidHostUi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import dev.ide.ui.backend.TreeNode
* no WebView — ads are optional, so a failed init must never take down the IDE.
*/
internal fun initAds(context: Context) {
if (BuildConfig.DEBUG) {
Log.logger("ide.ads").info("Skipping MobileAds initialization in debug build")
return
}
runCatching {
com.google.android.gms.ads.MobileAds.initialize(context) { status ->
val adapters = status.adapterStatusMap.entries.joinToString { (name, s) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class MainActivity : ComponentActivity() {
inbound.value = extractStream(intent)
// Gather UMP consent BEFORE initializing the Ads SDK (mediation + EEA/UK requirement); only initialize
// once consent allows ad requests. Failure resolves too, so a consent hiccup never blocks the IDE.
adConsent.gather(this) { if (adConsent.canRequestAds) initAds(applicationContext) }
adConsent.gather(this) { if (adConsent.canRequestAds && !BuildConfig.DEBUG) initAds(applicationContext) }

setContent {
var backend by remember { mutableStateOf<IdeBackend?>(null) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -54,7 +53,6 @@ import dev.ide.ui.backend.UiDrawable
import dev.ide.ui.backend.UiProjectIcon
import dev.ide.ui.editor.preview.decodeImageBytes
import dev.ide.ui.editor.preview.drawUiDrawable
import dev.ide.ui.components.BetaBanner
import dev.ide.ui.components.CenteredDialog
import dev.ide.ui.components.darken
import dev.ide.ui.components.ProjectTile
Expand All @@ -78,6 +76,7 @@ import dev.ide.ui.generated.resources.import_gradle_subtitle
import dev.ide.ui.generated.resources.clone_repository_subtitle
import dev.ide.ui.generated.resources.clone_repository_title
import dev.ide.ui.generated.resources.import_gradle_title
import dev.ide.ui.generated.resources.import_project
import dev.ide.ui.generated.resources.join_the_community
import dev.ide.ui.generated.resources.join_the_community_content
import dev.ide.ui.generated.resources.modules
Expand All @@ -93,12 +92,6 @@ import dev.ide.ui.generated.resources.project_opened_weeks
import dev.ide.ui.generated.resources.projects
import dev.ide.ui.generated.resources.recovered_projects
import dev.ide.ui.generated.resources.recovered_projects_content
import dev.ide.ui.generated.resources.support_chip_free
import dev.ide.ui.generated.resources.support_chip_open_source
import dev.ide.ui.generated.resources.support_content
import dev.ide.ui.generated.resources.support_sponsor
import dev.ide.ui.generated.resources.support_star
import dev.ide.ui.generated.resources.support_title
import dev.ide.ui.generated.resources.your_files
import dev.ide.ui.generated.resources.your_projects
import dev.ide.ui.icons.CaIcons
Expand Down Expand Up @@ -179,15 +172,9 @@ fun ProjectPickerScreen(
// has nowhere else to start a clone from.
if (onCloneRepository != null) CloneRepositoryCard(onCloneRepository)

// The support card: CodeAssist is free, ad-free and open source, so the only "monetisation"
// is an optional sponsor/star. Shown whenever the host can open links.
if (onSponsor != null || onStarOnGitHub != null) {
SupportCard(onSponsor = onSponsor, onStar = onStarOnGitHub)
}
if (showLegacyRecovery && compatibilityCount > 0) {
LegacyRecoveryBanner(count = compatibilityCount, onDismiss = onDismissLegacyRecovery)
}
if (onJoinDiscord != null) DiscordCard(onJoinDiscord)

if (projects.isEmpty()) {
Text(stringResource(Res.string.no_project_yet), color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodyMedium)
Expand All @@ -211,7 +198,6 @@ fun ProjectPickerScreen(
// A native ad below the project list — an idle "between tasks" spot, never over the actions.
// Renders nothing unless ads are active (host available, enabled, not a supporter).
AdSlot(AdPlacement.PROJECTS)
BetaBanner(onSubmit = onSubmitSuggestions)
StorageAccessCard(path = storagePath, onOpenInFiles = onOpenInFiles)
}
}
Expand Down Expand Up @@ -397,129 +383,6 @@ private fun SectionLabel(text: String, count: Int? = null) {
}
}

/** Discord brand "blurple" — used only for the community card's icon/accent. */
private val DiscordBlurple = Color(0xFF5865F2)

/** A slim, blurple-tinted card inviting the user to join the community Discord. */
@Composable
private fun DiscordCard(onClick: () -> Unit) {
val interaction = remember { MutableInteractionSource() }
val shape = RoundedCornerShape(Ca.radius.lg)
Row(
Modifier
.fillMaxWidth()
.pressScale(interaction)
.background(DiscordBlurple.copy(alpha = 0.12f), shape)
.border(1.dp, DiscordBlurple.copy(alpha = 0.35f), shape)
.clickable(interaction, indication = null, onClick = onClick)
.padding(14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Box(
Modifier.size(40.dp).background(DiscordBlurple, RoundedCornerShape(Ca.radius.md)),
contentAlignment = Alignment.Center,
) {
Icon(CaIcons.discord, null, Modifier.size(22.dp), tint = Color.White)
}
Column(Modifier.weight(1f)) {
Text(stringResource(Res.string.join_the_community), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium)
Text(stringResource(Res.string.join_the_community_content), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
}
Icon(CaIcons.chevronRight, null, Modifier.size(20.dp), tint = DiscordBlurple)
}
}

/** GitHub Sponsors' pink — the support card's icon/accent, kept distinct from the app accent. */
private val SponsorPink = Color(0xFFDB61A2)

/**
* A persistent card asking for support. CodeAssist is free, ad-free and fully open source, so the only
* ask is an optional GitHub sponsorship or a star. [onSponsor]/[onStar] are wired by the host to open the
* respective URLs; a null action simply hides that button.
*/
@Composable
private fun SupportCard(onSponsor: (() -> Unit)?, onStar: (() -> Unit)?) {
val shape = RoundedCornerShape(Ca.radius.lg)
Column(
Modifier
.fillMaxWidth()
.background(SponsorPink.copy(alpha = 0.10f), shape)
.border(1.dp, SponsorPink.copy(alpha = 0.30f), shape)
.padding(14.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Box(
Modifier.size(40.dp).background(SponsorPink, RoundedCornerShape(Ca.radius.md)),
contentAlignment = Alignment.Center,
) {
Icon(CaIcons.heart, null, Modifier.size(22.dp), tint = Color.White)
}
Column(Modifier.weight(1f)) {
Text(stringResource(Res.string.support_title), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium)
Text(stringResource(Res.string.support_content), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
}
}

// The ads on/off switch used to live here; it moved to Settings → Privacy & Data. This card is now
// just the support actions (Sponsor / Star), kept separate from ads.
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
if (onSponsor != null) {
SupportButton(stringResource(Res.string.support_sponsor), CaIcons.heart, Modifier.weight(1f), filled = true, onClick = onSponsor)
}
if (onStar != null) {
SupportButton(stringResource(Res.string.support_star), CaIcons.star, Modifier.weight(1f), filled = false, onClick = onStar)
}
}
}
}


@Composable
private fun SupportChip(text: String) {
Row(
Modifier
.background(MaterialTheme.colorScheme.surfaceContainerHigh, RoundedCornerShape(Ca.radius.pill))
.padding(horizontal = 10.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
Icon(CaIcons.check, null, Modifier.size(13.dp), tint = SponsorPink)
Text(text, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, fontWeight = FontWeight.Medium)
}
}

/** A support action: a filled (sponsor) or outlined (star) button with a leading glyph. */
@Composable
private fun SupportButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, filled: Boolean, onClick: () -> Unit) {
val interaction = remember { MutableInteractionSource() }
val shape = RoundedCornerShape(Ca.radius.control)
val surface =
if (filled) Modifier.background(SponsorPink, shape)
else Modifier.background(MaterialTheme.colorScheme.surfaceContainerHigh, shape).border(1.dp, MaterialTheme.colorScheme.outlineVariant, shape)
Row(
modifier
.pressScale(interaction)
.then(surface)
.clickable(interaction, indication = null, onClick = onClick)
.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Icon(icon, null, Modifier.size(16.dp), tint = if (filled) Color.White else MaterialTheme.colorScheme.onSurfaceVariant)
Spacer(Modifier.size(6.dp))
Text(
text,
color = if (filled) Color.White else MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

@Composable
private fun ProjectCard(
project: ProjectInfo,
Expand Down