mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:18:42 +03:00
@@ -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>
|
||||
Reference in New Issue
Block a user