1
0
mirror of git://f0xx.org/ac/ac-mobile-android synced 2026-08-12 18:12:13 +03:00

OTA channel selector with dev/staging/prod presets.

Developer settings spinner prefills manifest URL from selected channel; prod is default.
This commit is contained in:
Anton Afanasyeu
2026-08-12 14:08:17 +02:00
parent 3e170646f0
commit da821cf0bc
6 changed files with 166 additions and 9 deletions

View File

@@ -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);
}

View File

@@ -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<String> 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),

View File

@@ -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()) {

View File

@@ -42,6 +42,9 @@ final class OtaSettingsStore {
LinkedHashMap<String, Boolean> 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);
}

View File

@@ -333,6 +333,19 @@
android:layout_marginTop="8dp"
android:textSize="13sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/ota_channel_label"
android:textSize="13sp" />
<Spinner
android:id="@+id/spinner_ota_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -397,8 +397,12 @@
<string name="dev_av_audio_dolby_51">Dolby 5.1 (stereo headset)</string>
<string name="dev_av_audio_dolby_71">Dolby 7.1 (stereo headset)</string>
<string name="ota_section_title">App updates (OTA)</string>
<string name="ota_section_hint">v0 layout: https://host/v0/ota/channel/stable.json → manifest → APK. Service checks every 1 minute by default (overridable in private settings.json).</string>
<string name="ota_manifest_url_hint">OTA channel URL (v0 stable.json)</string>
<string name="ota_section_hint">v0 layout: channel JSON → manifest → APK (or bundle). Default channel: production (stable.json). Checks run every 1 minute when enabled.</string>
<string name="ota_channel_label">OTA channel</string>
<string name="ota_channel_dev">Development (dev)</string>
<string name="ota_channel_staging">Staging (QA)</string>
<string name="ota_channel_prod">Production (default)</string>
<string name="ota_manifest_url_hint">OTA channel URL (full https URL)</string>
<string name="ota_check_on_launch">Enable OTA periodic checks (default every 1 minute)</string>
<string name="ota_check_now">Check for updates now</string>
<string name="ota_installed_version">Installed: %1$s (%2$d)</string>