refactor(notification): 将通知显示从Activity改为DialogFragment

重构通知显示机制,使用DialogFragment替代Activity,提升性能和用户体验:
1. 删除CommonNotificationActivity及相关资源
2. 新增NotificationDialogFragment实现全屏弹窗
3. 更新NotificationManager适配新机制
4. 添加全屏样式和布局文件
5. 优化系统UI隐藏逻辑
This commit is contained in:
peng 2025-09-17 14:42:17 +08:00
parent 13a2bc5c00
commit e01fd86961
16 changed files with 417 additions and 682 deletions

View File

View File

@ -61,11 +61,7 @@
android:name="com.ismart.ib86.feature.network.TestBSSIDActivity"
android:exported="true" />
<!-- 通用通知Activity -->
<activity
android:name="com.ismart.ib86.common.notification.CommonNotificationActivity"
android:exported="false"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
<!-- 网络状态测试Activity -->
<activity

View File

@ -224,7 +224,7 @@ public class MainActivity extends AppCompatActivity {
if (permissionsNeeded.isEmpty()) {
// 所有权限已授予
initializeParams();
//initializeSDK();
initializeSDK();
} else {
// 显示权限说明对话框
showPermissionRationaleDialog(permissionsNeeded);
@ -259,7 +259,7 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "权限不足,应用可能无法正常运行", Toast.LENGTH_LONG).show();
// 即使权限不足也尝试初始化SDK让应用尽可能运行
initializeParams();
//initializeSDK();
initializeSDK();
})
.show();
}
@ -284,7 +284,7 @@ public class MainActivity extends AppCompatActivity {
// 所有权限已授予
Log.d(TAG, "所有权限已授予");
initializeParams();
//initializeSDK();
initializeSDK();
} else {
// 有权限被拒绝
Log.w(TAG, "部分权限被拒绝: " + deniedPermissions);
@ -325,7 +325,7 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "权限不足,应用可能无法正常运行", Toast.LENGTH_LONG).show();
// 即使权限不足也尝试初始化SDK让应用尽可能运行
initializeParams();
//initializeSDK();
initializeSDK();
})
.show();
}
@ -351,7 +351,7 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "权限不足,应用可能无法正常运行", Toast.LENGTH_LONG).show();
// 即使权限不足也尝试初始化SDK让应用尽可能运行
initializeParams();
//initializeSDK();
initializeSDK();
})
.show();
}
@ -825,7 +825,7 @@ public class MainActivity extends AppCompatActivity {
authParams.setApiKey("sk-e5fe272222374ffca089a27cd1a75d11");
authParams.setWorkspaceId("llm-ux9gbx66pglk2h3j");
authParams.setVqaImageLink("https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/7043267371/p909896.png");
authParams.setDialogMode(Constant.DialogMode.DUPLEX);
authParams.setDialogMode(Constant.DialogMode.TAP2TALK);
authParams.setModelId("multimodal-dialog");
authParams.setEnableDebug(true);
authParams.setChainMode(Constant.ChainMode.WEBSOCKET);
@ -1009,7 +1009,7 @@ public class MainActivity extends AppCompatActivity {
audioRecorder.pause();
}
Thread.sleep(1000);
//initializeSDK();
initializeSDK();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
@ -1244,7 +1244,7 @@ public class MainActivity extends AppCompatActivity {
try {
String cmdName = new JSONArray(command).getJSONObject(0).getString("name");
Log.d(TAG, "执行命令: " + cmdName);
switch (cmdName) {
case "check_battery":
multiModalDialog.requestToRespond("transcript", "当前电量为87%", null);
@ -1252,6 +1252,12 @@ public class MainActivity extends AppCompatActivity {
case "self_health_get_bloodoxygen":
multiModalDialog.requestToRespond("transcript", "查询到最近一个月的平均血氧饱和度为93%", null);
break;
//增大音量
case "increase_volume_default":
break;
case "decrease_volume_default":
break;
// case "visual_qa":
// executeVQACommand();
// break;

View File

@ -1,228 +0,0 @@
package com.ismart.ib86.common.notification;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import com.ismart.ib86.app.R;
/**
* 通用通知Activity
*/
public class CommonNotificationActivity extends Activity {
public static final String ACTION_CLOSE_ACTIVITY = "com.ismart.ib86.notification.CLOSE_COMMON_NOTIFICATION_ACTIVITY";
public static final String ACTION_NOTIFICATION_CLOSED = "com.ismart.ib86.notification.NOTIFICATION_CLOSED";
public static final String EXTRA_MESSAGE = "message";
public static final String EXTRA_ICON = "icon";
public static final String EXTRA_DURATION = "duration";
public static final String EXTRA_TYPE = "type";
public static final String EXTRA_SOUND_PATH = "sound_path";
private static final String TAG = "CommonNotificationActivity";
private ImageView ivIcon;
private TextView tvMessage;
private String notificationType;
private String soundPath;
private BroadcastReceiver closeReceiver;
private MediaPlayer mediaPlayer;
private Handler handler;
private Runnable finishRunnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_common_notification);
// 设置窗口属性确保在锁屏时也能显示并且全屏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_FULLSCREEN);
// 隐藏状态栏和导航栏API 19及以上
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
initializeViews();
handleIntent(getIntent());
setupCloseReceiver();
playNotificationSound();
}
private void initializeViews() {
ivIcon = findViewById(R.id.iv_icon);
tvMessage = findViewById(R.id.tv_message);
}
private void handleIntent(Intent intent) {
if (intent != null) {
String message = intent.getStringExtra(EXTRA_MESSAGE);
int iconResId = intent.getIntExtra(EXTRA_ICON, android.R.drawable.ic_dialog_alert); // 默认使用系统警告图标
int durationSeconds = intent.getIntExtra(EXTRA_DURATION, 5); // 默认5秒
notificationType = intent.getStringExtra(EXTRA_TYPE);
soundPath = intent.getStringExtra(EXTRA_SOUND_PATH);
Log.d(TAG, "Received intent with duration: " + durationSeconds + " seconds");
if (message != null) {
tvMessage.setText(message);
}
if (iconResId != 0) {
ivIcon.setImageResource(iconResId);
}
// 设置自动关闭
if (durationSeconds > 0) {
handler = new Handler();
finishRunnable = () -> {
Log.d(TAG, "Auto closing notification activity after " + durationSeconds + " seconds");
finish();
};
handler.postDelayed(finishRunnable, durationSeconds * 1000L);
Log.d(TAG, "Scheduled auto close in " + durationSeconds + " seconds");
} else {
Log.d(TAG, "Duration is 0 or negative, no auto close scheduled");
}
} else {
Log.w(TAG, "Received null intent");
}
}
private void setupCloseReceiver() {
closeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_CLOSE_ACTIVITY.equals(intent.getAction())) {
Log.d(TAG, "Received close activity broadcast");
finish();
}
}
};
IntentFilter filter = new IntentFilter(ACTION_CLOSE_ACTIVITY);
registerReceiver(closeReceiver, filter);
}
private void playNotificationSound() {
try {
// 如果提供了自定义音效路径则使用自定义音效
if (soundPath != null && !soundPath.isEmpty()) {
if (soundPath.startsWith("android.resource://")) {
// 处理res/raw资源
Uri uri = Uri.parse(soundPath);
mediaPlayer = MediaPlayer.create(this, uri);
} else {
// 处理文件路径
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(soundPath);
mediaPlayer.prepare();
}
} else if (notificationType != null) {
// 根据通知类型使用对应的默认音效
switch (notificationType) {
case "NETWORK_LOST":
mediaPlayer = MediaPlayer.create(this, R.raw.network_lost_notification);
break;
case "NETWORK_CONFIG":
mediaPlayer = MediaPlayer.create(this, R.raw.network_config_notification);
break;
case "BATTERY_LOW":
mediaPlayer = MediaPlayer.create(this, R.raw.battery_low_notification);
break;
default:
mediaPlayer = MediaPlayer.create(this, R.raw.default_notification);
break;
}
} else {
// 使用系统默认提示音
// mediaPlayer = MediaPlayer.create(this, android.R.raw.notification_sound);
}
if (mediaPlayer != null) {
mediaPlayer.setOnCompletionListener(mp -> {
try {
// 在播放完成后释放MediaPlayer资源
mp.release();
// 将mediaPlayer引用置为null避免在onDestroy中重复释放
mediaPlayer = null;
} catch (Exception e) {
Log.e(TAG, "Error releasing media player", e);
}
});
mediaPlayer.start();
}
} catch (Exception e) {
Log.e(TAG, "Failed to play notification sound", e);
}
}
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy called");
super.onDestroy();
// 通知NotificationManager重置显示状态
Intent intent = new Intent(ACTION_NOTIFICATION_CLOSED);
sendBroadcast(intent);
// 清理资源
if (handler != null && finishRunnable != null) {
Log.d(TAG, "Removing callbacks from handler");
handler.removeCallbacks(finishRunnable);
}
if (mediaPlayer != null) {
try {
// 在调用isPlaying()之前检查MediaPlayer状态避免IllegalStateException
if (mediaPlayer.isPlaying()) {
Log.d(TAG, "Stopping media player");
mediaPlayer.stop();
}
} catch (IllegalStateException e) {
// MediaPlayer可能已经处于无效状态忽略这个异常
Log.w(TAG, "MediaPlayer is in invalid state, skipping stop()", e);
} catch (Exception e) {
Log.e(TAG, "Error stopping media player", e);
}
try {
Log.d(TAG, "Releasing media player");
mediaPlayer.release();
mediaPlayer = null;
} catch (Exception e) {
Log.e(TAG, "Error releasing media player", e);
}
}
if (closeReceiver != null) {
try {
Log.d(TAG, "Unregistering receiver");
unregisterReceiver(closeReceiver);
} catch (Exception e) {
Log.e(TAG, "Error unregistering receiver", e);
}
closeReceiver = null;
}
Log.d(TAG, "onDestroy completed");
}
}

View File

@ -0,0 +1,321 @@
package com.ismart.ib86.common.notification;
import android.app.Dialog;
import android.app.DialogFragment;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import com.ismart.ib86.app.R;
/**
* 通知弹窗DialogFragment
*/
public class NotificationDialogFragment extends DialogFragment {
public static final String ACTION_NOTIFICATION_CLOSED = "com.ismart.ib86.notification.NOTIFICATION_CLOSED";
public static final String ACTION_CLOSE_DIALOG = "com.ismart.ib86.notification.CLOSE_DIALOG";
private static final String ARG_MESSAGE = "message";
private static final String ARG_ICON_RES_ID = "icon_res_id";
private static final String ARG_DURATION = "duration";
private static final String ARG_TYPE = "type";
private static final String ARG_SOUND_PATH = "sound_path";
private static final String TAG = "NotificationDialog";
private String message;
private int iconResId;
private int durationSeconds;
private String notificationType;
private String soundPath;
private MediaPlayer mediaPlayer;
private Handler handler;
private Runnable dismissRunnable;
public static NotificationDialogFragment newInstance(String message, int iconResId,
int durationSeconds, String type, String soundPath) {
NotificationDialogFragment fragment = new NotificationDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_MESSAGE, message);
args.putInt(ARG_ICON_RES_ID, iconResId);
args.putInt(ARG_DURATION, durationSeconds);
args.putString(ARG_TYPE, type);
args.putString(ARG_SOUND_PATH, soundPath);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL, R.style.FullScreenDialog);
// 设置DialogFragment的窗口标志以隐藏系统UI
setShowsDialog(true);
if (getArguments() != null) {
message = getArguments().getString(ARG_MESSAGE);
iconResId = getArguments().getInt(ARG_ICON_RES_ID);
durationSeconds = getArguments().getInt(ARG_DURATION);
notificationType = getArguments().getString(ARG_TYPE);
soundPath = getArguments().getString(ARG_SOUND_PATH);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
if (dialog.getWindow() != null) {
Window window = dialog.getWindow();
window.requestFeature(Window.FEATURE_NO_TITLE);
// 添加窗口标志以确保全屏并隐藏系统UI
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); // 防止导航栏显示
// 设置窗口布局参数
WindowManager.LayoutParams params = window.getAttributes();
params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.MATCH_PARENT;
window.setAttributes(params);
}
return dialog;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.dialog_notification, container, false);
return view;
}
@Override
public void onStart() {
super.onStart();
// 设置Dialog为全屏并隐藏系统UI
Dialog dialog = getDialog();
if (dialog != null && dialog.getWindow() != null) {
Window window = dialog.getWindow();
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
// 设置窗口标志以确保全屏
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); // 防止导航栏显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_IMMERSIVE); // 添加额外的沉浸模式标志
}
// 延迟设置焦点以确保系统UI正确隐藏
window.getDecorView().post(() -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_IMMERSIVE);
}
});
}
}
@Override
public void onResume() {
super.onResume();
// 确保在恢复时系统UI状态正确
Dialog dialog = getDialog();
if (dialog != null && dialog.getWindow() != null) {
Window window = dialog.getWindow();
// 设置窗口标志以确保全屏
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); // 恢复焦点
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_IMMERSIVE);
}
// 延迟设置焦点以确保系统UI正确隐藏
window.getDecorView().post(() -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_IMMERSIVE);
}
});
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initializeViews(view);
playNotificationSound();
// 设置自动关闭
if (durationSeconds > 0) {
handler = new Handler();
dismissRunnable = () -> {
if (getDialog() != null && getDialog().isShowing()) {
dismissAllowingStateLoss();
}
};
handler.postDelayed(dismissRunnable, durationSeconds * 1000L);
}
}
private void initializeViews(View view) {
ImageView ivIcon = view.findViewById(R.id.iv_icon);
TextView tvMessage = view.findViewById(R.id.tv_message);
if (message != null) {
tvMessage.setText(message);
}
if (iconResId != 0) {
ivIcon.setImageResource(iconResId);
}
}
private void playNotificationSound() {
try {
// 如果提供了自定义音效路径则使用自定义音效
if (soundPath != null && !soundPath.isEmpty() && getActivity() != null) {
if (soundPath.startsWith("android.resource://")) {
// 处理res/raw资源
Uri uri = Uri.parse(soundPath);
mediaPlayer = MediaPlayer.create(getActivity(), uri);
} else {
// 处理文件路径
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(soundPath);
mediaPlayer.prepare();
}
} else if (notificationType != null && getActivity() != null) {
// 根据通知类型使用对应的默认音效
switch (notificationType) {
case "NETWORK_LOST":
mediaPlayer = MediaPlayer.create(getActivity(), R.raw.network_lost_notification);
break;
case "NETWORK_CONFIG":
mediaPlayer = MediaPlayer.create(getActivity(), R.raw.network_config_notification);
break;
case "BATTERY_LOW":
mediaPlayer = MediaPlayer.create(getActivity(), R.raw.battery_low_notification);
break;
default:
mediaPlayer = MediaPlayer.create(getActivity(), R.raw.default_notification);
break;
}
}
if (mediaPlayer != null) {
mediaPlayer.setOnCompletionListener(mp -> {
try {
// 在播放完成后释放MediaPlayer资源
mp.release();
// 将mediaPlayer引用置为null避免在onDestroy中重复释放
mediaPlayer = null;
} catch (Exception e) {
Log.e(TAG, "Error releasing media player", e);
}
});
mediaPlayer.start();
}
} catch (Exception e) {
Log.e(TAG, "Failed to play notification sound", e);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
// 在销毁视图时确保系统UI状态正确
if (getActivity() != null && getActivity().getWindow() != null) {
// 恢复Activity的系统UI状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
// 通知NotificationManager重置显示状态
if (getActivity() != null) {
android.content.Intent intent = new android.content.Intent(ACTION_NOTIFICATION_CLOSED);
getActivity().sendBroadcast(intent);
}
// 清理资源
if (handler != null && dismissRunnable != null) {
handler.removeCallbacks(dismissRunnable);
}
if (mediaPlayer != null) {
try {
// 在调用isPlaying()之前检查MediaPlayer状态避免IllegalStateException
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
} catch (IllegalStateException e) {
// MediaPlayer可能已经处于无效状态忽略这个异常
Log.w(TAG, "MediaPlayer is in invalid state, skipping stop()", e);
} catch (Exception e) {
Log.e(TAG, "Error stopping media player", e);
}
try {
mediaPlayer.release();
mediaPlayer = null;
} catch (Exception e) {
Log.e(TAG, "Error releasing media player", e);
}
}
}
}

View File

@ -1,6 +1,7 @@
package com.ismart.ib86.common.notification;
import android.app.Activity;
import android.app.FragmentManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -12,6 +13,8 @@ import android.util.Log;
import java.util.LinkedList;
import java.util.Queue;
import com.ismart.ib86.common.notification.NotificationDialogFragment;
/**
* 通知管理器
* 专门负责通知的显示和隐藏
@ -19,8 +22,8 @@ import java.util.Queue;
public class NotificationManager {
private static final String TAG = "NotificationManager";
private Context context;
private Activity activityContext; // 保存Activity上下文用于启动Activity
private boolean isActivityShowing = false;
private Activity activityContext; // 保存Activity上下文用于启动DialogFragment
private boolean isDialogShowing = false;
private NotificationClosedReceiver notificationClosedReceiver;
private NotificationRequest currentNotification; // 当前正在显示的通知
private Queue<NotificationRequest> notificationQueue = new LinkedList<>(); // 通知队列
@ -66,7 +69,7 @@ public class NotificationManager {
NotificationRequest request = new NotificationRequest(type, message, iconResId, durationSeconds, soundPath, priority);
// 如果当前没有通知在显示则直接显示
if (!isActivityShowing) {
if (!isDialogShowing) {
displayNotification(request);
} else {
// 如果有通知在显示检查优先级
@ -94,74 +97,75 @@ public class NotificationManager {
private void displayNotification(NotificationRequest request) {
Log.d(TAG, "Displaying notification: " + request);
if (isActivityShowing) {
Log.d(TAG, "Notification activity is already showing");
if (isDialogShowing) {
Log.d(TAG, "Notification dialog is already showing");
return;
}
// 检查是否有可用的Activity上下文
if (activityContext == null) {
Log.w(TAG, "Activity context is null, cannot show activity");
Log.w(TAG, "Activity context is null, cannot show dialog");
return;
}
// 检查Activity是否有效
if (activityContext.isFinishing() || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activityContext.isDestroyed())) {
Log.w(TAG, "Activity is not valid, cannot show activity");
Log.w(TAG, "Activity is not valid, cannot show dialog");
return;
}
activityContext.runOnUiThread(() -> {
try {
if (isActivityShowing) {
Log.d(TAG, "Notification activity is already showing");
if (isDialogShowing) {
Log.d(TAG, "Notification dialog is already showing");
return;
}
Intent intent = new Intent(activityContext, CommonNotificationActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(CommonNotificationActivity.EXTRA_MESSAGE, request.getMessage());
intent.putExtra(CommonNotificationActivity.EXTRA_ICON, request.getIconResId());
intent.putExtra(CommonNotificationActivity.EXTRA_DURATION, request.getDurationSeconds());
intent.putExtra(CommonNotificationActivity.EXTRA_TYPE, request.getType().name());
if (request.getSoundPath() != null) {
intent.putExtra(CommonNotificationActivity.EXTRA_SOUND_PATH, request.getSoundPath());
}
activityContext.startActivity(intent);
isActivityShowing = true;
// 创建并显示NotificationDialogFragment
NotificationDialogFragment dialogFragment = NotificationDialogFragment.newInstance(
request.getMessage(),
request.getIconResId(),
request.getDurationSeconds(),
request.getType().name(),
request.getSoundPath()
);
FragmentManager fragmentManager = activityContext.getFragmentManager();
dialogFragment.show(fragmentManager, "NotificationDialogFragment");
isDialogShowing = true;
currentNotification = request;
Log.d(TAG, "Notification activity shown for " + request.getDurationSeconds() + " seconds, type: " + request.getType() + ", priority: " + request.getPriority());
Log.d(TAG, "Notification dialog shown for " + request.getDurationSeconds() + " seconds, type: " + request.getType() + ", priority: " + request.getPriority());
} catch (Exception e) {
Log.e(TAG, "Failed to show notification activity", e);
Log.e(TAG, "Failed to show notification dialog", e);
}
});
}
public void hideNotification() {
Log.d(TAG, "hideNotification called, isActivityShowing: " + isActivityShowing);
// 通过发送广播通知CommonNotificationActivity关闭自身
if (isActivityShowing) {
Log.d(TAG, "hideNotification called, isDialogShowing: " + isDialogShowing);
// 通过发送广播通知NotificationDialogFragment关闭自身
if (isDialogShowing) {
try {
Intent intent = new Intent(CommonNotificationActivity.ACTION_CLOSE_ACTIVITY);
Intent intent = new Intent(NotificationDialogFragment.ACTION_CLOSE_DIALOG);
context.sendBroadcast(intent);
isActivityShowing = false;
isDialogShowing = false;
currentNotification = null;
Log.d(TAG, "Notification activity hidden");
Log.d(TAG, "Notification dialog hidden");
} catch (Exception e) {
Log.e(TAG, "Failed to hide notification activity", e);
Log.e(TAG, "Failed to hide notification dialog", e);
}
} else {
Log.d(TAG, "Notification activity is not showing, nothing to hide");
Log.d(TAG, "Notification dialog is not showing, nothing to hide");
}
}
public boolean isActivityShowing() {
return isActivityShowing;
public boolean isDialogShowing() {
return isDialogShowing;
}
// 添加一个方法用于重置显示状态
public void resetShowingState() {
isActivityShowing = false;
isDialogShowing = false;
currentNotification = null;
processNextNotification();
}
@ -193,7 +197,7 @@ public class NotificationManager {
private void registerNotificationClosedReceiver() {
notificationClosedReceiver = new NotificationClosedReceiver();
IntentFilter filter = new IntentFilter(CommonNotificationActivity.ACTION_NOTIFICATION_CLOSED);
IntentFilter filter = new IntentFilter(NotificationDialogFragment.ACTION_NOTIFICATION_CLOSED);
context.registerReceiver(notificationClosedReceiver, filter);
}
@ -211,7 +215,7 @@ public class NotificationManager {
private class NotificationClosedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (CommonNotificationActivity.ACTION_NOTIFICATION_CLOSED.equals(intent.getAction())) {
if (NotificationDialogFragment.ACTION_NOTIFICATION_CLOSED.equals(intent.getAction())) {
Log.d(TAG, "Received notification closed broadcast, resetting showing state");
resetShowingState();
}

View File

@ -7,7 +7,6 @@ import android.os.Build;
import android.util.Log;
import com.ismart.ib86.app.R;
import com.ismart.ib86.common.notification.CommonNotificationActivity;
import com.ismart.ib86.common.notification.NotificationType;
import com.ismart.ib86.common.notification.NotificationManager;
import com.ismart.ib86.common.notification.NotificationPriority;
@ -121,8 +120,8 @@ public class NetworkStateManager {
}
}
public boolean isActivityShowing() {
return notificationManager != null ? notificationManager.isActivityShowing() : false;
public boolean isDialogShowing() {
return notificationManager != null ? notificationManager.isDialogShowing() : false;
}
public NetworkState getCurrentNetworkState() {

View File

@ -1,58 +0,0 @@
# NetworkStateManager 设计方案比较
## 方案一:分离式设计(当前实现)
### 组件结构
1. **NetworkStateMonitor** - 负责网络状态监听
2. **NetworkStateManager** - 负责状态管理和通知显示
### 优点
1. **职责分离**:网络监听和状态管理完全分离
2. **高内聚低耦合**:每个类只负责单一职责
3. **可复用性**NetworkStateMonitor可以被其他不涉及通知显示的组件复用
4. **可测试性**:可以独立测试网络监听功能
5. **可扩展性**可以在NetworkStateManager中添加更多的状态管理逻辑
### 缺点
1. **复杂性**:增加了类的数量和层级
2. **间接性**:需要通过多个类来完成完整的功能
## 方案二:合并式设计(简化版本)
### 组件结构
1. **SimpleNetworkStateManager** - 同时负责网络状态监听和状态管理
### 优点
1. **简洁性**:只有一个类负责所有功能
2. **直接性**:减少了组件间的依赖关系
3. **易理解**:代码结构更简单,易于理解
### 缺点
1. **职责混合**:一个类承担了多个职责
2. **可复用性差**:网络监听功能难以被其他组件复用
3. **可测试性差**:难以独立测试网络监听功能
4. **扩展性差**:添加新功能时可能需要修改现有代码
## 推荐方案
### 如果项目需求简单且稳定
推荐使用**方案二(合并式设计)**,因为:
- 代码更简洁,易于维护
- 减少了不必要的复杂性
- 满足当前需求即可
### 如果项目需求复杂或多变
推荐使用**方案一(分离式设计)**,因为:
- 更好的代码组织和架构
- 更高的可扩展性和可维护性
- 符合软件设计原则(单一职责、开闭原则等)
## 当前实现的选择
当前实现采用了方案一(分离式设计),主要考虑:
1. 项目可能需要扩展更多的网络状态管理功能
2. 网络监听功能可能在其他地方被复用
3. 遵循了良好的软件设计原则
4. 便于单元测试和维护
如果确定项目只需要基本的网络状态监听和通知功能,可以考虑切换到方案二以简化代码结构。

View File

@ -1,213 +0,0 @@
package com.ismart.ib86.feature.network;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Build;
import android.util.Log;
import com.ismart.ib86.common.notification.NotificationType;
import com.ismart.ib86.common.notification.CommonNotificationActivity;
/**
* 简化版网络状态管理器
* 将网络监听和状态管理合并为一个类
*/
public class SimpleNetworkStateManager {
private static final String TAG = "SimpleNetworkStateManager";
private Context context;
private ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;
private NetworkState currentState;
private boolean isActivityShowing = false;
public SimpleNetworkStateManager(Context context) {
this.context = context.getApplicationContext();
this.connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
this.currentState = NetworkState.NO_NETWORK;
initializeNetworkCallback();
}
private void initializeNetworkCallback() {
networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
Log.d(TAG, "Network available: " + network);
updateNetworkState();
}
@Override
public void onLost(Network network) {
super.onLost(network);
Log.d(TAG, "Network lost: " + network);
updateNetworkState();
}
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities);
Log.d(TAG, "Network capabilities changed: " + network);
updateNetworkState();
}
};
}
public void registerNetworkCallback() {
if (connectivityManager == null) {
Log.e(TAG, "ConnectivityManager is null");
return;
}
NetworkRequest networkRequest = new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.build();
try {
connectivityManager.registerNetworkCallback(networkRequest, networkCallback);
updateNetworkState(); // 初始化当前状态
Log.d(TAG, "Network callback registered");
} catch (SecurityException e) {
Log.e(TAG, "Failed to register network callback due to permission issue", e);
} catch (Exception e) {
Log.e(TAG, "Failed to register network callback", e);
}
}
public void unregisterNetworkCallback() {
if (connectivityManager == null) {
Log.e(TAG, "ConnectivityManager is null");
return;
}
try {
connectivityManager.unregisterNetworkCallback(networkCallback);
Log.d(TAG, "Network callback unregistered");
} catch (Exception e) {
Log.e(TAG, "Failed to unregister network callback", e);
}
}
private void updateNetworkState() {
NetworkState newState = getNetworkState();
if (newState != currentState) {
Log.d(TAG, "Network state changed from " + currentState + " to " + newState);
currentState = newState;
// 处理状态变化
if (currentState == NetworkState.NO_NETWORK) {
showNotification(NotificationType.NETWORK_LOST, "设备已断开网络连接请检查WiFi或移动数据设置。", android.R.drawable.ic_dialog_alert, 5, null);
} else {
hideNotification();
}
}
}
public NetworkState getNetworkState() {
if (connectivityManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork != null) {
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
if (capabilities != null) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return NetworkState.WIFI_CONNECTED;
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return NetworkState.MOBILE_CONNECTED;
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return NetworkState.ETHERNET_CONNECTED;
}
}
}
} else {
// 兼容旧版本API
android.net.NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
if (activeNetworkInfo != null && activeNetworkInfo.isConnected()) {
int type = activeNetworkInfo.getType();
if (type == ConnectivityManager.TYPE_WIFI) {
return NetworkState.WIFI_CONNECTED;
} else if (type == ConnectivityManager.TYPE_MOBILE) {
return NetworkState.MOBILE_CONNECTED;
} else if (type == ConnectivityManager.TYPE_ETHERNET) {
return NetworkState.ETHERNET_CONNECTED;
}
}
}
}
return NetworkState.NO_NETWORK;
}
public void showNotification(NotificationType type, String message, int iconResId, int durationSeconds) {
showNotification(type, message, iconResId, durationSeconds, null);
}
public void showNotification(NotificationType type, String message, int iconResId, int durationSeconds, String soundPath) {
if (isActivityShowing) {
Log.d(TAG, "Notification activity is already showing");
return;
}
if (!(context instanceof Activity)) {
Log.w(TAG, "Context is not an Activity, cannot show activity");
return;
}
Activity activity = (Activity) context;
if (activity.isFinishing() || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed())) {
Log.w(TAG, "Activity is not valid, cannot show activity");
return;
}
activity.runOnUiThread(() -> {
try {
if (isActivityShowing) {
Log.d(TAG, "Notification activity is already showing");
return;
}
Intent intent = new Intent(activity, CommonNotificationActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.putExtra(CommonNotificationActivity.EXTRA_MESSAGE, message);
intent.putExtra(CommonNotificationActivity.EXTRA_ICON, iconResId);
intent.putExtra(CommonNotificationActivity.EXTRA_DURATION, durationSeconds);
intent.putExtra(CommonNotificationActivity.EXTRA_TYPE, type.name());
if (soundPath != null) {
intent.putExtra(CommonNotificationActivity.EXTRA_SOUND_PATH, soundPath);
}
activity.startActivity(intent);
isActivityShowing = true;
Log.d(TAG, "Notification activity shown for " + durationSeconds + " seconds, type: " + type);
} catch (Exception e) {
Log.e(TAG, "Failed to show notification activity", e);
}
});
}
public void hideNotification() {
// 通过发送广播通知CommonNotificationActivity关闭自身
if (isActivityShowing) {
try {
Intent intent = new Intent(CommonNotificationActivity.ACTION_CLOSE_ACTIVITY);
context.sendBroadcast(intent);
isActivityShowing = false;
Log.d(TAG, "Notification activity hidden");
} catch (Exception e) {
Log.e(TAG, "Failed to hide notification activity", e);
}
}
}
public boolean isActivityShowing() {
return isActivityShowing;
}
public NetworkState getCurrentState() {
return currentState;
}
}

View File

@ -1,62 +0,0 @@
package com.ismart.ib86.feature.network;
import android.util.Log;
import java.util.Arrays;
import java.util.List;
/**
* 测试WiFi扫描结果发送逻辑
*/
public class TestWiFiScanResult {
private static final String TAG = "TestWiFiScanResult";
/**
* 测试格式化WiFi扫描结果
*/
public static void testFormatScanResult() {
Log.i(TAG, "=== 开始测试WiFi扫描结果格式化 ===");
// 模拟WiFi扫描结果
List<String> mockSSIDs = Arrays.asList("1603", "1603-5G", "ismart_hw_2.4g");
Log.i(TAG, "模拟WiFi扫描结果: " + mockSSIDs);
// 创建ATCommandProcessor实例进行测试
ATCommandProcessor processor = new ATCommandProcessor(null, null, null);
// 调用格式化方法
String result = null;
try {
// 使用反射调用私有方法
java.lang.reflect.Method method = ATCommandProcessor.class.getDeclaredMethod("formatScanResult", List.class);
method.setAccessible(true);
result = (String) method.invoke(processor, mockSSIDs);
} catch (Exception e) {
Log.e(TAG, "调用formatScanResult方法时出错", e);
}
Log.i(TAG, "格式化结果: " + result);
// 验证结果
if (result != null && result.startsWith("AT+SSID=3,")) {
Log.i(TAG, "✅ 格式化结果正确");
} else {
Log.e(TAG, "❌ 格式化结果不正确");
}
Log.i(TAG, "=== WiFi扫描结果格式化测试完成 ===");
}
/**
* 测试完整的WiFi扫描结果发送流程
*/
public static void testSendScanResult() {
Log.i(TAG, "=== 开始测试WiFi扫描结果发送 ===");
// 这个测试需要实际的BLE环境所以只是模拟日志输出
Log.i(TAG, "模拟WiFi扫描完成找到3个网络: [1603, 1603-5G, ismart_hw_2.4g]");
Log.i(TAG, "应该发送格式化结果: AT+SSID=3,1603,1603-5G,ismart_hw_2.4g");
Log.i(TAG, "通过UUID 0xEA01 发送通知给小程序");
Log.i(TAG, "=== WiFi扫描结果发送测试完成 ===");
}
}

View File

@ -1,69 +0,0 @@
package com.ismart.ib86.feature.network;
import android.util.Log;
import java.util.Arrays;
import java.util.List;
/**
* 测试WiFi扫描结果发送逻辑包含BSSID
*/
public class TestWiFiScanResultWithBSSID {
private static final String TAG = "TestWiFiScanResultWithBSSID";
/**
* 测试格式化WiFi扫描结果包含BSSID
*/
public static void testFormatScanResultWithBSSID() {
Log.i(TAG, "=== 开始测试WiFi扫描结果格式化包含BSSID ===");
// 模拟WiFi扫描结果包含BSSID信息
List<WiFiNetworkInfo> mockNetworks = Arrays.asList(
new WiFiNetworkInfo("1603", "e0:0c:e5:13:90:00", -87, 2437),
new WiFiNetworkInfo("1603-5G", "c4:2b:44:f1:25:80", -60, 5745),
new WiFiNetworkInfo("ismart_hw_2.4g", "c0:61:18:c9:53:2b", -95, 2412)
);
Log.i(TAG, "模拟WiFi扫描结果: " + mockNetworks);
// 创建ATCommandProcessor实例进行测试
ATCommandProcessor processor = new ATCommandProcessor(null, null, null);
// 调用新的格式化方法
String result = null;
try {
// 使用反射调用私有方法
java.lang.reflect.Method method = ATCommandProcessor.class.getDeclaredMethod("formatScanResultWithBSSID", List.class);
method.setAccessible(true);
result = (String) method.invoke(processor, mockNetworks);
} catch (Exception e) {
Log.e(TAG, "调用formatScanResultWithBSSID方法时出错", e);
}
Log.i(TAG, "格式化结果包含BSSID: " + result);
// 验证结果
if (result != null && result.startsWith("AT+SSID=3,") && result.contains(":")) {
Log.i(TAG, "✅ 格式化结果包含BSSID正确");
} else {
Log.e(TAG, "❌ 格式化结果包含BSSID不正确");
}
Log.i(TAG, "=== WiFi扫描结果格式化测试包含BSSID完成 ===");
}
/**
* 测试完整的WiFi扫描结果发送流程包含BSSID
*/
public static void testSendScanResultWithBSSID() {
Log.i(TAG, "=== 开始测试WiFi扫描结果发送包含BSSID ===");
// 这个测试需要实际的BLE环境所以只是模拟日志输出
Log.i(TAG, "模拟WiFi扫描完成找到3个网络:");
Log.i(TAG, " [0] SSID: '1603-5G', BSSID: c4:2b:44:f1:25:80");
Log.i(TAG, " [1] SSID: '1603', BSSID: e0:0c:e5:13:90:00");
Log.i(TAG, " [2] SSID: 'ismart_hw_2.4g', BSSID: c0:61:18:c9:53:2b");
Log.i(TAG, "应该发送格式化结果: AT+SSID=3,1603-5G:c4:2b:44:f1:25:80,1603:e0:0c:e5:13:90:00,ismart_hw_2.4g:c0:61:18:c9:53:2b");
Log.i(TAG, "通过UUID 0xEA01 发送通知给小程序");
Log.i(TAG, "=== WiFi扫描结果发送测试包含BSSID完成 ===");
}
}

View File

@ -0,0 +1,25 @@
<?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:gravity="center"
android:background="#000000"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="-80dp"
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="30sp"
android:textColor="#FFFFFF"
android:gravity="center" />
</LinearLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FullScreenDialog" parent="android:Theme.Dialog">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@android:color/black</item>
<!-- 隐藏导航栏 -->
<item name="android:windowTranslucentStatus">true</item>
<item name="android:windowTranslucentNavigation">true</item>
</style>
</resources>

View File

@ -1,9 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.IB86" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Base.Theme.IB86" parent="Theme.AppCompat.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="android:windowFullscreen">true</item>
</style>
<style name="Theme.IB86" parent="Base.Theme.IB86" />
</resources>