1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:58:14 +03:00

Add Diagnostics tab to developer settings with async probes.

Split settings into Settings and Diagnostics tabs; run background read-only tests for codecs, camera/audio modes, network reachability, and casting/display extras. DevDiagnosticsReport.toJson() prepares future BE upload.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Anton Afanasyeu
2026-05-25 10:48:56 +02:00
parent 272c6e5947
commit ac8d4ea341
10 changed files with 1062 additions and 313 deletions

View File

@@ -35,11 +35,16 @@ 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.dev.DevDiagnosticsController;
import com.foxx.androidcast.stats.ui.SessionStatsAnalyzerActivity;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.TextInputEditText;
/** Hidden developer settings (opened via app-info area easter egg). */
public class DeveloperSettingsActivity extends AppCompatActivity {
private DevDiagnosticsController diagnosticsController;
private android.view.View settingsPanel;
private android.view.View diagnosticsPanel;
private Spinner videoPresetSpinner;
private Spinner audioPresetSpinner;
private SeekBar intensitySeek;
@@ -57,6 +62,11 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
setContentView(R.layout.activity_developer_settings);
setTitle(R.string.developer_settings_title);
settingsPanel = findViewById(R.id.dev_panel_settings);
diagnosticsPanel = findViewById(R.id.dev_panel_diagnostics);
diagnosticsController = new DevDiagnosticsController(this);
bindDeveloperTabs();
CheckBox debugOverlay = findViewById(R.id.check_receiver_debug_overlay);
CheckBox codecScores = findViewById(R.id.check_show_codec_priority_scores);
CheckBox sessionStats = findViewById(R.id.check_grab_session_stats);
@@ -94,6 +104,14 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
bindCommercialSection();
}
@Override
protected void onDestroy() {
if (diagnosticsController != null) {
diagnosticsController.onDestroy();
}
super.onDestroy();
}
@Override
protected void onPause() {
saveAvPresets();
@@ -378,6 +396,36 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
void write(Context context, boolean value);
}
private void bindDeveloperTabs() {
TabLayout tabs = findViewById(R.id.dev_settings_tabs);
if (tabs == null) {
return;
}
tabs.addTab(tabs.newTab().setText(R.string.dev_tab_settings));
tabs.addTab(tabs.newTab().setText(R.string.dev_tab_diagnostics));
tabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
boolean diagnostics = tab.getPosition() == 1;
settingsPanel.setVisibility(diagnostics ? android.view.View.GONE : android.view.View.VISIBLE);
diagnosticsPanel.setVisibility(diagnostics ? android.view.View.VISIBLE : android.view.View.GONE);
if (diagnostics) {
diagnosticsController.onDiagnosticsTabShown();
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
if (tab.getPosition() == 1) {
diagnosticsController.onDiagnosticsTabShown();
}
}
});
}
private void bindDeveloperCheckbox(CheckBox box, boolean checked, BoolPrefWriter writer) {
if (box == null) {
return;

View File

@@ -0,0 +1,173 @@
package com.foxx.androidcast.dev;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.foxx.androidcast.R;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/** Runs async diagnostics and binds results to the Diagnostics tab cards. */
public final class DevDiagnosticsController {
public enum Section {
HW(R.id.dev_diag_hw, R.string.dev_diag_hw_title),
CAMERA(R.id.dev_diag_camera, R.string.dev_diag_camera_title),
AUDIO(R.id.dev_diag_audio, R.string.dev_diag_audio_title),
NETWORK(R.id.dev_diag_network, R.string.dev_diag_network_title),
CASTING(R.id.dev_diag_casting, R.string.dev_diag_casting_title);
final int includeId;
final int titleRes;
Section(int includeId, int titleRes) {
this.includeId = includeId;
this.titleRes = titleRes;
}
}
private final AppCompatActivity activity;
private final View diagnosticsRoot;
private final Map<Section, CardViews> cards = new LinkedHashMap<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Handler main = new Handler(Looper.getMainLooper());
private final AtomicBoolean cancel = new AtomicBoolean(false);
private final AtomicBoolean running = new AtomicBoolean(false);
private DevDiagnosticsReport lastReport;
public DevDiagnosticsController(AppCompatActivity activity) {
this.activity = activity;
this.diagnosticsRoot = activity.findViewById(R.id.dev_panel_diagnostics);
for (Section s : Section.values()) {
View cardRoot = diagnosticsRoot.findViewById(s.includeId);
TextView title = cardRoot.findViewById(R.id.dev_diag_card_title);
TextView body = cardRoot.findViewById(R.id.dev_diag_card_body);
ProgressBar spinner = cardRoot.findViewById(R.id.dev_diag_card_spinner);
title.setText(activity.getString(s.titleRes));
body.setText(R.string.dev_diag_pending);
cards.put(s, new CardViews(activity, body, spinner));
}
Button stop = diagnosticsRoot.findViewById(R.id.btn_dev_diag_stop);
stop.setOnClickListener(v -> stopTests());
}
public void onDiagnosticsTabShown() {
if (!running.get()) {
startTests();
}
}
public void onDestroy() {
cancel.set(true);
executor.shutdownNow();
}
public void stopTests() {
cancel.set(true);
}
/** Last completed report (for future BE upload). */
public DevDiagnosticsReport getLastReport() {
return lastReport;
}
private void startTests() {
cancel.set(false);
running.set(true);
for (CardViews c : cards.values()) {
c.showRunning();
}
executor.execute(this::runSuite);
}
private void runSuite() {
Map<String, String> sectionText = new LinkedHashMap<>();
try {
runSection(Section.HW, () ->
DevDiagnosticsProbe.probeHardwareAcceleration(activity), sectionText);
runSection(Section.CAMERA, () ->
DevDiagnosticsProbe.probeCameraCapture(activity), sectionText);
runSection(Section.AUDIO, () ->
DevDiagnosticsProbe.probeAudioCapture(activity), sectionText);
runSection(Section.NETWORK, () ->
DevDiagnosticsProbe.probeNetwork(activity, cancel), sectionText);
runSection(Section.CASTING, () ->
DevDiagnosticsProbe.probeCastingExtras(activity), sectionText);
} finally {
if (!cancel.get()) {
lastReport = new DevDiagnosticsReport(System.currentTimeMillis(), sectionText);
}
main.post(() -> {
running.set(false);
if (cancel.get()) {
for (CardViews c : cards.values()) {
c.showStopped();
}
}
});
}
}
private void runSection(Section section, Probe probe, Map<String, String> out) {
if (cancel.get()) {
main.post(() -> cards.get(section).showStopped());
return;
}
main.post(() -> cards.get(section).showRunning());
String text;
try {
text = probe.run();
} catch (Exception e) {
text = "Error: " + e.getMessage();
}
if (cancel.get()) {
main.post(() -> cards.get(section).showStopped());
return;
}
out.put(section.name().toLowerCase(Locale.ROOT), text);
String finalText = text;
main.post(() -> cards.get(section).showResult(finalText));
}
private interface Probe {
String run();
}
private static final class CardViews {
private final AppCompatActivity activity;
private final TextView body;
private final ProgressBar spinner;
CardViews(AppCompatActivity activity, TextView body, ProgressBar spinner) {
this.activity = activity;
this.body = body;
this.spinner = spinner;
}
void showRunning() {
spinner.setVisibility(View.VISIBLE);
body.setText(activity.getString(R.string.dev_diag_running));
}
void showResult(String text) {
spinner.setVisibility(View.GONE);
body.setText(text);
}
void showStopped() {
spinner.setVisibility(View.GONE);
body.setText(activity.getString(R.string.dev_diag_stopped));
}
}
}

View File

@@ -0,0 +1,317 @@
package com.foxx.androidcast.dev;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.Uri;
import android.os.Build;
import android.util.Size;
import androidx.core.content.ContextCompat;
import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.PlatformCastSupport;
import com.foxx.androidcast.crash.CrashSettingsStore;
import com.foxx.androidcast.display.WiredDisplayMonitor;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.sender.CodecCatalog;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
/** Read-only device probes for developer diagnostics (no user interaction). */
public final class DevDiagnosticsProbe {
private static final int HTTP_TIMEOUT_MS = 6_000;
private DevDiagnosticsProbe() {}
public static String probeHardwareAcceleration(Context context) {
CodecCatalog.invalidateCapabilities();
StringBuilder sb = new StringBuilder();
sb.append("API ").append(Build.VERSION.SDK_INT).append('\n');
sb.append("libvpx JNI: ").append(NativeCodecBridge.isLibvpxAvailable() ? "yes" : "no").append('\n');
sb.append('\n').append("Video encoders:\n");
appendVideoCodecLines(sb, true);
sb.append('\n').append("Video decoders:\n");
appendVideoCodecLines(sb, false);
sb.append('\n').append("MIME summary (catalog):\n");
sb.append(" advertised enc: ").append(join(CodecCatalog.localEncoderMimes())).append('\n');
sb.append(" advertised dec: ").append(join(CodecCatalog.localDecoderMimes()));
return sb.toString().trim();
}
public static String probeCameraCapture(Context context) {
StringBuilder sb = new StringBuilder();
sb.append("Camera2 capture modes (cast settings):\n");
List<String> modes = new ArrayList<>();
modes.add(context.getString(com.foxx.androidcast.R.string.capture_full_screen));
if (PlatformCastSupport.isCaptureUserChoiceSupported()) {
modes.add(context.getString(com.foxx.androidcast.R.string.capture_user_choice));
} else {
modes.add(context.getString(com.foxx.androidcast.R.string.capture_user_choice_unavailable,
Build.VERSION.SDK_INT));
}
if (PlatformCastSupport.isCameraCaptureModeSupported()) {
modes.add(context.getString(com.foxx.androidcast.R.string.capture_camera_rear));
if (PlatformCastSupport.hasFrontCamera(context)) {
modes.add(context.getString(com.foxx.androidcast.R.string.capture_camera_front));
}
} else {
modes.add(context.getString(com.foxx.androidcast.R.string.capture_camera_unavailable));
}
modes.add(context.getString(com.foxx.androidcast.R.string.capture_calibration_test));
for (String m : modes) {
sb.append("").append(m).append('\n');
}
sb.append('\n').append("Physical cameras:\n");
appendCameraDevices(context, sb);
return sb.toString().trim();
}
public static String probeAudioCapture(Context context) {
StringBuilder sb = new StringBuilder();
boolean api = PlatformCastSupport.isAudioCaptureSupported();
sb.append("Playback capture (MediaProjection): ")
.append(api ? "supported (API 29+)" : "unavailable").append('\n');
boolean micPerm = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED;
sb.append("RECORD_AUDIO granted: ").append(micPerm ? "yes" : "no").append('\n');
sb.append('\n').append("Runtime modes (AudioCapture):\n");
if (!api) {
sb.append(" • (none — API < 29)\n");
} else {
sb.append(" • playback\n");
sb.append(" • playback+mic (when mic available)\n");
sb.append(" • mic (calibration / no projection)\n");
}
sb.append('\n').append("Voice codecs (settings):\n");
for (CodecCatalog.AudioCodecChoice c : CodecCatalog.audioCodecChoicesForUi()) {
sb.append("").append(c.codec.name())
.append(c.selectable ? "" : " (N/A)")
.append('\n');
}
return sb.toString().trim();
}
public static String probeNetwork(Context context, AtomicBoolean cancel) {
StringBuilder sb = new StringBuilder();
sb.append(connectivitySummary(context)).append('\n');
String crashUrl = resolveCrashUploadUrl(context);
sb.append('\n').append(probeHttp("Crash upload", crashUrl, cancel)).append('\n');
String otaUrl = resolveOtaUrl(context);
if (!otaUrl.isEmpty()) {
sb.append('\n').append(probeHttp("OTA channel", otaUrl, cancel));
}
return sb.toString().trim();
}
public static String probeCastingExtras(Context context) {
StringBuilder sb = new StringBuilder();
PackageManager pm = context.getPackageManager();
sb.append("WiFi Direct (OS): ")
.append(pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT) ? "yes" : "no")
.append('\n');
sb.append("Miracast / WiFi Display: ")
.append(miracastSummary(pm))
.append(" (in-app cast uses MediaProjection, not WFD)\n");
WiredDisplayMonitor monitor = WiredDisplayMonitor.getInstance(context);
monitor.ensureRegistered();
sb.append('\n').append("HDMI / external display:\n ")
.append(monitor.getSummaryLine().replace("\n", "\n "))
.append('\n');
sb.append('\n').append("MediaProjection screen capture: ")
.append(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? "available" : "N/A")
.append('\n');
sb.append("App/screen picker (API 34+): ")
.append(PlatformCastSupport.isCaptureUserChoiceSupported() ? "yes" : "no")
.append('\n');
sb.append("Google Cast resolver: ")
.append(resolveCastIntent(pm) ? "activity found" : "none")
.append('\n');
sb.append("Nearby / Share resolver: ")
.append(resolveNearbyShare(pm) ? "activity found" : "none");
return sb.toString().trim();
}
private static void appendVideoCodecLines(StringBuilder sb, boolean encoder) {
List<String> lines = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo info : list.getCodecInfos()) {
if (info.isEncoder() != encoder) {
continue;
}
boolean hw = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isHardwareAccelerated();
for (String type : info.getSupportedTypes()) {
if (type == null || !type.startsWith("video/")) {
continue;
}
lines.add(String.format(Locale.US, " %s [%s] %s", type, hw ? "HW" : "SW", info.getName()));
}
}
if (lines.isEmpty()) {
sb.append(" (none)\n");
return;
}
for (String line : lines) {
sb.append(line).append('\n');
}
}
private static void appendCameraDevices(Context context, StringBuilder sb) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
sb.append(" (Camera2 modes need API 29+)\n");
return;
}
try {
CameraManager mgr = context.getSystemService(CameraManager.class);
if (mgr == null) {
sb.append(" (no CameraManager)\n");
return;
}
for (String id : mgr.getCameraIdList()) {
CameraCharacteristics ch = mgr.getCameraCharacteristics(id);
Integer facing = ch.get(CameraCharacteristics.LENS_FACING);
String facingStr = facing == null ? "?" :
facing == CameraCharacteristics.LENS_FACING_FRONT ? "front" :
facing == CameraCharacteristics.LENS_FACING_BACK ? "back" : "other";
StreamConfigurationMap map = ch.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
int outputs = 0;
if (map != null) {
Size[] sizes = map.getOutputSizes(android.graphics.ImageFormat.YUV_420_888);
outputs = sizes != null ? sizes.length : 0;
}
sb.append(" • id=").append(id)
.append(" ").append(facingStr)
.append(", YUV outputs=").append(outputs)
.append('\n');
}
} catch (CameraAccessException | SecurityException e) {
sb.append(" Error: ").append(e.getMessage()).append('\n');
}
}
private static String connectivitySummary(Context context) {
ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
if (cm == null) {
return "Connectivity: unknown";
}
android.net.Network net = cm.getActiveNetwork();
if (net == null) {
return "Connectivity: no active network";
}
NetworkCapabilities caps = cm.getNetworkCapabilities(net);
if (caps == null) {
return "Connectivity: active network (no caps)";
}
List<String> parts = new ArrayList<>();
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
parts.add("WiFi");
}
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
parts.add("cellular");
}
if (caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
parts.add("ethernet");
}
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
parts.add("internet");
}
return "Connectivity: " + (parts.isEmpty() ? "other" : String.join(", ", parts));
}
private static String probeHttp(String label, String url, AtomicBoolean cancel) {
if (cancel != null && cancel.get()) {
return label + ": stopped";
}
if (url == null || url.trim().isEmpty()) {
return label + ": (no URL configured)";
}
String trimmed = url.trim();
long t0 = System.currentTimeMillis();
try {
String host = Uri.parse(trimmed).getHost();
if (host != null && !host.isEmpty()) {
InetAddress addr = InetAddress.getByName(host);
long dnsMs = System.currentTimeMillis() - t0;
if (cancel != null && cancel.get()) {
return label + ": stopped";
}
HttpURLConnection conn = (HttpURLConnection) new URL(trimmed).openConnection();
conn.setConnectTimeout(HTTP_TIMEOUT_MS);
conn.setReadTimeout(HTTP_TIMEOUT_MS);
conn.setRequestMethod("HEAD");
conn.setInstanceFollowRedirects(true);
int code = conn.getResponseCode();
long totalMs = System.currentTimeMillis() - t0;
conn.disconnect();
return label + ": HTTP " + code + ", DNS " + dnsMs + " ms, total " + totalMs + " ms"
+ " (" + addr.getHostAddress() + ")";
}
return label + ": invalid URL";
} catch (IOException e) {
return label + ": fail — " + e.getClass().getSimpleName() + ": " + e.getMessage()
+ " (" + (System.currentTimeMillis() - t0) + " ms)";
}
}
private static String resolveCrashUploadUrl(Context context) {
return CrashSettingsStore.load(context).uploadUrl;
}
private static String resolveOtaUrl(Context context) {
String url = AppPreferences.getOtaChannelUrl(context);
if (url.isEmpty() && BuildConfig.OTA_CHANNEL_URL_DEFAULT != null) {
url = BuildConfig.OTA_CHANNEL_URL_DEFAULT.trim();
}
return url;
}
private static String miracastSummary(PackageManager pm) {
if (pm.hasSystemFeature(PackageManager.FEATURE_WIFI_DIRECT)) {
return "likely via OS WiFi Display stack";
}
return "not reported";
}
private static boolean resolveCastIntent(PackageManager pm) {
Intent i = new Intent("android.settings.CAST_SETTINGS");
return i.resolveActivity(pm) != null;
}
private static boolean resolveNearbyShare(PackageManager pm) {
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("video/*");
return i.resolveActivity(pm) != null;
}
private static String join(List<String> list) {
if (list == null || list.isEmpty()) {
return "(none)";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(list.get(i));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,60 @@
package com.foxx.androidcast.dev;
import android.os.Build;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* In-memory diagnostics snapshot for developer settings.
* {@link #toJson()} is ready for a future BE upload endpoint (not wired yet).
*/
public final class DevDiagnosticsReport {
public final long collectedAtEpochMs;
public final String deviceModel;
public final int sdkInt;
public final Map<String, String> sections;
public DevDiagnosticsReport(long collectedAtEpochMs, Map<String, String> sections) {
this.collectedAtEpochMs = collectedAtEpochMs;
this.deviceModel = Build.MANUFACTURER + " " + Build.MODEL;
this.sdkInt = Build.VERSION.SDK_INT;
this.sections = new LinkedHashMap<>(sections);
}
public JSONObject toJson() {
JSONObject root = new JSONObject();
try {
root.put("schema_version", 1);
root.put("kind", "dev_diagnostics");
root.put("collected_at_ms", collectedAtEpochMs);
root.put("device_model", deviceModel);
root.put("sdk_int", sdkInt);
JSONObject body = new JSONObject();
for (Map.Entry<String, String> e : sections.entrySet()) {
body.put(e.getKey(), e.getValue());
}
root.put("sections", body);
root.put("sections_array", sectionsToArray());
} catch (Exception ignored) {
}
return root;
}
private JSONArray sectionsToArray() {
JSONArray arr = new JSONArray();
for (Map.Entry<String, String> e : sections.entrySet()) {
JSONObject item = new JSONObject();
try {
item.put("id", e.getKey());
item.put("text", e.getValue());
arr.put(item);
} catch (Exception ignored) {
}
}
return arr;
}
}

View File

@@ -0,0 +1,57 @@
<?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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dev_diag_intro"
android:textSize="13sp" />
<Button
android:id="@+id/btn_dev_diag_stop"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_diag_stop" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:id="@+id/dev_diag_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="16dp">
<include
android:id="@+id/dev_diag_hw"
layout="@layout/view_dev_diag_card" />
<include
android:id="@+id/dev_diag_camera"
layout="@layout/view_dev_diag_card" />
<include
android:id="@+id/dev_diag_audio"
layout="@layout/view_dev_diag_card" />
<include
android:id="@+id/dev_diag_network"
layout="@layout/view_dev_diag_card" />
<include
android:id="@+id/dev_diag_casting"
layout="@layout/view_dev_diag_card" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,315 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<CheckBox
android:id="@+id/check_receiver_debug_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dev_show_receiver_debug_overlay" />
<CheckBox
android:id="@+id/check_show_codec_priority_scores"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_show_codec_priority_scores" />
<CheckBox
android:id="@+id/check_grab_session_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_grab_session_stats" />
<CheckBox
android:id="@+id/check_receiver_adaptive_display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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"
android:layout_marginTop="28dp"
android:text="@string/dev_av_presets_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_av_video_preset"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_dev_video_preset"
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="12dp"
android:text="@string/dev_av_audio_preset"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_dev_audio_preset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/label_dev_av_intensity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_av_intensity"
android:textSize="14sp" />
<SeekBar
android:id="@+id/seek_dev_av_intensity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:max="10"
android:progress="5" />
<TextView
android:id="@+id/text_dev_av_intensity_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_av_presets_hint"
android:textSize="12sp" />
<Button
android:id="@+id/btn_open_session_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/session_stats_open" />
<Button
android:id="@+id/btn_reload_codecs_json"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_reload_codecs_json" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_reload_codecs_json_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/ota_section_title"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_ota_installed_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="13sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/ota_manifest_url_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_ota_manifest_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/check_ota_on_launch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="true"
android:text="@string/ota_check_on_launch" />
<Button
android:id="@+id/btn_check_ota"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/ota_check_now" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/ota_section_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/dev_wired_display_section"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_wired_display_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="13sp" />
<CheckBox
android:id="@+id/check_wired_display_hints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_wired_display_hints" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_external_capture_policy"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_external_capture_policy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/text_external_capture_policy_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp" />
<CheckBox
android:id="@+id/check_usb_tether_transport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_usb_tether_transport" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_wired_display_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/dev_commercial_section"
android:textSize="16sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/check_commercial_ads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_commercial_ads" />
<CheckBox
android:id="@+id/check_commercial_iap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_commercial_iap" />
<CheckBox
android:id="@+id/check_commercial_play_store"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_commercial_play_store" />
<Button
android:id="@+id/btn_open_play_store_listing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_open_play_store_listing" />
<Button
android:id="@+id/btn_request_play_review"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_request_play_review" />
<Button
android:id="@+id/btn_query_iap_products"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_query_iap_products" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_commercial_hint"
android:textSize="12sp" />
</LinearLayout>
</ScrollView>

View File

@@ -1,323 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
android:orientation="vertical">
<LinearLayout
<com.google.android.material.tabs.TabLayout
android:id="@+id/dev_settings_tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
app:tabMode="fixed"
app:tabGravity="fill" />
<TextView
<FrameLayout
android:id="@+id/dev_settings_tab_host"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/developer_settings_title"
android:textSize="20sp"
android:textStyle="bold" />
android:layout_height="0dp"
android:layout_weight="1">
<CheckBox
android:id="@+id/check_receiver_debug_overlay"
<include
android:id="@+id/dev_panel_settings"
layout="@layout/activity_dev_settings_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/dev_show_receiver_debug_overlay" />
android:layout_height="match_parent" />
<CheckBox
android:id="@+id/check_show_codec_priority_scores"
<include
android:id="@+id/dev_panel_diagnostics"
layout="@layout/activity_dev_diagnostics_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_show_codec_priority_scores" />
<CheckBox
android:id="@+id/check_grab_session_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_grab_session_stats" />
<CheckBox
android:id="@+id/check_receiver_adaptive_display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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"
android:layout_marginTop="28dp"
android:text="@string/dev_av_presets_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_av_video_preset"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_dev_video_preset"
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="12dp"
android:text="@string/dev_av_audio_preset"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_dev_audio_preset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/label_dev_av_intensity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_av_intensity"
android:textSize="14sp" />
<SeekBar
android:id="@+id/seek_dev_av_intensity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:max="10"
android:progress="5" />
<TextView
android:id="@+id/text_dev_av_intensity_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_av_presets_hint"
android:textSize="12sp" />
<Button
android:id="@+id/btn_open_session_stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/session_stats_open" />
<Button
android:id="@+id/btn_reload_codecs_json"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_reload_codecs_json" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_reload_codecs_json_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/ota_section_title"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_ota_installed_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="13sp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/ota_manifest_url_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_ota_manifest_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/check_ota_on_launch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:checked="true"
android:text="@string/ota_check_on_launch" />
<Button
android:id="@+id/btn_check_ota"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/ota_check_now" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/ota_section_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/dev_wired_display_section"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_wired_display_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="13sp" />
<CheckBox
android:id="@+id/check_wired_display_hints"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_wired_display_hints" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_external_capture_policy"
android:textSize="14sp" />
<Spinner
android:id="@+id/spinner_external_capture_policy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/text_external_capture_policy_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="12sp" />
<CheckBox
android:id="@+id/check_usb_tether_transport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_usb_tether_transport" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_wired_display_hint"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/dev_commercial_section"
android:textSize="16sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/check_commercial_ads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_commercial_ads" />
<CheckBox
android:id="@+id/check_commercial_iap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_commercial_iap" />
<CheckBox
android:id="@+id/check_commercial_play_store"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_commercial_play_store" />
<Button
android:id="@+id/btn_open_play_store_listing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_open_play_store_listing" />
<Button
android:id="@+id/btn_request_play_review"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_request_play_review" />
<Button
android:id="@+id/btn_query_iap_products"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_query_iap_products" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_commercial_hint"
android:textSize="12sp" />
android:layout_height="match_parent"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardUseCompatPadding="true">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/dev_diag_card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:id="@+id/dev_diag_card_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="monospace"
android:textSize="12sp"
android:textIsSelectable="true" />
</LinearLayout>
<ProgressBar
android:id="@+id/dev_diag_card_spinner"
style="?android:attr/progressBarStyle"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="top|end"
android:visibility="gone" />
</FrameLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -196,6 +196,18 @@
<string name="licenses_title">Лицензии</string>
<string name="licenses_load_error">Не могу найти факл лицензий</string>
<string name="developer_settings_title">Для разработчиков</string>
<string name="dev_tab_settings">Настройки</string>
<string name="dev_tab_diagnostics">Диагностика</string>
<string name="dev_diag_intro">Поддерживаемые возможности — быстрые проверки без ввода. Запускаются в фоне при открытии вкладки.</string>
<string name="dev_diag_stop">Остановить тесты</string>
<string name="dev_diag_pending">Ожидание…</string>
<string name="dev_diag_running">Выполняется…</string>
<string name="dev_diag_stopped">Остановлено</string>
<string name="dev_diag_hw_title">Аппаратное ускорение (видео)</string>
<string name="dev_diag_camera_title">Захват с камеры</string>
<string name="dev_diag_audio_title">Захват звука</string>
<string name="dev_diag_network_title">Сеть</string>
<string name="dev_diag_casting_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>

View File

@@ -207,6 +207,18 @@
<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_tab_settings">Settings</string>
<string name="dev_tab_diagnostics">Diagnostics</string>
<string name="dev_diag_intro">Supported features — quick read-only probes. Tests run in the background when you open this tab.</string>
<string name="dev_diag_stop">Stop tests</string>
<string name="dev_diag_pending">Waiting…</string>
<string name="dev_diag_running">Running…</string>
<string name="dev_diag_stopped">Stopped</string>
<string name="dev_diag_hw_title">Hardware acceleration (video)</string>
<string name="dev_diag_camera_title">Camera capture</string>
<string name="dev_diag_audio_title">Sound capture</string>
<string name="dev_diag_network_title">Network</string>
<string name="dev_diag_casting_title">Other casting / display</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>