Skip to content
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ AppHarbor already has a working installable development build and the core multi
- broader source support
- full AppHarbor visual identity, launcher artwork, and fresh screenshots

## Support AppHarbor

AppHarbor is developed as a free, open-source Android app without ads or paid search placement. If you find it useful, you can [support its development on Buy Me a Coffee](https://buymeacoffee.com/stupidgiraffe). Support is voluntary and does not affect catalog visibility, app updates, or trust signals.

## Build from source

AppHarbor currently builds with JDK 17 and the Android SDK.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import com.looker.droidify.utility.common.sharedSourceUrl
import com.looker.droidify.utility.common.openUnknownAppSourcesSettings
import com.looker.droidify.utility.common.requestNotificationPermission
import com.looker.droidify.utility.common.wallpaperAccentColor
import com.looker.droidify.work.CreatorDiscoveryScheduler
import com.looker.droidify.work.SyncWorker
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -112,6 +113,9 @@ class MainComposeActivity : ComponentActivity() {
@Inject
lateinit var externalApi: ExternalApi

@Inject
lateinit var creatorDiscoveryScheduler: CreatorDiscoveryScheduler

companion object {
const val ACTION_UPDATES = "${BuildConfig.APPLICATION_ID}.intent.action.UPDATES"

Expand All @@ -126,6 +130,7 @@ class MainComposeActivity : ComponentActivity() {
private const val KEY_OMNIFY_SEED = "omnify_seed_v6"
private const val KEY_OMNIFY_CURATED_MIGRATED = "omnify_curated_migrated_v1"
private const val KEY_APPHARBOR_SOURCE_MIGRATED = "appharbor_source_migrated_v1"
private const val KEY_UPSTREAM_CURATED_CLEANUP_V1 = "upstream_curated_cleanup_v1"
private const val KEY_TV_PACK_SEEDED_V1 = "tv_pack_seeded_v1"
private const val KEY_TV_PACK_ICONS_BACKFILLED_V1 = "tv_pack_icons_backfilled_v1"
private const val KEY_TV_PACK_CURATEDTV_BACKFILLED_V1 = "tv_pack_curatedtv_backfilled_v1"
Expand All @@ -145,20 +150,6 @@ class MainComposeActivity : ComponentActivity() {
curated = true,
)

/** The developer's whole GitHub account as a second, opt-in source: disabled by default (the user
* enables it to get every app of the account), forks included (the apps are published as forks),
* labelled with the account name but shown with the app's own logo (see [ExternalAccount.OMNIFY_KEY]).
* The AppHarbor repo above is tracked separately, so the account won't absorb it. */
private fun victorAccount(): ExternalAccount = ExternalAccount(
provider = SourceProvider.GITHUB,
owner = "Victor-root",
label = "Victor-root",
description = getString(R.string.external_account_omnify_description),
enabled = false,
includeForks = true,
curated = true,
)

/**
* A hand-picked set of FOSS Android TV apps, seeded once as disabled "AppHarbor's picks" entries (see
* [KEY_TV_PACK_SEEDED_V1]). F-Droid's catalogue turned out to have almost nothing genuinely tagged
Expand Down Expand Up @@ -492,12 +483,10 @@ class MainComposeActivity : ComponentActivity() {
.forEach { repository.enableRepository(it, enable = true) }
}

// Seed AppHarbor's own update source on a fresh install while keeping Victor-root's
// upstream account available as a separate, opt-in source.
// Keep the legacy first-run flag for existing installations, but seed only our own
// update channel. Users can add upstream maintainers as ordinary sources if they wish.
if (!firstRunPrefs.getBoolean(KEY_OMNIFY_SEED, false)) {
externalAppRepository.removeAppsByAccount(ExternalAccount.OMNIFY_KEY)
externalAppRepository.upsertApp(appHarborUpdateSource())
externalAppRepository.upsertAccount(victorAccount())
firstRunPrefs.edit().putBoolean(KEY_OMNIFY_SEED, true).apply()
}

Expand All @@ -520,17 +509,35 @@ class MainComposeActivity : ComponentActivity() {
}

// Keep the inherited migration flag name for install compatibility, but apply the curated
// marker to AppHarbor's own source. Victor-root's account remains an explicit upstream credit.
// marker only to AppHarbor's own source.
if (!firstRunPrefs.getBoolean(KEY_OMNIFY_CURATED_MIGRATED, false)) {
externalAppRepository.getApps()
.firstOrNull { it.key == ExternalApp.APPHARBOR_REPO_KEY && !it.curated }
?.let { externalAppRepository.upsertApp(it.copy(curated = true)) }
externalAppRepository.getAccounts()
.firstOrNull { it.key == ExternalAccount.OMNIFY_KEY && !it.curated }
?.let { externalAppRepository.upsertAccount(it.copy(curated = true)) }
firstRunPrefs.edit().putBoolean(KEY_OMNIFY_CURATED_MIGRATED, true).apply()
}

// Remove the inherited account from AppHarbor's promotional picks, once, without
// removing user-managed sources. An enabled upstream account is a deliberate follow,
// so retain it as an ordinary account and keep all its discovered apps. An inactive
// inherited curated entry was a default suggestion: cancel any residual scan and
// clean its dependent results. Accounts added by users are never touched.
if (!firstRunPrefs.getBoolean(KEY_UPSTREAM_CURATED_CLEANUP_V1, false)) {
externalAppRepository.getAccounts()
.firstOrNull { it.key == ExternalAccount.OMNIFY_KEY && it.curated }
?.let { upstream ->
if (upstream.enabled) {
externalAppRepository.upsertAccount(upstream.copy(curated = false))
Comment on lines +529 to +530

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent account rescans from undoing the migration

Coordinate this update with creator discovery before marking the migration complete. Droidify.kt:105 concurrently resumes and schedules eligible enabled accounts during application startup, and a worker that read the old curated == true account passes that snapshot to rescanAccountNow; its final ExternalRefresher.kt:260 upsert writes the stale account back with only lastScan changed. If that scan finishes after this upsert, Victor-root returns to the curated picks while KEY_UPSTREAM_CURATED_CLEANUP_V1 remains set, so the migration never retries.

Useful? React with 👍 / 👎.

} else {
Comment on lines +529 to +531

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve previously followed disabled accounts

Do not use enabled as the test for whether this is an untouched promotional entry. A user can enable the pre-seeded Victor-root account, complete discovery, and later disable it through the normal account toggle; it then remains curated == true but has enabled == false. On their first launch after this upgrade, the else branch deletes the account, all discovered apps, their settings, and scan history. Use evidence that the entry was never followed, such as lastScan == 0L and no dependent apps, and otherwise retain it as an ordinary account.

Useful? React with 👍 / 👎.

creatorDiscoveryScheduler.cancel(upstream.key)
externalAppRepository.removeAppsByAccount(upstream.key)
externalAppRepository.removeAccount(upstream.key)
externalAppRepository.removeDiscoveryJob(upstream.key)
}
}
firstRunPrefs.edit().putBoolean(KEY_UPSTREAM_CURATED_CLEANUP_V1, true).apply()
}

// One-time: seed the curated Android TV app pack (see curatedTvPack's doc comment), disabled
// by default like every other curated entry. addApp (not upsertApp): skips silently if a key
// already exists — e.g. the user had already added one of these repos themselves before this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -980,23 +980,14 @@ private fun ExternalAccountItem(
.clickable(onClick = onOpen)
.padding(vertical = 4.dp),
) {
if (account.key == ExternalAccount.OMNIFY_KEY) {
// The built-in Omnify source is branded with the app's own logo so it's recognisable.
AppLauncherIcon(
modifier = Modifier
.size(48.dp)
.alpha(contentAlpha),
)
} else {
RepoIcon(
iconUrl = account.iconUrl,
fallbackUrl = null,
name = account.label,
modifier = Modifier
.size(48.dp)
.alpha(contentAlpha),
)
}
RepoIcon(
iconUrl = account.iconUrl,
fallbackUrl = null,
name = account.label,
Comment on lines +983 to +986

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove upstream branding from every account surface

Apply this avatar change to the other account renderers as well. When an enabled Victor-root entry is retained as an ordinary source, this phone row now shows its GitHub avatar, but ExternalAccountDetailScreen.kt:222 and TvRepoListScreen.kt:672 still special-case the same key and display AppHarbor's launcher icon. Opening the account or viewing it on TV therefore continues to present an unrelated upstream maintainer as AppHarbor-branded, contrary to the migration's removal of that promotion.

Useful? React with 👍 / 👎.

modifier = Modifier
.size(48.dp)
.alpha(contentAlpha),
)
Spacer(modifier = Modifier.size(16.dp))
Column(
modifier = Modifier
Expand Down
Loading