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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ branch names, credentials, signing material, or other secrets.
the dedicated release workflow/Fastlane lane.
- Tagged releases publish signed APKs and `SHA256SUMS` to GitHub Releases. Google Play integration
is removed. Document installation from release assets; PR APKs are for testing only.
- Update checks default to daily background checks: official F-Droid installer uses its API, any
other named installer uses GitHub, and unknown installers require a source choice. Preserve the
installed APK variant and existing version-code offsets. Downloads require explicit consent;
- Update checks default to daily background checks: official F-Droid installers and installer packages handling
fdroidrepo/fdroidrepos links use its API; other named installers use GitHub. Unknown installers
or failed capability lookup require a source choice. Preserve the installed APK variant and existing version-code offsets. Downloads require explicit consent;
ignored notifications repeat after a week, with skip-version and disable-checks actions.
- The app is not yet available in F-Droid; do not document it as an installation source.
- After creating and verifying a release, update the corresponding F-Droid submission/build recipe
Expand Down
18 changes: 16 additions & 2 deletions app/src/main/java/net/megaproxy487/AppUpdates.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package net.megaproxy487

import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import java.io.ByteArrayOutputStream
Expand All @@ -18,9 +20,11 @@ import org.json.JSONObject

internal enum class UpdateSource { FDROID, GITHUB }

internal fun updateSourceForInstaller(installer: String?): UpdateSource? = when {
internal fun updateSourceForInstaller(installer: String?, handlesFdroidRepos: Boolean? = false): UpdateSource? = when {
installer.isNullOrBlank() -> null
installer == "org.fdroid.fdroid" || installer == "org.fdroid.fdroid.privileged" -> UpdateSource.FDROID
handlesFdroidRepos == null -> null
handlesFdroidRepos -> UpdateSource.FDROID
else -> UpdateSource.GITHUB
}

Expand Down Expand Up @@ -105,7 +109,17 @@ internal class AppUpdates(private val context: Context) {
if (Build.VERSION.SDK_INT >= 30) manager.getInstallSourceInfo(context.packageName).installingPackageName
else manager.getInstallerPackageName(context.packageName)
} catch (_: Exception) { null }
return updateSourceForInstaller(installer)
if (installer.isNullOrBlank()) return null
val handlesRepos = try {
listOf("fdroidrepo", "fdroidrepos").any { scheme ->
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$scheme://f-droid.org/repo"))
.addCategory(Intent.CATEGORY_BROWSABLE)
.setPackage(installer)
manager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
.any { it.activityInfo?.packageName == installer }
}
} catch (_: Exception) { null }
return updateSourceForInstaller(installer, handlesRepos)
}

fun select(source: UpdateSource) {
Expand Down
8 changes: 1 addition & 7 deletions app/src/main/java/net/megaproxy487/UpdatesScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,7 @@ internal fun UpdatesScreen(activity: Activity, onBack: () -> Unit, model: Update
if (update.source == UpdateSource.FDROID) {
Button(onClick = {
try {
try {
activity.startActivity(Intent(Intent.ACTION_VIEW,
Uri.parse("fdroid.app://details?id=${activity.packageName}"))
.setPackage("org.fdroid.fdroid"))
} catch (_: android.content.ActivityNotFoundException) {
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(FDROID_APP_URL)))
}
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(FDROID_APP_URL)))
} catch (_: Exception) { model.reportOpenError() }
}) { Text(stringResource(R.string.update_in_fdroid)) }
} else if (model.apk == null) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/test/java/net/megaproxy487/AppUpdatesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class AppUpdatesTest {
@Test fun installerSelectionDoesNotGuessWhenUnknown() {
assertNull(updateSourceForInstaller(null))
assertNull(updateSourceForInstaller(""))
assertEquals(UpdateSource.FDROID, updateSourceForInstaller("org.example.fdroid", true))
assertNull(updateSourceForInstaller("org.example.fdroid", null))
assertEquals(UpdateSource.FDROID, updateSourceForInstaller("org.fdroid.fdroid"))
assertEquals(UpdateSource.FDROID, updateSourceForInstaller("org.fdroid.fdroid.privileged"))
for (installer in listOf("com.android.chrome", "com.android.documentsui", "org.example.other", "net.megaproxy487")) {
Expand Down
77 changes: 77 additions & 0 deletions app/src/test/java/net/megaproxy487/InstallerUpdateSourceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package net.megaproxy487

import android.app.Application
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [26, 35], application = Application::class)
class InstallerUpdateSourceTest {
private val app get() = RuntimeEnvironment.getApplication()

@Before fun clearSource() {
app.getSharedPreferences("updates", 0).edit().clear().commit()
}

@Suppress("DEPRECATION")
private fun installer(name: String?) {
if (Build.VERSION.SDK_INT >= 30) {
shadowOf(app.packageManager).setInstallSourceInfo(app.packageName, name, name)
} else {
app.packageManager.setInstallerPackageName(app.packageName, name)
}
}

@Suppress("DEPRECATION")
private fun handlesRepository(name: String, scheme: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$scheme://f-droid.org/repo"))
.addCategory(Intent.CATEGORY_BROWSABLE).setPackage(name)
shadowOf(app.packageManager).addResolveInfoForIntent(intent, ResolveInfo().apply {
activityInfo = ActivityInfo().apply {
packageName = name
this.name = "$name.RepositoryActivity"
exported = true
enabled = true
}
})
}

@Test fun detectsAlternativeClientWithEitherRepositoryScheme() {
for (scheme in listOf("fdroidrepo", "fdroidrepos")) {
val name = "org.example.$scheme"
installer(name)
handlesRepository(name, scheme)
assertEquals(UpdateSource.FDROID, AppUpdates(app).source())
}
}

@Test fun unrelatedFdroidClientDoesNotChangeBrowserInstallerSource() {
handlesRepository("org.example.store", "fdroidrepos")
installer("com.android.chrome")
assertEquals(UpdateSource.GITHUB, AppUpdates(app).source())
}

@Test fun unknownInstallerRequiresChoiceEvenWithFdroidInstalled() {
handlesRepository("org.example.store", "fdroidrepo")
installer(null)
assertNull(AppUpdates(app).source())
}

@Test fun manualSourceTakesPriorityAndOfficialPrivilegedInstallerStillWorks() {
installer("org.fdroid.fdroid.privileged")
assertEquals(UpdateSource.FDROID, AppUpdates(app).source())
AppUpdates(app).select(UpdateSource.GITHUB)
assertEquals(UpdateSource.GITHUB, AppUpdates(app).source())
}
}
9 changes: 8 additions & 1 deletion app/src/test/java/net/megaproxy487/UpdatesUiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ class UpdatesUiTest : MainUiTestBase() {
node(R.string.update_check).performScrollTo().performClick()
node(R.string.update_in_fdroid).assertExists()
node(R.string.update_download).assertDoesNotExist()
compose.runOnIdle { model.download(); assertNull(model.apk) }
node(R.string.update_in_fdroid).performScrollTo().performClick()
compose.runOnIdle {
val intent = org.robolectric.Shadows.shadowOf(activity).nextStartedActivity
assertEquals(FDROID_APP_URL, intent.data.toString())
assertNull(intent.`package`)
model.download()
assertNull(model.apk)
}
}
}
2 changes: 1 addition & 1 deletion docs/en/release-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ visible in the matrix; an emulator result never silently substitutes for it.

| ID | Steps → required observation |
| --- | --- |
| U01 | Official F-Droid installer identity, another installer, unknown installer and manual override. Check chosen API and persistence. An unpublished F-Droid package gives an explicit unavailable result without switching to GitHub. Simulated installer identity is not installation from the repository. |
| U01 | Official and alternative F-Droid installers (fdroidrepo/fdroidrepos handlers), browser/file installer, unknown installer and manual override. An unrelated installed F-Droid client must not change a browser installation's source. Check chosen API and persistence. An unpublished F-Droid package gives an explicit unavailable result without switching to GitHub. Simulated installer identity is not installation from the repository. |
| U02 | New/same/older/malformed release metadata and unavailable network. Explicit download consent/cancel, APK size/digest/package/version/signer/ABI checks, corruption and download interruption. Use unit tests for deliberately invalid APKs and isolate destructive fixtures to emulators. |
| U03 | Universal stays universal, ABI-specific stays that ABI; system install permission denied/granted, cancellation, return from settings and successful update. Read back installed APK/version/signature and verify real connectivity afterward. |
| U04 | Notifications allowed/denied/channel disabled; Update, Skip version, Disable checks; manual check after skip; disable while job is in flight; job restoration after reboot. Test no early reminder and reminder after seven days by controlled timestamp injection on an emulator. Report forced jobs/time simulation separately from real daily/weekly delivery. |
Expand Down
15 changes: 9 additions & 6 deletions docs/en/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ which package installed it:
| Installer reported by Android | Update source |
| --- | --- |
| Official F-Droid (`org.fdroid.fdroid`) or its privileged installer | F-Droid |
| Any other named installer, including a browser or file manager | GitHub |
| Missing, empty, or unreadable installer information | Ask the user to choose; no background requests until then |

The installer identifies an app, not the original download website. Other F-Droid clients count
as other installers under this rule; select F-Droid manually if needed. Installing an APK downloaded
| Another installer that handles `fdroidrepo://` or `fdroidrepos://` links | F-Droid |
| Another named installer without those handlers, including a browser or file manager | GitHub |
| Missing, empty, or unreadable installer information, or failed handler lookup | Ask the user to choose; no background requests until then |

Repository-link support is checked only in the installer package; an unrelated installed F-Droid
client does not change the source. This is a capability heuristic, not proof of the APK's origin.
Clients without these handlers need manual source selection. The installer identifies an app,
not the original download website. Installing an APK downloaded
from the F-Droid website through a browser also selects GitHub initially. This is a default choice,
not proof of the APK's origin. Source changes do not bypass signature checks.

Expand All @@ -36,7 +39,7 @@ or use the notification permission button on the update screen.
The notification offers:

- **Update**: open the update screen and check the selected source again. F-Droid updates open
its client (or the app's web page if unavailable); GitHub updates require explicit download consent.
the package web link in a compatible client or browser; GitHub updates require explicit download consent.
- **Skip this version**: suppress automatic notifications for this version in this source.
Later versions can still notify. Manual checks still show a skipped update.
- **Disable auto-checks**: stop scheduled checks and remove the notification. Manual checks remain
Expand Down
2 changes: 1 addition & 1 deletion docs/ru/release-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Doze и разрушительные проверки данных сначал

| ID | Действия → требуемый результат |
| --- | --- |
| U01 | Installer F-Droid, другой, неизвестный; ручной выбор. Проверить API и persistence. Неопубликованный F-Droid пакет даёт понятную ошибку без перехода на GitHub. Подмена installer metadata не является установкой из репозитория. |
| U01 | Официальный и сторонний F-Droid (обработчики fdroidrepo/fdroidrepos), браузер/файловый установщик, неизвестный; ручной выбор. Посторонний установленный клиент F-Droid не меняет источник установки из браузера. Проверить API и persistence. Неопубликованный F-Droid пакет даёт понятную ошибку без перехода на GitHub. Подмена installer metadata не является установкой из репозитория. |
| U02 | Новая/та же/старая/повреждённая metadata, offline. Download consent/cancel; size/digest/package/version/signer/ABI, повреждение/обрыв APK. Заведомо неверные APK проверять unit-тестами и изолированными эмуляторами. |
| U03 | Universal остаётся universal, ABI-specific — тем же ABI; отказ/выдача install permission, Cancel, возврат из Settings, установка. Прочитать обратно APK/version/signature и проверить передачу данных. |
| U04 | Notification permission/channel; Update/Skip/Disable; manual после Skip; выключение при работающем job; восстановление job после reboot. Раннего напоминания нет, после семи дней есть — контролируемой правкой времени на эмуляторе. Forced jobs/time simulation не называть реальной суточной/недельной доставкой. |
Expand Down
12 changes: 7 additions & 5 deletions docs/ru/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ APK и установка без подтверждения не включен
| Установщик, указанный Android | Источник обновлений |
| --- | --- |
| Официальный F-Droid (`org.fdroid.fdroid`) или его привилегированный установщик | F-Droid |
| Любое другое приложение, включая браузер и файловый менеджер | GitHub |
| Установщик не указан, пуст или сведения недоступны | Предложить выбор; до выбора фоновых запросов нет |
| Другой установщик, обрабатывающий ссылки `fdroidrepo://` или `fdroidrepos://` | F-Droid |
| Другой именованный установщик без этих обработчиков, включая браузер и файловый менеджер | GitHub |
| Установщик не указан, пуст, сведения недоступны или проверка обработчиков завершилась ошибкой | Предложить выбор; до выбора фоновых запросов нет |

Это имя установщика, а не сайт скачивания. Другие клиенты F-Droid по этому правилу относятся
к другим приложениям: при необходимости выберите F-Droid вручную. APK с сайта F-Droid, открытый
Поддержка ссылок проверяется только у установщика: наличие другого клиента F-Droid на устройстве
не меняет источник. Это эвристика возможностей, не доказательство происхождения APK. Для клиентов
без этих обработчиков нужен ручной выбор. Имя установщика не указывает сайт скачивания. APK с сайта F-Droid, открытый
через браузер, также первоначально получает источник GitHub. Это выбор по умолчанию, а не
доказательство происхождения APK. Смена источника не отменяет проверку подписи.

Expand All @@ -37,7 +39,7 @@ Android планирует проверку примерно раз в сутк
Действия в уведомлении:

- **Обновить** — открыть экран обновлений и заново проверить выбранный источник. Для F-Droid
обновление выполняется через его клиент (при отсутствии — открывается страница приложения).
ссылка карточки открывается в совместимом клиенте или браузере.
Для GitHub требуется отдельное согласие на скачивание.
- **Пропустить версию** — больше не напоминать автоматически об этой версии в этом источнике.
О следующих версиях уведомления появятся. Ручная проверка покажет и пропущенную версию.
Expand Down
Loading