From 0647e0b2a0433d8412d4a0fd3a6c97d948b43ac0 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Mon, 18 May 2026 22:19:39 +0200 Subject: [PATCH] ci stuff Signed-off-by: Anton Afanasyeu --- Dockerfile | 67 ++++++++++ app/build.gradle | 24 +++- .../com/foxx/androidcast/AppPreferences.java | 8 +- .../foxx/androidcast/CastSettingsBinder.java | 61 ++++----- .../DeveloperSettingsActivity.java | 42 ++++-- .../androidcast/ui/SettingsEnumSpinner.java | 31 ++++- .../ui/SettingsScoredArrayAdapter.java | 70 ++++++++++ .../ui/SettingsStringArrayAdapter.java | 2 +- .../layout/activity_developer_settings.xml | 2 - .../layout/spinner_dropdown_item_scored.xml | 47 +++++++ .../main/res/layout/spinner_item_scored.xml | 33 +++++ rebuild.sh | 8 ++ scripts/build-native-codecs.sh | 8 ++ scripts/ci-build.sh | 32 +++++ scripts/native-build-cache.sh | 124 ++++++++++++++++++ 15 files changed, 502 insertions(+), 57 deletions(-) create mode 100644 Dockerfile create mode 100644 app/src/main/java/com/foxx/androidcast/ui/SettingsScoredArrayAdapter.java create mode 100644 app/src/main/res/layout/spinner_dropdown_item_scored.xml create mode 100644 app/src/main/res/layout/spinner_item_scored.xml create mode 100755 scripts/ci-build.sh create mode 100644 scripts/native-build-cache.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9988d0d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +# Minimal Android Cast CI image (glibc-based; NDK prebuilts require linux-x86_64 glibc). +# +# Build: +# docker build -t androidcast-ci:latest -f Dockerfile . +# +# Run CI build (mount repo): +# docker run --rm -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest +# +# Interactive: +# docker run --rm -it -v "$(pwd)":/workspace -w /workspace androidcast-ci:latest bash +# +# Note: Alpine/musl breaks NDK clang prebuilts; use Debian slim instead of rust:* for size. + +FROM debian:bookworm-slim + +ARG DEBIAN_FRONTEND=noninteractive +ARG ANDROID_SDK_ROOT=/opt/android-sdk +ARG ANDROID_NDK_VERSION=27.0.12077973 +ARG ANDROID_CMAKE_VERSION=3.22.1 +ARG CMDLINE_TOOLS_REV=11076708 + +ENV ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}" \ + ANDROID_HOME="${ANDROID_SDK_ROOT}" \ + ANDROID_NDK_HOME="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" \ + ANDROID_NDK_ROOT="${ANDROID_SDK_ROOT}/ndk/${ANDROID_NDK_VERSION}" \ + PATH="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${PATH}" \ + ANDROIDCAST_CCACHE=1 \ + ANDROIDCAST_USE_DISTCC=0 \ + GRADLE_USER_HOME=/root/.gradle \ + JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash ca-certificates curl git openjdk-21-jdk-headless unzip wget \ + ninja-build cmake ccache \ + && rm -rf /var/lib/apt/lists/* + +# Optional distributed compile (off unless ANDROIDCAST_USE_DISTCC=1 + DISTCC_HOSTS at run time). +RUN apt-get update && apt-get install -y --no-install-recommends distcc \ + && rm -rf /var/lib/apt/lists/* || true + +RUN mkdir -p "${ANDROID_SDK_ROOT}/cmdline-tools" \ + && curl -fsSL "https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS_REV}_latest.zip" \ + -o /tmp/cmdline-tools.zip \ + && unzip -q /tmp/cmdline-tools.zip -d "${ANDROID_SDK_ROOT}/cmdline-tools" \ + && mv "${ANDROID_SDK_ROOT}/cmdline-tools/cmdline-tools" "${ANDROID_SDK_ROOT}/cmdline-tools/latest" \ + && rm -f /tmp/cmdline-tools.zip + +RUN yes | sdkmanager --licenses >/dev/null \ + && sdkmanager \ + "platform-tools" \ + "platforms;android-34" \ + "build-tools;34.0.0" \ + "ndk;${ANDROID_NDK_VERSION}" \ + "cmake;${ANDROID_CMAKE_VERSION}" + +WORKDIR /workspace + +# Pre-create ccache dir (volume can override at runtime). +RUN mkdir -p /workspace/build/.ccache || true + +COPY scripts/ci-build.sh scripts/android-ndk.sh scripts/native-build-cache.sh scripts/build-native-codecs.sh \ + /workspace/scripts/ + +RUN chmod +x /workspace/scripts/ci-build.sh /workspace/scripts/build-native-codecs.sh + +# Full tree is bind-mounted in CI; default command runs the pipeline. +CMD ["./scripts/ci-build.sh"] diff --git a/app/build.gradle b/app/build.gradle index da27e16..82d8d91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,24 @@ plugins { id 'com.android.application' } +def resolveCcachePath = { + def fromEnv = System.getenv('CCACHE') ?: System.getenv('NDK_CCACHE') + if (fromEnv) { + return fromEnv + } + if (System.getenv('ANDROIDCAST_CCACHE') in ['0', 'false', 'no', 'off']) { + return null + } + try { + def out = providers.exec { + commandLine 'sh', '-c', 'command -v ccache || true' + }.standardOutput.asText.get().trim() + return out ?: null + } catch (Exception ignored) { + return null + } +}() + def gitCommitShort = 'unknown' def gitCommitAuthor = 'unknown' def buildTimeEpochMs = System.currentTimeMillis() @@ -31,7 +49,11 @@ android { buildConfigField 'String', 'BUILD_TIME_DISPLAY', "\"${buildTimeDisplay.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')}\"" externalNativeBuild { cmake { - arguments "-DANDROID_STL=c++_shared" + def cmakeArgs = ["-DANDROID_STL=c++_shared"] + if (resolveCcachePath) { + cmakeArgs += "-DANDROID_CCACHE=${resolveCcachePath}" + } + arguments cmakeArgs cppFlags "" } } diff --git a/app/src/main/java/com/foxx/androidcast/AppPreferences.java b/app/src/main/java/com/foxx/androidcast/AppPreferences.java index bf7cdad..c2a96cb 100644 --- a/app/src/main/java/com/foxx/androidcast/AppPreferences.java +++ b/app/src/main/java/com/foxx/androidcast/AppPreferences.java @@ -115,14 +115,8 @@ public final class AppPreferences { prefs(context).edit().putBoolean(KEY_DEV_GRAB_SESSION_STATS, grab).apply(); } - /** - * Show priority score suffix in codec settings spinners (also on when receiver debug overlay is on). - */ + /** Developer: show priority scores in codec settings spinners (default off). */ public static boolean isShowCodecPriorityScores(Context context) { - return isShowReceiverDebugOverlay(context) || isDevShowCodecPriorityScoresExplicit(context); - } - - public static boolean isDevShowCodecPriorityScoresExplicit(Context context) { return prefs(context).getBoolean(KEY_DEV_SHOW_CODEC_PRIORITY_SCORES, false); } diff --git a/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java b/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java index ba06f25..a369747 100644 --- a/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java +++ b/app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java @@ -180,10 +180,13 @@ public final class CastSettingsBinder { List choices = CodecCatalog.videoCodecChoicesForUi(settings); CastSettings.VideoCodec[] values = new CastSettings.VideoCodec[choices.size()]; String[] labels = new String[choices.size()]; + int[] scores = new int[choices.size()]; boolean[] selectable = new boolean[choices.size()]; + boolean showScores = AppPreferences.isShowCodecPriorityScores(activity); for (int i = 0; i < choices.size(); i++) { values[i] = choices.get(i).codec; - labels[i] = labelVideoCodec(activity, choices.get(i).codec, choices.get(i).priorityScore); + labels[i] = labelVideoCodec(activity, choices.get(i).codec); + scores[i] = codecSpinnerScore(showScores, choices.get(i).codec, choices.get(i).priorityScore); selectable[i] = choices.get(i).selectable; } CastSettings.VideoCodec current = settings.getVideoCodec(); @@ -191,7 +194,7 @@ public final class CastSettingsBinder { current = CastSettings.VideoCodec.AUTO; settings.setVideoCodec(current); } - SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setVideoCodec); + SettingsEnumSpinner.bind(spinner, values, labels, scores, selectable, current, settings::setVideoCodec); } /** Rebind codec spinners after {@code codecs.json} reload or debug score toggle. */ @@ -226,10 +229,13 @@ public final class CastSettingsBinder { List choices = CodecCatalog.audioCodecChoicesForUi(settings); CastSettings.AudioCodec[] values = new CastSettings.AudioCodec[choices.size()]; String[] labels = new String[choices.size()]; + int[] scores = new int[choices.size()]; boolean[] selectable = new boolean[choices.size()]; + boolean showScores = AppPreferences.isShowCodecPriorityScores(activity); for (int i = 0; i < choices.size(); i++) { values[i] = choices.get(i).codec; - labels[i] = labelAudioCodec(activity, choices.get(i).codec, choices.get(i).priorityScore); + labels[i] = labelAudioCodec(activity, choices.get(i).codec); + scores[i] = codecSpinnerScore(showScores, choices.get(i).codec, choices.get(i).priorityScore); selectable[i] = choices.get(i).selectable; } CastSettings.AudioCodec current = settings.getAudioCodec(); @@ -240,7 +246,7 @@ public final class CastSettingsBinder { break; } } - SettingsEnumSpinner.bind(spinner, values, labels, selectable, current, settings::setAudioCodec); + SettingsEnumSpinner.bind(spinner, values, labels, scores, selectable, current, settings::setAudioCodec); } private static void readAudioCodec(AppCompatActivity activity, CastSettings settings) { @@ -560,57 +566,46 @@ public final class CastSettingsBinder { } } - private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec, - int priorityScore) { - String base; + private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec) { switch (codec) { case H264: - base = activity.getString(R.string.codec_h264); - break; + return activity.getString(R.string.codec_h264); case H265: - base = activity.getString(R.string.codec_h265); - break; + return activity.getString(R.string.codec_h265); case LIBVPX_VP8: - base = activity.getString(R.string.codec_libvpx_vp8); - break; + return activity.getString(R.string.codec_libvpx_vp8); case LIBVPX_VP9: - base = activity.getString(R.string.codec_libvpx_vp9); - break; + return activity.getString(R.string.codec_libvpx_vp9); case PASSTHROUGH: - base = activity.getString(R.string.codec_passthrough); - break; + return activity.getString(R.string.codec_passthrough); case AUTO: default: return activity.getString(R.string.option_auto); } - return appendPriorityScore(activity, base, codec != CastSettings.VideoCodec.AUTO, priorityScore); } - private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec, - int priorityScore) { - String base; + private static String labelAudioCodec(AppCompatActivity activity, CastSettings.AudioCodec codec) { switch (codec) { case AAC: - base = activity.getString(R.string.codec_aac); - break; + return activity.getString(R.string.codec_aac); case SPEEX: - base = activity.getString(R.string.codec_speex); - break; + return activity.getString(R.string.codec_speex); case OPUS: - base = activity.getString(R.string.codec_opus); - break; + return activity.getString(R.string.codec_opus); case AUTO: default: return activity.getString(R.string.codec_audio_auto); } - return appendPriorityScore(activity, base, codec != CastSettings.AudioCodec.AUTO, priorityScore); } - private static String appendPriorityScore(AppCompatActivity activity, String base, boolean showScore, - int priorityScore) { - if (!showScore || !AppPreferences.isShowCodecPriorityScores(activity)) { - return base; + private static int codecSpinnerScore(boolean showScores, Enum codec, int priorityScore) { + if (!showScores || codec == null) { + return com.foxx.androidcast.ui.SettingsScoredArrayAdapter.NO_SCORE; } - return base + activity.getString(R.string.codec_priority_score_suffix, priorityScore); + String name = codec.name(); + if ("AUTO".equals(name)) { + return com.foxx.androidcast.ui.SettingsScoredArrayAdapter.NO_SCORE; + } + return priorityScore; } } diff --git a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java index dac3756..b54e1b7 100644 --- a/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java +++ b/app/src/main/java/com/foxx/androidcast/DeveloperSettingsActivity.java @@ -29,20 +29,42 @@ public class DeveloperSettingsActivity extends AppCompatActivity { CheckBox sessionStats = findViewById(R.id.check_grab_session_stats); Button reloadCodecs = findViewById(R.id.btn_reload_codecs_json); - debugOverlay.setChecked(AppPreferences.isShowReceiverDebugOverlay(this)); - codecScores.setChecked(AppPreferences.isDevShowCodecPriorityScoresExplicit(this)); - sessionStats.setChecked(AppPreferences.isGrabSessionStats(this)); - - debugOverlay.setOnCheckedChangeListener((buttonView, isChecked) -> - AppPreferences.setShowReceiverDebugOverlay(this, isChecked)); - codecScores.setOnCheckedChangeListener((buttonView, isChecked) -> - AppPreferences.setShowCodecPriorityScores(this, isChecked)); - sessionStats.setOnCheckedChangeListener((buttonView, isChecked) -> - AppPreferences.setGrabSessionStats(this, isChecked)); + refreshFromPreferences(debugOverlay, codecScores, sessionStats); reloadCodecs.setOnClickListener(v -> { String summary = CodecPriorityCatalog.reload(this); Toast.makeText(this, summary, Toast.LENGTH_LONG).show(); }); } + + @Override + protected void onResume() { + super.onResume(); + refreshFromPreferences( + findViewById(R.id.check_receiver_debug_overlay), + findViewById(R.id.check_show_codec_priority_scores), + findViewById(R.id.check_grab_session_stats)); + } + + private void refreshFromPreferences(CheckBox debugOverlay, CheckBox codecScores, CheckBox sessionStats) { + bindDeveloperCheckbox(debugOverlay, AppPreferences.isShowReceiverDebugOverlay(this), + AppPreferences::setShowReceiverDebugOverlay); + bindDeveloperCheckbox(codecScores, AppPreferences.isShowCodecPriorityScores(this), + AppPreferences::setShowCodecPriorityScores); + bindDeveloperCheckbox(sessionStats, AppPreferences.isGrabSessionStats(this), + AppPreferences::setGrabSessionStats); + } + + private interface BoolPrefWriter { + void write(Context context, boolean value); + } + + private void bindDeveloperCheckbox(CheckBox box, boolean checked, BoolPrefWriter writer) { + if (box == null) { + return; + } + box.setOnCheckedChangeListener(null); + box.setChecked(checked); + box.setOnCheckedChangeListener((buttonView, isChecked) -> writer.write(this, isChecked)); + } } diff --git a/app/src/main/java/com/foxx/androidcast/ui/SettingsEnumSpinner.java b/app/src/main/java/com/foxx/androidcast/ui/SettingsEnumSpinner.java index c76b19f..f539198 100644 --- a/app/src/main/java/com/foxx/androidcast/ui/SettingsEnumSpinner.java +++ b/app/src/main/java/com/foxx/androidcast/ui/SettingsEnumSpinner.java @@ -21,9 +21,16 @@ public final class SettingsEnumSpinner { public static void bind(Spinner spinner, T[] values, String[] labels, boolean[] selectable, T current, java.util.function.Consumer onSelect) { + bind(spinner, values, labels, null, selectable, current, onSelect); + } + + public static void bind(Spinner spinner, T[] values, String[] labels, int[] priorityScores, + boolean[] selectable, T current, java.util.function.Consumer onSelect) { spinner.setTag(R.id.tag_spinner_enum, values); spinner.setTag(R.id.tag_spinner_selectable, selectable); - DisabledAdapter adapter = new DisabledAdapter(spinner.getContext(), labels, selectable); + SettingsStringArrayAdapter adapter = priorityScores != null + ? new ScoredDisabledAdapter(spinner.getContext(), labels, priorityScores, selectable) + : new DisabledAdapter(spinner.getContext(), labels, selectable); spinner.setBackgroundResource(R.drawable.bg_settings_spinner); spinner.setAdapter(adapter); spinner.setPopupBackgroundResource(R.drawable.bg_settings_spinner_popup); @@ -35,7 +42,7 @@ public final class SettingsEnumSpinner { } } spinner.setSelection(selectedIndex[0]); - adapter.setSelectedPosition(selectedIndex[0]); + setAdapterSelectedPosition(adapter, selectedIndex[0]); SettingsStringArrayAdapter.wireSelectionRefresh(spinner, adapter); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override @@ -48,7 +55,7 @@ public final class SettingsEnumSpinner { return; } selectedIndex[0] = position; - adapter.setSelectedPosition(position); + setAdapterSelectedPosition(adapter, position); onSelect.accept(values[position]); } @@ -57,6 +64,10 @@ public final class SettingsEnumSpinner { }); } + private static void setAdapterSelectedPosition(SettingsStringArrayAdapter adapter, int position) { + adapter.setSelectedPosition(position); + } + @SuppressWarnings("unchecked") public static > T read(Spinner spinner, T fallback) { Object tag = spinner.getTag(R.id.tag_spinner_enum); @@ -86,4 +97,18 @@ public final class SettingsEnumSpinner { return position >= 0 && position < selectable.length && selectable[position]; } } + + private static final class ScoredDisabledAdapter extends SettingsScoredArrayAdapter { + private final boolean[] selectable; + + ScoredDisabledAdapter(Context context, String[] labels, int[] scores, boolean[] selectable) { + super(context, labels, scores); + this.selectable = selectable; + } + + @Override + public boolean isEnabled(int position) { + return position >= 0 && position < selectable.length && selectable[position]; + } + } } diff --git a/app/src/main/java/com/foxx/androidcast/ui/SettingsScoredArrayAdapter.java b/app/src/main/java/com/foxx/androidcast/ui/SettingsScoredArrayAdapter.java new file mode 100644 index 0000000..028897f --- /dev/null +++ b/app/src/main/java/com/foxx/androidcast/ui/SettingsScoredArrayAdapter.java @@ -0,0 +1,70 @@ +package com.foxx.androidcast.ui; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.foxx.androidcast.R; + +/** Settings spinner row with label left and optional priority score right-aligned. */ +public class SettingsScoredArrayAdapter extends SettingsStringArrayAdapter { + /** Hide score column when negative. */ + public static final int NO_SCORE = -1; + + private final int[] scores; + + public SettingsScoredArrayAdapter(Context context, String[] labels, int[] scores) { + super(context, labels, R.layout.spinner_dropdown_item_scored); + this.scores = scores != null ? scores : new int[labels.length]; + } + + @Override + @NonNull + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + View row = convertView; + if (!isScoredRow(row)) { + row = LayoutInflater.from(getContext()).inflate(R.layout.spinner_item_scored, parent, false); + } + bindRow(row, position); + row.setAlpha(isEnabled(position) ? 1f : 0.45f); + return row; + } + + @Override + public View getDropDownView(int position, View convertView, @NonNull ViewGroup parent) { + View row = convertView; + if (!isScoredRow(row)) { + row = LayoutInflater.from(getContext()) + .inflate(R.layout.spinner_dropdown_item_scored, parent, false); + } + bindRow(row, position); + SettingsSpinnerDropdownStyle.apply(row, position == selectedPosition, isEnabled(position)); + return row; + } + + private void bindRow(View row, int position) { + TextView label = row.findViewById(R.id.spinner_item_label); + TextView score = row.findViewById(R.id.spinner_item_score); + if (label != null) { + label.setText(getItem(position)); + } + if (score != null) { + int value = position >= 0 && position < scores.length ? scores[position] : NO_SCORE; + if (value >= 0) { + score.setVisibility(View.VISIBLE); + score.setText(String.valueOf(value)); + } else { + score.setVisibility(View.GONE); + } + } + } + + private static boolean isScoredRow(View row) { + return row != null && row.findViewById(R.id.spinner_item_label) != null; + } + +} diff --git a/app/src/main/java/com/foxx/androidcast/ui/SettingsStringArrayAdapter.java b/app/src/main/java/com/foxx/androidcast/ui/SettingsStringArrayAdapter.java index e13c1f2..f33aee7 100644 --- a/app/src/main/java/com/foxx/androidcast/ui/SettingsStringArrayAdapter.java +++ b/app/src/main/java/com/foxx/androidcast/ui/SettingsStringArrayAdapter.java @@ -16,7 +16,7 @@ import com.foxx.androidcast.R; /** Settings spinner adapter: plain row when closed, styled card rows in the dropdown. */ public class SettingsStringArrayAdapter extends ArrayAdapter { private final int dropdownLayout; - private int selectedPosition; + protected int selectedPosition; public SettingsStringArrayAdapter(Context context, String[] labels) { this(context, labels, R.layout.spinner_dropdown_item_settings); diff --git a/app/src/main/res/layout/activity_developer_settings.xml b/app/src/main/res/layout/activity_developer_settings.xml index 7066d3b..fe4da1e 100644 --- a/app/src/main/res/layout/activity_developer_settings.xml +++ b/app/src/main/res/layout/activity_developer_settings.xml @@ -22,7 +22,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="24dp" - android:checked="true" android:text="@string/dev_show_receiver_debug_overlay" />