- * 由后端 {@code DeviceService.mirror} 下发,推送通道已打通,
- * 具体实现(如开启投屏/屏幕流推送)待补充。
+ * 由后端 {@code DeviceService.mirror} 下发,推送通道已打通。
+ * 收到命令后启动屏幕采集授权 Activity,经 MediaProjection 授权后
+ * 启动 {@link ScreenCaptureService} 进行 WebRTC 远程屏幕串流。
*/
- private void mirror() {
- Logger.e(TAG, "mirror: 收到屏幕镜像推送,功能待实现");
+ /**
+ * 屏幕镜像(contentType = 16, mirror)。
+ *
+ * extra 可携带以下字段(由后端 {@code DeviceService.mirror} 下发):
+ * - remote_assistance_key : 是否为远程协助请求,1=是(需弹出采集界面)
+ * - accept_remote_assistance_key : 是否自动接受,1=自动接收并采集,0=需用户确认
+ *
+ * 处理规则:
+ * - remote_assistance_key != 1 → 忽略(非远程协助镜像命令)
+ * - accept_remote_assistance_key == 1 → 自动接收并启动屏幕采集串流
+ * - accept_remote_assistance_key == 0 → 弹出确认对话框,用户确认后再采集
+ */
+ private void mirror(String extra) {
+ Logger.e(TAG, "mirror: 收到屏幕镜像推送,extra=" + extra);
+
+ int remoteKey = mMMKV.decodeInt(CommonConfig.KEY_REMOTE_ASSISTANCE_ENABLE, 0);
+ int acceptKey = mMMKV.decodeInt(CommonConfig.KEY_AUTO_REMOTE_ASSISTANCE_ENABLE, 0);
+
+ // 非远程协助请求:不弹出采集界面
+ if (remoteKey != 1) {
+ Logger.d(TAG, "mirror: remote_assistance_key != 1,忽略");
+ return;
+ }
+
+ try {
+ // 若已处于串流中,不重复启动
+ if (ScreenCaptureService.getInstance() != null
+ && ScreenCaptureService.getInstance().isStreaming()) {
+ Logger.d(TAG, "mirror: 屏幕串流已在运行,忽略重复命令");
+ return;
+ }
+
+ if (acceptKey == 1 || isAutoRemoteAssistanceEnabled()) {
+ // 自动接收并采集窗口串流(推送指定自动,或本地已开启自动接收开关)
+ Logger.d(TAG, "mirror: 自动接收远程协助,启动采集");
+ ScreenCaptureActivity.start(mContext);
+ } else {
+ // 需用户确认:弹出询问对话框
+ Logger.d(TAG, "mirror: 需用户确认,弹出询问对话框");
+ showRemoteAssistanceConfirmDialog();
+ }
+ } catch (Exception e) {
+ Logger.e(TAG, "mirror: 启动屏幕串流失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 远程协助询问对话框:用户确认后启动屏幕采集。
+ * 在老人设备上最小化交互,仅一个"允许"按钮。
+ */
+ private void showRemoteAssistanceConfirmDialog() {
+ new Handler(Looper.getMainLooper()).post(() -> {
+ try {
+ AlertDialog dialog = new AlertDialog.Builder(
+ new ContextThemeWrapper(mContext, android.R.style.Theme_DeviceDefault_Light_Dialog_Alert))
+ .setTitle(R.string.remote_assistance_confirm_title)
+ .setMessage(R.string.remote_assistance_confirm_message)
+ .setPositiveButton(R.string.remote_assistance_confirm_ok,
+ (d, which) -> ScreenCaptureActivity.start(mContext))
+ .setNegativeButton(R.string.remote_assistance_confirm_cancel,
+ (d, which) -> d.dismiss())
+ .setCancelable(false)
+ .create();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+ } else {
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ }
+ dialog.show();
+ } catch (Exception e) {
+ Logger.e(TAG, "mirror: 显示询问对话框失败,直接启动采集: " + e.getMessage());
+ ScreenCaptureActivity.start(mContext);
+ }
+ });
+ }
+
+ /**
+ * 读取本地"自动接收远程协助"开关(与设置页 siAutoRemoteAssistance 同步)。
+ */
+ private boolean isAutoRemoteAssistanceEnabled() {
+ try {
+ MMKV mmkv = MMKV.mmkvWithID("MMKV_DEFAULT");
+ return mmkv != null && mmkv.decodeBool("AutoRemoteAssistanceEnable", false);
+ } catch (Exception e) {
+ return false;
+ }
}
/**
diff --git a/app/src/main/java/com/ttstd/dialer/service/main/MainService.java b/app/src/main/java/com/ttstd/dialer/service/main/MainService.java
index 5e94a59..170fbc1 100644
--- a/app/src/main/java/com/ttstd/dialer/service/main/MainService.java
+++ b/app/src/main/java/com/ttstd/dialer/service/main/MainService.java
@@ -31,6 +31,7 @@ import com.tencent.mmkv.MMKV;
import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.R;
import com.ttstd.dialer.activity.main.MainActivity;
+import com.ttstd.dialer.activity.settings.assist.SettingsAssistActivity;
import com.ttstd.dialer.base.BaseService;
import com.ttstd.dialer.bean.req.SnLocationReq;
import com.ttstd.dialer.config.CommonConfig;
@@ -39,6 +40,7 @@ import com.ttstd.dialer.manager.ContactSyncManager;
import com.ttstd.dialer.manager.MqttManager;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.utils.SystemUtils;
+import com.ttstd.dialer.webrtc.signaling.WebSocketClient;
public class MainService extends BaseService implements NetworkUtils.OnNetworkStatusChangedListener, ViewModelStoreOwner {
private static final String TAG = "MainService";
@@ -127,6 +129,14 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
if (enableFloatWindow) {
showFloatWindow();
}
+ boolean remote = mMMKV.decodeInt(CommonConfig.KEY_REMOTE_ASSISTANCE_ENABLE, 0) == 1;
+ Logger.e(TAG, "onCreate: remote = " + remote);
+ if (remote) {
+ // 开启:后台建立全局信令连接
+ WebSocketClient client = WebSocketClient.getInstance();
+ client.setServerUrl(CommonConfig.SIGNAL_SERVER_URL);
+ client.connect();
+ }
}
diff --git a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java
index d04dd96..992b618 100644
--- a/app/src/main/java/com/ttstd/dialer/view/SettingItem.java
+++ b/app/src/main/java/com/ttstd/dialer/view/SettingItem.java
@@ -16,21 +16,26 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import com.ttstd.dialer.view.SwitchButton;
import com.ttstd.dialer.R;
+import com.ttstd.dialer.config.CommonConfig;
+import com.tencent.mmkv.MMKV;
import org.jetbrains.annotations.NotNull;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
public class SettingItem extends ConstraintLayout {
private OnClickListener mRootOnClickListener;
private SwitchButton.OnCheckedChangeListener mOnToggleChanged;
private static final String DefaultOptionsText = "设置选项";
- private static final String DefaultEnableText = "开启描述";
- private static final String DefaultDisableText = "关闭描述";
+ private static final String DefaultHintText = "设置描述";
private String mOptionsText = "";
- private String mEnableText = "";
- private String mDisableText = "";
+ private String mHintText = "";
private int mOptionsTextColor = 0xFF000000;
private int mHintTextColor = 0xFF9D9D9D;
@@ -41,6 +46,43 @@ public class SettingItem extends ConstraintLayout {
private boolean mLinkage = true;
private boolean mShowDivider = true;
+ // ============================================================
+ // 联动配置(方案 A:View 内部自管理)
+ // ============================================================
+ /** 本项对外暴露的联动名称(供其他项依赖)。为空时默认使用 optionsText。 */
+ private String mLinkageName = "";
+ /** 本项依赖的联动名称:仅当名称为该值的项开启时,本项才可用。为空表示不依赖任何项。 */
+ private String mEnabledBy = "";
+ /** 是否已被外部通过 setEnabled(false) 显式禁用。 */
+ private boolean mHardDisabled = false;
+
+ /** 本项实际是否可交互(联动 + 硬禁用综合结果)。 */
+ private boolean mInteractive = true;
+
+ /** 联动注册表:以根视图对象为作用域,key 为联动名称,value 为该名称下的 SettingItem 列表。 */
+ private static final Map