1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:39:09 +03:00

git flow, infra descriptions, cast flow changes (mobile side)

This commit is contained in:
Anton Afanasyeu
2026-05-23 12:18:43 +02:00
parent 41aa6659f5
commit a5ee5d7140
28 changed files with 1426 additions and 25 deletions

View File

@@ -20,6 +20,7 @@ import android.text.TextUtils;
import com.foxx.androidcast.display.ExternalDisplayCapturePolicy;
import com.foxx.androidcast.receiver.av.ReceiverAudioPreset;
import com.foxx.androidcast.receiver.av.ReceiverVideoPreset;
import com.foxx.androidcast.sender.SenderResolutionUiMode;
/**
* Global app preferences: identity, PIN, tray icon, and default sender/receiver cast options.
@@ -52,6 +53,7 @@ public final class AppPreferences {
private static final String KEY_DEV_WIRED_DISPLAY_HINTS = "dev_wired_display_hints";
private static final String KEY_DEV_USB_TETHER_TRANSPORT = "dev_usb_tether_transport";
private static final String KEY_DEV_SENDER_RESOLUTION_UI_MODE = "dev_sender_resolution_ui_mode";
private static final String KEY_DEV_EXTERNAL_CAPTURE_POLICY = "dev_external_capture_policy";
private static final String KEY_DEV_COMMERCIAL_ADS = "dev_commercial_ads";
private static final String KEY_DEV_COMMERCIAL_IAP = "dev_commercial_iap";
@@ -330,6 +332,22 @@ public final class AppPreferences {
prefs(context).edit().putBoolean(KEY_DEV_USB_TETHER_TRANSPORT, enabled).apply();
}
/** Default: {@link SenderResolutionUiMode#SEPARATED}. */
public static SenderResolutionUiMode getDevSenderResolutionUiMode(Context context) {
String name = prefs(context).getString(
KEY_DEV_SENDER_RESOLUTION_UI_MODE, SenderResolutionUiMode.SEPARATED.name());
try {
return SenderResolutionUiMode.valueOf(name);
} catch (IllegalArgumentException e) {
return SenderResolutionUiMode.SEPARATED;
}
}
public static void setDevSenderResolutionUiMode(Context context, SenderResolutionUiMode mode) {
SenderResolutionUiMode value = mode != null ? mode : SenderResolutionUiMode.SEPARATED;
prefs(context).edit().putString(KEY_DEV_SENDER_RESOLUTION_UI_MODE, value.name()).apply();
}
public static ExternalDisplayCapturePolicy getExternalDisplayCapturePolicy(Context context) {
return ExternalDisplayCapturePolicy.fromName(
prefs(context).getString(KEY_DEV_EXTERNAL_CAPTURE_POLICY, null),

View File

@@ -102,7 +102,9 @@ public class CastSettings implements Serializable {
HD_720P(1.0f, 720, 0),
FIXED_1080P(0f, 1080, 1920),
FIXED_720P(0f, 720, 1280),
ADAPTIVE(1.0f, 0, 0);
ADAPTIVE(1.0f, 0, 0),
/** Long-edge cap 480p (aspect preserved). */
SD_480P(1.0f, 480, 0);
public final float scale;
public final int maxHeight;

View File

@@ -106,7 +106,7 @@ public final class CastSettingsBinder {
bindVideoCodec(activity, settings, R.id.spinner_sender_video_codec);
bindBitrate(activity, settings);
bindNetworkAdoption(activity, settings, R.id.spinner_sender_network);
bindSenderResolution(activity, settings);
com.foxx.androidcast.sender.SenderResolutionUi.bindSenderResolution(activity, settings);
bindCaptureMode(activity, settings);
bindStreamProtection(activity, settings);
bindAudioSpinner(activity, settings, R.id.spinner_sender_audio);
@@ -323,27 +323,8 @@ public final class CastSettingsBinder {
readEnumSpinner(activity.findViewById(spinnerId), settings::setNetworkAdoption);
}
private static void bindSenderResolution(AppCompatActivity activity, CastSettings settings) {
Spinner spinner = activity.findViewById(R.id.spinner_sender_resolution);
CastSettings.Resolution[] modes = {
CastSettings.Resolution.ADAPTIVE,
CastSettings.Resolution.FIXED_1080P,
CastSettings.Resolution.FIXED_720P,
CastSettings.Resolution.FULL,
CastSettings.Resolution.THREE_QUARTERS
};
String[] labels = {
activity.getString(R.string.resolution_adaptive),
activity.getString(R.string.resolution_fixed_1080p),
activity.getString(R.string.resolution_fixed_720p),
activity.getString(R.string.resolution_full),
activity.getString(R.string.resolution_75)
};
bindEnumSpinner(spinner, modes, labels, settings.getResolution(), settings::setResolution);
}
private static void readSenderResolution(AppCompatActivity activity, CastSettings settings) {
readEnumSpinner(activity.findViewById(R.id.spinner_sender_resolution), settings::setResolution);
com.foxx.androidcast.sender.SenderResolutionUi.readSenderResolution(activity, settings);
}
public static void bindReceiverPip(AppCompatActivity activity) {
@@ -551,7 +532,7 @@ public final class CastSettingsBinder {
});
}
private static <T extends Enum<T>> void bindEnumSpinner(Spinner spinner, T[] values, String[] labels,
public static <T extends Enum<T>> void bindEnumSpinner(Spinner spinner, T[] values, String[] labels,
T current, java.util.function.Consumer<T> onSelect) {
spinner.setTag(R.id.tag_spinner_enum, values);
SettingsStringArrayAdapter adapter = new SettingsStringArrayAdapter(spinner.getContext(), labels);
@@ -581,7 +562,7 @@ public final class CastSettingsBinder {
});
}
private static <T extends Enum<T>> void readEnumSpinner(Spinner spinner,
public static <T extends Enum<T>> void readEnumSpinner(Spinner spinner,
java.util.function.Consumer<T> consumer) {
Object tag = spinner.getTag(R.id.tag_spinner_enum);
if (tag instanceof Enum[]) {

View File

@@ -54,6 +54,9 @@ public final class CastTuningEngine {
case HD_720P:
capLong = 720;
break;
case SD_480P:
capLong = 480;
break;
case FIXED_1080P:
boxW = 1920;
boxH = 1080;

View File

@@ -34,6 +34,7 @@ import com.foxx.androidcast.ota.OtaUpdateCoordinator;
import com.foxx.androidcast.receiver.av.ReceiverAudioPreset;
import com.foxx.androidcast.receiver.av.ReceiverAvRuntime;
import com.foxx.androidcast.receiver.av.ReceiverVideoPreset;
import com.foxx.androidcast.sender.SenderResolutionUiMode;
import com.foxx.androidcast.stats.ui.SessionStatsAnalyzerActivity;
import com.google.android.material.textfield.TextInputEditText;
@@ -88,6 +89,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
});
bindAvPresets();
bindSenderResolutionUiModeSection();
bindWiredDisplaySection();
bindCommercialSection();
}
@@ -111,10 +113,37 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
findViewById(R.id.edit_ota_manifest_url),
findViewById(R.id.check_ota_on_launch));
bindAvPresets();
bindSenderResolutionUiModeSection();
bindWiredDisplaySection();
bindCommercialSection();
}
private void bindSenderResolutionUiModeSection() {
Spinner spinner = findViewById(R.id.spinner_dev_sender_resolution_ui_mode);
if (spinner == null) {
return;
}
final SenderResolutionUiMode[] modes = SenderResolutionUiMode.values();
String[] labels = {
getString(R.string.dev_sender_resolution_ui_separated),
getString(R.string.dev_sender_resolution_ui_legacy),
getString(R.string.dev_sender_resolution_ui_combined),
};
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, labels);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(null);
SenderResolutionUiMode current = AppPreferences.getDevSenderResolutionUiMode(this);
spinner.setSelection(current.ordinal(), false);
spinner.setOnItemSelectedListener(simpleSelection(() -> {
int pos = spinner.getSelectedItemPosition();
if (pos >= 0 && pos < modes.length) {
AppPreferences.setDevSenderResolutionUiMode(this, modes[pos]);
}
}));
}
private void bindWiredDisplaySection() {
TextView status = findViewById(R.id.text_wired_display_status);
if (status != null) {

View File

@@ -93,6 +93,8 @@ public final class CastDiagnosticsLabels {
return "50% display scale";
case HD_720P:
return "720p long-edge cap";
case SD_480P:
return "480p long-edge cap";
case FIXED_1080P:
return "Fixed 1080p box";
case FIXED_720P:

View File

@@ -0,0 +1,107 @@
package com.foxx.androidcast.sender;
import android.content.Context;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.Spinner;
import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.CastSettingsBinder;
import com.foxx.androidcast.R;
/** Binds {@link R.id#spinner_sender_resolution} per {@link SenderResolutionUiMode}. */
public final class SenderResolutionUi {
private SenderResolutionUi() {}
public static SenderResolutionUiMode mode(Context context) {
return AppPreferences.getDevSenderResolutionUiMode(context);
}
public static void bindSenderResolution(AppCompatActivity activity, CastSettings settings) {
Spinner spinner = activity.findViewById(R.id.spinner_sender_resolution);
if (spinner == null) {
return;
}
switch (mode(activity)) {
case LEGACY:
bindLegacy(activity, settings, spinner);
break;
case COMBINED:
bindCombined(activity, settings, spinner);
break;
case SEPARATED:
default:
bindSeparated(activity, settings, spinner);
break;
}
}
public static void readSenderResolution(AppCompatActivity activity, CastSettings settings) {
Spinner spinner = activity.findViewById(R.id.spinner_sender_resolution);
if (spinner != null) {
CastSettingsBinder.readEnumSpinner(spinner, settings::setResolution);
}
}
/** Original / 720p / 480p / Auto. */
private static void bindSeparated(AppCompatActivity activity, CastSettings settings, Spinner spinner) {
CastSettings.Resolution[] modes = {
CastSettings.Resolution.FULL,
CastSettings.Resolution.HD_720P,
CastSettings.Resolution.SD_480P,
CastSettings.Resolution.ADAPTIVE,
};
String[] labels = {
activity.getString(R.string.resolution_preset_original),
activity.getString(R.string.resolution_preset_720p),
activity.getString(R.string.resolution_preset_480p),
activity.getString(R.string.resolution_preset_auto),
};
CastSettingsBinder.bindEnumSpinner(spinner, modes, labels, settings.getResolution(),
settings::setResolution);
}
/** Fixed 480p + fixed 720p + native original + bandwidth adaptive. */
private static void bindCombined(AppCompatActivity activity, CastSettings settings, Spinner spinner) {
CastSettings.Resolution[] modes = {
CastSettings.Resolution.SD_480P,
CastSettings.Resolution.HD_720P,
CastSettings.Resolution.FULL,
CastSettings.Resolution.ADAPTIVE,
};
String[] labels = {
activity.getString(R.string.resolution_preset_480p),
activity.getString(R.string.resolution_preset_720p),
activity.getString(R.string.resolution_combined_original),
activity.getString(R.string.resolution_combined_adaptive),
};
CastSettingsBinder.bindEnumSpinner(spinner, modes, labels, settings.getResolution(),
settings::setResolution);
}
/** Pre-refactor sender resolution list (subset previously in CastSettingsBinder). */
private static void bindLegacy(AppCompatActivity activity, CastSettings settings, Spinner spinner) {
CastSettings.Resolution[] modes = {
CastSettings.Resolution.ADAPTIVE,
CastSettings.Resolution.FIXED_1080P,
CastSettings.Resolution.FIXED_720P,
CastSettings.Resolution.FULL,
CastSettings.Resolution.THREE_QUARTERS,
CastSettings.Resolution.HALF,
CastSettings.Resolution.HD_720P,
};
String[] labels = {
activity.getString(R.string.resolution_adaptive),
activity.getString(R.string.resolution_fixed_1080p),
activity.getString(R.string.resolution_fixed_720p),
activity.getString(R.string.resolution_full),
activity.getString(R.string.resolution_75),
activity.getString(R.string.resolution_half),
activity.getString(R.string.resolution_720p),
};
CastSettingsBinder.bindEnumSpinner(spinner, modes, labels, settings.getResolution(),
settings::setResolution);
}
}

View File

@@ -0,0 +1,15 @@
package com.foxx.androidcast.sender;
/**
* Developer-only: how the sender resolution spinner is presented.
* <ul>
* <li>{@link #SEPARATED} — Original / 720p / 480p / Auto (default)</li>
* <li>{@link #LEGACY} — full pre-refactor preset list</li>
* <li>{@link #COMBINED} — fixed 480p + fixed 720p + full original + BW adaptive</li>
* </ul>
*/
public enum SenderResolutionUiMode {
SEPARATED,
LEGACY,
COMBINED
}

View File

@@ -45,6 +45,34 @@
android:layout_marginTop="12dp"
android:text="@string/dev_receiver_adaptive_display" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/dev_sender_resolution_ui_section"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_sender_resolution_ui_mode"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_dev_sender_resolution_ui_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_sender_resolution_ui_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@@ -46,6 +46,12 @@
<string name="resolution_fixed_1080p">1080p (1920×1080)</string>
<string name="resolution_fixed_720p">720p (1280×720)</string>
<string name="resolution_adaptive">Адаптивный (предсказание, в разработке)</string>
<string name="resolution_preset_original">Оригинал</string>
<string name="resolution_preset_720p">720p</string>
<string name="resolution_preset_480p">480p</string>
<string name="resolution_preset_auto">Авто</string>
<string name="resolution_combined_original">Оригинал (нативное)</string>
<string name="resolution_combined_adaptive">Адаптивный (оценка канала)</string>
<string name="label_video_codec">Видео кодек</string>
<string name="label_adjustment_preset">Тонкие быстрые настройки</string>
<string name="label_bitrate_mode">Bitrate</string>
@@ -188,6 +194,12 @@
<string name="licenses_title">Лицензии</string>
<string name="licenses_load_error">Не могу найти факл лицензий</string>
<string name="developer_settings_title">Для разработчиков</string>
<string name="dev_sender_resolution_ui_section">Разрешение отправителя (тест)</string>
<string name="dev_sender_resolution_ui_mode">Режим списка разрешений</string>
<string name="dev_sender_resolution_ui_separated">Раздельный (Оригинал / 720p / 480p / Авто)</string>
<string name="dev_sender_resolution_ui_legacy">Старый (полный список)</string>
<string name="dev_sender_resolution_ui_combined">Комбинированный (480p + 720p + оригинал + адаптив)</string>
<string name="dev_sender_resolution_ui_hint">Меняет список разрешений в настройках трансляции. По умолчанию: Раздельный. Откройте настройки cast заново.</string>
<string name="dev_wired_display_section">USB-C / HDMI (план D)</string>
<string name="dev_commercial_section">Монетизация (выкл. по умолчанию)</string>
<string name="transport_usb_tether">USB-тетер (эксперимент)</string>

View File

@@ -46,6 +46,12 @@
<string name="resolution_fixed_1080p">Fixed 1080p (fit in 1920×1080)</string>
<string name="resolution_fixed_720p">Fixed 720p (fit in 1280×720)</string>
<string name="resolution_adaptive">Adaptive (BW estimator, preview)</string>
<string name="resolution_preset_original">Original</string>
<string name="resolution_preset_720p">720p</string>
<string name="resolution_preset_480p">480p</string>
<string name="resolution_preset_auto">Auto</string>
<string name="resolution_combined_original">Original (native)</string>
<string name="resolution_combined_adaptive">Adaptive (BW estimator)</string>
<string name="label_video_codec">Video codec</string>
<string name="label_adjustment_preset">Cast adjustment preset</string>
<string name="label_bitrate_mode">Bitrate</string>
@@ -199,6 +205,12 @@
<string name="licenses_title">Licenses</string>
<string name="licenses_load_error">Could not load license text.</string>
<string name="developer_settings_title">Developer settings</string>
<string name="dev_sender_resolution_ui_section">Sender resolution UI (test)</string>
<string name="dev_sender_resolution_ui_mode">Resolution preset mode</string>
<string name="dev_sender_resolution_ui_separated">Separated (Original / 720p / 480p / Auto)</string>
<string name="dev_sender_resolution_ui_legacy">Legacy (full preset list)</string>
<string name="dev_sender_resolution_ui_combined">Combined (480p + 720p + original + adaptive)</string>
<string name="dev_sender_resolution_ui_hint">Changes the sender resolution spinner in cast settings. Default: Separated. Re-open cast settings to refresh labels.</string>
<string name="dev_wired_display_section">USB-C / HDMI (roadmap D)</string>
<string name="dev_wired_display_hints">Show sender status when external display is plugged in</string>
<string name="dev_wired_display_hint">Wired HDMI uses the OS display stack, not the cast protocol. If no second screen appears in system Display settings, adapters cannot be fixed in-app. See docs/USB_HDMI_CAST.md.</string>

View File

@@ -11,6 +11,8 @@ package com.foxx.androidcast;
* - Cursor Agent (project assistant)
* Digest: SHA256 6839d31665eec94c6c24a1a54c86dfaaef483d19196fe6b6a754eb7156c09b26
**********************************************************************/
import android.util.DisplayMetrics;
import com.foxx.androidcast.sender.calibration.CalibrationSourceDimensions;
import org.junit.Test;
@@ -27,4 +29,18 @@ public class CastResolutionTest {
assertEquals(400, size.height);
assertTrue(size.width > size.height);
}
@Test
public void sd480pCapsLongEdge() {
CastSettings settings = new CastSettings();
settings.setResolution(CastSettings.Resolution.SD_480P);
DisplayMetrics metrics = new DisplayMetrics();
metrics.widthPixels = 2000;
metrics.heightPixels = 1200;
metrics.densityDpi = 320;
CastTuningEngine.CaptureLimits limits = CastTuningEngine.resolveCaptureLimits(settings, null);
assertEquals(480, limits.maxHeight);
CastResolution.Size size = CastResolution.compute(metrics, settings, null);
assertEquals(480, Math.max(size.width, size.height));
}
}