From f6bc48d5d2f137729233ae57b88ef2bb2f22e596 Mon Sep 17 00:00:00 2001 From: MiMo Date: Mon, 20 Jul 2026 20:47:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BB=BF=E8=89=B2?= =?UTF-8?q?=E9=80=9A=E9=81=93=E6=82=AC=E6=B5=AE=E7=AA=97=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 GreenChannelFloatingWindow 悬浮窗管理类,支持 API26+ 窗口类型适配 - 新增 view_green_channel_floating.xml 悬浮窗布局,LimitedTimeGreenChannelView 占满全屏无边距 - DebugToolButtonsView 调试工具按钮组添加绿色通道按钮,点击打开悬浮窗 --- .../GreenChannelFloatingWindow.java | 284 ++++++++++++++++++ .../contacts/views/DebugToolButtonsView.java | 30 ++ .../res/layout/view_debug_tool_buttons.xml | 8 + .../layout/view_green_channel_floating.xml | 15 + 4 files changed, 337 insertions(+) create mode 100644 contacts/src/main/java/cc/winboll/studio/contacts/floatingwindow/GreenChannelFloatingWindow.java create mode 100644 contacts/src/main/res/layout/view_green_channel_floating.xml diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/floatingwindow/GreenChannelFloatingWindow.java b/contacts/src/main/java/cc/winboll/studio/contacts/floatingwindow/GreenChannelFloatingWindow.java new file mode 100644 index 0000000..7d77cea --- /dev/null +++ b/contacts/src/main/java/cc/winboll/studio/contacts/floatingwindow/GreenChannelFloatingWindow.java @@ -0,0 +1,284 @@ +package cc.winboll.studio.contacts.floatingwindow; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.PixelFormat; +import android.os.Build; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import cc.winboll.studio.contacts.R; +import cc.winboll.studio.contacts.services.LimitedTimeGreenChannelService; +import cc.winboll.studio.contacts.utils.PermissionUtils; +import cc.winboll.studio.libappbase.LogUtils; + +/** + * @Author ZhanGSKen&豆包大模型 + * @Date 2026/07/20 15:00:00 + * @Describe 绿色通道悬浮窗管理类 + * 负责悬浮窗的创建、显示、隐藏和销毁 + * 适配 Android API 26+ Java 7 语法规范 + */ +public class GreenChannelFloatingWindow { + + // ====================== 常量定义区 ====================== + public static final String TAG = "GreenChannelFloatingWindow"; + + // Android版本常量(适配Java7) + private static final int ANDROID_8_API = 26; + private static final int ANDROID_10_API = 29; + private static final int ANDROID_19_API = 19; + + // ====================== 成员变量区 ====================== + private Context mContext; + private WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; + private View mFloatingView; + private boolean mIsShowing; + private android.content.BroadcastReceiver mGreenChannelReceiver; + + // ====================== 构造函数区 ====================== + /** + * 构造函数 + * @param context 上下文 + */ + public GreenChannelFloatingWindow(Context context) { + this.mContext = context; + this.mIsShowing = false; + LogUtils.d(TAG, "GreenChannelFloatingWindow: 构造函数调用完成"); + } + + // ====================== 公共方法区 ====================== + /** + * 显示悬浮窗 + * @return true=显示成功,false=显示失败(权限不足或已显示) + */ + public boolean show() { + LogUtils.d(TAG, "show: 尝试显示悬浮窗"); + + // 检查是否已显示 + if (mIsShowing) { + LogUtils.w(TAG, "show: 悬浮窗已显示,跳过重复显示"); + return true; + } + + // 检查悬浮窗权限 + if (!PermissionUtils.isOverlayPermissionGranted(mContext)) { + LogUtils.e(TAG, "show: 悬浮窗权限未授予,无法显示"); + return false; + } + + // 初始化窗口管理器 + if (!initWindowManager()) { + LogUtils.e(TAG, "show: 窗口管理器初始化失败"); + return false; + } + + // 初始化悬浮窗视图 + if (!initFloatingView()) { + LogUtils.e(TAG, "show: 悬浮窗视图初始化失败"); + return false; + } + + // 显示悬浮窗 + try { + mWindowManager.addView(mFloatingView, mWindowParams); + mIsShowing = true; + // 注册绿色通道状态变更广播接收器 + registerGreenChannelReceiver(); + LogUtils.d(TAG, "show: 悬浮窗显示成功"); + return true; + } catch (SecurityException e) { + LogUtils.e(TAG, "show: 悬浮窗显示失败(安全异常)", e); + return false; + } catch (Exception e) { + LogUtils.e(TAG, "show: 悬浮窗显示异常", e); + return false; + } + } + + /** + * 隐藏悬浮窗 + */ + public void dismiss() { + LogUtils.d(TAG, "dismiss: 尝试隐藏悬浮窗"); + + if (!mIsShowing) { + LogUtils.d(TAG, "dismiss: 悬浮窗未显示,跳过隐藏操作"); + return; + } + + // 反注册绿色通道状态变更广播接收器 + unregisterGreenChannelReceiver(); + + if (mFloatingView != null && mWindowManager != null) { + try { + mWindowManager.removeView(mFloatingView); + mIsShowing = false; + LogUtils.d(TAG, "dismiss: 悬浮窗隐藏成功"); + } catch (Exception e) { + LogUtils.e(TAG, "dismiss: 悬浮窗隐藏异常", e); + } + } + } + + /** + * 检查悬浮窗是否正在显示 + * @return true=正在显示,false=未显示 + */ + public boolean isShowing() { + return mIsShowing; + } + + /** + * 获取悬浮窗视图 + * @return 悬浮窗视图,未初始化时返回null + */ + public View getFloatingView() { + return mFloatingView; + } + + // ====================== 私有方法区 ====================== + /** + * 初始化窗口管理器 + * @return true=初始化成功,false=初始化失败 + */ + private boolean initWindowManager() { + if (mWindowManager == null) { + mWindowManager = (WindowManager) mContext.getApplicationContext() + .getSystemService(Context.WINDOW_SERVICE); + if (mWindowManager == null) { + LogUtils.e(TAG, "initWindowManager: WindowManager获取失败"); + return false; + } + LogUtils.d(TAG, "initWindowManager: WindowManager初始化成功"); + } + + // 初始化窗口参数 + initWindowParams(); + return true; + } + + /** + * 初始化窗口参数(适配API26+窗口类型) + */ + private void initWindowParams() { + mWindowParams = new WindowManager.LayoutParams(); + + // 窗口位置:顶部居中 + mWindowParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + + // 窗口大小:宽度全屏,高度自适应 + mWindowParams.width = WindowManager.LayoutParams.MATCH_PARENT; + mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; + + // 窗口格式:半透明 + mWindowParams.format = PixelFormat.TRANSLUCENT; + + // 窗口类型(适配API26+) + if (Build.VERSION.SDK_INT >= ANDROID_10_API) { + mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + LogUtils.d(TAG, "initWindowParams: API29+ 悬浮窗类型=TYPE_APPLICATION_OVERLAY"); + } else if (Build.VERSION.SDK_INT >= ANDROID_8_API) { + mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + LogUtils.d(TAG, "initWindowParams: API26-28 悬浮窗类型=TYPE_APPLICATION_OVERLAY"); + } else { + mWindowParams.type = WindowManager.LayoutParams.TYPE_PHONE; + LogUtils.d(TAG, "initWindowParams: API26以下 悬浮窗类型=TYPE_PHONE"); + } + + // 窗口标志:无焦点(不抢占输入)、全屏布局 + mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; + + // API19+ 添加透明状态栏/导航栏标志 + if (Build.VERSION.SDK_INT >= ANDROID_19_API) { + mWindowParams.flags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + } + + LogUtils.d(TAG, "initWindowParams: 窗口参数初始化完成"); + } + + /** + * 初始化悬浮窗视图 + * @return true=初始化成功,false=初始化失败 + */ + private boolean initFloatingView() { + if (mFloatingView != null) { + LogUtils.d(TAG, "initFloatingView: 视图已初始化,跳过重复创建"); + return true; + } + + try { + // 创建FrameLayout拦截返回键 + FrameLayout keyInterceptorLayout = new FrameLayout(mContext) { + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // 拦截返回键,避免误关闭悬浮窗 + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + LogUtils.d(TAG, "dispatchKeyEvent: 拦截悬浮窗返回键事件"); + return true; + } + return super.dispatchKeyEvent(event); + } + }; + + // 加载悬浮窗布局 + mFloatingView = LayoutInflater.from(mContext) + .inflate(R.layout.view_green_channel_floating, keyInterceptorLayout); + + LogUtils.d(TAG, "initFloatingView: 悬浮窗视图初始化完成"); + return true; + } catch (Exception e) { + LogUtils.e(TAG, "initFloatingView: 悬浮窗视图初始化异常", e); + return false; + } + } + + /** + * 注册绿色通道状态变更广播接收器 + * 接收 LimitedTimeGreenChannelView 发出的通道开启与关闭消息 + */ + private void registerGreenChannelReceiver() { + LogUtils.d(TAG, "registerGreenChannelReceiver: 注册绿色通道状态变更广播接收器"); + mGreenChannelReceiver = new android.content.BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (LimitedTimeGreenChannelService.ACTION_GREEN_CHANNEL_STATE_CHANGED.equals(intent.getAction())) { + boolean isEnabled = intent.getBooleanExtra( + LimitedTimeGreenChannelService.EXTRA_GREEN_CHANNEL_ENABLED, false); + LogUtils.d(TAG, "registerGreenChannelReceiver: 收到绿色通道状态变更广播, isEnabled=" + isEnabled); + + // 如果绿色通道关闭,关闭悬浮窗 + if (!isEnabled) { + LogUtils.d(TAG, "registerGreenChannelReceiver: 绿色通道已关闭,准备关闭悬浮窗"); + dismiss(); + } + } + } + }; + + IntentFilter filter = new IntentFilter(LimitedTimeGreenChannelService.ACTION_GREEN_CHANNEL_STATE_CHANGED); + LocalBroadcastManager.getInstance(mContext).registerReceiver(mGreenChannelReceiver, filter); + LogUtils.d(TAG, "registerGreenChannelReceiver: 广播接收器注册完成"); + } + + /** + * 反注册绿色通道状态变更广播接收器 + */ + private void unregisterGreenChannelReceiver() { + if (mGreenChannelReceiver != null) { + LogUtils.d(TAG, "unregisterGreenChannelReceiver: 反注册绿色通道状态变更广播接收器"); + LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mGreenChannelReceiver); + mGreenChannelReceiver = null; + LogUtils.d(TAG, "unregisterGreenChannelReceiver: 反注册完成"); + } + } +} diff --git a/contacts/src/main/java/cc/winboll/studio/contacts/views/DebugToolButtonsView.java b/contacts/src/main/java/cc/winboll/studio/contacts/views/DebugToolButtonsView.java index 7b49bfa..2fa3c28 100644 --- a/contacts/src/main/java/cc/winboll/studio/contacts/views/DebugToolButtonsView.java +++ b/contacts/src/main/java/cc/winboll/studio/contacts/views/DebugToolButtonsView.java @@ -8,6 +8,7 @@ import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import cc.winboll.studio.contacts.R; +import cc.winboll.studio.contacts.floatingwindow.GreenChannelFloatingWindow; import cc.winboll.studio.libappbase.LogActivity; import cc.winboll.studio.libappbase.LogUtils; @@ -21,6 +22,7 @@ public class DebugToolButtonsView extends LinearLayout { private static final String TAG = "DebugToolButtonsView"; private Context mContext; + private GreenChannelFloatingWindow mGreenChannelFloatingWindow; public DebugToolButtonsView(Context context) { super(context); @@ -51,6 +53,7 @@ public class DebugToolButtonsView extends LinearLayout { Button btnLayoutPreview = (Button) findViewById(R.id.btn_debug_layout_preview); Button btnUnitTest = (Button) findViewById(R.id.btn_debug_unit_test); Button btnLogView = (Button) findViewById(R.id.btn_debug_logview); + Button btnGreenChannel = (Button) findViewById(R.id.btn_debug_green_channel); btnCrash.setOnClickListener(new OnClickListener() { @Override @@ -79,6 +82,13 @@ public class DebugToolButtonsView extends LinearLayout { onLogView(); } }); + + btnGreenChannel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onOpenGreenChannel(); + } + }); } private void onTestCrash() { @@ -121,4 +131,24 @@ public class DebugToolButtonsView extends LinearLayout { LogUtils.d(TAG, "onLogView: 打开日志窗口"); LogActivity.startLogActivity(getContext(), true); } + + /** + * 打开绿色通道悬浮窗 + */ + private void onOpenGreenChannel() { + LogUtils.d(TAG, "onOpenGreenChannel: 打开绿色通道悬浮窗"); + if (mGreenChannelFloatingWindow == null) { + mGreenChannelFloatingWindow = new GreenChannelFloatingWindow(mContext); + } + if (mGreenChannelFloatingWindow.isShowing()) { + LogUtils.d(TAG, "onOpenGreenChannel: 悬浮窗已显示,跳过"); + return; + } + boolean result = mGreenChannelFloatingWindow.show(); + if (result) { + LogUtils.d(TAG, "onOpenGreenChannel: 悬浮窗显示成功"); + } else { + LogUtils.e(TAG, "onOpenGreenChannel: 悬浮窗显示失败,可能缺少悬浮窗权限"); + } + } } diff --git a/contacts/src/main/res/layout/view_debug_tool_buttons.xml b/contacts/src/main/res/layout/view_debug_tool_buttons.xml index 0089c42..fe7136e 100644 --- a/contacts/src/main/res/layout/view_debug_tool_buttons.xml +++ b/contacts/src/main/res/layout/view_debug_tool_buttons.xml @@ -28,6 +28,14 @@ android:layout_height="wrap_content" android:gravity="right"> +