diff --git a/app/src/main/java/com/foxx/androidcast/AppPreferences.java b/app/src/main/java/com/foxx/androidcast/AppPreferences.java index c7a46ac..c09e6f9 100644 --- a/app/src/main/java/com/foxx/androidcast/AppPreferences.java +++ b/app/src/main/java/com/foxx/androidcast/AppPreferences.java @@ -18,6 +18,7 @@ import android.provider.Settings; import android.text.TextUtils; import com.foxx.androidcast.display.ExternalDisplayCapturePolicy; +import com.foxx.androidcast.ota.OtaDefaults; import com.foxx.androidcast.receiver.buffer.JitterBufferConfig; import com.foxx.androidcast.receiver.buffer.JitterBufferMode; import com.foxx.androidcast.receiver.av.ReceiverAudioPreset; @@ -55,6 +56,7 @@ public final class AppPreferences { private static final String KEY_RECEIVER_PIP_USER_DEFINED = "receiver_pip_user_defined"; private static final String KEY_RECEIVER_PIP_ENABLED = "receiver_pip_enabled"; private static final String KEY_OTA_CHANNEL_URL = "ota_channel_url"; + private static final String KEY_OTA_CHANNEL_PRESET = "ota_channel_preset"; private static final String KEY_OTA_MANIFEST_URL_LEGACY = "ota_manifest_url"; private static final String KEY_OTA_CHECK_ON_LAUNCH = "ota_check_on_launch"; private static final String KEY_OTA_LAST_CHECK_MS = "ota_last_check_ms"; @@ -312,6 +314,19 @@ public final class AppPreferences { .apply(); } + /** {@code dev}, {@code staging}, or {@code prod} (default). */ + public static String getOtaChannelPreset(Context context) { + return prefs(context).getString(KEY_OTA_CHANNEL_PRESET, OtaDefaults.defaultPreset()); + } + + public static void setOtaChannelPreset(Context context, String preset) { + String v = preset != null ? preset.trim() : ""; + if (v.isEmpty()) { + v = OtaDefaults.defaultPreset(); + } + prefs(context).edit().putString(KEY_OTA_CHANNEL_PRESET, v).apply(); + } + public static boolean isOtaCheckOnLaunch(Context context) { return prefs(context).getBoolean(KEY_OTA_CHECK_ON_LAUNCH, true); } diff --git a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java index d8a8522..7351326 100644 --- a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java +++ b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java @@ -107,19 +107,24 @@ public class DeveloperSettingsActivity extends AppCompatActivity { }); TextView installedVersion = findViewById(R.id.text_ota_installed_version); + Spinner otaChannelSpinner = findViewById(R.id.spinner_ota_channel); TextInputEditText manifestUrl = findViewById(R.id.edit_ota_manifest_url); CheckBox otaOnLaunch = findViewById(R.id.check_ota_on_launch); Button checkOta = findViewById(R.id.btn_check_ota); - refreshOtaUi(installedVersion, manifestUrl, otaOnLaunch); + bindOtaChannelSpinner(otaChannelSpinner, manifestUrl); + + refreshOtaUi(installedVersion, otaChannelSpinner, manifestUrl, otaOnLaunch); otaOnLaunch.setOnCheckedChangeListener((buttonView, isChecked) -> AppPreferences.setOtaCheckOnLaunch(this, isChecked)); checkOta.setOnClickListener(v -> { + String preset = presetFromSpinner(otaChannelSpinner); + AppPreferences.setOtaChannelPreset(this, preset); String url = manifestUrl.getText() != null ? manifestUrl.getText().toString().trim() : ""; if (url.isEmpty()) { - url = OtaDefaults.effectiveChannelUrl(this); + url = OtaDefaults.channelUrlForPreset(preset); } AppPreferences.setOtaChannelUrl(this, url); OtaUpdateCoordinator.requestManualCheck(this); @@ -165,6 +170,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity { bindDiagPingSection(); refreshOtaUi( findViewById(R.id.text_ota_installed_version), + findViewById(R.id.spinner_ota_channel), findViewById(R.id.edit_ota_manifest_url), findViewById(R.id.check_ota_on_launch)); bindAvPresets(); @@ -704,14 +710,26 @@ public class DeveloperSettingsActivity extends AppCompatActivity { }; } - private void refreshOtaUi(TextView installedVersion, TextInputEditText manifestUrl, CheckBox otaOnLaunch) { + private void refreshOtaUi(TextView installedVersion, Spinner otaChannelSpinner, + TextInputEditText manifestUrl, CheckBox otaOnLaunch) { if (installedVersion != null) { installedVersion.setText(getString(R.string.ota_installed_version, OtaUpdateChecker.installedVersionName(this), OtaUpdateChecker.installedVersionCode(this))); } + String preset = AppPreferences.getOtaChannelPreset(this); + if (otaChannelSpinner != null) { + int idx = presetIndex(preset); + if (otaChannelSpinner.getSelectedItemPosition() != idx) { + otaChannelSpinner.setSelection(idx, false); + } + } if (manifestUrl != null) { - manifestUrl.setText(OtaDefaults.effectiveChannelUrl(this)); + String url = AppPreferences.getOtaChannelUrl(this); + if (url.isEmpty()) { + url = OtaDefaults.channelUrlForPreset(preset); + } + manifestUrl.setText(url); } if (otaOnLaunch != null) { otaOnLaunch.setOnCheckedChangeListener(null); @@ -721,6 +739,62 @@ public class DeveloperSettingsActivity extends AppCompatActivity { } } + private void bindOtaChannelSpinner(Spinner spinner, TextInputEditText manifestUrl) { + if (spinner == null) { + return; + } + ArrayAdapter adapter = new ArrayAdapter<>(this, + android.R.layout.simple_spinner_item, + new String[]{ + getString(R.string.ota_channel_dev), + getString(R.string.ota_channel_staging), + getString(R.string.ota_channel_prod), + }); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(android.widget.AdapterView parent, android.view.View view, + int position, long id) { + String preset = presetFromIndex(position); + AppPreferences.setOtaChannelPreset(DeveloperSettingsActivity.this, preset); + if (manifestUrl != null) { + manifestUrl.setText(OtaDefaults.channelUrlForPreset(preset)); + } + } + + @Override + public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } + + private static int presetIndex(String preset) { + if (OtaDefaults.CHANNEL_DEV.equals(preset)) { + return 0; + } + if (OtaDefaults.CHANNEL_STAGING.equals(preset)) { + return 1; + } + return 2; + } + + private static String presetFromIndex(int index) { + if (index == 0) { + return OtaDefaults.CHANNEL_DEV; + } + if (index == 1) { + return OtaDefaults.CHANNEL_STAGING; + } + return OtaDefaults.CHANNEL_PROD; + } + + private static String presetFromSpinner(Spinner spinner) { + if (spinner == null) { + return OtaDefaults.defaultPreset(); + } + return presetFromIndex(spinner.getSelectedItemPosition()); + } + private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats, CheckBox streamDump, CheckBox adaptiveDisplay, CheckBox backendNtpSync) { bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this), diff --git a/app/src/main/java/com/foxx/androidcast/ota/OtaDefaults.java b/app/src/main/java/com/foxx/androidcast/ota/OtaDefaults.java index fc5f201..5e78a85 100644 --- a/app/src/main/java/com/foxx/androidcast/ota/OtaDefaults.java +++ b/app/src/main/java/com/foxx/androidcast/ota/OtaDefaults.java @@ -7,18 +7,66 @@ import com.foxx.androidcast.BuildConfig; /** Built-in OTA channel fallback when prefs / BuildConfig / settings.json omit a URL. */ public final class OtaDefaults { - /** Canonical stable channel on the public apps host. */ + /** Public apps host — channel JSON lives under {@code /v0/ota/channel/}. */ + public static final String OTA_HOST_BASE = "https://apps.f0xx.org"; + + public static final String CHANNEL_DEV = "dev"; + public static final String CHANNEL_STAGING = "staging"; + public static final String CHANNEL_PROD = "prod"; + + /** Legacy alias; prod channel file is {@code stable.json}. */ public static final String FALLBACK_CHANNEL_URL = - "https://apps.f0xx.org/v0/ota/channel/stable.json"; + channelUrlForPreset(CHANNEL_PROD); private OtaDefaults() {} - /** Preference → BuildConfig channel → BuildConfig manifest → {@link #FALLBACK_CHANNEL_URL}. */ + /** Maps UI preset to on-disk channel filename (prod → stable.json). */ + public static String channelFileForPreset(String preset) { + if (CHANNEL_DEV.equals(preset)) { + return "dev.json"; + } + if (CHANNEL_STAGING.equals(preset)) { + return "staging.json"; + } + return "stable.json"; + } + + public static String channelUrlForPreset(String preset) { + return OTA_HOST_BASE + "/v0/ota/channel/" + channelFileForPreset(preset); + } + + public static String defaultPreset() { + return CHANNEL_PROD; + } + + /** Preset from stored URL when possible (dev/staging/prod). */ + public static String presetForUrl(String url) { + if (url == null || url.isEmpty()) { + return defaultPreset(); + } + String lower = url.toLowerCase(); + if (lower.contains("/channel/dev.json")) { + return CHANNEL_DEV; + } + if (lower.contains("/channel/staging.json")) { + return CHANNEL_STAGING; + } + if (lower.contains("/channel/stable.json")) { + return CHANNEL_PROD; + } + return defaultPreset(); + } + + /** Preference URL → preset URL → BuildConfig → {@link #FALLBACK_CHANNEL_URL}. */ public static String effectiveChannelUrl(Context context) { String url = AppPreferences.getOtaChannelUrl(context); if (!url.isEmpty()) { return url; } + String preset = AppPreferences.getOtaChannelPreset(context); + if (preset != null && !preset.isEmpty()) { + return channelUrlForPreset(preset); + } if (BuildConfig.OTA_CHANNEL_URL_DEFAULT != null) { url = BuildConfig.OTA_CHANNEL_URL_DEFAULT.trim(); if (!url.isEmpty()) { diff --git a/app/src/main/java/com/foxx/androidcast/ota/OtaSettingsStore.java b/app/src/main/java/com/foxx/androidcast/ota/OtaSettingsStore.java index 9ac1792..5ef4176 100644 --- a/app/src/main/java/com/foxx/androidcast/ota/OtaSettingsStore.java +++ b/app/src/main/java/com/foxx/androidcast/ota/OtaSettingsStore.java @@ -42,6 +42,9 @@ final class OtaSettingsStore { LinkedHashMap dedup = new LinkedHashMap<>(); addSource(dedup, AppPreferences.getOtaChannelUrl(context)); + if (AppPreferences.getOtaChannelUrl(context).isEmpty()) { + addSource(dedup, OtaDefaults.channelUrlForPreset(AppPreferences.getOtaChannelPreset(context))); + } if (BuildConfig.OTA_CHANNEL_URL_DEFAULT != null) { addSource(dedup, BuildConfig.OTA_CHANNEL_URL_DEFAULT); } diff --git a/app/src/main/res/layout/activity_dev_settings_panel.xml b/app/src/main/res/layout/activity_dev_settings_panel.xml index a044ae6..d6f3e9f 100644 --- a/app/src/main/res/layout/activity_dev_settings_panel.xml +++ b/app/src/main/res/layout/activity_dev_settings_panel.xml @@ -333,6 +333,19 @@ android:layout_marginTop="8dp" android:textSize="13sp" /> + + + + Dolby 5.1 (stereo headset) Dolby 7.1 (stereo headset) App updates (OTA) - v0 layout: https://host/v0/ota/channel/stable.json → manifest → APK. Service checks every 1 minute by default (overridable in private settings.json). - OTA channel URL (v0 stable.json) + v0 layout: channel JSON → manifest → APK (or bundle). Default channel: production (stable.json). Checks run every 1 minute when enabled. + OTA channel + Development (dev) + Staging (QA) + Production (default) + OTA channel URL (full https URL) Enable OTA periodic checks (default every 1 minute) Check for updates now Installed: %1$s (%2$d)