1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:18:42 +03:00
Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
Anton Afanasyeu
2026-05-18 22:19:39 +02:00
parent 115e0e1548
commit 0647e0b2a0
15 changed files with 502 additions and 57 deletions

View File

@@ -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 ""
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View 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>

View 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>