mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
67
Dockerfile
Normal file
67
Dockerfile
Normal file
@@ -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"]
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,10 +180,13 @@ public final class CastSettingsBinder {
|
||||
List<CodecCatalog.VideoCodecChoice> 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<CodecCatalog.AudioCodecChoice> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,16 @@ public final class SettingsEnumSpinner {
|
||||
|
||||
public static <T> void bind(Spinner spinner, T[] values, String[] labels, boolean[] selectable,
|
||||
T current, java.util.function.Consumer<T> onSelect) {
|
||||
bind(spinner, values, labels, null, selectable, current, onSelect);
|
||||
}
|
||||
|
||||
public static <T> void bind(Spinner spinner, T[] values, String[] labels, int[] priorityScores,
|
||||
boolean[] selectable, T current, java.util.function.Consumer<T> 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 extends Enum<T>> 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> {
|
||||
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);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
<CheckBox
|
||||
@@ -37,7 +36,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:checked="true"
|
||||
android:text="@string/dev_grab_session_stats" />
|
||||
|
||||
<Button
|
||||
|
||||
47
app/src/main/res/layout/spinner_dropdown_item_scored.xml
Normal file
47
app/src/main/res/layout/spinner_dropdown_item_scored.xml
Normal file
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/spinner_dropdown_item_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@drawable/bg_settings_spinner_item"
|
||||
android:clipToOutline="true"
|
||||
android:elevation="4dp"
|
||||
android:outlineProvider="background">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/spinner_item_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/settings_spinner_text"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/spinner_item_score"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="end"
|
||||
android:minWidth="40dp"
|
||||
android:textColor="@color/settings_spinner_text"
|
||||
android:textSize="14sp"
|
||||
android:alpha="0.72" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
33
app/src/main/res/layout/spinner_item_scored.xml
Normal file
33
app/src/main/res/layout/spinner_item_scored.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/spinner_item_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/settings_spinner_text"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/spinner_item_score"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="end"
|
||||
android:minWidth="36dp"
|
||||
android:textColor="@color/settings_spinner_text"
|
||||
android:textSize="14sp"
|
||||
android:alpha="0.72" />
|
||||
</LinearLayout>
|
||||
@@ -109,9 +109,17 @@ reconnect_known_devices() {
|
||||
|
||||
# shellcheck source=scripts/android-ndk.sh
|
||||
source "$ROOT/scripts/android-ndk.sh"
|
||||
# shellcheck source=scripts/native-build-cache.sh
|
||||
source "$ROOT/scripts/native-build-cache.sh"
|
||||
export ANDROIDCAST_ROOT="$ROOT"
|
||||
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||
echo "==> Using NDK: $ANDROID_NDK_HOME"
|
||||
if androidcast_ccache_wanted 2>/dev/null; then
|
||||
echo "==> Native ccache: enabled (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
|
||||
else
|
||||
echo "==> Native ccache: disabled"
|
||||
fi
|
||||
|
||||
# Match app/build.gradle defaultConfig.ndk.abiFilters
|
||||
ARCH_ABIS="armeabi-v7a arm64-v8a x86_64"
|
||||
|
||||
@@ -74,6 +74,10 @@ export ANDROID_NDK_HOME="$NDK"
|
||||
export ANDROID_NDK_ROOT="$NDK"
|
||||
export PATH="$NDK_HOST_BIN:$PATH"
|
||||
|
||||
# shellcheck source=native-build-cache.sh
|
||||
source "$ROOT/scripts/native-build-cache.sh"
|
||||
androidcast_apply_native_build_accelerators "$ROOT" "$ABI" "$NDK" "$NDK_HOST_BIN"
|
||||
|
||||
VPX_CONFIGURE_EXTRA=()
|
||||
APP_CFLAGS_EXTRA=""
|
||||
case "$ABI" in
|
||||
@@ -88,6 +92,9 @@ case "$ABI" in
|
||||
;;
|
||||
esac
|
||||
|
||||
## common native optimization options
|
||||
APP_CFLAGS_EXTRA+=" -O2 -fdata-sections -ffunction-sections -Wl,--gc-sections -ftree-vectorize -O2 -s -funroll-loops -fomit-frame-pointer "
|
||||
|
||||
echo "Building libvpx for $ABI (target=$VPX_TARGET) ..."
|
||||
echo " source: $VPX_SRC"
|
||||
echo " build: $BUILD_DIR"
|
||||
@@ -125,6 +132,7 @@ elif [[ "$ABI" == x86 || "$ABI" == x86_64 ]] && [[ -f "$BUILD_DIR/vpx_config.h"
|
||||
fi
|
||||
|
||||
if $need_configure; then
|
||||
# CC/CXX may be ccache-wrapped NDK clang (see native-build-cache.sh).
|
||||
"$VPX_SRC/configure" \
|
||||
--target="$VPX_TARGET" \
|
||||
--disable-examples \
|
||||
|
||||
32
scripts/ci-build.sh
Executable file
32
scripts/ci-build.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# CI entry: native codecs (all ABIs) + Gradle debug APK + unit tests.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export ANDROIDCAST_ROOT="$ROOT"
|
||||
export ANDROIDCAST_CCACHE="${ANDROIDCAST_CCACHE:-1}"
|
||||
export ANDROIDCAST_USE_DISTCC="${ANDROIDCAST_USE_DISTCC:-0}"
|
||||
|
||||
# shellcheck source=android-ndk.sh
|
||||
source "$ROOT/scripts/android-ndk.sh"
|
||||
export ANDROID_NDK_HOME="$(androidcast_find_ndk_root "$ROOT")"
|
||||
export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"
|
||||
|
||||
if [[ -f .gitmodules ]]; then
|
||||
git submodule update --init --recursive
|
||||
fi
|
||||
|
||||
ABIS="${ANDROIDCAST_CI_ABIS:-armeabi-v7a arm64-v8a x86_64}"
|
||||
echo "==> Building libvpx for: $ABIS"
|
||||
rm -rf "$ROOT/build/native"
|
||||
mkdir -p "$ROOT/build/native"
|
||||
for abi in $ABIS; do
|
||||
./scripts/build-native-codecs.sh "$abi"
|
||||
done
|
||||
|
||||
echo "==> Gradle assembleDebug + unit tests"
|
||||
./gradlew --no-daemon clean assembleDebug testDebugUnitTest
|
||||
|
||||
echo "==> Done: $ROOT/app/build/outputs/apk/debug/app-debug.apk"
|
||||
124
scripts/native-build-cache.sh
Normal file
124
scripts/native-build-cache.sh
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host build accelerators for native codecs (libvpx / NDK). Source, do not execute.
|
||||
#
|
||||
# ccache (default: auto-detect):
|
||||
# ANDROIDCAST_CCACHE=auto|1|0
|
||||
# CCACHE_DIR — default: <repo>/build/.ccache
|
||||
#
|
||||
# distcc (off by default; experimental for libvpx configure/make only, not ndk-build):
|
||||
# ANDROIDCAST_USE_DISTCC=1
|
||||
# DISTCC_HOSTS=localhost/8 192.168.1.10/16
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
[[ -n "${ANDROIDCAST_NATIVE_CACHE_SOURCED:-}" ]] && return 0
|
||||
ANDROIDCAST_NATIVE_CACHE_SOURCED=1
|
||||
|
||||
androidcast_project_root() {
|
||||
if [[ -n "${ANDROIDCAST_ROOT:-}" ]]; then
|
||||
printf '%s\n' "$ANDROIDCAST_ROOT"
|
||||
return 0
|
||||
fi
|
||||
local here
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
printf '%s\n' "$here"
|
||||
}
|
||||
|
||||
androidcast_ccache_wanted() {
|
||||
case "${ANDROIDCAST_CCACHE:-auto}" in
|
||||
0 | false | FALSE | no | NO | off | OFF) return 1 ;;
|
||||
1 | true | TRUE | yes | YES | on | ON) return 0 ;;
|
||||
auto | AUTO | "")
|
||||
command -v ccache >/dev/null 2>&1
|
||||
;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
androidcast_ndk_clang_basename() {
|
||||
case "${1:-}" in
|
||||
arm64-v8a) echo aarch64-linux-android29-clang ;;
|
||||
armeabi-v7a) echo armv7a-linux-androideabi29-clang ;;
|
||||
x86_64) echo x86_64-linux-android29-clang ;;
|
||||
x86) echo i686-linux-android29-clang ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
androidcast_ndk_clangxx_basename() {
|
||||
case "${1:-}" in
|
||||
arm64-v8a) echo aarch64-linux-android29-clang++ ;;
|
||||
armeabi-v7a) echo armv7a-linux-androideabi29-clang++ ;;
|
||||
x86_64) echo x86_64-linux-android29-clang++ ;;
|
||||
x86) echo i686-linux-android29-clang++ ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Configure ccache + optional distcc; set CC/CXX/NDK_CCACHE for libvpx + ndk-build.
|
||||
# Args: project_root abi ndk_root ndk_host_bin
|
||||
androidcast_apply_native_build_accelerators() {
|
||||
local root="${1:?root}"
|
||||
local abi="${2:?abi}"
|
||||
local ndk_bin="${4:?ndk_host_bin}"
|
||||
local clang clangxx
|
||||
clang="$(androidcast_ndk_clang_basename "$abi")"
|
||||
clangxx="$(androidcast_ndk_clangxx_basename "$abi")"
|
||||
if [[ -z "$clang" || ! -x "$ndk_bin/$clang" ]]; then
|
||||
echo "WARN: NDK clang not found for $abi under $ndk_bin" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local prefix=""
|
||||
local ccache_on=false
|
||||
local distcc_on=false
|
||||
|
||||
if androidcast_ccache_wanted && command -v ccache >/dev/null 2>&1; then
|
||||
ccache_on=true
|
||||
export CCACHE_DIR="${CCACHE_DIR:-$root/build/.ccache}"
|
||||
mkdir -p "$CCACHE_DIR"
|
||||
export CCACHE_BASEDIR="$root"
|
||||
export CCACHE_COMPRESS="${CCACHE_COMPRESS:-1}"
|
||||
export CCACHE_COMPILERCHECK="${CCACHE_COMPILERCHECK:-content}"
|
||||
export CCACHE_MAXSIZE="${CCACHE_MAXSIZE:-5G}"
|
||||
export NDK_CCACHE="${NDK_CCACHE:-ccache}"
|
||||
prefix="ccache "
|
||||
echo "ccache: ON (dir=$CCACHE_DIR, NDK_CCACHE=$NDK_CCACHE)"
|
||||
ccache -s 2>/dev/null | sed 's/^/ /' || true
|
||||
elif androidcast_ccache_wanted; then
|
||||
echo "ccache: requested but not installed (install ccache or ANDROIDCAST_CCACHE=0)"
|
||||
else
|
||||
echo "ccache: off (ANDROIDCAST_CCACHE=${ANDROIDCAST_CCACHE:-auto})"
|
||||
fi
|
||||
|
||||
case "${ANDROIDCAST_USE_DISTCC:-0}" in
|
||||
1 | true | TRUE | yes | YES | on | ON)
|
||||
if command -v distcc >/dev/null 2>&1 && [[ -n "${DISTCC_HOSTS:-}" ]]; then
|
||||
distcc_on=true
|
||||
export DISTCC_VERBOSE="${DISTCC_VERBOSE:-0}"
|
||||
prefix="distcc ${prefix}"
|
||||
echo "distcc: ON (experimental, libvpx make/configure only; hosts=$DISTCC_HOSTS)"
|
||||
elif command -v distcc >/dev/null 2>&1; then
|
||||
echo "distcc: ANDROIDCAST_USE_DISTCC=1 but DISTCC_HOSTS empty — skipped" >&2
|
||||
else
|
||||
echo "distcc: requested but distcc not installed — skipped" >&2
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "distcc: off (set ANDROIDCAST_USE_DISTCC=1 and DISTCC_HOSTS to try)"
|
||||
;;
|
||||
esac
|
||||
|
||||
export CC="${prefix}${ndk_bin}/${clang}"
|
||||
if [[ -n "$clangxx" && -x "$ndk_bin/$clangxx" ]]; then
|
||||
export CXX="${prefix}${ndk_bin}/${clangxx}"
|
||||
else
|
||||
export CXX="${prefix}${ndk_bin}/${clang}"
|
||||
fi
|
||||
export AR="${ndk_bin}/llvm-ar"
|
||||
export RANLIB="${ndk_bin}/llvm-ranlib"
|
||||
|
||||
# libvpx configure reads these when cross-compiling.
|
||||
export AS="$CC"
|
||||
export LD="${ndk_bin}/ld.lld"
|
||||
}
|
||||
Reference in New Issue
Block a user