mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:18:42 +03:00
ref
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".AndroidCastApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher_foreground"
|
android:icon="@drawable/ic_launcher_foreground"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -42,6 +43,13 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" />
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".CastSettingsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/cast_settings_title"
|
||||||
|
android:parentActivityName=".MainActivity"
|
||||||
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".receiver.ReceiverPlaybackActivity"
|
android:name=".receiver.ReceiverPlaybackActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
|
||||||
|
/** Applies saved theme mode at process start. */
|
||||||
|
public class AndroidCastApplication extends Application {
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
CastThemeHelper.applyNightMode(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
179
app/src/main/java/com/foxx/androidcast/AppPreferences.java
Normal file
179
app/src/main/java/com/foxx/androidcast/AppPreferences.java
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global app preferences: identity, PIN, tray icon, and default sender/receiver cast options.
|
||||||
|
*/
|
||||||
|
public final class AppPreferences {
|
||||||
|
private static final String PREFS = "androidcast_app";
|
||||||
|
|
||||||
|
private static final String KEY_USERNAME = "username";
|
||||||
|
private static final String KEY_PIN = "pin";
|
||||||
|
private static final String KEY_SHOW_TRAY = "show_tray_icon";
|
||||||
|
private static final String KEY_THEME = "theme_mode";
|
||||||
|
private static final String KEY_PLAY_INCOMING_AUDIO = "play_incoming_audio";
|
||||||
|
|
||||||
|
private static final String PREFIX_SENDER = "sender_";
|
||||||
|
private static final String PREFIX_RECEIVER = "receiver_";
|
||||||
|
|
||||||
|
private AppPreferences() {}
|
||||||
|
|
||||||
|
public static String getUsername(Context context) {
|
||||||
|
String stored = prefs(context).getString(KEY_USERNAME, null);
|
||||||
|
if (!TextUtils.isEmpty(stored)) {
|
||||||
|
return stored.trim();
|
||||||
|
}
|
||||||
|
return defaultDeviceName(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setUsername(Context context, String username) {
|
||||||
|
String v = username != null ? username.trim() : "";
|
||||||
|
if (v.isEmpty()) {
|
||||||
|
v = defaultDeviceName(context);
|
||||||
|
}
|
||||||
|
prefs(context).edit().putString(KEY_USERNAME, v).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPin(Context context) {
|
||||||
|
return prefs(context).getString(KEY_PIN, CastConfig.DEFAULT_PIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPin(Context context, String pin) {
|
||||||
|
prefs(context).edit().putString(KEY_PIN, pin != null ? pin : "").apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isShowTrayIcon(Context context) {
|
||||||
|
return prefs(context).getBoolean(KEY_SHOW_TRAY, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setShowTrayIcon(Context context, boolean show) {
|
||||||
|
prefs(context).edit().putBoolean(KEY_SHOW_TRAY, show).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CastThemeHelper.AppThemeMode getThemeMode(Context context) {
|
||||||
|
String name = prefs(context).getString(KEY_THEME, CastThemeHelper.AppThemeMode.SYSTEM.name());
|
||||||
|
try {
|
||||||
|
return CastThemeHelper.AppThemeMode.valueOf(name);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return CastThemeHelper.AppThemeMode.SYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setThemeMode(Context context, CastThemeHelper.AppThemeMode mode) {
|
||||||
|
prefs(context).edit().putString(KEY_THEME,
|
||||||
|
mode != null ? mode.name() : CastThemeHelper.AppThemeMode.SYSTEM.name()).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPlayIncomingAudio(Context context) {
|
||||||
|
return prefs(context).getBoolean(KEY_PLAY_INCOMING_AUDIO, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPlayIncomingAudio(Context context, boolean play) {
|
||||||
|
prefs(context).edit().putBoolean(KEY_PLAY_INCOMING_AUDIO, play).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CastSettings loadSenderDefaults(Context context) {
|
||||||
|
CastSettings s = new CastSettings();
|
||||||
|
applyStoredCastSettings(context, PREFIX_SENDER, s);
|
||||||
|
s.setAdjustmentPreset(CastSettings.AdjustmentPreset.AUTO);
|
||||||
|
s.setQuality(CastSettings.Quality.MEDIUM);
|
||||||
|
if (s.getBitrateMode() == CastSettings.BitrateMode.AUTO) {
|
||||||
|
s.setBitrateMode(CastSettings.BitrateMode.ESTIMATED);
|
||||||
|
}
|
||||||
|
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
||||||
|
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveSenderDefaults(Context context, CastSettings settings) {
|
||||||
|
if (settings == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storeCastSettings(context, PREFIX_SENDER, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CastSettings loadReceiverDefaults(Context context) {
|
||||||
|
CastSettings s = new CastSettings();
|
||||||
|
applyStoredCastSettings(context, PREFIX_RECEIVER, s);
|
||||||
|
s.setAdjustmentPreset(CastSettings.AdjustmentPreset.AUTO);
|
||||||
|
s.setQuality(CastSettings.Quality.MEDIUM);
|
||||||
|
if (s.getBitrateMode() == CastSettings.BitrateMode.AUTO) {
|
||||||
|
s.setBitrateMode(CastSettings.BitrateMode.ESTIMATED);
|
||||||
|
}
|
||||||
|
if (s.getNetworkAdoption() == CastSettings.NetworkAdoption.AUTO) {
|
||||||
|
s.setNetworkAdoption(CastSettings.NetworkAdoption.ESTIMATED);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveReceiverDefaults(Context context, CastSettings settings) {
|
||||||
|
if (settings == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storeCastSettings(context, PREFIX_RECEIVER, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String defaultDeviceName(Context context) {
|
||||||
|
String name = Settings.Global.getString(context.getContentResolver(), Settings.Global.DEVICE_NAME);
|
||||||
|
if (TextUtils.isEmpty(name)) {
|
||||||
|
name = Build.MODEL;
|
||||||
|
}
|
||||||
|
return name != null ? name : "Android device";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyStoredCastSettings(Context context, String prefix, CastSettings s) {
|
||||||
|
SharedPreferences p = prefs(context);
|
||||||
|
String transport = p.getString(prefix + "transport", CastConfig.DEFAULT_TRANSPORT);
|
||||||
|
s.setTransport(transport);
|
||||||
|
s.setResolution(readEnum(p, prefix + "resolution", CastSettings.Resolution.values(),
|
||||||
|
CastSettings.Resolution.ADAPTIVE));
|
||||||
|
s.setVideoCodec(readEnum(p, prefix + "video_codec", CastSettings.VideoCodec.values(),
|
||||||
|
CastSettings.VideoCodec.AUTO));
|
||||||
|
s.setBitrateMode(readEnum(p, prefix + "bitrate_mode", CastSettings.BitrateMode.values(),
|
||||||
|
CastSettings.BitrateMode.ESTIMATED));
|
||||||
|
s.setNetworkAdoption(readEnum(p, prefix + "network_adoption", CastSettings.NetworkAdoption.values(),
|
||||||
|
CastSettings.NetworkAdoption.ESTIMATED));
|
||||||
|
s.setCaptureMode(readEnum(p, prefix + "capture_mode", CastSettings.CaptureMode.values(),
|
||||||
|
PlatformCastSupport.defaultCaptureMode()));
|
||||||
|
s.setStreamProtection(readEnum(p, prefix + "stream_protection", CastSettings.StreamProtection.values(),
|
||||||
|
CastSettings.StreamProtection.AUTO));
|
||||||
|
s.setReceiverDisplayResolution(readEnum(p, prefix + "receiver_display",
|
||||||
|
CastSettings.ReceiverDisplayResolution.values(),
|
||||||
|
CastSettings.ReceiverDisplayResolution.AUTO_FIT));
|
||||||
|
s.setAudioEnabled(p.getBoolean(prefix + "audio_enabled", PlatformCastSupport.isAudioCaptureSupported()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void storeCastSettings(Context context, String prefix, CastSettings s) {
|
||||||
|
prefs(context).edit()
|
||||||
|
.putString(prefix + "transport", s.getTransport())
|
||||||
|
.putString(prefix + "resolution", s.getResolution().name())
|
||||||
|
.putString(prefix + "video_codec", s.getVideoCodec().name())
|
||||||
|
.putString(prefix + "bitrate_mode", s.getBitrateMode().name())
|
||||||
|
.putString(prefix + "network_adoption", s.getNetworkAdoption().name())
|
||||||
|
.putString(prefix + "capture_mode", s.getCaptureMode().name())
|
||||||
|
.putString(prefix + "stream_protection", s.getStreamProtection().name())
|
||||||
|
.putString(prefix + "receiver_display", s.getReceiverDisplayResolution().name())
|
||||||
|
.putBoolean(prefix + "audio_enabled", s.isAudioRequested())
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Enum<T>> T readEnum(SharedPreferences p, String key, T[] values, T fallback) {
|
||||||
|
String name = p.getString(key, fallback.name());
|
||||||
|
for (T v : values) {
|
||||||
|
if (v.name().equals(name)) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences prefs(Context context) {
|
||||||
|
return context.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
/** Process-wide cast session flags for UI back-key handling. */
|
/** Process-wide cast session flags for UI back-key handling. */
|
||||||
public final class CastActiveState {
|
public final class CastActiveState {
|
||||||
private static final AtomicBoolean senderCasting = new AtomicBoolean(false);
|
private static final AtomicBoolean senderCasting = new AtomicBoolean(false);
|
||||||
|
private static final AtomicBoolean receiverListening = new AtomicBoolean(false);
|
||||||
|
|
||||||
private CastActiveState() {}
|
private CastActiveState() {}
|
||||||
|
|
||||||
@@ -15,4 +16,12 @@ public final class CastActiveState {
|
|||||||
public static boolean isSenderCasting() {
|
public static boolean isSenderCasting() {
|
||||||
return senderCasting.get();
|
return senderCasting.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setReceiverListening(boolean active) {
|
||||||
|
receiverListening.set(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isReceiverListening() {
|
||||||
|
return receiverListening.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
/** Remembers the last foreground activity for tray / notification resume. */
|
||||||
|
public final class CastActivityTracker {
|
||||||
|
private static volatile Class<? extends Activity> lastActivityClass;
|
||||||
|
|
||||||
|
private CastActivityTracker() {}
|
||||||
|
|
||||||
|
public static void onResume(Activity activity) {
|
||||||
|
if (activity != null) {
|
||||||
|
lastActivityClass = activity.getClass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Intent resumeIntent(Context context) {
|
||||||
|
Class<? extends Activity> cls = lastActivityClass;
|
||||||
|
if (cls != null) {
|
||||||
|
Intent intent = new Intent(context, cls);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
return new Intent(context, MainActivity.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,9 +42,9 @@ public final class CastNotifications {
|
|||||||
private static Notification build(
|
private static Notification build(
|
||||||
Service service, String channel, int id, String text,
|
Service service, String channel, int id, String text,
|
||||||
Class<?> serviceClass, String stopAction) {
|
Class<?> serviceClass, String stopAction) {
|
||||||
Intent open = new Intent(service, MainActivity.class);
|
|
||||||
PendingIntent pi = PendingIntent.getActivity(
|
PendingIntent pi = PendingIntent.getActivity(
|
||||||
service, 0, open, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
service, 0, CastActivityTracker.resumeIntent(service),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
Intent stop = new Intent(service, serviceClass);
|
Intent stop = new Intent(service, serviceClass);
|
||||||
stop.setAction(stopAction);
|
stop.setAction(stopAction);
|
||||||
PendingIntent stopPi = PendingIntent.getService(
|
PendingIntent stopPi = PendingIntent.getService(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import java.io.Serializable;
|
|||||||
/** User-selected cast parameters (passed sender → service → receiver via protocol). */
|
/** User-selected cast parameters (passed sender → service → receiver via protocol). */
|
||||||
public class CastSettings implements Serializable {
|
public class CastSettings implements Serializable {
|
||||||
public static final String EXTRA = "cast_settings";
|
public static final String EXTRA = "cast_settings";
|
||||||
public static final int SETTINGS_VERSION = 3;
|
public static final int SETTINGS_VERSION = 4;
|
||||||
|
|
||||||
public enum Quality {
|
public enum Quality {
|
||||||
LOW(1_500_000, 24),
|
LOW(1_500_000, 24),
|
||||||
@@ -49,11 +49,25 @@ public class CastSettings implements Serializable {
|
|||||||
USER_CHOICE,
|
USER_CHOICE,
|
||||||
/** Generated TV calibration pattern (no MediaProjection). */
|
/** Generated TV calibration pattern (no MediaProjection). */
|
||||||
CALIBRATION_TEST,
|
CALIBRATION_TEST,
|
||||||
/**
|
/** Camera2 rear camera as video source. */
|
||||||
* Future: Camera2 preview as video source (not MediaProjection app picker).
|
CAMERA,
|
||||||
* {@link PlatformCastSupport#isCameraCaptureModeSupported()}
|
/** Camera2 front camera as video source. */
|
||||||
*/
|
CAMERA_FRONT
|
||||||
CAMERA
|
}
|
||||||
|
|
||||||
|
/** UDP stream protection hint (negotiated with peer; stubs until implemented). */
|
||||||
|
public enum StreamProtection {
|
||||||
|
AUTO,
|
||||||
|
FEC_3_4,
|
||||||
|
FEC_REED_SOLOMON,
|
||||||
|
NACK,
|
||||||
|
FEC_NACK
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How the receiver fits incoming video on its display. */
|
||||||
|
public enum ReceiverDisplayResolution {
|
||||||
|
AUTO_FIT,
|
||||||
|
ADAPTIVE_EXPERIMENTAL
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Resolution {
|
public enum Resolution {
|
||||||
@@ -78,13 +92,15 @@ public class CastSettings implements Serializable {
|
|||||||
|
|
||||||
private String transport = CastConfig.DEFAULT_TRANSPORT;
|
private String transport = CastConfig.DEFAULT_TRANSPORT;
|
||||||
private Quality quality = Quality.MEDIUM;
|
private Quality quality = Quality.MEDIUM;
|
||||||
private Resolution resolution = Resolution.HD_720P;
|
private Resolution resolution = Resolution.ADAPTIVE;
|
||||||
private boolean audioEnabled;
|
private boolean audioEnabled;
|
||||||
private VideoCodec videoCodec = VideoCodec.AUTO;
|
private VideoCodec videoCodec = VideoCodec.AUTO;
|
||||||
private AdjustmentPreset adjustmentPreset = AdjustmentPreset.AUTO;
|
private AdjustmentPreset adjustmentPreset = AdjustmentPreset.AUTO;
|
||||||
private BitrateMode bitrateMode = BitrateMode.AUTO;
|
private BitrateMode bitrateMode = BitrateMode.AUTO;
|
||||||
private NetworkAdoption networkAdoption = NetworkAdoption.AUTO;
|
private NetworkAdoption networkAdoption = NetworkAdoption.AUTO;
|
||||||
private CaptureMode captureMode = PlatformCastSupport.defaultCaptureMode();
|
private CaptureMode captureMode = PlatformCastSupport.defaultCaptureMode();
|
||||||
|
private StreamProtection streamProtection = StreamProtection.AUTO;
|
||||||
|
private ReceiverDisplayResolution receiverDisplayResolution = ReceiverDisplayResolution.AUTO_FIT;
|
||||||
/** Filled after codec handshake (e.g. video/avc). */
|
/** Filled after codec handshake (e.g. video/avc). */
|
||||||
private transient String negotiatedVideoMime;
|
private transient String negotiatedVideoMime;
|
||||||
|
|
||||||
@@ -200,4 +216,47 @@ public class CastSettings implements Serializable {
|
|||||||
public void setCaptureMode(CaptureMode captureMode) {
|
public void setCaptureMode(CaptureMode captureMode) {
|
||||||
this.captureMode = captureMode != null ? captureMode : CaptureMode.FULL_SCREEN;
|
this.captureMode = captureMode != null ? captureMode : CaptureMode.FULL_SCREEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public StreamProtection getStreamProtection() {
|
||||||
|
return streamProtection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamProtection(StreamProtection streamProtection) {
|
||||||
|
this.streamProtection = streamProtection != null ? streamProtection : StreamProtection.AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReceiverDisplayResolution getReceiverDisplayResolution() {
|
||||||
|
return receiverDisplayResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReceiverDisplayResolution(ReceiverDisplayResolution receiverDisplayResolution) {
|
||||||
|
this.receiverDisplayResolution = receiverDisplayResolution != null
|
||||||
|
? receiverDisplayResolution : ReceiverDisplayResolution.AUTO_FIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCameraCapture() {
|
||||||
|
return captureMode == CaptureMode.CAMERA || captureMode == CaptureMode.CAMERA_FRONT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFrontCamera() {
|
||||||
|
return captureMode == CaptureMode.CAMERA_FRONT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyFrom(CastSettings other) {
|
||||||
|
if (other == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transport = other.transport;
|
||||||
|
quality = other.quality;
|
||||||
|
resolution = other.resolution;
|
||||||
|
audioEnabled = other.audioEnabled;
|
||||||
|
videoCodec = other.videoCodec;
|
||||||
|
adjustmentPreset = other.adjustmentPreset;
|
||||||
|
bitrateMode = other.bitrateMode;
|
||||||
|
networkAdoption = other.networkAdoption;
|
||||||
|
captureMode = other.captureMode;
|
||||||
|
streamProtection = other.streamProtection;
|
||||||
|
receiverDisplayResolution = other.receiverDisplayResolution;
|
||||||
|
negotiatedVideoMime = other.negotiatedVideoMime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.receiver.ReceiverPrefs;
|
||||||
|
|
||||||
|
/** Global, sender, and receiver default cast settings. */
|
||||||
|
public class CastSettingsActivity extends AppCompatActivity {
|
||||||
|
private final CastSettings senderSettings = new CastSettings();
|
||||||
|
private final CastSettings receiverSettings = new CastSettings();
|
||||||
|
|
||||||
|
private EditText usernameInput;
|
||||||
|
private EditText pinInput;
|
||||||
|
private CheckBox trayIconCheck;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_cast_settings);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameInput = findViewById(R.id.edit_username);
|
||||||
|
pinInput = findViewById(R.id.edit_settings_pin);
|
||||||
|
trayIconCheck = findViewById(R.id.check_tray_icon);
|
||||||
|
|
||||||
|
senderSettings.copyFrom(AppPreferences.loadSenderDefaults(this));
|
||||||
|
receiverSettings.copyFrom(AppPreferences.loadReceiverDefaults(this));
|
||||||
|
|
||||||
|
CastSettingsBinder.bindGlobal(this, usernameInput, pinInput, trayIconCheck);
|
||||||
|
CastSettingsBinder.bindSender(this, senderSettings);
|
||||||
|
CastSettingsBinder.bindReceiver(this, receiverSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
CastActivityTracker.onResume(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
saveAll();
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
finish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveAll() {
|
||||||
|
CastSettingsBinder.saveGlobal(this, usernameInput, pinInput, trayIconCheck);
|
||||||
|
CastSettingsBinder.saveSender(this, senderSettings);
|
||||||
|
CastSettingsBinder.saveReceiver(this, receiverSettings);
|
||||||
|
ReceiverPrefs.setAllowAudio(this, AppPreferences.isPlayIncomingAudio(this));
|
||||||
|
Toast.makeText(this, R.string.settings_saved, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
443
app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java
Normal file
443
app/src/main/java/com/foxx/androidcast/CastSettingsBinder.java
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.sender.CodecCatalog;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Binds settings UI widgets to {@link AppPreferences} and {@link CastSettings}. */
|
||||||
|
public final class CastSettingsBinder {
|
||||||
|
private CastSettingsBinder() {}
|
||||||
|
|
||||||
|
public static void bindGlobal(AppCompatActivity activity, EditText username, EditText pin,
|
||||||
|
CheckBox trayIcon) {
|
||||||
|
username.setText(AppPreferences.getUsername(activity));
|
||||||
|
pin.setText(AppPreferences.getPin(activity));
|
||||||
|
trayIcon.setChecked(AppPreferences.isShowTrayIcon(activity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveGlobal(AppCompatActivity activity, EditText username, EditText pin,
|
||||||
|
CheckBox trayIcon) {
|
||||||
|
String name = username.getText().toString().trim();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
name = AppPreferences.defaultDeviceName(activity);
|
||||||
|
username.setText(name);
|
||||||
|
}
|
||||||
|
AppPreferences.setUsername(activity, name);
|
||||||
|
AppPreferences.setPin(activity, pin.getText().toString().trim());
|
||||||
|
AppPreferences.setShowTrayIcon(activity, trayIcon.isChecked());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindTheme(AppCompatActivity activity, Spinner spinner) {
|
||||||
|
CastThemeHelper.AppThemeMode[] modes = CastThemeHelper.AppThemeMode.values();
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.theme_system),
|
||||||
|
activity.getString(R.string.theme_dark),
|
||||||
|
activity.getString(R.string.theme_light),
|
||||||
|
activity.getString(R.string.theme_inverted)
|
||||||
|
};
|
||||||
|
bindEnumSpinner(spinner, modes, labels, AppPreferences.getThemeMode(activity),
|
||||||
|
mode -> AppPreferences.setThemeMode(activity, mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveTheme(AppCompatActivity activity, Spinner spinner) {
|
||||||
|
readEnumSpinner(spinner, (CastThemeHelper.AppThemeMode mode) ->
|
||||||
|
AppPreferences.setThemeMode(activity, mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CastThemeHelper.AppThemeMode themeModeAt(int position) {
|
||||||
|
CastThemeHelper.AppThemeMode[] modes = CastThemeHelper.AppThemeMode.values();
|
||||||
|
if (position >= 0 && position < modes.length) {
|
||||||
|
return modes[position];
|
||||||
|
}
|
||||||
|
return CastThemeHelper.AppThemeMode.SYSTEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindSender(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
bindTransport(activity, settings, R.id.spinner_sender_transport, true);
|
||||||
|
bindVideoCodec(activity, settings, R.id.spinner_sender_video_codec);
|
||||||
|
bindBitrate(activity, settings);
|
||||||
|
bindNetworkAdoption(activity, settings, R.id.spinner_sender_network);
|
||||||
|
bindSenderResolution(activity, settings);
|
||||||
|
bindCaptureMode(activity, settings);
|
||||||
|
bindStreamProtection(activity, settings);
|
||||||
|
bindAudioSpinner(activity, settings, R.id.spinner_sender_audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindReceiver(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
bindTransport(activity, settings, R.id.spinner_receiver_transport, true);
|
||||||
|
bindVideoCodec(activity, settings, R.id.spinner_receiver_video_codec);
|
||||||
|
bindNetworkAdoption(activity, settings, R.id.spinner_receiver_network);
|
||||||
|
bindReceiverDisplay(activity, settings);
|
||||||
|
Spinner audioPlay = activity.findViewById(R.id.spinner_receiver_play_audio);
|
||||||
|
if (audioPlay != null) {
|
||||||
|
bindOnOffSpinner(activity, audioPlay, AppPreferences.isPlayIncomingAudio(activity),
|
||||||
|
on -> AppPreferences.setPlayIncomingAudio(activity, on));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveSender(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readTransport(activity, settings, R.id.spinner_sender_transport);
|
||||||
|
readVideoCodec(activity, settings, R.id.spinner_sender_video_codec);
|
||||||
|
readBitrate(activity, settings);
|
||||||
|
readNetworkAdoption(activity, settings, R.id.spinner_sender_network);
|
||||||
|
readSenderResolution(activity, settings);
|
||||||
|
readCaptureMode(activity, settings);
|
||||||
|
readStreamProtection(activity, settings);
|
||||||
|
readAudioSpinner(activity, settings, R.id.spinner_sender_audio);
|
||||||
|
settings.setAdjustmentPreset(CastSettings.AdjustmentPreset.AUTO);
|
||||||
|
AppPreferences.saveSenderDefaults(activity, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveReceiver(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readTransport(activity, settings, R.id.spinner_receiver_transport);
|
||||||
|
readVideoCodec(activity, settings, R.id.spinner_receiver_video_codec);
|
||||||
|
readNetworkAdoption(activity, settings, R.id.spinner_receiver_network);
|
||||||
|
readReceiverDisplay(activity, settings);
|
||||||
|
Spinner audioPlay = activity.findViewById(R.id.spinner_receiver_play_audio);
|
||||||
|
if (audioPlay != null) {
|
||||||
|
AppPreferences.setPlayIncomingAudio(activity, audioPlay.getSelectedItemPosition() == 0);
|
||||||
|
}
|
||||||
|
settings.setAdjustmentPreset(CastSettings.AdjustmentPreset.AUTO);
|
||||||
|
AppPreferences.saveReceiverDefaults(activity, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindTransport(AppCompatActivity activity, CastSettings settings, int spinnerId,
|
||||||
|
boolean unused) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.transport_udp),
|
||||||
|
activity.getString(R.string.transport_tcp),
|
||||||
|
activity.getString(R.string.transport_quic)
|
||||||
|
};
|
||||||
|
String[] values = {
|
||||||
|
CastConfig.TRANSPORT_UDP,
|
||||||
|
CastConfig.TRANSPORT_TCP,
|
||||||
|
CastConfig.TRANSPORT_QUIC
|
||||||
|
};
|
||||||
|
bindStringSpinner(spinner, labels, values, settings.getTransport(), v -> {
|
||||||
|
settings.setTransport(v);
|
||||||
|
updateStreamProtectionEnabled(activity, settings);
|
||||||
|
});
|
||||||
|
updateStreamProtectionEnabled(activity, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readTransport(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
Object tag = spinner.getTag(R.id.tag_spinner_values);
|
||||||
|
if (tag instanceof String[]) {
|
||||||
|
int pos = spinner.getSelectedItemPosition();
|
||||||
|
String[] values = (String[]) tag;
|
||||||
|
if (pos >= 0 && pos < values.length) {
|
||||||
|
settings.setTransport(values[pos]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindVideoCodec(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
List<CastSettings.VideoCodec> codecs = CodecCatalog.supportedChoices();
|
||||||
|
List<String> labels = new ArrayList<>();
|
||||||
|
for (CastSettings.VideoCodec c : codecs) {
|
||||||
|
labels.add(labelVideoCodec(activity, c));
|
||||||
|
}
|
||||||
|
bindEnumSpinner(spinner, codecs.toArray(new CastSettings.VideoCodec[0]),
|
||||||
|
labels.toArray(new String[0]), settings.getVideoCodec(),
|
||||||
|
v -> settings.setVideoCodec(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readVideoCodec(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
Object tag = spinner.getTag(R.id.tag_spinner_enum);
|
||||||
|
if (tag instanceof CastSettings.VideoCodec[]) {
|
||||||
|
CastSettings.VideoCodec[] values = (CastSettings.VideoCodec[]) tag;
|
||||||
|
int pos = spinner.getSelectedItemPosition();
|
||||||
|
if (pos >= 0 && pos < values.length) {
|
||||||
|
settings.setVideoCodec(values[pos]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindBitrate(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
Spinner spinner = activity.findViewById(R.id.spinner_sender_bitrate);
|
||||||
|
CastSettings.BitrateMode[] modes = {
|
||||||
|
CastSettings.BitrateMode.ESTIMATED,
|
||||||
|
CastSettings.BitrateMode.QUALITY,
|
||||||
|
CastSettings.BitrateMode.SPEED,
|
||||||
|
CastSettings.BitrateMode.BALANCED
|
||||||
|
};
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.bitrate_estimated),
|
||||||
|
activity.getString(R.string.option_quality),
|
||||||
|
activity.getString(R.string.option_speed),
|
||||||
|
activity.getString(R.string.option_balanced)
|
||||||
|
};
|
||||||
|
CastSettings.BitrateMode current = settings.getBitrateMode();
|
||||||
|
if (current == CastSettings.BitrateMode.AUTO) {
|
||||||
|
current = CastSettings.BitrateMode.ESTIMATED;
|
||||||
|
}
|
||||||
|
bindEnumSpinner(spinner, modes, labels, current, settings::setBitrateMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readBitrate(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readEnumSpinner(activity.findViewById(R.id.spinner_sender_bitrate), settings::setBitrateMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindNetworkAdoption(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
CastSettings.NetworkAdoption[] modes = {
|
||||||
|
CastSettings.NetworkAdoption.ESTIMATED,
|
||||||
|
CastSettings.NetworkAdoption.QUALITY,
|
||||||
|
CastSettings.NetworkAdoption.SPEED
|
||||||
|
};
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.network_estimated_auto),
|
||||||
|
activity.getString(R.string.option_quality),
|
||||||
|
activity.getString(R.string.option_speed)
|
||||||
|
};
|
||||||
|
CastSettings.NetworkAdoption current = settings.getNetworkAdoption();
|
||||||
|
if (current == CastSettings.NetworkAdoption.AUTO) {
|
||||||
|
current = CastSettings.NetworkAdoption.ESTIMATED;
|
||||||
|
}
|
||||||
|
bindEnumSpinner(spinner, modes, labels, current, settings::setNetworkAdoption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readNetworkAdoption(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
readEnumSpinner(activity.findViewById(spinnerId), settings::setNetworkAdoption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindSenderResolution(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
Spinner spinner = activity.findViewById(R.id.spinner_sender_resolution);
|
||||||
|
CastSettings.Resolution[] modes = {
|
||||||
|
CastSettings.Resolution.ADAPTIVE,
|
||||||
|
CastSettings.Resolution.FIXED_1080P,
|
||||||
|
CastSettings.Resolution.FIXED_720P,
|
||||||
|
CastSettings.Resolution.FULL,
|
||||||
|
CastSettings.Resolution.THREE_QUARTERS
|
||||||
|
};
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.resolution_adaptive),
|
||||||
|
activity.getString(R.string.resolution_fixed_1080p),
|
||||||
|
activity.getString(R.string.resolution_fixed_720p),
|
||||||
|
activity.getString(R.string.resolution_full),
|
||||||
|
activity.getString(R.string.resolution_75)
|
||||||
|
};
|
||||||
|
bindEnumSpinner(spinner, modes, labels, settings.getResolution(), settings::setResolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readSenderResolution(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readEnumSpinner(activity.findViewById(R.id.spinner_sender_resolution), settings::setResolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindReceiverDisplay(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
Spinner spinner = activity.findViewById(R.id.spinner_receiver_resolution);
|
||||||
|
CastSettings.ReceiverDisplayResolution[] modes = CastSettings.ReceiverDisplayResolution.values();
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.receiver_display_auto_fit),
|
||||||
|
activity.getString(R.string.receiver_display_adaptive)
|
||||||
|
};
|
||||||
|
bindEnumSpinner(spinner, modes, labels, settings.getReceiverDisplayResolution(),
|
||||||
|
settings::setReceiverDisplayResolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readReceiverDisplay(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readEnumSpinner(activity.findViewById(R.id.spinner_receiver_resolution),
|
||||||
|
settings::setReceiverDisplayResolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindCaptureMode(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
Spinner spinner = activity.findViewById(R.id.spinner_sender_capture_mode);
|
||||||
|
List<String> labels = new ArrayList<>();
|
||||||
|
List<CastSettings.CaptureMode> values = new ArrayList<>();
|
||||||
|
labels.add(activity.getString(R.string.capture_full_screen));
|
||||||
|
values.add(CastSettings.CaptureMode.FULL_SCREEN);
|
||||||
|
if (PlatformCastSupport.isCaptureUserChoiceSupported()) {
|
||||||
|
labels.add(activity.getString(R.string.capture_user_choice));
|
||||||
|
values.add(CastSettings.CaptureMode.USER_CHOICE);
|
||||||
|
}
|
||||||
|
if (PlatformCastSupport.isCameraCaptureModeSupported()) {
|
||||||
|
labels.add(activity.getString(R.string.capture_camera_rear));
|
||||||
|
values.add(CastSettings.CaptureMode.CAMERA);
|
||||||
|
if (PlatformCastSupport.hasFrontCamera(activity)) {
|
||||||
|
labels.add(activity.getString(R.string.capture_camera_front));
|
||||||
|
values.add(CastSettings.CaptureMode.CAMERA_FRONT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labels.add(activity.getString(R.string.capture_calibration_test));
|
||||||
|
values.add(CastSettings.CaptureMode.CALIBRATION_TEST);
|
||||||
|
bindEnumSpinner(spinner, values.toArray(new CastSettings.CaptureMode[0]),
|
||||||
|
labels.toArray(new String[0]), settings.getCaptureMode(), settings::setCaptureMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readCaptureMode(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readEnumSpinner(activity.findViewById(R.id.spinner_sender_capture_mode), settings::setCaptureMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindStreamProtection(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
Spinner spinner = activity.findViewById(R.id.spinner_sender_stream_protection);
|
||||||
|
CastSettings.StreamProtection[] modes = CastSettings.StreamProtection.values();
|
||||||
|
String[] labels = {
|
||||||
|
activity.getString(R.string.option_auto),
|
||||||
|
activity.getString(R.string.protection_fec_34),
|
||||||
|
activity.getString(R.string.protection_fec_rs),
|
||||||
|
activity.getString(R.string.protection_nack),
|
||||||
|
activity.getString(R.string.protection_fec_nack)
|
||||||
|
};
|
||||||
|
bindEnumSpinner(spinner, modes, labels, settings.getStreamProtection(),
|
||||||
|
settings::setStreamProtection);
|
||||||
|
updateStreamProtectionEnabled(activity, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readStreamProtection(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
readEnumSpinner(activity.findViewById(R.id.spinner_sender_stream_protection),
|
||||||
|
settings::setStreamProtection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateStreamProtectionEnabled(AppCompatActivity activity, CastSettings settings) {
|
||||||
|
Spinner protection = activity.findViewById(R.id.spinner_sender_stream_protection);
|
||||||
|
if (protection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean udp = CastConfig.TRANSPORT_UDP.equals(settings.getTransport());
|
||||||
|
protection.setEnabled(udp);
|
||||||
|
TextView hint = activity.findViewById(R.id.text_stream_protection_hint);
|
||||||
|
if (hint != null) {
|
||||||
|
hint.setVisibility(udp ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindAudioSpinner(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
TextView hint = activity.findViewById(R.id.text_sender_audio_hint);
|
||||||
|
if (!PlatformCastSupport.isAudioCaptureSupported()) {
|
||||||
|
spinner.setEnabled(false);
|
||||||
|
if (hint != null) {
|
||||||
|
hint.setVisibility(View.VISIBLE);
|
||||||
|
hint.setText(R.string.label_audio_unavailable);
|
||||||
|
}
|
||||||
|
settings.setAudioEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hint != null) {
|
||||||
|
hint.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
bindOnOffSpinner(activity, spinner, settings.isAudioRequested(),
|
||||||
|
settings::setAudioEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readAudioSpinner(AppCompatActivity activity, CastSettings settings, int spinnerId) {
|
||||||
|
Spinner spinner = activity.findViewById(spinnerId);
|
||||||
|
if (!PlatformCastSupport.isAudioCaptureSupported()) {
|
||||||
|
settings.setAudioEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settings.setAudioEnabled(spinner.getSelectedItemPosition() == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindOnOffSpinner(AppCompatActivity activity, Spinner spinner, boolean on,
|
||||||
|
java.util.function.Consumer<Boolean> consumer) {
|
||||||
|
String[] labels = {activity.getString(R.string.option_on), activity.getString(R.string.option_off)};
|
||||||
|
spinner.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_spinner_dropdown_item, labels));
|
||||||
|
spinner.setSelection(on ? 0 : 1);
|
||||||
|
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
consumer.accept(position == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(AdapterView<?> parent) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindStringSpinner(Spinner spinner, String[] labels, String[] values,
|
||||||
|
String current, java.util.function.Consumer<String> onSelect) {
|
||||||
|
spinner.setTag(R.id.tag_spinner_values, values);
|
||||||
|
spinner.setAdapter(new ArrayAdapter<>(spinner.getContext(),
|
||||||
|
android.R.layout.simple_spinner_dropdown_item, labels));
|
||||||
|
int sel = 0;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
if (values[i].equals(current)) {
|
||||||
|
sel = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spinner.setSelection(sel);
|
||||||
|
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
if (position >= 0 && position < values.length) {
|
||||||
|
onSelect.accept(values[position]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(AdapterView<?> parent) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Enum<T>> void bindEnumSpinner(Spinner spinner, T[] values, String[] labels,
|
||||||
|
T current, java.util.function.Consumer<T> onSelect) {
|
||||||
|
spinner.setTag(R.id.tag_spinner_enum, values);
|
||||||
|
spinner.setAdapter(new ArrayAdapter<>(spinner.getContext(),
|
||||||
|
android.R.layout.simple_spinner_dropdown_item, labels));
|
||||||
|
int sel = 0;
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
if (values[i] == current) {
|
||||||
|
sel = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spinner.setSelection(sel);
|
||||||
|
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
if (position >= 0 && position < values.length) {
|
||||||
|
onSelect.accept(values[position]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(AdapterView<?> parent) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends Enum<T>> void readEnumSpinner(Spinner spinner,
|
||||||
|
java.util.function.Consumer<T> consumer) {
|
||||||
|
Object tag = spinner.getTag(R.id.tag_spinner_enum);
|
||||||
|
if (tag instanceof Enum[]) {
|
||||||
|
Enum<?>[] values = (Enum<?>[]) tag;
|
||||||
|
int pos = spinner.getSelectedItemPosition();
|
||||||
|
if (pos >= 0 && pos < values.length) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T v = (T) values[pos];
|
||||||
|
consumer.accept(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String labelVideoCodec(AppCompatActivity activity, CastSettings.VideoCodec codec) {
|
||||||
|
switch (codec) {
|
||||||
|
case H264:
|
||||||
|
return activity.getString(R.string.codec_h264);
|
||||||
|
case H265:
|
||||||
|
return activity.getString(R.string.codec_h265);
|
||||||
|
case PASSTHROUGH:
|
||||||
|
return activity.getString(R.string.codec_passthrough);
|
||||||
|
case AUTO:
|
||||||
|
default:
|
||||||
|
return activity.getString(R.string.option_auto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
app/src/main/java/com/foxx/androidcast/CastSettingsFragment.java
Normal file
105
app/src/main/java/com/foxx/androidcast/CastSettingsFragment.java
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.receiver.ReceiverPrefs;
|
||||||
|
|
||||||
|
/** Settings drawer content — swipe from the left edge on main screens. */
|
||||||
|
public class CastSettingsFragment extends Fragment {
|
||||||
|
private final CastSettings senderSettings = new CastSettings();
|
||||||
|
private final CastSettings receiverSettings = new CastSettings();
|
||||||
|
|
||||||
|
private EditText usernameInput;
|
||||||
|
private EditText pinInput;
|
||||||
|
private CheckBox trayIconCheck;
|
||||||
|
private Spinner themeSpinner;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_cast_settings, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
TextView headerGlobal = view.findViewById(R.id.header_global);
|
||||||
|
TextView headerSender = view.findViewById(R.id.header_sender);
|
||||||
|
TextView headerReceiver = view.findViewById(R.id.header_receiver);
|
||||||
|
headerGlobal.setText(R.string.settings_section_global);
|
||||||
|
headerSender.setText(R.string.settings_section_sender);
|
||||||
|
headerReceiver.setText(R.string.settings_section_receiver);
|
||||||
|
|
||||||
|
usernameInput = view.findViewById(R.id.edit_username);
|
||||||
|
pinInput = view.findViewById(R.id.edit_settings_pin);
|
||||||
|
trayIconCheck = view.findViewById(R.id.check_tray_icon);
|
||||||
|
themeSpinner = view.findViewById(R.id.spinner_theme);
|
||||||
|
|
||||||
|
senderSettings.copyFrom(AppPreferences.loadSenderDefaults(requireContext()));
|
||||||
|
receiverSettings.copyFrom(AppPreferences.loadReceiverDefaults(requireContext()));
|
||||||
|
|
||||||
|
AppCompatActivity activity = (AppCompatActivity) requireActivity();
|
||||||
|
CastSettingsBinder.bindGlobal(activity, usernameInput, pinInput, trayIconCheck);
|
||||||
|
CastSettingsBinder.bindTheme(activity, themeSpinner);
|
||||||
|
CastSettingsBinder.bindSender(activity, senderSettings);
|
||||||
|
CastSettingsBinder.bindReceiver(activity, receiverSettings);
|
||||||
|
|
||||||
|
trayIconCheck.setOnCheckedChangeListener((btn, checked) -> {
|
||||||
|
AppPreferences.setShowTrayIcon(requireContext(), checked);
|
||||||
|
CastTrayNotifier.onPreferenceChanged(requireContext());
|
||||||
|
});
|
||||||
|
themeSpinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
|
||||||
|
private boolean first = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(android.widget.AdapterView<?> parent, View v, int position, long id) {
|
||||||
|
CastThemeHelper.AppThemeMode mode = CastSettingsBinder.themeModeAt(position);
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode != AppPreferences.getThemeMode(requireContext())) {
|
||||||
|
AppPreferences.setThemeMode(requireContext(), mode);
|
||||||
|
Toast.makeText(requireContext(), R.string.theme_apply_restart, Toast.LENGTH_LONG).show();
|
||||||
|
requireActivity().recreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(android.widget.AdapterView<?> parent) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
saveAll();
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveAll() {
|
||||||
|
if (usernameInput == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AppCompatActivity activity = (AppCompatActivity) requireActivity();
|
||||||
|
CastSettingsBinder.saveGlobal(activity, usernameInput, pinInput, trayIconCheck);
|
||||||
|
CastSettingsBinder.saveTheme(activity, themeSpinner);
|
||||||
|
CastSettingsBinder.saveSender(activity, senderSettings);
|
||||||
|
CastSettingsBinder.saveReceiver(activity, receiverSettings);
|
||||||
|
ReceiverPrefs.setAllowAudio(requireContext(), AppPreferences.isPlayIncomingAudio(requireContext()));
|
||||||
|
CastTrayNotifier.onPreferenceChanged(requireContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/src/main/java/com/foxx/androidcast/CastThemeHelper.java
Normal file
57
app/src/main/java/com/foxx/androidcast/CastThemeHelper.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
|
||||||
|
/** Applies user-selected app theme before {@link Activity#setContentView}. */
|
||||||
|
public final class CastThemeHelper {
|
||||||
|
private CastThemeHelper() {}
|
||||||
|
|
||||||
|
public enum AppThemeMode {
|
||||||
|
SYSTEM,
|
||||||
|
DARK,
|
||||||
|
LIGHT,
|
||||||
|
INVERTED
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyTheme(Activity activity) {
|
||||||
|
AppThemeMode mode = AppPreferences.getThemeMode(activity);
|
||||||
|
switch (mode) {
|
||||||
|
case DARK:
|
||||||
|
activity.setTheme(R.style.Theme_AndroidCast_Dark);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||||
|
break;
|
||||||
|
case LIGHT:
|
||||||
|
activity.setTheme(R.style.Theme_AndroidCast_Light);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||||
|
break;
|
||||||
|
case INVERTED:
|
||||||
|
activity.setTheme(R.style.Theme_AndroidCast_Inverted);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||||
|
break;
|
||||||
|
case SYSTEM:
|
||||||
|
default:
|
||||||
|
activity.setTheme(R.style.Theme_AndroidCast);
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyNightMode(Context context) {
|
||||||
|
switch (AppPreferences.getThemeMode(context)) {
|
||||||
|
case DARK:
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||||
|
break;
|
||||||
|
case LIGHT:
|
||||||
|
case INVERTED:
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||||
|
break;
|
||||||
|
case SYSTEM:
|
||||||
|
default:
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
app/src/main/java/com/foxx/androidcast/CastTrayNotifier.java
Normal file
101
app/src/main/java/com/foxx/androidcast/CastTrayNotifier.java
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistent status-bar icon while cast services run (when enabled in settings).
|
||||||
|
*/
|
||||||
|
public final class CastTrayNotifier {
|
||||||
|
public static final String CHANNEL_TRAY = "cast_tray";
|
||||||
|
public static final int ID_TRAY = 3;
|
||||||
|
|
||||||
|
private CastTrayNotifier() {}
|
||||||
|
|
||||||
|
/** Sync tray with cast service lifecycle (call from foreground services). */
|
||||||
|
public static void refresh(Context context) {
|
||||||
|
Context app = context.getApplicationContext();
|
||||||
|
if (!AppPreferences.isShowTrayIcon(app)) {
|
||||||
|
hide(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (CastActiveState.isSenderCasting() || CastActiveState.isReceiverListening()) {
|
||||||
|
show(app, false);
|
||||||
|
} else {
|
||||||
|
hide(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User toggled the preference — show or hide immediately. */
|
||||||
|
public static void onPreferenceChanged(Context context) {
|
||||||
|
Context app = context.getApplicationContext();
|
||||||
|
if (AppPreferences.isShowTrayIcon(app)) {
|
||||||
|
show(app, true);
|
||||||
|
} else {
|
||||||
|
hide(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void show(Context context, boolean idle) {
|
||||||
|
createChannel(context);
|
||||||
|
NotificationManager nm = context.getSystemService(NotificationManager.class);
|
||||||
|
if (nm == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String body = idle
|
||||||
|
? context.getString(R.string.tray_cast_idle)
|
||||||
|
: context.getString(R.string.tray_cast_active);
|
||||||
|
PendingIntent pi = PendingIntent.getActivity(
|
||||||
|
context, 0, CastActivityTracker.resumeIntent(context),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
Notification n = new NotificationCompat.Builder(context, CHANNEL_TRAY)
|
||||||
|
.setContentTitle(context.getString(R.string.app_name))
|
||||||
|
.setContentText(body)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.setContentIntent(pi)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.build();
|
||||||
|
nm.notify(ID_TRAY, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void hide(Context context) {
|
||||||
|
NotificationManager nm = context.getSystemService(NotificationManager.class);
|
||||||
|
if (nm != null) {
|
||||||
|
nm.cancel(ID_TRAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createChannel(Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationManager nm = context.getSystemService(NotificationManager.class);
|
||||||
|
if (nm == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationChannel existing = nm.getNotificationChannel(CHANNEL_TRAY);
|
||||||
|
if (existing != null && existing.getImportance() < NotificationManager.IMPORTANCE_DEFAULT) {
|
||||||
|
nm.deleteNotificationChannel(CHANNEL_TRAY);
|
||||||
|
}
|
||||||
|
if (nm.getNotificationChannel(CHANNEL_TRAY) == null) {
|
||||||
|
NotificationChannel ch = new NotificationChannel(
|
||||||
|
CHANNEL_TRAY,
|
||||||
|
context.getString(R.string.tray_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
ch.setDescription(context.getString(R.string.tray_channel_description));
|
||||||
|
ch.setShowBadge(true);
|
||||||
|
nm.createNotificationChannel(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.activity.OnBackPressedCallback;
|
||||||
|
import androidx.annotation.LayoutRes;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.view.GravityCompat;
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hosts main content and a start-edge settings drawer (swipe right from left edge to open).
|
||||||
|
*/
|
||||||
|
public abstract class DrawerHostActivity extends AppCompatActivity {
|
||||||
|
private DrawerLayout drawerLayout;
|
||||||
|
|
||||||
|
protected void applyActivityTheme() {
|
||||||
|
CastThemeHelper.applyTheme(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
applyActivityTheme();
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_drawer_host);
|
||||||
|
|
||||||
|
drawerLayout = findViewById(R.id.drawer_layout);
|
||||||
|
android.view.ViewGroup contentRoot = findViewById(R.id.content_root);
|
||||||
|
getLayoutInflater().inflate(getContentLayoutId(), contentRoot, true);
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.settings_drawer_container, new CastSettingsFragment())
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||||
|
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||||
|
@Override
|
||||||
|
public void handleOnBackPressed() {
|
||||||
|
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
|
drawerLayout.closeDrawer(GravityCompat.START);
|
||||||
|
} else {
|
||||||
|
setEnabled(false);
|
||||||
|
getOnBackPressedDispatcher().onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
CastActivityTracker.onResume(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void openSettingsDrawer() {
|
||||||
|
drawerLayout.openDrawer(GravityCompat.START);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void closeSettingsDrawer() {
|
||||||
|
drawerLayout.closeDrawer(GravityCompat.START);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract @LayoutRes int getContentLayoutId();
|
||||||
|
}
|
||||||
@@ -4,27 +4,30 @@ import android.content.Intent;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import com.foxx.androidcast.receiver.ReceiverPlaybackActivity;
|
||||||
|
|
||||||
import com.foxx.androidcast.receiver.ReceiverActivity;
|
|
||||||
import com.foxx.androidcast.sender.SenderActivity;
|
import com.foxx.androidcast.sender.SenderActivity;
|
||||||
import com.foxx.androidcast.stats.SessionStatsStore;
|
import com.foxx.androidcast.stats.SessionStatsStore;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends DrawerHostActivity {
|
||||||
|
@Override
|
||||||
|
protected int getContentLayoutId() {
|
||||||
|
return R.layout.activity_main;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
SessionStatsStore.pruneOnAppStart(this);
|
SessionStatsStore.pruneOnAppStart(this);
|
||||||
setContentView(R.layout.activity_main);
|
|
||||||
com.foxx.androidcast.network.CastTransportFactory.init(this);
|
com.foxx.androidcast.network.CastTransportFactory.init(this);
|
||||||
PermissionHelper.request(this);
|
PermissionHelper.request(this);
|
||||||
|
|
||||||
Button sendBtn = findViewById(R.id.btn_send);
|
findViewById(R.id.btn_send).setOnClickListener(v ->
|
||||||
Button recvBtn = findViewById(R.id.btn_receive);
|
|
||||||
sendBtn.setOnClickListener(v ->
|
|
||||||
startActivity(new Intent(this, SenderActivity.class)));
|
startActivity(new Intent(this, SenderActivity.class)));
|
||||||
recvBtn.setOnClickListener(v ->
|
findViewById(R.id.btn_receive).setOnClickListener(v -> {
|
||||||
startActivity(new Intent(this, ReceiverActivity.class)));
|
Intent i = new Intent(this, ReceiverPlaybackActivity.class);
|
||||||
|
i.putExtra(ReceiverPlaybackActivity.EXTRA_AUTO_START_LISTENING, true);
|
||||||
|
startActivity(i);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
38
app/src/main/java/com/foxx/androidcast/PinAuth.java
Normal file
38
app/src/main/java/com/foxx/androidcast/PinAuth.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package com.foxx.androidcast;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/** PIN hashing for discovery and handshake (never send raw PIN on LAN). */
|
||||||
|
public final class PinAuth {
|
||||||
|
private PinAuth() {}
|
||||||
|
|
||||||
|
public static String hash(String pin) {
|
||||||
|
if (pin == null) {
|
||||||
|
pin = "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] digest = md.digest(pin.getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder sb = new StringBuilder(digest.length * 2);
|
||||||
|
for (byte b : digest) {
|
||||||
|
sb.append(String.format("%02x", b & 0xff));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
return Integer.toHexString(pin.hashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean matches(String pin, String expectedHash) {
|
||||||
|
if (expectedHash == null || expectedHash.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hash(pin).equals(expectedHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isProtected(String pin) {
|
||||||
|
return pin != null && !pin.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,29 @@ public final class PlatformCastSupport {
|
|||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasFrontCamera(android.content.Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
android.hardware.camera2.CameraManager mgr =
|
||||||
|
context.getSystemService(android.hardware.camera2.CameraManager.class);
|
||||||
|
if (mgr == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String id : mgr.getCameraIdList()) {
|
||||||
|
android.hardware.camera2.CameraCharacteristics chars = mgr.getCameraCharacteristics(id);
|
||||||
|
Integer facing = chars.get(android.hardware.camera2.CameraCharacteristics.LENS_FACING);
|
||||||
|
if (facing != null
|
||||||
|
&& facing == android.hardware.camera2.CameraCharacteristics.LENS_FACING_FRONT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** Applies default audio flag: on for Android 10+, off otherwise. */
|
/** Applies default audio flag: on for Android 10+, off otherwise. */
|
||||||
public static void applyDefaultAudioSetting(CastSettings settings) {
|
public static void applyDefaultAudioSetting(CastSettings settings) {
|
||||||
settings.setAudioEnabled(isAudioCaptureSupported());
|
settings.setAudioEnabled(isAudioCaptureSupported());
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.util.Log;
|
|||||||
|
|
||||||
import com.foxx.androidcast.CastConfig;
|
import com.foxx.androidcast.CastConfig;
|
||||||
import com.foxx.androidcast.DeviceInfo;
|
import com.foxx.androidcast.DeviceInfo;
|
||||||
|
import com.foxx.androidcast.PinAuth;
|
||||||
import com.foxx.androidcast.network.CastCodecFlags;
|
import com.foxx.androidcast.network.CastCodecFlags;
|
||||||
import com.foxx.androidcast.network.CastPacketFramer;
|
import com.foxx.androidcast.network.CastPacketFramer;
|
||||||
import com.foxx.androidcast.network.CastProtoHeader;
|
import com.foxx.androidcast.network.CastProtoHeader;
|
||||||
@@ -53,13 +54,20 @@ public class DiscoveryManager {
|
|||||||
public final int audioCodecs;
|
public final int audioCodecs;
|
||||||
public final int placeholderId;
|
public final int placeholderId;
|
||||||
public final long protoVersion;
|
public final long protoVersion;
|
||||||
|
/** SHA-256 hex of receiver PIN, empty if anonymous. */
|
||||||
|
public final String pinAuth;
|
||||||
|
|
||||||
public DiscoveredDevice(String name, String host, int port, String transport, long lastSeenMs) {
|
public DiscoveredDevice(String name, String host, int port, String transport, long lastSeenMs) {
|
||||||
this(name, host, port, transport, lastSeenMs, 0, 0, 0, 0L);
|
this(name, host, port, transport, lastSeenMs, 0, 0, 0, 0L, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public DiscoveredDevice(String name, String host, int port, String transport, long lastSeenMs,
|
public DiscoveredDevice(String name, String host, int port, String transport, long lastSeenMs,
|
||||||
int videoCodecs, int audioCodecs, int placeholderId, long protoVersion) {
|
int videoCodecs, int audioCodecs, int placeholderId, long protoVersion) {
|
||||||
|
this(name, host, port, transport, lastSeenMs, videoCodecs, audioCodecs, placeholderId, protoVersion, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscoveredDevice(String name, String host, int port, String transport, long lastSeenMs,
|
||||||
|
int videoCodecs, int audioCodecs, int placeholderId, long protoVersion, String pinAuth) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
@@ -69,6 +77,11 @@ public class DiscoveryManager {
|
|||||||
this.audioCodecs = audioCodecs;
|
this.audioCodecs = audioCodecs;
|
||||||
this.placeholderId = placeholderId;
|
this.placeholderId = placeholderId;
|
||||||
this.protoVersion = protoVersion;
|
this.protoVersion = protoVersion;
|
||||||
|
this.pinAuth = pinAuth != null ? pinAuth : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean pinMatches(String pin) {
|
||||||
|
return PinAuth.matches(pin, pinAuth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,14 +146,19 @@ public class DiscoveryManager {
|
|||||||
|
|
||||||
public void startAnnouncing(Context context, String deviceName, int port, String transport,
|
public void startAnnouncing(Context context, String deviceName, int port, String transport,
|
||||||
boolean listening) {
|
boolean listening) {
|
||||||
|
startAnnouncing(context, deviceName, port, transport, listening, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAnnouncing(Context context, String deviceName, int port, String transport,
|
||||||
|
boolean listening, String pin) {
|
||||||
if (!running.compareAndSet(false, true)) {
|
if (!running.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
executor.execute(() -> announceLoop(context, deviceName, port, transport, listening));
|
executor.execute(() -> announceLoop(context, deviceName, port, transport, listening, pin));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startAnnouncing(String deviceName, int port, String transport, boolean listening) {
|
public void startAnnouncing(String deviceName, int port, String transport, boolean listening) {
|
||||||
startAnnouncing(appContext, deviceName, port, transport, listening);
|
startAnnouncing(appContext, deviceName, port, transport, listening, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void browseLoop() {
|
private void browseLoop() {
|
||||||
@@ -203,7 +221,7 @@ public class DiscoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void announceLoop(Context context, String deviceName, int port, String transport,
|
private void announceLoop(Context context, String deviceName, int port, String transport,
|
||||||
boolean listening) {
|
boolean listening, String pin) {
|
||||||
acquireMulticastLock();
|
acquireMulticastLock();
|
||||||
int placeholderId = DeviceInfo.getPlaceholderId(context);
|
int placeholderId = DeviceInfo.getPlaceholderId(context);
|
||||||
int videoMask = CastCodecFlags.localDecoderVideoMask();
|
int videoMask = CastCodecFlags.localDecoderVideoMask();
|
||||||
@@ -213,7 +231,7 @@ public class DiscoveryManager {
|
|||||||
sock.setBroadcast(true);
|
sock.setBroadcast(true);
|
||||||
sock.setSoTimeout(400);
|
sock.setSoTimeout(400);
|
||||||
while (running.get()) {
|
while (running.get()) {
|
||||||
sendAnnounce(sock, context, deviceName, port, transport, listening,
|
sendAnnounce(sock, context, deviceName, port, transport, listening, pin,
|
||||||
placeholderId, videoMask, audioMask);
|
placeholderId, videoMask, audioMask);
|
||||||
long listenUntil = System.currentTimeMillis() + 2_000;
|
long listenUntil = System.currentTimeMillis() + 2_000;
|
||||||
while (running.get() && System.currentTimeMillis() < listenUntil) {
|
while (running.get() && System.currentTimeMillis() < listenUntil) {
|
||||||
@@ -222,9 +240,9 @@ public class DiscoveryManager {
|
|||||||
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
||||||
sock.receive(packet);
|
sock.receive(packet);
|
||||||
if (isDiscoverProbe(packet)) {
|
if (isDiscoverProbe(packet)) {
|
||||||
sendAnnounce(sock, context, deviceName, port, transport, listening,
|
sendAnnounce(sock, context, deviceName, port, transport, listening, pin,
|
||||||
placeholderId, videoMask, audioMask);
|
placeholderId, videoMask, audioMask);
|
||||||
sendAnnounceUnicast(sock, context, deviceName, port, transport, listening,
|
sendAnnounceUnicast(sock, context, deviceName, port, transport, listening, pin,
|
||||||
placeholderId, videoMask, audioMask, packet.getAddress());
|
placeholderId, videoMask, audioMask, packet.getAddress());
|
||||||
}
|
}
|
||||||
} catch (java.net.SocketTimeoutException ignored) {
|
} catch (java.net.SocketTimeoutException ignored) {
|
||||||
@@ -269,9 +287,9 @@ public class DiscoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void sendAnnounce(DatagramSocket sock, Context context, String deviceName, int port,
|
private static void sendAnnounce(DatagramSocket sock, Context context, String deviceName, int port,
|
||||||
String transport, boolean listening, int placeholderId, int videoMask, int audioMask)
|
String transport, boolean listening, String pin, int placeholderId, int videoMask, int audioMask)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
byte[] wire = buildAnnounceWire(context, deviceName, port, transport, listening,
|
byte[] wire = buildAnnounceWire(context, deviceName, port, transport, listening, pin,
|
||||||
placeholderId, videoMask, audioMask);
|
placeholderId, videoMask, audioMask);
|
||||||
InetAddress broadcast = InetAddress.getByName("255.255.255.255");
|
InetAddress broadcast = InetAddress.getByName("255.255.255.255");
|
||||||
DatagramPacket packet = new DatagramPacket(wire, wire.length, broadcast, CastConfig.DISCOVERY_PORT);
|
DatagramPacket packet = new DatagramPacket(wire, wire.length, broadcast, CastConfig.DISCOVERY_PORT);
|
||||||
@@ -279,10 +297,10 @@ public class DiscoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void sendAnnounceUnicast(DatagramSocket sock, Context context, String deviceName, int port,
|
private static void sendAnnounceUnicast(DatagramSocket sock, Context context, String deviceName, int port,
|
||||||
String transport, boolean listening, int placeholderId, int videoMask, int audioMask,
|
String transport, boolean listening, String pin, int placeholderId, int videoMask, int audioMask,
|
||||||
InetAddress target) {
|
InetAddress target) {
|
||||||
try {
|
try {
|
||||||
byte[] wire = buildAnnounceWire(context, deviceName, port, transport, listening,
|
byte[] wire = buildAnnounceWire(context, deviceName, port, transport, listening, pin,
|
||||||
placeholderId, videoMask, audioMask);
|
placeholderId, videoMask, audioMask);
|
||||||
DatagramPacket packet = new DatagramPacket(wire, wire.length, target, CastConfig.DISCOVERY_PORT);
|
DatagramPacket packet = new DatagramPacket(wire, wire.length, target, CastConfig.DISCOVERY_PORT);
|
||||||
sock.send(packet);
|
sock.send(packet);
|
||||||
@@ -292,7 +310,7 @@ public class DiscoveryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] buildAnnounceWire(Context context, String deviceName, int port, String transport,
|
private static byte[] buildAnnounceWire(Context context, String deviceName, int port, String transport,
|
||||||
boolean listening, int placeholderId, int videoMask, int audioMask) throws Exception {
|
boolean listening, String pin, int placeholderId, int videoMask, int audioMask) throws Exception {
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
json.put("magic", CastConfig.DISCOVERY_MAGIC);
|
json.put("magic", CastConfig.DISCOVERY_MAGIC);
|
||||||
json.put("name", deviceName);
|
json.put("name", deviceName);
|
||||||
@@ -300,6 +318,9 @@ public class DiscoveryManager {
|
|||||||
json.put("transport", transport);
|
json.put("transport", transport);
|
||||||
json.put("listening", listening);
|
json.put("listening", listening);
|
||||||
json.put("device_uuid", DeviceInfo.getDeviceUuid(context));
|
json.put("device_uuid", DeviceInfo.getDeviceUuid(context));
|
||||||
|
if (PinAuth.isProtected(pin)) {
|
||||||
|
json.put("pin_auth", PinAuth.hash(pin));
|
||||||
|
}
|
||||||
byte[] jsonBytes = json.toString().getBytes(StandardCharsets.UTF_8);
|
byte[] jsonBytes = json.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
CastProtoHeader hdr = CastProtoHeader.forDiscovery(placeholderId, videoMask, audioMask);
|
CastProtoHeader hdr = CastProtoHeader.forDiscovery(placeholderId, videoMask, audioMask);
|
||||||
try {
|
try {
|
||||||
@@ -364,9 +385,10 @@ public class DiscoveryManager {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
String pinAuth = json.optString("pin_auth", "");
|
||||||
String key = host + ":" + port;
|
String key = host + ":" + port;
|
||||||
DiscoveredDevice device = new DiscoveredDevice(name, host, port, transport,
|
DiscoveredDevice device = new DiscoveredDevice(name, host, port, transport,
|
||||||
System.currentTimeMillis(), videoCodecs, audioCodecs, placeholderId, protoVersion);
|
System.currentTimeMillis(), videoCodecs, audioCodecs, placeholderId, protoVersion, pinAuth);
|
||||||
devices.put(key, device);
|
devices.put(key, device);
|
||||||
notifyListener();
|
notifyListener();
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ public final class CastProtocol {
|
|||||||
dos.writeInt(settings.getBitrateMode().ordinal());
|
dos.writeInt(settings.getBitrateMode().ordinal());
|
||||||
dos.writeInt(settings.getNetworkAdoption().ordinal());
|
dos.writeInt(settings.getNetworkAdoption().ordinal());
|
||||||
dos.writeInt(settings.getCaptureMode().ordinal());
|
dos.writeInt(settings.getCaptureMode().ordinal());
|
||||||
|
dos.writeInt(settings.getStreamProtection().ordinal());
|
||||||
|
dos.writeInt(settings.getReceiverDisplayResolution().ordinal());
|
||||||
dos.flush();
|
dos.flush();
|
||||||
return bos.toByteArray();
|
return bos.toByteArray();
|
||||||
}
|
}
|
||||||
@@ -115,6 +117,15 @@ public final class CastProtocol {
|
|||||||
settings.setCaptureMode(readEnum(CastSettings.CaptureMode.values(), dis.readInt(),
|
settings.setCaptureMode(readEnum(CastSettings.CaptureMode.values(), dis.readInt(),
|
||||||
CastSettings.CaptureMode.FULL_SCREEN));
|
CastSettings.CaptureMode.FULL_SCREEN));
|
||||||
}
|
}
|
||||||
|
if (dis.available() > 0) {
|
||||||
|
settings.setStreamProtection(readEnum(CastSettings.StreamProtection.values(), dis.readInt(),
|
||||||
|
CastSettings.StreamProtection.AUTO));
|
||||||
|
}
|
||||||
|
if (dis.available() > 0) {
|
||||||
|
settings.setReceiverDisplayResolution(
|
||||||
|
readEnum(CastSettings.ReceiverDisplayResolution.values(), dis.readInt(),
|
||||||
|
CastSettings.ReceiverDisplayResolution.AUTO_FIT));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dis.reset();
|
dis.reset();
|
||||||
settings.setTransport(dis.readUTF());
|
settings.setTransport(dis.readUTF());
|
||||||
|
|||||||
@@ -8,31 +8,25 @@ import android.os.Build;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastConfig;
|
import com.foxx.androidcast.AppPreferences;
|
||||||
|
import com.foxx.androidcast.CastActivityTracker;
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
import com.foxx.androidcast.ICastReceiverService;
|
import com.foxx.androidcast.ICastReceiverService;
|
||||||
import com.foxx.androidcast.ICastStatusCallback;
|
import com.foxx.androidcast.ICastStatusCallback;
|
||||||
import com.foxx.androidcast.PermissionHelper;
|
import com.foxx.androidcast.PermissionHelper;
|
||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
import com.foxx.androidcast.SettingsUi;
|
|
||||||
|
|
||||||
/** Receiver setup — listening runs in background; fullscreen playback opens when video stream starts. */
|
/** Receiver setup — options live in {@link com.foxx.androidcast.CastSettingsActivity}. */
|
||||||
public class ReceiverActivity extends AppCompatActivity {
|
public class ReceiverActivity extends AppCompatActivity {
|
||||||
private final CastSettings castSettings = new CastSettings();
|
private final CastSettings castSettings = new CastSettings();
|
||||||
|
|
||||||
private TextView statusText;
|
private TextView statusText;
|
||||||
private EditText pinInput;
|
|
||||||
private CheckBox allowAudioCheck;
|
|
||||||
|
|
||||||
private ICastReceiverService receiverService;
|
private ICastReceiverService receiverService;
|
||||||
private boolean bound;
|
private boolean bound;
|
||||||
|
|
||||||
@@ -93,19 +87,20 @@ public class ReceiverActivity extends AppCompatActivity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_receiver);
|
setContentView(R.layout.activity_receiver);
|
||||||
PermissionHelper.request(this);
|
PermissionHelper.request(this);
|
||||||
SettingsUi.bindReceiver(this, castSettings);
|
castSettings.copyFrom(AppPreferences.loadReceiverDefaults(this));
|
||||||
|
|
||||||
statusText = findViewById(R.id.text_status);
|
statusText = findViewById(R.id.text_status);
|
||||||
pinInput = findViewById(R.id.edit_pin);
|
|
||||||
pinInput.setText(CastConfig.DEFAULT_PIN);
|
|
||||||
allowAudioCheck = findViewById(R.id.check_allow_audio);
|
|
||||||
allowAudioCheck.setChecked(ReceiverPrefs.isAllowAudio(this));
|
|
||||||
allowAudioCheck.setOnCheckedChangeListener((btn, checked) ->
|
|
||||||
ReceiverPrefs.setAllowAudio(ReceiverActivity.this, checked));
|
|
||||||
|
|
||||||
findViewById(R.id.btn_start_receiver).setOnClickListener(v -> startReceiverService());
|
findViewById(R.id.btn_start_receiver).setOnClickListener(v -> startReceiverService());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
CastActivityTracker.onResume(this);
|
||||||
|
PermissionHelper.remindIfMissing(this, AppPreferences.isPlayIncomingAudio(this));
|
||||||
|
updateKeepScreenOn();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
@@ -126,13 +121,6 @@ public class ReceiverActivity extends AppCompatActivity {
|
|||||||
super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
PermissionHelper.remindIfMissing(this, allowAudioCheck.isChecked());
|
|
||||||
updateKeepScreenOn();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
@@ -148,11 +136,8 @@ public class ReceiverActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startReceiverService() {
|
private void startReceiverService() {
|
||||||
String pin = pinInput.getText().toString().trim();
|
String pin = AppPreferences.getPin(this);
|
||||||
if (TextUtils.isEmpty(pin)) {
|
boolean allowAudio = AppPreferences.isPlayIncomingAudio(this);
|
||||||
pin = CastConfig.DEFAULT_PIN;
|
|
||||||
}
|
|
||||||
boolean allowAudio = allowAudioCheck.isChecked();
|
|
||||||
ReceiverPrefs.setAllowAudio(this, allowAudio);
|
ReceiverPrefs.setAllowAudio(this, allowAudio);
|
||||||
|
|
||||||
Intent intent = new Intent(this, ReceiverCastService.class);
|
Intent intent = new Intent(this, ReceiverCastService.class);
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ import android.os.RemoteException;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.AppPreferences;
|
||||||
|
import com.foxx.androidcast.CastActiveState;
|
||||||
import com.foxx.androidcast.CastNotifications;
|
import com.foxx.androidcast.CastNotifications;
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.CastTrayNotifier;
|
||||||
import com.foxx.androidcast.ICastReceiverService;
|
import com.foxx.androidcast.ICastReceiverService;
|
||||||
import com.foxx.androidcast.ICastStatusCallback;
|
import com.foxx.androidcast.ICastStatusCallback;
|
||||||
import com.foxx.androidcast.IntentExtras;
|
import com.foxx.androidcast.IntentExtras;
|
||||||
@@ -166,19 +169,23 @@ public class ReceiverCastService extends Service {
|
|||||||
}
|
}
|
||||||
if (intent != null && ACTION_START.equals(intent.getAction())) {
|
if (intent != null && ACTION_START.equals(intent.getAction())) {
|
||||||
String pin = intent.getStringExtra(EXTRA_PIN);
|
String pin = intent.getStringExtra(EXTRA_PIN);
|
||||||
if (pin == null || pin.isEmpty()) {
|
if (pin == null) {
|
||||||
pin = com.foxx.androidcast.CastConfig.DEFAULT_PIN;
|
pin = AppPreferences.getPin(this);
|
||||||
}
|
}
|
||||||
CastSettings fromIntent = IntentExtras.getCastSettings(intent);
|
CastSettings fromIntent = IntentExtras.getCastSettings(intent);
|
||||||
if (fromIntent != null) {
|
if (fromIntent != null) {
|
||||||
settings = fromIntent;
|
settings = fromIntent;
|
||||||
|
} else {
|
||||||
|
settings = AppPreferences.loadReceiverDefaults(this);
|
||||||
}
|
}
|
||||||
allowIncomingAudio = intent.getBooleanExtra(EXTRA_ALLOW_AUDIO,
|
allowIncomingAudio = intent.getBooleanExtra(EXTRA_ALLOW_AUDIO,
|
||||||
ReceiverPrefs.isAllowAudio(this));
|
AppPreferences.isPlayIncomingAudio(this));
|
||||||
activePin = pin;
|
activePin = pin;
|
||||||
restartListening(pin);
|
restartListening(pin);
|
||||||
} else if (intent == null && phase == Phase.IDLE) {
|
} else if (intent == null && phase == Phase.IDLE) {
|
||||||
allowIncomingAudio = ReceiverPrefs.isAllowAudio(this);
|
allowIncomingAudio = AppPreferences.isPlayIncomingAudio(this);
|
||||||
|
settings = AppPreferences.loadReceiverDefaults(this);
|
||||||
|
activePin = AppPreferences.getPin(this);
|
||||||
restartListening(activePin);
|
restartListening(activePin);
|
||||||
}
|
}
|
||||||
return START_STICKY;
|
return START_STICKY;
|
||||||
@@ -203,6 +210,8 @@ public class ReceiverCastService extends Service {
|
|||||||
acquireWakeLock();
|
acquireWakeLock();
|
||||||
startForeground(CastNotifications.ID_RECEIVER,
|
startForeground(CastNotifications.ID_RECEIVER,
|
||||||
CastNotifications.receiver(this, getString(R.string.receiver_listening), ReceiverCastService.class));
|
CastNotifications.receiver(this, getString(R.string.receiver_listening), ReceiverCastService.class));
|
||||||
|
CastActiveState.setReceiverListening(true);
|
||||||
|
CastTrayNotifier.refresh(this);
|
||||||
|
|
||||||
if (videoDecoder != null) {
|
if (videoDecoder != null) {
|
||||||
videoDecoder.release();
|
videoDecoder.release();
|
||||||
@@ -592,8 +601,8 @@ public class ReceiverCastService extends Service {
|
|||||||
discovery = new DiscoveryManager(this);
|
discovery = new DiscoveryManager(this);
|
||||||
com.foxx.androidcast.network.CastTransportFactory.init(this);
|
com.foxx.androidcast.network.CastTransportFactory.init(this);
|
||||||
int announcePort = com.foxx.androidcast.CastConfig.portForTransport(settings.getTransport());
|
int announcePort = com.foxx.androidcast.CastConfig.portForTransport(settings.getTransport());
|
||||||
discovery.startAnnouncing(android.os.Build.MODEL, announcePort,
|
discovery.startAnnouncing(this, AppPreferences.getUsername(this), announcePort,
|
||||||
settings.getTransport(), true);
|
settings.getTransport(), true, activePin);
|
||||||
Log.i(TAG, "Discovery announcing on port " + announcePort);
|
Log.i(TAG, "Discovery announcing on port " + announcePort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,6 +887,8 @@ public class ReceiverCastService extends Service {
|
|||||||
|
|
||||||
private void releaseAll() {
|
private void releaseAll() {
|
||||||
finishSessionStatsAsync();
|
finishSessionStatsAsync();
|
||||||
|
CastActiveState.setReceiverListening(false);
|
||||||
|
CastTrayNotifier.refresh(this);
|
||||||
phase = Phase.IDLE;
|
phase = Phase.IDLE;
|
||||||
resetStreamState();
|
resetStreamState();
|
||||||
playbackLaunched = false;
|
playbackLaunched = false;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.foxx.androidcast.receiver;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@@ -21,9 +22,11 @@ import android.view.animation.AccelerateDecelerateInterpolator;
|
|||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import com.foxx.androidcast.AppPreferences;
|
||||||
|
|
||||||
import com.foxx.androidcast.CastConfig;
|
import com.foxx.androidcast.CastConfig;
|
||||||
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.DrawerHostActivity;
|
||||||
|
import com.foxx.androidcast.CastThemeHelper;
|
||||||
import com.foxx.androidcast.ICastReceiverService;
|
import com.foxx.androidcast.ICastReceiverService;
|
||||||
import com.foxx.androidcast.ICastStatusCallback;
|
import com.foxx.androidcast.ICastStatusCallback;
|
||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
@@ -37,12 +40,13 @@ import androidx.core.text.HtmlCompat;
|
|||||||
* Fullscreen display: dancing-robot overlay while listening / waiting;
|
* Fullscreen display: dancing-robot overlay while listening / waiting;
|
||||||
* video surface shown only after the first decoded frame.
|
* video surface shown only after the first decoded frame.
|
||||||
*/
|
*/
|
||||||
public class ReceiverPlaybackActivity extends AppCompatActivity {
|
public class ReceiverPlaybackActivity extends DrawerHostActivity {
|
||||||
public static final String EXTRA_WIDTH = "video_width";
|
public static final String EXTRA_WIDTH = "video_width";
|
||||||
public static final String EXTRA_HEIGHT = "video_height";
|
public static final String EXTRA_HEIGHT = "video_height";
|
||||||
public static final String EXTRA_FINISH = "finish_playback";
|
public static final String EXTRA_FINISH = "finish_playback";
|
||||||
public static final String EXTRA_SHOW_AWAITING = "show_awaiting";
|
public static final String EXTRA_SHOW_AWAITING = "show_awaiting";
|
||||||
public static final String EXTRA_AWAITING_MESSAGE = "awaiting_message";
|
public static final String EXTRA_AWAITING_MESSAGE = "awaiting_message";
|
||||||
|
public static final String EXTRA_AUTO_START_LISTENING = "auto_start_listening";
|
||||||
|
|
||||||
private AspectRatioFrameLayout aspectContainer;
|
private AspectRatioFrameLayout aspectContainer;
|
||||||
private TextureView textureView;
|
private TextureView textureView;
|
||||||
@@ -132,13 +136,23 @@ public class ReceiverPlaybackActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyActivityTheme() {
|
||||||
|
CastThemeHelper.applyTheme(this);
|
||||||
|
setTheme(R.style.Theme_AndroidCast_Fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getContentLayoutId() {
|
||||||
|
return R.layout.activity_playback;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
if (handleCommandIntent(getIntent())) {
|
if (handleCommandIntent(getIntent())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setContentView(R.layout.activity_playback);
|
|
||||||
applyFullscreen();
|
applyFullscreen();
|
||||||
|
|
||||||
readSizeFromIntent(getIntent());
|
readSizeFromIntent(getIntent());
|
||||||
@@ -154,12 +168,32 @@ public class ReceiverPlaybackActivity extends AppCompatActivity {
|
|||||||
textureView.setSurfaceTextureListener(surfaceListener);
|
textureView.setSurfaceTextureListener(surfaceListener);
|
||||||
applyAspectRatio();
|
applyAspectRatio();
|
||||||
|
|
||||||
if (getIntent().getBooleanExtra(EXTRA_SHOW_AWAITING, true)) {
|
if (getIntent().getBooleanExtra(EXTRA_AUTO_START_LISTENING, false)) {
|
||||||
|
startListeningService();
|
||||||
|
showAwaitingOverlay(R.string.playback_listening);
|
||||||
|
} else if (getIntent().getBooleanExtra(EXTRA_SHOW_AWAITING, true)) {
|
||||||
int msg = getIntent().getIntExtra(EXTRA_AWAITING_MESSAGE, R.string.playback_listening);
|
int msg = getIntent().getIntExtra(EXTRA_AWAITING_MESSAGE, R.string.playback_listening);
|
||||||
showAwaitingOverlay(msg);
|
showAwaitingOverlay(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startListeningService() {
|
||||||
|
String pin = AppPreferences.getPin(this);
|
||||||
|
boolean allowAudio = AppPreferences.isPlayIncomingAudio(this);
|
||||||
|
ReceiverPrefs.setAllowAudio(this, allowAudio);
|
||||||
|
CastSettings settings = AppPreferences.loadReceiverDefaults(this);
|
||||||
|
Intent intent = new Intent(this, ReceiverCastService.class);
|
||||||
|
intent.setAction(ReceiverCastService.ACTION_START);
|
||||||
|
intent.putExtra(ReceiverCastService.EXTRA_PIN, pin);
|
||||||
|
intent.putExtra(ReceiverCastService.EXTRA_ALLOW_AUDIO, allowAudio);
|
||||||
|
intent.putExtra(CastSettings.EXTRA, settings);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(intent);
|
||||||
|
} else {
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onNewIntent(Intent intent) {
|
protected void onNewIntent(Intent intent) {
|
||||||
super.onNewIntent(intent);
|
super.onNewIntent(intent);
|
||||||
|
|||||||
@@ -224,6 +224,8 @@ public final class MultiCastCoordinator {
|
|||||||
s.setBitrateMode(src.getBitrateMode());
|
s.setBitrateMode(src.getBitrateMode());
|
||||||
s.setNetworkAdoption(src.getNetworkAdoption());
|
s.setNetworkAdoption(src.getNetworkAdoption());
|
||||||
s.setCaptureMode(src.getCaptureMode());
|
s.setCaptureMode(src.getCaptureMode());
|
||||||
|
s.setStreamProtection(src.getStreamProtection());
|
||||||
|
s.setReceiverDisplayResolution(src.getReceiverDisplayResolution());
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import android.util.Log;
|
|||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.AppPreferences;
|
||||||
import com.foxx.androidcast.CastActiveState;
|
import com.foxx.androidcast.CastActiveState;
|
||||||
|
import com.foxx.androidcast.CastTrayNotifier;
|
||||||
import com.foxx.androidcast.CastNotifications;
|
import com.foxx.androidcast.CastNotifications;
|
||||||
import com.foxx.androidcast.CastConfig;
|
import com.foxx.androidcast.CastConfig;
|
||||||
import com.foxx.androidcast.CastResolution;
|
import com.foxx.androidcast.CastResolution;
|
||||||
@@ -63,6 +65,7 @@ public class ScreenCastService extends Service implements
|
|||||||
public static final String EXTRA_PORT = "port";
|
public static final String EXTRA_PORT = "port";
|
||||||
public static final String EXTRA_PIN = "pin";
|
public static final String EXTRA_PIN = "pin";
|
||||||
public static final String EXTRA_DEVICE_NAME = "device_name";
|
public static final String EXTRA_DEVICE_NAME = "device_name";
|
||||||
|
public static final String EXTRA_SENDER_NAME = "sender_name";
|
||||||
public static final String EXTRA_TARGETS = "cast_targets";
|
public static final String EXTRA_TARGETS = "cast_targets";
|
||||||
public static final String EXTRA_CALIBRATION = "calibration_mode";
|
public static final String EXTRA_CALIBRATION = "calibration_mode";
|
||||||
public static final String ACTION_STOP = "com.foxx.androidcast.STOP_SEND";
|
public static final String ACTION_STOP = "com.foxx.androidcast.STOP_SEND";
|
||||||
@@ -181,22 +184,28 @@ public class ScreenCastService extends Service implements
|
|||||||
String host = intent.getStringExtra(EXTRA_HOST);
|
String host = intent.getStringExtra(EXTRA_HOST);
|
||||||
int port = intent.getIntExtra(EXTRA_PORT, com.foxx.androidcast.CastConfig.CAST_PORT);
|
int port = intent.getIntExtra(EXTRA_PORT, com.foxx.androidcast.CastConfig.CAST_PORT);
|
||||||
String pin = intent.getStringExtra(EXTRA_PIN);
|
String pin = intent.getStringExtra(EXTRA_PIN);
|
||||||
|
if (pin == null) {
|
||||||
|
pin = "";
|
||||||
|
}
|
||||||
String deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
|
String deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME);
|
||||||
|
String senderName = intent.getStringExtra(EXTRA_SENDER_NAME);
|
||||||
CastSettings settings = IntentExtras.getCastSettings(intent);
|
CastSettings settings = IntentExtras.getCastSettings(intent);
|
||||||
|
|
||||||
boolean calibration = settings != null
|
boolean calibration = settings != null
|
||||||
&& settings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST;
|
&& settings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST;
|
||||||
boolean camera = settings != null
|
boolean camera = settings != null && settings.isCameraCapture();
|
||||||
&& settings.getCaptureMode() == CastSettings.CaptureMode.CAMERA;
|
|
||||||
if (!calibration && !camera && data == null) {
|
if (!calibration && !camera && data == null) {
|
||||||
failAndStop(getString(R.string.error_projection_data));
|
failAndStop(getString(R.string.error_projection_data));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (host == null || pin == null || settings == null) {
|
if (host == null || settings == null) {
|
||||||
failAndStop(getString(R.string.error_missing_params));
|
failAndStop(getString(R.string.error_missing_params));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cameraMode = settings.getCaptureMode() == CastSettings.CaptureMode.CAMERA;
|
if (senderName == null || senderName.isEmpty()) {
|
||||||
|
senderName = AppPreferences.getUsername(this);
|
||||||
|
}
|
||||||
|
cameraMode = settings.isCameraCapture();
|
||||||
calibrationMode = settings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST;
|
calibrationMode = settings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST;
|
||||||
if (cameraMode && !com.foxx.androidcast.PlatformCastSupport.isCameraCaptureModeSupported()) {
|
if (cameraMode && !com.foxx.androidcast.PlatformCastSupport.isCameraCaptureModeSupported()) {
|
||||||
failAndStop(getString(R.string.capture_camera_not_implemented));
|
failAndStop(getString(R.string.capture_camera_not_implemented));
|
||||||
@@ -222,8 +231,7 @@ public class ScreenCastService extends Service implements
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
updateStatus(getString(R.string.connecting_to, host));
|
updateStatus(getString(R.string.connecting_to, host));
|
||||||
multiCoordinator.connectAndHandshake(targets, settings,
|
multiCoordinator.connectAndHandshake(targets, settings, senderName, pin);
|
||||||
deviceName != null ? deviceName : "Sender", pin);
|
|
||||||
List<MultiCastCoordinator.ActivePeer> peers = multiCoordinator.getPeers();
|
List<MultiCastCoordinator.ActivePeer> peers = multiCoordinator.getPeers();
|
||||||
if (peers.isEmpty()) {
|
if (peers.isEmpty()) {
|
||||||
throw new IOException("No receivers connected");
|
throw new IOException("No receivers connected");
|
||||||
@@ -247,6 +255,7 @@ public class ScreenCastService extends Service implements
|
|||||||
}
|
}
|
||||||
casting.set(true);
|
casting.set(true);
|
||||||
CastActiveState.setSenderCasting(true);
|
CastActiveState.setSenderCasting(true);
|
||||||
|
CastTrayNotifier.refresh(this);
|
||||||
activeSettings = settings;
|
activeSettings = settings;
|
||||||
sessionStatsRecorder = new SessionStatsRecorder("sender", settings.getTransport());
|
sessionStatsRecorder = new SessionStatsRecorder("sender", settings.getTransport());
|
||||||
sessionStatsRecorder.mergeSettings(settings);
|
sessionStatsRecorder.mergeSettings(settings);
|
||||||
@@ -382,7 +391,8 @@ public class ScreenCastService extends Service implements
|
|||||||
}
|
}
|
||||||
cameraCapture = new com.foxx.androidcast.sender.camera.CameraCastCapture(this);
|
cameraCapture = new com.foxx.androidcast.sender.camera.CameraCastCapture(this);
|
||||||
try {
|
try {
|
||||||
cameraCapture.start(surface, captureWidth, captureHeight);
|
cameraCapture.start(surface, captureWidth, captureHeight, activeSettings != null
|
||||||
|
&& activeSettings.isFrontCamera());
|
||||||
} catch (android.hardware.camera2.CameraAccessException e) {
|
} catch (android.hardware.camera2.CameraAccessException e) {
|
||||||
throw new IOException("Camera open failed: " + e.getMessage(), e);
|
throw new IOException("Camera open failed: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@@ -790,6 +800,7 @@ public class ScreenCastService extends Service implements
|
|||||||
stopping.set(true);
|
stopping.set(true);
|
||||||
casting.set(false);
|
casting.set(false);
|
||||||
CastActiveState.setSenderCasting(false);
|
CastActiveState.setSenderCasting(false);
|
||||||
|
CastTrayNotifier.refresh(this);
|
||||||
networkFeedback = null;
|
networkFeedback = null;
|
||||||
calibrationMode = false;
|
calibrationMode = false;
|
||||||
cameraMode = false;
|
cameraMode = false;
|
||||||
|
|||||||
@@ -2,16 +2,12 @@ package com.foxx.androidcast.sender;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.drawable.Animatable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.media.projection.MediaProjectionConfig;
|
import android.media.projection.MediaProjectionConfig;
|
||||||
import android.media.projection.MediaProjectionManager;
|
import android.media.projection.MediaProjectionManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.provider.Settings;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
@@ -22,33 +18,37 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.OnBackPressedCallback;
|
import androidx.activity.OnBackPressedCallback;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.AppPreferences;
|
||||||
|
import com.foxx.androidcast.DrawerHostActivity;
|
||||||
import com.foxx.androidcast.CastActiveState;
|
import com.foxx.androidcast.CastActiveState;
|
||||||
|
import com.foxx.androidcast.CastActivityTracker;
|
||||||
import com.foxx.androidcast.CastConfig;
|
import com.foxx.androidcast.CastConfig;
|
||||||
import com.foxx.androidcast.sender.CastReceiverTarget;
|
|
||||||
import com.foxx.androidcast.CastSettings;
|
import com.foxx.androidcast.CastSettings;
|
||||||
import com.foxx.androidcast.PermissionHelper;
|
import com.foxx.androidcast.PermissionHelper;
|
||||||
import com.foxx.androidcast.PlatformCastSupport;
|
import com.foxx.androidcast.PlatformCastSupport;
|
||||||
import com.foxx.androidcast.R;
|
import com.foxx.androidcast.R;
|
||||||
import com.foxx.androidcast.SettingsUi;
|
|
||||||
import com.foxx.androidcast.discovery.DiscoveryManager;
|
import com.foxx.androidcast.discovery.DiscoveryManager;
|
||||||
|
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||||
|
|
||||||
public class SenderActivity extends AppCompatActivity {
|
public class SenderActivity extends DrawerHostActivity {
|
||||||
private static final int REQUEST_MEDIA_PROJECTION = 2001;
|
private static final int REQUEST_MEDIA_PROJECTION = 2001;
|
||||||
private static final long BACK_STOP_INTERVAL_MS = 2_000;
|
private static final int REQUEST_PREVIEW_PROJECTION = 2002;
|
||||||
private static final int DEV_TAP_UNLOCK = 7;
|
|
||||||
|
|
||||||
private final CastSettings castSettings = new CastSettings();
|
private final CastSettings castSettings = new CastSettings();
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
private DiscoveryManager discovery;
|
private DiscoveryManager discovery;
|
||||||
private DeviceListAdapter deviceAdapter;
|
private DeviceListAdapter deviceAdapter;
|
||||||
|
private SenderCapturePreview capturePreview;
|
||||||
|
private SenderScreenPreview screenPreview;
|
||||||
|
|
||||||
private TextView statusText;
|
private TextView statusText;
|
||||||
private TextView titleText;
|
|
||||||
private ProgressBar discoveryProgress;
|
private ProgressBar discoveryProgress;
|
||||||
private Button refreshButton;
|
private EditText pinInput;
|
||||||
private View advancedPanel;
|
private SwitchMaterial muteSwitch;
|
||||||
|
private Button startButton;
|
||||||
|
private boolean pinLocked;
|
||||||
|
|
||||||
private enum DiscoveryPhase {
|
private enum DiscoveryPhase {
|
||||||
IDLE, SCANNING, PAUSED
|
IDLE, SCANNING, PAUSED
|
||||||
@@ -57,62 +57,55 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
private DiscoveryPhase discoveryPhase = DiscoveryPhase.IDLE;
|
private DiscoveryPhase discoveryPhase = DiscoveryPhase.IDLE;
|
||||||
private int discoverySecondsLeft;
|
private int discoverySecondsLeft;
|
||||||
private final Runnable autoScanRunnable = () -> beginScanPass(false);
|
private final Runnable autoScanRunnable = () -> beginScanPass(false);
|
||||||
private final Runnable discoveryCountdownTick = new Runnable() {
|
|
||||||
|
private String pendingPin;
|
||||||
|
private String pendingHost;
|
||||||
|
private int pendingPort;
|
||||||
|
private String pendingDeviceName;
|
||||||
|
private java.util.ArrayList<CastReceiverTarget> pendingTargets;
|
||||||
|
|
||||||
|
private boolean pendingCameraLaunch;
|
||||||
|
private boolean previewConsentDeclined;
|
||||||
|
private boolean previewConsentInFlight;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
protected int getContentLayoutId() {
|
||||||
if (discoveryPhase == DiscoveryPhase.SCANNING || discoveryPhase == DiscoveryPhase.PAUSED) {
|
return R.layout.activity_sender;
|
||||||
if (discoverySecondsLeft > 0) {
|
|
||||||
discoverySecondsLeft--;
|
|
||||||
}
|
}
|
||||||
updateRefreshButtonLabel();
|
|
||||||
handler.postDelayed(this, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private EditText pinInput;
|
|
||||||
private EditText manualHostInput;
|
|
||||||
private EditText manualPortInput;
|
|
||||||
|
|
||||||
private int titleTapCount;
|
|
||||||
private long lastTitleTapMs;
|
|
||||||
private boolean useManualConnect;
|
|
||||||
private long lastBackPressMs;
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_sender);
|
|
||||||
PermissionHelper.request(this);
|
PermissionHelper.request(this);
|
||||||
SettingsUi.bindSender(this, castSettings);
|
|
||||||
|
|
||||||
titleText = findViewById(R.id.text_sender_title);
|
castSettings.copyFrom(AppPreferences.loadSenderDefaults(this));
|
||||||
pinInput = findViewById(R.id.edit_pin);
|
screenPreview = new SenderScreenPreview(this);
|
||||||
pinInput.setText(CastConfig.DEFAULT_PIN);
|
|
||||||
statusText = findViewById(R.id.text_status);
|
statusText = findViewById(R.id.text_status);
|
||||||
|
pinInput = findViewById(R.id.edit_pin);
|
||||||
|
muteSwitch = findViewById(R.id.switch_mute);
|
||||||
|
startButton = findViewById(R.id.btn_start_cast);
|
||||||
discoveryProgress = findViewById(R.id.progress_discovery);
|
discoveryProgress = findViewById(R.id.progress_discovery);
|
||||||
refreshButton = findViewById(R.id.btn_refresh);
|
|
||||||
advancedPanel = findViewById(R.id.panel_advanced);
|
|
||||||
manualHostInput = findViewById(R.id.edit_manual_host);
|
|
||||||
manualPortInput = findViewById(R.id.edit_manual_port);
|
|
||||||
|
|
||||||
titleText.setOnClickListener(v -> {
|
pinInput.setText(AppPreferences.getPin(this));
|
||||||
long now = System.currentTimeMillis();
|
muteSwitch.setChecked(!castSettings.isAudioRequested());
|
||||||
if (now - lastTitleTapMs > 2000) {
|
muteSwitch.setOnCheckedChangeListener((btn, muted) ->
|
||||||
titleTapCount = 0;
|
castSettings.setAudioEnabled(!muted));
|
||||||
|
|
||||||
|
capturePreview = new SenderCapturePreview(
|
||||||
|
findViewById(R.id.texture_preview),
|
||||||
|
findViewById(R.id.text_preview_label),
|
||||||
|
screenPreview);
|
||||||
|
View.OnClickListener requestPreviewConsent = v -> {
|
||||||
|
if (screenPreview.needsProjection(castSettings.getCaptureMode())) {
|
||||||
|
previewConsentDeclined = false;
|
||||||
|
maybeRequestPreviewConsent();
|
||||||
}
|
}
|
||||||
lastTitleTapMs = now;
|
};
|
||||||
titleTapCount++;
|
findViewById(R.id.text_preview_label).setOnClickListener(requestPreviewConsent);
|
||||||
if (titleTapCount >= DEV_TAP_UNLOCK) {
|
findViewById(R.id.texture_preview).setOnClickListener(requestPreviewConsent);
|
||||||
titleTapCount = 0;
|
refreshPreview();
|
||||||
advancedPanel.setVisibility(
|
maybeRequestPreviewConsent();
|
||||||
advancedPanel.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
|
|
||||||
Toast.makeText(this,
|
|
||||||
advancedPanel.getVisibility() == View.VISIBLE
|
|
||||||
? R.string.advanced_unlocked : R.string.advanced_hidden,
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ListView listView = findViewById(R.id.list_devices);
|
ListView listView = findViewById(R.id.list_devices);
|
||||||
listView.setChoiceMode(CastConfig.MULTI_RECEIVER_ENABLED
|
listView.setChoiceMode(CastConfig.MULTI_RECEIVER_ENABLED
|
||||||
@@ -121,7 +114,6 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
listView.setAdapter(deviceAdapter);
|
listView.setAdapter(deviceAdapter);
|
||||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
deviceAdapter.setSelectedPosition(position);
|
deviceAdapter.setSelectedPosition(position);
|
||||||
useManualConnect = false;
|
|
||||||
java.util.List<DiscoveryManager.DiscoveredDevice> selectedList =
|
java.util.List<DiscoveryManager.DiscoveredDevice> selectedList =
|
||||||
deviceAdapter.getSelectedDevices();
|
deviceAdapter.getSelectedDevices();
|
||||||
if (selectedList.isEmpty()) {
|
if (selectedList.isEmpty()) {
|
||||||
@@ -158,18 +150,14 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
public void onScanStarted() {
|
public void onScanStarted() {
|
||||||
discoveryPhase = DiscoveryPhase.SCANNING;
|
discoveryPhase = DiscoveryPhase.SCANNING;
|
||||||
discoverySecondsLeft = (int) (CastConfig.DISCOVERY_SCAN_PASS_MS / 1000);
|
discoverySecondsLeft = (int) (CastConfig.DISCOVERY_SCAN_PASS_MS / 1000);
|
||||||
updateRefreshButtonLabel();
|
discoveryProgress.setVisibility(View.VISIBLE);
|
||||||
handler.removeCallbacks(discoveryCountdownTick);
|
|
||||||
handler.post(discoveryCountdownTick);
|
|
||||||
showDiscoveryProgress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScanEnded() {
|
public void onScanEnded() {
|
||||||
hideDiscoveryProgress();
|
|
||||||
discoveryPhase = DiscoveryPhase.PAUSED;
|
discoveryPhase = DiscoveryPhase.PAUSED;
|
||||||
discoverySecondsLeft = (int) (CastConfig.DISCOVERY_PAUSE_MS / 1000);
|
discoverySecondsLeft = (int) (CastConfig.DISCOVERY_PAUSE_MS / 1000);
|
||||||
updateRefreshButtonLabel();
|
discoveryProgress.setVisibility(View.GONE);
|
||||||
handler.removeCallbacks(autoScanRunnable);
|
handler.removeCallbacks(autoScanRunnable);
|
||||||
handler.postDelayed(autoScanRunnable, CastConfig.DISCOVERY_PAUSE_MS);
|
handler.postDelayed(autoScanRunnable, CastConfig.DISCOVERY_PAUSE_MS);
|
||||||
}
|
}
|
||||||
@@ -179,35 +167,73 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
if (CastActiveState.isSenderCasting()) {
|
if (CastActiveState.isSenderCasting()) {
|
||||||
long now = System.currentTimeMillis();
|
Toast.makeText(SenderActivity.this, R.string.cast_in_progress_pin_locked,
|
||||||
if (now - lastBackPressMs < BACK_STOP_INTERVAL_MS) {
|
Toast.LENGTH_SHORT).show();
|
||||||
stopCastingAndExit();
|
|
||||||
} else {
|
|
||||||
lastBackPressMs = now;
|
|
||||||
Toast.makeText(SenderActivity.this,
|
|
||||||
R.string.press_back_again_to_stop_cast, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
refreshButton.setOnClickListener(v -> onRefreshClicked());
|
startButton.setOnClickListener(v ->
|
||||||
findViewById(R.id.btn_start_cast).setOnClickListener(v ->
|
|
||||||
PlatformCastSupport.runWithAudioPolicy(this, castSettings, this::startCastFlow));
|
PlatformCastSupport.runWithAudioPolicy(this, castSettings, this::startCastFlow));
|
||||||
findViewById(R.id.btn_manual_cast).setOnClickListener(v -> {
|
|
||||||
useManualConnect = true;
|
|
||||||
PlatformCastSupport.runWithAudioPolicy(this, castSettings, this::startCastFlow);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
castSettings.copyFrom(AppPreferences.loadSenderDefaults(this));
|
||||||
|
muteSwitch.setChecked(!castSettings.isAudioRequested());
|
||||||
|
pinLocked = CastActiveState.isSenderCasting();
|
||||||
|
pinInput.setEnabled(!pinLocked);
|
||||||
|
startButton.setEnabled(!pinLocked);
|
||||||
|
muteSwitch.setEnabled(!pinLocked);
|
||||||
|
if (pinLocked) {
|
||||||
|
Toast.makeText(this, R.string.cast_in_progress_pin_locked, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
refreshPreview();
|
||||||
PermissionHelper.remindIfMissing(this, castSettings.isAudioEnabled());
|
PermissionHelper.remindIfMissing(this, castSettings.isAudioEnabled());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void refreshPreview() {
|
||||||
|
if (capturePreview == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
capturePreview.updateForMode(castSettings.getCaptureMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeRequestPreviewConsent() {
|
||||||
|
if (previewConsentDeclined || previewConsentInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CastSettings.CaptureMode mode = castSettings.getCaptureMode();
|
||||||
|
if (!screenPreview.needsProjection(mode) || screenPreview.hasConsent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previewConsentInFlight = true;
|
||||||
|
MediaProjectionManager mgr = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||||
|
startActivityForResult(createScreenCaptureIntent(mgr), REQUEST_PREVIEW_PROJECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
if (capturePreview != null) {
|
||||||
|
capturePreview.pause();
|
||||||
|
}
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
if (capturePreview != null) {
|
||||||
|
capturePreview.stop();
|
||||||
|
}
|
||||||
|
if (screenPreview != null) {
|
||||||
|
screenPreview.releaseConsent();
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
@@ -216,25 +242,14 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
handler.removeCallbacks(discoveryCountdownTick);
|
|
||||||
handler.removeCallbacks(autoScanRunnable);
|
handler.removeCallbacks(autoScanRunnable);
|
||||||
if (discovery != null) {
|
if (discovery != null) {
|
||||||
discovery.stop();
|
discovery.stop();
|
||||||
}
|
}
|
||||||
discoveryPhase = DiscoveryPhase.IDLE;
|
discoveryPhase = DiscoveryPhase.IDLE;
|
||||||
updateRefreshButtonLabel();
|
|
||||||
hideDiscoveryProgress();
|
|
||||||
super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRefreshClicked() {
|
|
||||||
if (discoveryPhase == DiscoveryPhase.SCANNING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handler.removeCallbacks(autoScanRunnable);
|
|
||||||
beginScanPass(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void beginScanPass(boolean userInitiated) {
|
private void beginScanPass(boolean userInitiated) {
|
||||||
if (discovery != null && discovery.isBrowsing()) {
|
if (discovery != null && discovery.isBrowsing()) {
|
||||||
return;
|
return;
|
||||||
@@ -254,42 +269,6 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
discovery.startBrowsing();
|
discovery.startBrowsing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateRefreshButtonLabel() {
|
|
||||||
if (refreshButton == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (discoveryPhase == DiscoveryPhase.SCANNING || discoveryPhase == DiscoveryPhase.PAUSED) {
|
|
||||||
refreshButton.setText(getString(R.string.refresh_countdown, Math.max(0, discoverySecondsLeft)));
|
|
||||||
} else {
|
|
||||||
refreshButton.setText(R.string.refresh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showDiscoveryProgress() {
|
|
||||||
discoveryProgress.setVisibility(View.VISIBLE);
|
|
||||||
discoveryProgress.bringToFront();
|
|
||||||
Drawable indeterminate = discoveryProgress.getIndeterminateDrawable();
|
|
||||||
if (indeterminate instanceof Animatable) {
|
|
||||||
((Animatable) indeterminate).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideDiscoveryProgress() {
|
|
||||||
discoveryProgress.setVisibility(View.GONE);
|
|
||||||
Drawable indeterminate = discoveryProgress.getIndeterminateDrawable();
|
|
||||||
if (indeterminate instanceof Animatable) {
|
|
||||||
((Animatable) indeterminate).stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopCastingAndExit() {
|
|
||||||
Intent stop = new Intent(this, ScreenCastService.class);
|
|
||||||
stop.setAction(ScreenCastService.ACTION_STOP);
|
|
||||||
startService(stop);
|
|
||||||
CastActiveState.setSenderCasting(false);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startCastFlow() {
|
private void startCastFlow() {
|
||||||
PermissionHelper.remindIfMissing(this, castSettings.isAudioEnabled());
|
PermissionHelper.remindIfMissing(this, castSettings.isAudioEnabled());
|
||||||
if (!PermissionHelper.hasAll(this)) {
|
if (!PermissionHelper.hasAll(this)) {
|
||||||
@@ -299,39 +278,31 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
Toast.makeText(this, R.string.codec_passthrough_unavailable, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.codec_passthrough_unavailable, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String pin = pinInput.getText().toString().trim();
|
String pin = pinInput.getText().toString().trim();
|
||||||
if (TextUtils.isEmpty(pin)) {
|
if (!AppPreferences.getPin(this).equals(pin)) {
|
||||||
Toast.makeText(this, R.string.enter_pin, Toast.LENGTH_SHORT).show();
|
AppPreferences.setPin(this, pin);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useManualConnect) {
|
|
||||||
String host = manualHostInput.getText().toString().trim();
|
|
||||||
String portStr = manualPortInput.getText().toString().trim();
|
|
||||||
if (TextUtils.isEmpty(host) || TextUtils.isEmpty(portStr)) {
|
|
||||||
Toast.makeText(this, R.string.manual_connect_missing, Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingHost = host;
|
|
||||||
try {
|
|
||||||
pendingPort = Integer.parseInt(portStr);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
Toast.makeText(this, R.string.manual_connect_missing, Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingDeviceName = host;
|
|
||||||
} else {
|
|
||||||
java.util.List<DiscoveryManager.DiscoveredDevice> selectedList =
|
java.util.List<DiscoveryManager.DiscoveredDevice> selectedList =
|
||||||
deviceAdapter.getSelectedDevices();
|
deviceAdapter.getSelectedDevices();
|
||||||
if (selectedList.isEmpty()) {
|
if (selectedList.isEmpty()) {
|
||||||
Toast.makeText(this, R.string.select_receiver, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.select_receiver, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
for (DiscoveryManager.DiscoveredDevice d : selectedList) {
|
||||||
|
if (!d.pinMatches(pin)) {
|
||||||
|
showPinMismatchDialog(selectedList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (CastConfig.MULTI_RECEIVER_ENABLED && selectedList.size() > CastConfig.MAX_RECEIVERS) {
|
if (CastConfig.MULTI_RECEIVER_ENABLED && selectedList.size() > CastConfig.MAX_RECEIVERS) {
|
||||||
Toast.makeText(this, getString(R.string.too_many_receivers, CastConfig.MAX_RECEIVERS),
|
Toast.makeText(this, getString(R.string.too_many_receivers, CastConfig.MAX_RECEIVERS),
|
||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingTargets = new java.util.ArrayList<>();
|
pendingTargets = new java.util.ArrayList<>();
|
||||||
for (DiscoveryManager.DiscoveredDevice d : selectedList) {
|
for (DiscoveryManager.DiscoveredDevice d : selectedList) {
|
||||||
if (!CastConfig.MULTI_RECEIVER_ENABLED
|
if (!CastConfig.MULTI_RECEIVER_ENABLED
|
||||||
@@ -347,14 +318,52 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
pendingPort = first.port;
|
pendingPort = first.port;
|
||||||
pendingDeviceName = selectedList.size() > 1
|
pendingDeviceName = selectedList.size() > 1
|
||||||
? first.name + " +" + (selectedList.size() - 1) : first.name;
|
? first.name + " +" + (selectedList.size() - 1) : first.name;
|
||||||
|
pendingPin = pin;
|
||||||
|
|
||||||
|
proceedCaptureConsent();
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingPin = pin;
|
private void showPinMismatchDialog(java.util.List<DiscoveryManager.DiscoveredDevice> selectedList) {
|
||||||
|
final EditText input = new EditText(this);
|
||||||
|
input.setHint(R.string.pin_session_hint);
|
||||||
|
input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER
|
||||||
|
| android.text.InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.pin_mismatch_title)
|
||||||
|
.setMessage(R.string.pin_mismatch_message)
|
||||||
|
.setView(input)
|
||||||
|
.setPositiveButton(android.R.string.ok, (d, w) -> {
|
||||||
|
String sessionPin = input.getText().toString().trim();
|
||||||
|
pinInput.setText(sessionPin);
|
||||||
|
for (DiscoveryManager.DiscoveredDevice dev : selectedList) {
|
||||||
|
if (!dev.pinMatches(sessionPin)) {
|
||||||
|
Toast.makeText(this, R.string.pin_mismatch_title, Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingPin = sessionPin;
|
||||||
|
pendingTargets = new java.util.ArrayList<>();
|
||||||
|
for (DiscoveryManager.DiscoveredDevice dev : selectedList) {
|
||||||
|
pendingTargets.add(new CastReceiverTarget(dev.name, dev.host, dev.port,
|
||||||
|
dev.transport, dev.videoCodecs, dev.audioCodecs, dev.placeholderId));
|
||||||
|
}
|
||||||
|
DiscoveryManager.DiscoveredDevice first = selectedList.get(0);
|
||||||
|
pendingHost = first.host;
|
||||||
|
pendingPort = first.port;
|
||||||
|
pendingDeviceName = selectedList.size() > 1
|
||||||
|
? first.name + " +" + (selectedList.size() - 1) : first.name;
|
||||||
|
proceedCaptureConsent();
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void proceedCaptureConsent() {
|
||||||
if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST) {
|
if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST) {
|
||||||
launchCastService(null, Activity.RESULT_OK);
|
launchCastService(null, Activity.RESULT_OK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CAMERA) {
|
if (castSettings.isCameraCapture()) {
|
||||||
if (androidx.core.content.ContextCompat.checkSelfPermission(this,
|
if (androidx.core.content.ContextCompat.checkSelfPermission(this,
|
||||||
android.Manifest.permission.CAMERA) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
android.Manifest.permission.CAMERA) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||||
androidx.core.app.ActivityCompat.requestPermissions(this,
|
androidx.core.app.ActivityCompat.requestPermissions(this,
|
||||||
@@ -365,6 +374,11 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
launchCastService(null, Activity.RESULT_OK);
|
launchCastService(null, Activity.RESULT_OK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (screenPreview.hasConsent()) {
|
||||||
|
screenPreview.stop();
|
||||||
|
launchCastService(screenPreview.getProjectionData(), screenPreview.getProjectionResultCode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
MediaProjectionManager mgr = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
MediaProjectionManager mgr = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||||
startActivityForResult(createScreenCaptureIntent(mgr), REQUEST_MEDIA_PROJECTION);
|
startActivityForResult(createScreenCaptureIntent(mgr), REQUEST_MEDIA_PROJECTION);
|
||||||
}
|
}
|
||||||
@@ -378,14 +392,6 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
return mgr.createScreenCaptureIntent();
|
return mgr.createScreenCaptureIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pendingCameraLaunch;
|
|
||||||
|
|
||||||
private String pendingPin;
|
|
||||||
private String pendingHost;
|
|
||||||
private int pendingPort;
|
|
||||||
private String pendingDeviceName;
|
|
||||||
private java.util.ArrayList<CastReceiverTarget> pendingTargets;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
@@ -402,6 +408,17 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == REQUEST_PREVIEW_PROJECTION) {
|
||||||
|
previewConsentInFlight = false;
|
||||||
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||||
|
screenPreview.setProjectionResult(resultCode, data);
|
||||||
|
previewConsentDeclined = false;
|
||||||
|
} else {
|
||||||
|
previewConsentDeclined = true;
|
||||||
|
}
|
||||||
|
refreshPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (requestCode != REQUEST_MEDIA_PROJECTION) {
|
if (requestCode != REQUEST_MEDIA_PROJECTION) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -409,14 +426,19 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
Toast.makeText(this, R.string.capture_denied, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.capture_denied, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
screenPreview.setProjectionResult(resultCode, data);
|
||||||
launchCastService(data, resultCode);
|
launchCastService(data, resultCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void launchCastService(Intent projectionData, int resultCode) {
|
private void launchCastService(Intent projectionData, int resultCode) {
|
||||||
String deviceName = Settings.Global.getString(getContentResolver(), Settings.Global.DEVICE_NAME);
|
if (screenPreview != null) {
|
||||||
if (deviceName == null) {
|
screenPreview.stop();
|
||||||
deviceName = Build.MODEL;
|
|
||||||
}
|
}
|
||||||
|
pinLocked = true;
|
||||||
|
pinInput.setEnabled(false);
|
||||||
|
startButton.setEnabled(false);
|
||||||
|
muteSwitch.setEnabled(false);
|
||||||
|
|
||||||
Intent serviceIntent = new Intent(this, ScreenCastService.class);
|
Intent serviceIntent = new Intent(this, ScreenCastService.class);
|
||||||
serviceIntent.putExtra(ScreenCastService.EXTRA_RESULT_CODE, resultCode);
|
serviceIntent.putExtra(ScreenCastService.EXTRA_RESULT_CODE, resultCode);
|
||||||
if (projectionData != null) {
|
if (projectionData != null) {
|
||||||
@@ -424,8 +446,9 @@ public class SenderActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
serviceIntent.putExtra(ScreenCastService.EXTRA_HOST, pendingHost);
|
serviceIntent.putExtra(ScreenCastService.EXTRA_HOST, pendingHost);
|
||||||
serviceIntent.putExtra(ScreenCastService.EXTRA_PORT, pendingPort);
|
serviceIntent.putExtra(ScreenCastService.EXTRA_PORT, pendingPort);
|
||||||
serviceIntent.putExtra(ScreenCastService.EXTRA_PIN, pendingPin);
|
serviceIntent.putExtra(ScreenCastService.EXTRA_PIN, pendingPin != null ? pendingPin : "");
|
||||||
serviceIntent.putExtra(ScreenCastService.EXTRA_DEVICE_NAME, pendingDeviceName != null ? pendingDeviceName : deviceName);
|
serviceIntent.putExtra(ScreenCastService.EXTRA_DEVICE_NAME, pendingDeviceName);
|
||||||
|
serviceIntent.putExtra(ScreenCastService.EXTRA_SENDER_NAME, AppPreferences.getUsername(this));
|
||||||
serviceIntent.putExtra(CastSettings.EXTRA, castSettings);
|
serviceIntent.putExtra(CastSettings.EXTRA, castSettings);
|
||||||
if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST) {
|
if (castSettings.getCaptureMode() == CastSettings.CaptureMode.CALIBRATION_TEST) {
|
||||||
serviceIntent.putExtra(ScreenCastService.EXTRA_CALIBRATION, true);
|
serviceIntent.putExtra(ScreenCastService.EXTRA_CALIBRATION, true);
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package com.foxx.androidcast.sender;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.hardware.camera2.CameraAccessException;
|
||||||
|
import android.hardware.camera2.CameraCharacteristics;
|
||||||
|
import android.hardware.camera2.CameraDevice;
|
||||||
|
import android.hardware.camera2.CameraManager;
|
||||||
|
import android.hardware.camera2.CaptureRequest;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.Size;
|
||||||
|
import android.view.TextureView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
import com.foxx.androidcast.R;
|
||||||
|
import com.foxx.androidcast.sender.calibration.TvCalibrationGenerator;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/** Live preview of the configured capture source on the sender screen (not the cast pipeline). */
|
||||||
|
public final class SenderCapturePreview {
|
||||||
|
private static final String TAG = "SenderCapturePreview";
|
||||||
|
|
||||||
|
private final TextureView textureView;
|
||||||
|
private final TextView labelView;
|
||||||
|
private final SenderScreenPreview screenPreview;
|
||||||
|
private HandlerThread thread;
|
||||||
|
private Handler handler;
|
||||||
|
private CameraDevice camera;
|
||||||
|
private android.hardware.camera2.CameraCaptureSession session;
|
||||||
|
private boolean useFront;
|
||||||
|
|
||||||
|
public SenderCapturePreview(TextureView texture, TextView label, SenderScreenPreview screen) {
|
||||||
|
this.textureView = texture;
|
||||||
|
this.labelView = label;
|
||||||
|
this.screenPreview = screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SenderScreenPreview getScreenPreview() {
|
||||||
|
return screenPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateForMode(CastSettings.CaptureMode mode) {
|
||||||
|
stopCamera();
|
||||||
|
if (mode == CastSettings.CaptureMode.CAMERA) {
|
||||||
|
useFront = false;
|
||||||
|
startCameraPreview();
|
||||||
|
} else if (mode == CastSettings.CaptureMode.CAMERA_FRONT) {
|
||||||
|
useFront = true;
|
||||||
|
startCameraPreview();
|
||||||
|
} else if (mode == CastSettings.CaptureMode.CALIBRATION_TEST) {
|
||||||
|
showCalibrationPreview();
|
||||||
|
} else if (screenPreview != null && screenPreview.needsProjection(mode)) {
|
||||||
|
if (screenPreview.hasProjection()) {
|
||||||
|
labelView.setVisibility(android.view.View.GONE);
|
||||||
|
textureView.setVisibility(android.view.View.VISIBLE);
|
||||||
|
screenPreview.attach(textureView, mode);
|
||||||
|
} else {
|
||||||
|
showLabel(R.string.preview_tap_for_screen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showLabel(R.string.preview_placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLabel(int textRes) {
|
||||||
|
if (screenPreview != null) {
|
||||||
|
screenPreview.stop();
|
||||||
|
}
|
||||||
|
textureView.setVisibility(android.view.View.GONE);
|
||||||
|
labelView.setVisibility(android.view.View.VISIBLE);
|
||||||
|
labelView.setText(labelView.getContext().getString(textRes));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCalibrationPreview() {
|
||||||
|
if (screenPreview != null) {
|
||||||
|
screenPreview.stop();
|
||||||
|
}
|
||||||
|
labelView.setVisibility(android.view.View.GONE);
|
||||||
|
textureView.setVisibility(android.view.View.VISIBLE);
|
||||||
|
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
|
||||||
|
drawCalibrationFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
|
||||||
|
drawCalibrationFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
|
||||||
|
});
|
||||||
|
if (textureView.isAvailable()) {
|
||||||
|
drawCalibrationFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCalibrationFrame() {
|
||||||
|
int w = Math.max(320, textureView.getWidth());
|
||||||
|
int h = Math.max(240, textureView.getHeight());
|
||||||
|
Canvas canvas = textureView.lockCanvas();
|
||||||
|
if (canvas == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TvCalibrationGenerator.drawChart(canvas, w, h, textureView.getContext());
|
||||||
|
} finally {
|
||||||
|
textureView.unlockCanvasAndPost(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCameraPreview() {
|
||||||
|
if (screenPreview != null) {
|
||||||
|
screenPreview.stop();
|
||||||
|
}
|
||||||
|
Context ctx = textureView.getContext();
|
||||||
|
if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.CAMERA)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
showLabel(R.string.capture_camera_not_implemented);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
labelView.setVisibility(android.view.View.GONE);
|
||||||
|
textureView.setVisibility(android.view.View.VISIBLE);
|
||||||
|
if (textureView.isAvailable()) {
|
||||||
|
openCamera(textureView.getSurfaceTexture());
|
||||||
|
} else {
|
||||||
|
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
|
||||||
|
openCamera(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
||||||
|
stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openCamera(SurfaceTexture surface) {
|
||||||
|
Context ctx = textureView.getContext().getApplicationContext();
|
||||||
|
thread = new HandlerThread("SenderPreviewCam");
|
||||||
|
thread.start();
|
||||||
|
handler = new Handler(thread.getLooper());
|
||||||
|
try {
|
||||||
|
CameraManager mgr = ctx.getSystemService(CameraManager.class);
|
||||||
|
String id = chooseCamera(mgr, useFront);
|
||||||
|
if (id == null) {
|
||||||
|
showLabel(R.string.capture_camera_not_implemented);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
surface.setDefaultBufferSize(640, 480);
|
||||||
|
mgr.openCamera(id, new CameraDevice.StateCallback() {
|
||||||
|
@Override
|
||||||
|
public void onOpened(CameraDevice device) {
|
||||||
|
camera = device;
|
||||||
|
try {
|
||||||
|
android.view.Surface previewSurface = new android.view.Surface(surface);
|
||||||
|
CaptureRequest.Builder b =
|
||||||
|
device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
|
||||||
|
b.addTarget(previewSurface);
|
||||||
|
device.createCaptureSession(Collections.singletonList(previewSurface),
|
||||||
|
new android.hardware.camera2.CameraCaptureSession.StateCallback() {
|
||||||
|
@Override
|
||||||
|
public void onConfigured(
|
||||||
|
android.hardware.camera2.CameraCaptureSession s) {
|
||||||
|
session = s;
|
||||||
|
try {
|
||||||
|
s.setRepeatingRequest(b.build(), null, handler);
|
||||||
|
} catch (CameraAccessException e) {
|
||||||
|
Log.w(TAG, "Preview request failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigureFailed(
|
||||||
|
android.hardware.camera2.CameraCaptureSession s) {
|
||||||
|
Log.w(TAG, "Preview session failed");
|
||||||
|
}
|
||||||
|
}, handler);
|
||||||
|
} catch (CameraAccessException e) {
|
||||||
|
Log.w(TAG, "Preview session", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected(CameraDevice device) {
|
||||||
|
device.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(CameraDevice device, int error) {
|
||||||
|
device.close();
|
||||||
|
}
|
||||||
|
}, handler);
|
||||||
|
} catch (CameraAccessException | SecurityException e) {
|
||||||
|
Log.w(TAG, "Open camera failed", e);
|
||||||
|
showLabel(R.string.capture_camera_not_implemented);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String chooseCamera(CameraManager mgr, boolean front) throws CameraAccessException {
|
||||||
|
int want = front ? CameraCharacteristics.LENS_FACING_FRONT
|
||||||
|
: CameraCharacteristics.LENS_FACING_BACK;
|
||||||
|
for (String id : mgr.getCameraIdList()) {
|
||||||
|
Integer facing = mgr.getCameraCharacteristics(id)
|
||||||
|
.get(CameraCharacteristics.LENS_FACING);
|
||||||
|
if (facing != null && facing == want) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String[] ids = mgr.getCameraIdList();
|
||||||
|
return ids.length > 0 ? ids[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
stopCamera();
|
||||||
|
if (screenPreview != null) {
|
||||||
|
screenPreview.pausePreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
stopCamera();
|
||||||
|
if (screenPreview != null) {
|
||||||
|
screenPreview.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCamera() {
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
session = null;
|
||||||
|
}
|
||||||
|
if (camera != null) {
|
||||||
|
try {
|
||||||
|
camera.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
camera = null;
|
||||||
|
}
|
||||||
|
if (thread != null) {
|
||||||
|
thread.quitSafely();
|
||||||
|
thread = null;
|
||||||
|
}
|
||||||
|
handler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.foxx.androidcast.sender;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.hardware.display.DisplayManager;
|
||||||
|
import android.hardware.display.VirtualDisplay;
|
||||||
|
import android.media.projection.MediaProjection;
|
||||||
|
import android.media.projection.MediaProjectionManager;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.TextureView;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import com.foxx.androidcast.CastSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live screen preview via MediaProjection into a {@link TextureView} (prep only, not the cast encoder).
|
||||||
|
*/
|
||||||
|
public final class SenderScreenPreview {
|
||||||
|
private static final String TAG = "SenderScreenPreview";
|
||||||
|
private static final int PREVIEW_DPI = 160;
|
||||||
|
|
||||||
|
private final Context appContext;
|
||||||
|
private MediaProjection projection;
|
||||||
|
private VirtualDisplay virtualDisplay;
|
||||||
|
private int resultCode;
|
||||||
|
private Intent resultData;
|
||||||
|
|
||||||
|
public SenderScreenPreview(Context context) {
|
||||||
|
appContext = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needsProjection(CastSettings.CaptureMode mode) {
|
||||||
|
return mode == CastSettings.CaptureMode.FULL_SCREEN
|
||||||
|
|| mode == CastSettings.CaptureMode.USER_CHOICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectionResult(int resultCode, Intent data) {
|
||||||
|
this.resultCode = resultCode;
|
||||||
|
this.resultData = data != null ? new Intent(data) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Intent getProjectionData() {
|
||||||
|
return resultData != null ? new Intent(resultData) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProjectionResultCode() {
|
||||||
|
return resultCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User granted capture consent (may not have an active {@link MediaProjection} after pause). */
|
||||||
|
public boolean hasConsent() {
|
||||||
|
return resultCode == Activity.RESULT_OK && resultData != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasProjection() {
|
||||||
|
return hasConsent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void attach(TextureView textureView, CastSettings.CaptureMode mode) {
|
||||||
|
stop();
|
||||||
|
if (!needsProjection(mode) || resultData == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Runnable start = () -> startOnTexture(textureView, mode);
|
||||||
|
if (textureView.isAvailable()) {
|
||||||
|
start.run();
|
||||||
|
} else {
|
||||||
|
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
|
||||||
|
startOnTexture(textureView, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
||||||
|
stop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startOnTexture(TextureView textureView, CastSettings.CaptureMode mode) {
|
||||||
|
try {
|
||||||
|
MediaProjectionManager mgr =
|
||||||
|
appContext.getSystemService(MediaProjectionManager.class);
|
||||||
|
projection = mgr.getMediaProjection(resultCode, resultData);
|
||||||
|
if (projection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DisplayMetrics metrics = new DisplayMetrics();
|
||||||
|
WindowManager wm = (WindowManager) appContext.getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
wm.getDefaultDisplay().getRealMetrics(metrics);
|
||||||
|
int w = Math.max(320, textureView.getWidth() > 0 ? textureView.getWidth() : metrics.widthPixels / 3);
|
||||||
|
int h = Math.max(240, textureView.getHeight() > 0 ? textureView.getHeight() : metrics.heightPixels / 3);
|
||||||
|
SurfaceTexture st = textureView.getSurfaceTexture();
|
||||||
|
if (st == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
st.setDefaultBufferSize(w, h);
|
||||||
|
Surface surface = new Surface(st);
|
||||||
|
virtualDisplay = projection.createVirtualDisplay(
|
||||||
|
"CastPreview",
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
PREVIEW_DPI,
|
||||||
|
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
|
surface,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
Log.i(TAG, "Screen preview " + w + "x" + h);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Screen preview failed: " + e.getMessage());
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tear down preview display only; keep consent for resume or cast START. */
|
||||||
|
public void pausePreview() {
|
||||||
|
if (virtualDisplay != null) {
|
||||||
|
virtualDisplay.release();
|
||||||
|
virtualDisplay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
pausePreview();
|
||||||
|
if (projection != null) {
|
||||||
|
projection.stop();
|
||||||
|
projection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Release consent and active capture (leaving sender screen). */
|
||||||
|
public void releaseConsent() {
|
||||||
|
stop();
|
||||||
|
resultCode = 0;
|
||||||
|
resultData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,16 +32,17 @@ public final class CameraCastCapture {
|
|||||||
appContext = context.getApplicationContext();
|
appContext = context.getApplicationContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start(Surface encoderSurface, int targetWidth, int targetHeight) throws CameraAccessException {
|
public void start(Surface encoderSurface, int targetWidth, int targetHeight, boolean useFrontCamera)
|
||||||
|
throws CameraAccessException {
|
||||||
stop();
|
stop();
|
||||||
thread = new HandlerThread("CameraCast");
|
thread = new HandlerThread("CameraCast");
|
||||||
thread.start();
|
thread.start();
|
||||||
handler = new Handler(thread.getLooper());
|
handler = new Handler(thread.getLooper());
|
||||||
CameraManager mgr = appContext.getSystemService(CameraManager.class);
|
CameraManager mgr = appContext.getSystemService(CameraManager.class);
|
||||||
cameraId = chooseCameraId(mgr);
|
cameraId = chooseCameraId(mgr, useFrontCamera);
|
||||||
if (cameraId == null) {
|
if (cameraId == null) {
|
||||||
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR,
|
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR,
|
||||||
"No back camera available");
|
useFrontCamera ? "No front camera available" : "No back camera available");
|
||||||
}
|
}
|
||||||
Size preview = choosePreviewSize(mgr, cameraId, targetWidth, targetHeight);
|
Size preview = choosePreviewSize(mgr, cameraId, targetWidth, targetHeight);
|
||||||
Log.i(TAG, "Opening camera " + cameraId + " preview " + preview.getWidth() + "x" + preview.getHeight());
|
Log.i(TAG, "Opening camera " + cameraId + " preview " + preview.getWidth() + "x" + preview.getHeight());
|
||||||
@@ -97,11 +98,14 @@ public final class CameraCastCapture {
|
|||||||
}, handler);
|
}, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String chooseCameraId(CameraManager mgr) throws CameraAccessException {
|
private static String chooseCameraId(CameraManager mgr, boolean useFrontCamera) throws CameraAccessException {
|
||||||
|
int want = useFrontCamera
|
||||||
|
? CameraCharacteristics.LENS_FACING_FRONT
|
||||||
|
: CameraCharacteristics.LENS_FACING_BACK;
|
||||||
for (String id : mgr.getCameraIdList()) {
|
for (String id : mgr.getCameraIdList()) {
|
||||||
CameraCharacteristics chars = mgr.getCameraCharacteristics(id);
|
CameraCharacteristics chars = mgr.getCameraCharacteristics(id);
|
||||||
Integer facing = chars.get(CameraCharacteristics.LENS_FACING);
|
Integer facing = chars.get(CameraCharacteristics.LENS_FACING);
|
||||||
if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
|
if (facing != null && facing == want) {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
app/src/main/res/drawable/bg_settings_section_header.xml
Normal file
24
app/src/main/res/drawable/bg_settings_section_header.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
<gradient
|
||||||
|
android:angle="0"
|
||||||
|
android:endColor="@color/settings_header_gradient_end"
|
||||||
|
android:startColor="@color/settings_header_gradient_start"
|
||||||
|
android:type="linear" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:gravity="bottom">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="10dp"
|
||||||
|
android:bottomRightRadius="10dp"
|
||||||
|
android:topLeftRadius="0dp"
|
||||||
|
android:topRightRadius="0dp" />
|
||||||
|
<size android:height="4dp" />
|
||||||
|
<solid android:color="@color/settings_header_shadow" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
100
app/src/main/res/layout-land/activity_sender.xml
Normal file
100
app/src/main/res/layout-land/activity_sender.xml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?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="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_status"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/searching" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/list_devices"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="0.32"
|
||||||
|
android:choiceMode="singleChoice" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/panel_preview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="0.68"
|
||||||
|
android:background="#101010">
|
||||||
|
|
||||||
|
<TextureView
|
||||||
|
android:id="@+id/texture_preview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_preview_label"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="@string/preview_placeholder"
|
||||||
|
android:textColor="#CCCCCC" />
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_pin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/pin_hint"
|
||||||
|
android:inputType="numberPassword"
|
||||||
|
android:maxLength="32" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_mute"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="false"
|
||||||
|
android:text="@string/label_mute" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_start_cast"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:minWidth="120dp"
|
||||||
|
android:text="@string/start_cast" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/panel_advanced"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_discovery"
|
||||||
|
style="?android:attr/progressBarStyleSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
239
app/src/main/res/layout/activity_cast_settings.xml
Normal file
239
app/src/main/res/layout/activity_cast_settings.xml
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
<?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="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/cast_settings_title"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/settings_section_global"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_username" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/pin_hint" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_settings_pin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/pin_anonymous_hint"
|
||||||
|
android:inputType="numberPassword"
|
||||||
|
android:maxLength="32" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/check_tray_icon"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/label_show_tray_icon" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="@string/settings_section_sender"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_capture_mode" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_capture_mode"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_resolution" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_resolution"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_network_adoption" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_network"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_bitrate_mode" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_bitrate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_video_codec" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/codec_hint_negotiated"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_video_codec"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_transport" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_transport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_audio" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_audio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sender_audio_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_stream_protection" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_stream_protection_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/stream_protection_hint"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_stream_protection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:text="@string/settings_section_receiver"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_receiver_display" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_resolution"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_transport" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_transport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_network_adoption" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_network"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_video_codec" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_video_codec"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_receiver_audio" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_play_audio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
21
app/src/main/res/layout/activity_drawer_host.xml
Normal file
21
app/src/main/res/layout/activity_drawer_host.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/drawer_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/content_root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/settings_drawer_container"
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:background="?android:attr/colorBackground"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:fitsSystemWindows="true" />
|
||||||
|
</androidx.drawerlayout.widget.DrawerLayout>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
android:id="@+id/btn_receive"
|
android:id="@+id/btn_receive"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
android:text="@string/receive_screen" />
|
android:text="@string/receive_screen" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -33,4 +34,12 @@
|
|||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="24dp"
|
||||||
android:text="@string/main_hint"
|
android:text="@string/main_hint"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/swipe_for_settings"
|
||||||
|
android:textSize="13sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,49 +1,39 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="16dp">
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
<LinearLayout
|
android:padding="24dp">
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
android:text="@string/receiver_title"
|
android:text="@string/receiver_title"
|
||||||
android:textSize="18sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<include layout="@layout/activity_settings_panel" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_pin"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/pin_hint"
|
|
||||||
android:inputType="numberPassword" />
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
android:id="@+id/check_allow_audio"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="@string/label_receiver_audio" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_start_receiver"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="@string/start_receiver" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_status"
|
android:id="@+id/text_status"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center"
|
||||||
android:text="@string/waiting_sender" />
|
android:text="@string/waiting_sender" />
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
<Button
|
||||||
|
android:id="@+id/btn_start_receiver"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="@string/start_receiver" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/receiver_use_settings_hint"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,126 +1,100 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="16dp">
|
android:padding="12dp">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_sender_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/sender_title"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_status"
|
android:id="@+id/text_status"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
android:text="@string/searching" />
|
android:text="@string/searching" />
|
||||||
|
|
||||||
<include layout="@layout/activity_settings_panel" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/panel_advanced"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:orientation="vertical"
|
android:layout_weight="1"
|
||||||
android:visibility="gone">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/list_devices"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="0.4"
|
||||||
|
android:choiceMode="singleChoice" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/panel_preview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_weight="0.6"
|
||||||
|
android:background="#101010">
|
||||||
|
|
||||||
|
<TextureView
|
||||||
|
android:id="@+id/texture_preview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/text_preview_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:text="@string/advanced_connect_title"
|
android:gravity="center"
|
||||||
android:textStyle="bold" />
|
android:padding="16dp"
|
||||||
|
android:text="@string/preview_placeholder"
|
||||||
<EditText
|
android:textColor="#CCCCCC" />
|
||||||
android:id="@+id/edit_manual_host"
|
</FrameLayout>
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/manual_host_hint"
|
|
||||||
android:inputType="text" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_manual_port"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/manual_port_hint"
|
|
||||||
android:inputType="number"
|
|
||||||
android:text="41235" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_gateway_url"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:enabled="false"
|
|
||||||
android:hint="@string/gateway_url_hint"
|
|
||||||
android:inputType="textUri" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_manual_cast"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/manual_cast" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/edit_pin"
|
android:id="@+id/edit_pin"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="8dp"
|
||||||
android:hint="@string/pin_hint"
|
android:hint="@string/pin_hint"
|
||||||
android:inputType="numberPassword"
|
android:inputType="numberPassword"
|
||||||
android:maxLength="16" />
|
android:maxLength="32" />
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/list_devices"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="200dp"
|
|
||||||
android:layout_marginTop="8dp" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<FrameLayout
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
android:layout_width="0dp"
|
android:id="@+id/switch_mute"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:checked="false"
|
||||||
android:layout_weight="1">
|
android:text="@string/label_mute" />
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_refresh"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/refresh" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progress_discovery"
|
|
||||||
style="?android:attr/progressBarStyleHorizontal"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:alpha="0.85"
|
|
||||||
android:clickable="false"
|
|
||||||
android:focusable="false"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_start_cast"
|
android:id="@+id/btn_start_cast"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_marginStart="16dp"
|
||||||
|
android:minWidth="120dp"
|
||||||
android:text="@string/start_cast" />
|
android:text="@string/start_cast" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
<FrameLayout
|
||||||
|
android:id="@+id/panel_advanced"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_discovery"
|
||||||
|
style="?android:attr/progressBarStyleSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
248
app/src/main/res/layout/fragment_cast_settings.xml
Normal file
248
app/src/main/res/layout/fragment_cast_settings.xml
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<?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="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:text="@string/cast_settings_title"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/include_settings_section_header"
|
||||||
|
android:id="@+id/header_global"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_username" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:maxLines="1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/pin_hint" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_settings_pin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/pin_anonymous_hint"
|
||||||
|
android:inputType="numberPassword"
|
||||||
|
android:maxLength="32" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_theme" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_theme"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/check_tray_icon"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/label_show_tray_icon" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/include_settings_section_header"
|
||||||
|
android:id="@+id/header_sender"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_capture_mode" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_capture_mode"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_resolution" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_resolution"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_network_adoption" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_network"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_bitrate_mode" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_bitrate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_video_codec" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/codec_hint_negotiated"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_video_codec"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_transport" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_transport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_audio" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_audio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_sender_audio_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_stream_protection" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_stream_protection_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/stream_protection_hint"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_sender_stream_protection"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/include_settings_section_header"
|
||||||
|
android:id="@+id/header_receiver"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_receiver_display" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_resolution"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_transport" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_transport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_network_adoption" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_network"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_video_codec" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_video_codec"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/label_receiver_audio" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinner_receiver_play_audio"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
15
app/src/main/res/layout/include_settings_section_header.xml
Normal file
15
app/src/main/res/layout/include_settings_section_header.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@drawable/bg_settings_section_header"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:paddingStart="14dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingEnd="14dp"
|
||||||
|
android:paddingBottom="14dp"
|
||||||
|
android:textColor="@color/settings_header_text"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="device_item_selected">#661565C0</color>
|
<!-- Section headers: blue (left) → black (right) -->
|
||||||
|
<color name="settings_header_gradient_start">#FF1565C0</color>
|
||||||
|
<color name="settings_header_gradient_end">#FF000000</color>
|
||||||
|
<color name="settings_header_text">#FFFFFFFF</color>
|
||||||
|
<color name="settings_header_shadow">#99000000</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="ic_launcher_background">#0D47A1</color>
|
<color name="ic_launcher_background">#1565C0</color>
|
||||||
<color name="device_item_selected">#4D1565C0</color>
|
<color name="device_item_selected">#331565C0</color>
|
||||||
|
<!-- Section headers: blue (left) → dark (right) -->
|
||||||
|
<color name="settings_header_gradient_start">#FF1565C0</color>
|
||||||
|
<color name="settings_header_gradient_end">#FF212121</color>
|
||||||
|
<color name="settings_header_text">#FFFFFFFF</color>
|
||||||
|
<color name="settings_header_shadow">#66000000</color>
|
||||||
|
<color name="inverted_window_bg">#FFF5F5F5</color>
|
||||||
|
<color name="inverted_primary">#FF212121</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
5
app/src/main/res/values/ids.xml
Normal file
5
app/src/main/res/values/ids.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item type="id" name="tag_spinner_values" />
|
||||||
|
<item type="id" name="tag_spinner_enum" />
|
||||||
|
</resources>
|
||||||
@@ -121,4 +121,50 @@
|
|||||||
<string name="audio_skipped">Video only (audio skipped)</string>
|
<string name="audio_skipped">Video only (audio skipped)</string>
|
||||||
<string name="casting_rotated">Rotated — now %1$dx%2$d</string>
|
<string name="casting_rotated">Rotated — now %1$dx%2$d</string>
|
||||||
<string name="waiting_receiver">Waiting for receiver at %1$s:%2$d…</string>
|
<string name="waiting_receiver">Waiting for receiver at %1$s:%2$d…</string>
|
||||||
|
<string name="open_settings">Settings</string>
|
||||||
|
<string name="cast_settings_title">Android Cast settings</string>
|
||||||
|
<string name="settings_section_global">Global app settings</string>
|
||||||
|
<string name="settings_section_sender">Sender settings</string>
|
||||||
|
<string name="settings_section_receiver">Receiver settings</string>
|
||||||
|
<string name="label_username">Username</string>
|
||||||
|
<string name="label_show_tray_icon">Show tray icon while cast service is running</string>
|
||||||
|
<string name="pin_anonymous_hint">Leave empty for anonymous cast</string>
|
||||||
|
<string name="label_receiver_display">Resolution</string>
|
||||||
|
<string name="receiver_display_auto_fit">AUTO (fits screen)</string>
|
||||||
|
<string name="receiver_display_adaptive">Adaptive (experimental)</string>
|
||||||
|
<string name="label_stream_protection">Stream protection (UDP)</string>
|
||||||
|
<string name="stream_protection_hint">Hint only — negotiated with receiver. UDP transports only.</string>
|
||||||
|
<string name="protection_fec_34">FEC 3/4</string>
|
||||||
|
<string name="protection_fec_rs">FEC (Reed-Solomon)</string>
|
||||||
|
<string name="protection_nack">NACK</string>
|
||||||
|
<string name="protection_fec_nack">FEC + NACK</string>
|
||||||
|
<string name="network_estimated_auto">Estimated (auto)</string>
|
||||||
|
<string name="bitrate_estimated">Estimated</string>
|
||||||
|
<string name="option_on">ON</string>
|
||||||
|
<string name="option_off">OFF</string>
|
||||||
|
<string name="codec_hint_negotiated">Priority hint — codec is negotiated during handshake.</string>
|
||||||
|
<string name="capture_camera_rear">Camera (rear)</string>
|
||||||
|
<string name="capture_camera_front">Camera (front)</string>
|
||||||
|
<string name="label_mute">Mute</string>
|
||||||
|
<string name="preview_placeholder">Source preview\n(screen capture starts after START)</string>
|
||||||
|
<string name="preview_camera">Camera preview</string>
|
||||||
|
<string name="preview_calibration">Calibration test pattern</string>
|
||||||
|
<string name="receiver_use_settings_hint">Stream options are in Android Cast settings.</string>
|
||||||
|
<string name="pin_mismatch_title">PIN mismatch</string>
|
||||||
|
<string name="pin_mismatch_message">The receiver expects a different PIN. Enter the PIN for this cast session.</string>
|
||||||
|
<string name="pin_session_hint">Session PIN</string>
|
||||||
|
<string name="settings_saved">Settings saved</string>
|
||||||
|
<string name="cast_in_progress_pin_locked">PIN cannot be changed while casting</string>
|
||||||
|
<string name="tray_cast_active">Cast active — tap to open app</string>
|
||||||
|
<string name="tray_cast_idle">Android Cast — tap to open app</string>
|
||||||
|
<string name="tray_channel_name">Cast tray</string>
|
||||||
|
<string name="tray_channel_description">Persistent cast status icon in the notification bar</string>
|
||||||
|
<string name="swipe_for_settings">Swipe from the left edge for settings</string>
|
||||||
|
<string name="label_theme">Theme</string>
|
||||||
|
<string name="theme_system">System (default)</string>
|
||||||
|
<string name="theme_dark">Dark</string>
|
||||||
|
<string name="theme_light">Light</string>
|
||||||
|
<string name="theme_inverted">Inverted</string>
|
||||||
|
<string name="theme_apply_restart">Theme updated</string>
|
||||||
|
<string name="preview_tap_for_screen">Screen preview — grant capture when prompted</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,4 +17,26 @@
|
|||||||
<item name="android:windowIsTranslucent">true</item>
|
<item name="android:windowIsTranslucent">true</item>
|
||||||
<item name="android:windowBackground">@android:color/transparent</item>
|
<item name="android:windowBackground">@android:color/transparent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.AndroidCast.Light" parent="Theme.MaterialComponents.Light.DarkActionBar">
|
||||||
|
<item name="colorPrimary">#1565C0</item>
|
||||||
|
<item name="colorPrimaryVariant">#0D47A1</item>
|
||||||
|
<item name="colorOnPrimary">#FFFFFF</item>
|
||||||
|
<item name="colorSecondary">#26A69A</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.AndroidCast.Dark" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<item name="colorPrimary">#1565C0</item>
|
||||||
|
<item name="colorPrimaryVariant">#0D47A1</item>
|
||||||
|
<item name="colorOnPrimary">#FFFFFF</item>
|
||||||
|
<item name="colorSecondary">#26A69A</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.AndroidCast.Inverted" parent="Theme.MaterialComponents.Light.DarkActionBar">
|
||||||
|
<item name="colorPrimary">@color/inverted_primary</item>
|
||||||
|
<item name="colorPrimaryVariant">#000000</item>
|
||||||
|
<item name="colorOnPrimary">#FFFFFF</item>
|
||||||
|
<item name="colorSecondary">#455A64</item>
|
||||||
|
<item name="android:windowBackground">@color/inverted_window_bg</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user