fix(apk 0.2.1): in-app installer "nic się nie dzieje" + oo launcher icon

INSTALL BUG ("klikam Install i nic się nie dzieje"): ApkInstallerModule
commitował PackageInstaller.Session z PendingIntent → MainActivity, ale Android
po commit odsyła STATUS_PENDING_USER_ACTION + EXTRA_INTENT który MUSI być
startActivity()-owany żeby pokazać systemowy dialog "Install update?". Nic tego
nie obsługiwało → download OK, sesja commit OK, ale dialog NIGDY się nie
pokazywał. Fix: getBroadcast + runtime BroadcastReceiver → na PENDING_USER_ACTION
launchuje EXTRA_INTENT (FLAG_ACTIVITY_NEW_TASK), unregister na terminal status.
(Native — działa dla 0.2.1→przyszłe; do 0.2.1 user sideload z goon-foss.org.)

LAUNCHER ICON: regenerowane mipmapy (oo logo) z assets/icon.png przez PIL —
ic_launcher / ic_launcher_round / ic_launcher_foreground we wszystkich 5
densities (webp). iconBackground #0E1018 (stary navy) → #15110D (warm charcoal).

version 0.2.1 / versionCode 11. Build verified: podpis SHA-256 == ALLOWED,
Running "main" bez crasha. Deployed: /version=0.2.1, /static + webroot + landing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-05-31 13:15:37 +02:00
parent 0281e449fe
commit 55503136e6
20 changed files with 46 additions and 12 deletions

View file

@ -115,7 +115,7 @@ def version() -> dict[str, str | None]:
# mobile sklei z baseUrl. # mobile sklei z baseUrl.
public_url = os.environ.get("BACKEND_PUBLIC_URL", "").rstrip("/") public_url = os.environ.get("BACKEND_PUBLIC_URL", "").rstrip("/")
apk_url = f"{public_url}/static/app-release.apk" if public_url else "/static/app-release.apk" apk_url = f"{public_url}/static/app-release.apk" if public_url else "/static/app-release.apk"
return {"version": "0.2.0", "apk_url": apk_url} return {"version": "0.2.1", "apk_url": apk_url}
@app.get("/readyz") @app.get("/readyz")

View file

@ -93,8 +93,8 @@ android {
applicationId 'com.goon.mobile' applicationId 'com.goon.mobile'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 10 versionCode 11
versionName "0.2.0" versionName "0.2.1"
} }
signingConfigs { signingConfigs {
debug { debug {

View file

@ -1,7 +1,10 @@
package com.goon.mobile package com.goon.mobile
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -122,22 +125,53 @@ class ApkInstallerModule(reactContext: ReactApplicationContext) :
} }
} }
// PendingIntent musi być MUTABLE bo PackageInstaller dorzuca extras // BUG-FIX 2026-05-31 ("klikam Install i nic się nie dzieje"):
// (PackageInstaller.EXTRA_STATUS) przed delivery do nas. // commit() NIE pokazuje od razu dialogu. Android najpierw odsyła
val intent = Intent(ctx, MainActivity::class.java).apply { // STATUS_PENDING_USER_ACTION + EXTRA_INTENT, który MUSIMY ręcznie
action = "com.goon.mobile.APK_INSTALL_RESULT" // startActivity() żeby pokazać systemowy dialog "Install update?".
// Poprzednio target był MainActivity bez obsługi tego statusu →
// download się udawał, sesja commitowała, ale dialog nigdy nie
// wyskakiwał. Teraz: getBroadcast → receiver → launch EXTRA_INTENT.
val action = "com.goon.mobile.APK_INSTALL_STATUS"
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context, i: Intent) {
val status = i.getIntExtra(
PackageInstaller.EXTRA_STATUS,
PackageInstaller.STATUS_FAILURE,
)
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
@Suppress("DEPRECATION")
val confirm = i.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
confirm?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (confirm != null) c.startActivity(confirm)
} else {
// terminal (SUCCESS/FAILURE/ABORTED) — sprzątamy receiver.
try { c.applicationContext.unregisterReceiver(this) } catch (_: Exception) {}
}
}
} }
val filter = IntentFilter(action)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ctx.applicationContext.registerReceiver(
receiver, filter, Context.RECEIVER_NOT_EXPORTED,
)
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
ctx.applicationContext.registerReceiver(receiver, filter)
}
val intent = Intent(action).setPackage(ctx.packageName)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
} else { } else {
PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_UPDATE_CURRENT
} }
val pendingIntent = PendingIntent.getActivity(ctx, sessionId, intent, flags) val pendingIntent = PendingIntent.getBroadcast(ctx, sessionId, intent, flags)
s.commit(pendingIntent.intentSender) s.commit(pendingIntent.intentSender)
} }
// commit() jest async — Android pokaże dialog "Install update?" w tej samej // commit() async — receiver odpali systemowy dialog gdy Android poprosi
// chwili. Resolwujemy od razu — JS nie czeka na user choice (Android UI). // o user-action. JS nie czeka na wynik (to Android UI).
promise.resolve(null) promise.resolve(null)
} catch (e: Exception) { } catch (e: Exception) {
promise.reject("ERR_INSTALL", e.message ?: "install fail", e) promise.reject("ERR_INSTALL", e.message ?: "install fail", e)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,6 +1,6 @@
<resources> <resources>
<color name="splashscreen_background">#0E1018</color> <color name="splashscreen_background">#0E1018</color>
<color name="iconBackground">#0E1018</color> <color name="iconBackground">#15110D</color>
<color name="colorPrimary">#023c69</color> <color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#0E1018</color> <color name="colorPrimaryDark">#0E1018</color>
</resources> </resources>

View file

@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "goon", "name": "goon",
"slug": "goon", "slug": "goon",
"version": "0.2.0", "version": "0.2.1",
"orientation": "portrait", "orientation": "portrait",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": false, "newArchEnabled": false,