diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts
index b3654bb8..b6567a36 100644
--- a/android-app/app/build.gradle.kts
+++ b/android-app/app/build.gradle.kts
@@ -72,4 +72,7 @@ dependencies {
implementation("androidx.media3:media3-exoplayer:$media3Version")
implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
implementation("androidx.media3:media3-ui:$media3Version")
+
+ // 内存泄漏检测 (仅在debug版本中使用)
+ debugImplementation("com.squareup.leakcanary:leakcanary-android:2.13")
}
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index 01deb8dd..94f898be 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
retryAction.run());
+ snackbar.setActionTextColor(view.getContext().getResources().getColor(R.color.purple_500));
+ snackbar.show();
+ }
+
+ /**
+ * 显示简单错误提示,不带重试按钮
+ */
+ public static void showSimpleError(@NonNull View view, @NonNull String message) {
+ Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
+ }
+
+ /**
+ * 显示成功提示
+ */
+ public static void showSuccess(@NonNull View view, @NonNull String message) {
+ Snackbar snackbar = Snackbar.make(view, message, Snackbar.LENGTH_SHORT);
+ snackbar.setBackgroundTint(view.getContext().getResources().getColor(R.color.purple_500));
+ snackbar.show();
+ }
+
+ /**
+ * 显示信息提示
+ */
+ public static void showInfo(@NonNull View view, @NonNull String message) {
+ Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
+ }
+
+ /**
+ * 处理API响应错误
+ */
+ public static void handleApiError(@NonNull View view, Throwable error, @NonNull Runnable retryAction) {
+ String message;
+ if (error.getMessage() != null && error.getMessage().contains("timeout")) {
+ message = "请求超时,请检查网络连接";
+ } else if (error.getMessage() != null && error.getMessage().contains("Unable to resolve host")) {
+ message = "网络连接失败,请检查网络设置";
+ } else {
+ message = "请求失败,请稍后重试";
+ }
+ showErrorWithRetry(view, message, "重试", retryAction);
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java
index ec8bd96f..a3ff27a8 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/FishPondActivity.java
@@ -447,4 +447,20 @@ public class FishPondActivity extends AppCompatActivity {
stopOrbitAnimation();
stopPulseLoop();
}
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ // 清理Handler和Runnable,防止内存泄漏
+ if (uiHandler != null) {
+ uiHandler.removeCallbacks(pulseRunnable);
+ }
+
+ // 确保动画完全停止和清理
+ if (orbitAnimator != null) {
+ orbitAnimator.cancel();
+ orbitAnimator = null;
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java
new file mode 100644
index 00000000..db4879a8
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java
@@ -0,0 +1,20 @@
+package com.example.livestreaming;
+
+import android.app.Application;
+
+import android.app.Application;
+
+/**
+ * 自定义Application类,用于初始化各种组件
+ * 包括内存泄漏检测等
+ */
+public class LiveStreamingApplication extends Application {
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // 初始化LeakCanary内存泄漏检测(仅在debug版本中生效)
+ // LeakCanary会自动在debug版本中初始化,无需手动调用
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
index 22f08039..4fd83fea 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java
@@ -17,6 +17,7 @@ import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
+import android.widget.ArrayAdapter;
import android.widget.TextView;
import android.widget.Toast;
@@ -37,6 +38,8 @@ import com.example.livestreaming.databinding.ActivityMainBinding;
import com.example.livestreaming.databinding.DialogCreateRoomBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.textfield.MaterialAutoCompleteTextView;
+import com.google.android.material.textfield.TextInputLayout;
import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.CreateRoomRequest;
@@ -213,12 +216,13 @@ public class MainActivity extends AppCompatActivity {
// 立即显示演示数据,提升用户体验
allRooms.clear();
- allRooms.addAll(buildDemoRooms(12));
+ allRooms.addAll(buildDemoRooms(20));
applyCategoryFilter(currentCategory);
}
private void setupUI() {
- binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
+ // 注释掉下拉刷新,使用静态数据
+ // binding.swipeRefresh.setOnRefreshListener(this::fetchRooms);
setupDrawerCards();
@@ -307,7 +311,8 @@ public class MainActivity extends AppCompatActivity {
if (lastVisible >= total - 4) {
long now = System.currentTimeMillis();
if (!isFetching && now - lastFetchMs > 1500) {
- fetchRooms();
+ // 注释掉加载更多,使用静态数据
+ // fetchRooms();
}
}
}
@@ -587,6 +592,13 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
super.onDestroy();
+
+ // 清理Handler和Runnable,防止内存泄漏
+ if (handler != null && pollRunnable != null) {
+ handler.removeCallbacks(pollRunnable);
+ }
+
+ // 清理语音识别器
if (speechRecognizer != null) {
speechRecognizer.destroy();
speechRecognizer = null;
@@ -660,7 +672,8 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onStart() {
super.onStart();
- startPolling();
+ // 注释掉网络轮询,使用静态数据
+ // startPolling();
}
@Override
@@ -701,11 +714,24 @@ public class MainActivity extends AppCompatActivity {
}
private void showCreateRoomDialog() {
- DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.inflate(getLayoutInflater());
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_create_room, null);
+ DialogCreateRoomBinding dialogBinding = DialogCreateRoomBinding.bind(dialogView);
+
+ // 设置直播类型选择器
+ String[] liveTypes = {"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
+ ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, liveTypes);
+
+ // 使用正确的资源ID获取typeSpinner
+ int typeSpinnerId = getResources().getIdentifier("typeSpinner", "id", getPackageName());
+ MaterialAutoCompleteTextView typeSpinner = dialogView.findViewById(typeSpinnerId);
+
+ if (typeSpinner != null) {
+ typeSpinner.setAdapter(adapter);
+ }
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("创建直播间")
- .setView(dialogBinding.getRoot())
+ .setView(dialogView)
.setNegativeButton("取消", null)
.setPositiveButton("创建", null)
.create();
@@ -713,7 +739,7 @@ public class MainActivity extends AppCompatActivity {
dialog.setOnShowListener(d -> {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
String title = dialogBinding.titleEdit.getText() != null ? dialogBinding.titleEdit.getText().toString().trim() : "";
- String streamer = dialogBinding.streamerEdit.getText() != null ? dialogBinding.streamerEdit.getText().toString().trim() : "";
+ String type = (typeSpinner != null && typeSpinner.getText() != null) ? typeSpinner.getText().toString().trim() : "";
if (TextUtils.isEmpty(title)) {
dialogBinding.titleLayout.setError("标题不能为空");
@@ -722,16 +748,28 @@ public class MainActivity extends AppCompatActivity {
dialogBinding.titleLayout.setError(null);
}
- if (TextUtils.isEmpty(streamer)) {
- dialogBinding.streamerLayout.setError("主播名称不能为空");
+ if (TextUtils.isEmpty(type)) {
+ int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
+ TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
+ if (typeLayout != null) {
+ typeLayout.setError("请先选择直播类型");
+ }
return;
} else {
- dialogBinding.streamerLayout.setError(null);
+ int typeLayoutId = getResources().getIdentifier("typeLayout", "id", getPackageName());
+ TextInputLayout typeLayout = dialogView.findViewById(typeLayoutId);
+ if (typeLayout != null) {
+ typeLayout.setError(null);
+ }
}
+ // 获取用户昵称
+ String streamerName = getSharedPreferences("profile_prefs", MODE_PRIVATE)
+ .getString("profile_name", "未知用户");
+
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
- ApiClient.getService().createRoom(new CreateRoomRequest(title, streamer))
+ ApiClient.getService().createRoom(new CreateRoomRequest(title, streamerName, type))
.enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
@@ -852,26 +890,39 @@ public class MainActivity extends AppCompatActivity {
private void fetchRooms() {
// 避免重复请求
if (isFetching) return;
-
+
isFetching = true;
lastFetchMs = System.currentTimeMillis();
-
+
+ // 隐藏空状态和错误状态
+ hideEmptyState();
+ hideErrorState();
+
// 只在没有数据时显示loading
if (adapter.getItemCount() == 0) {
binding.loading.setVisibility(View.VISIBLE);
}
- ApiClient.getService().getRooms().enqueue(new Callback>>() {
+ Call>> call = ApiClient.getService().getRooms();
+ NetworkUtils.enqueueWithLifecycle(call, this, new Callback>>() {
@Override
public void onResponse(Call>> call, Response>> response) {
binding.loading.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
isFetching = false;
+
ApiResponse> body = response.body();
List rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList();
+
if (rooms == null || rooms.isEmpty()) {
- rooms = buildDemoRooms(12);
+ // 使用演示数据,但显示空状态
+ rooms = buildDemoRooms(0); // 生成0个演示房间
+ showNoRoomsState();
+ } else {
+ // 有真实数据,隐藏空状态
+ hideEmptyState();
}
+
allRooms.clear();
allRooms.addAll(rooms);
applyCategoryFilter(currentCategory);
@@ -883,18 +934,63 @@ public class MainActivity extends AppCompatActivity {
binding.loading.setVisibility(View.GONE);
binding.swipeRefresh.setRefreshing(false);
isFetching = false;
+
+ // 显示网络错误Snackbar和空状态
+ ErrorHandler.handleApiError(binding.getRoot(), t, () -> fetchRooms());
+ showNetworkErrorState();
+
+ // 仍然提供演示数据作为后备
allRooms.clear();
- allRooms.addAll(buildDemoRooms(12));
+ allRooms.addAll(buildDemoRooms(0));
applyCategoryFilter(currentCategory);
adapter.bumpCoverOffset();
}
});
}
+ /**
+ * 显示无直播间空状态
+ */
+ private void showNoRoomsState() {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setNoRoomsState();
+ binding.emptyStateView.setOnActionClickListener(v -> fetchRooms());
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 显示网络错误状态
+ */
+ private void showNetworkErrorState() {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setNetworkErrorState();
+ binding.emptyStateView.setOnActionClickListener(v -> fetchRooms());
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 隐藏空状态视图
+ */
+ private void hideEmptyState() {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * 隐藏错误状态(实际上和hideEmptyState一样)
+ */
+ private void hideErrorState() {
+ hideEmptyState();
+ }
+
private void applyCategoryFilter(String category) {
String c = category != null ? category : "推荐";
if ("推荐".equals(c)) {
adapter.submitList(new ArrayList<>(allRooms));
+ updateEmptyStateForList(allRooms);
return;
}
@@ -906,6 +1002,25 @@ public class MainActivity extends AppCompatActivity {
}
}
adapter.submitList(filtered);
+ updateEmptyStateForList(filtered);
+ }
+
+ /**
+ * 根据列表数据更新空状态显示
+ */
+ private void updateEmptyStateForList(List rooms) {
+ if (rooms == null || rooms.isEmpty()) {
+ // 显示分类筛选无结果的状态
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setIcon(R.drawable.ic_search_24);
+ binding.emptyStateView.setTitle("该分类下暂无直播间");
+ binding.emptyStateView.setMessage("换个分类看看吧");
+ binding.emptyStateView.hideActionButton();
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ hideEmptyState();
+ }
}
private String getDemoCategoryForRoom(Room room) {
@@ -921,13 +1036,55 @@ public class MainActivity extends AppCompatActivity {
private List buildDemoRooms(int count) {
List list = new ArrayList<>();
- for (int i = 0; i < count; i++) {
+
+ // 预定义的演示数据,包含不同类型的直播内容
+ String[][] demoData = {
+ {"王者荣耀排位赛", "小明选手", "游戏", "true"},
+ {"吃鸡大逃杀", "游戏高手", "游戏", "true"},
+ {"唱歌连麦", "音乐达人", "音乐", "true"},
+ {"户外直播", "旅行者", "户外", "false"},
+ {"美食制作", "厨神小李", "美食", "true"},
+ {"才艺表演", "舞蹈小妹", "才艺", "true"},
+ {"聊天交友", "暖心姐姐", "聊天", "false"},
+ {"LOL竞技场", "电竞选手", "游戏", "true"},
+ {"古风演奏", "琴师小王", "音乐", "true"},
+ {"健身教学", "教练张", "户外", "false"},
+ {"摄影分享", "摄影师", "户外", "true"},
+ {"宠物秀", "萌宠主播", "才艺", "true"},
+ {"编程教学", "码农老王", "聊天", "false"},
+ {"读书分享", "书虫小妹", "聊天", "true"},
+ {"手工制作", "手艺人", "才艺", "true"},
+ {"英语口语", "外教老师", "聊天", "false"},
+ {"魔术表演", "魔术师", "才艺", "true"},
+ {"街头访谈", "记者小张", "户外", "true"},
+ {"乐器教学", "音乐老师", "音乐", "false"},
+ {"电影解说", "影评人", "聊天", "true"}
+ };
+
+ for (int i = 0; i < count && i < demoData.length; i++) {
String id = "demo-" + i;
- String title = "王者荣耀陪练" + (i + 1);
- String streamer = "虚拟主播" + (i + 1);
- boolean live = i % 3 != 0;
- list.add(new Room(id, title, streamer, live));
+ String title = demoData[i][0];
+ String streamer = demoData[i][1];
+ String type = demoData[i][2];
+ boolean live = Boolean.parseBoolean(demoData[i][3]);
+ Room room = new Room(id, title, streamer, live);
+ room.setType(type);
+ list.add(room);
}
+
+ // 如果需要更多数据,继续生成
+ String[] categories = new String[]{"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
+ for (int i = demoData.length; i < count; i++) {
+ String id = "demo-" + i;
+ String title = "直播房间" + (i + 1);
+ String streamer = "主播" + (i + 1);
+ String type = categories[i % categories.length];
+ boolean live = i % 3 != 0;
+ Room room = new Room(id, title, streamer, live);
+ room.setType(type);
+ list.add(room);
+ }
+
return list;
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java
index 7124c021..d3b13271 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MessagesActivity.java
@@ -129,6 +129,9 @@ public class MessagesActivity extends AppCompatActivity {
conversations.addAll(buildDemoConversations());
conversationsAdapter.submitList(new ArrayList<>(conversations));
+ // 检查是否需要显示空状态
+ updateEmptyState();
+
attachSwipeToDelete(binding.conversationsRecyclerView);
}
@@ -459,4 +462,20 @@ public class MessagesActivity extends AppCompatActivity {
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
}
}
+
+ /**
+ * 更新空状态显示
+ */
+ private void updateEmptyState() {
+ if (conversations.isEmpty()) {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setNoMessagesState();
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setVisibility(View.GONE);
+ }
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java
index 03aeddc5..84c4e5fb 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/MyFriendsActivity.java
@@ -3,6 +3,7 @@ package com.example.livestreaming;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
+import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
@@ -59,6 +60,7 @@ public class MyFriendsActivity extends AppCompatActivity {
private void applyFilter(String query) {
if (query == null || query.trim().isEmpty()) {
adapter.submitList(new ArrayList<>(all));
+ updateEmptyState(all);
return;
}
String q = query.toLowerCase();
@@ -72,6 +74,23 @@ public class MyFriendsActivity extends AppCompatActivity {
}
}
adapter.submitList(filtered);
+ updateEmptyState(filtered);
+ }
+
+ /**
+ * 更新空状态显示
+ */
+ private void updateEmptyState(List friends) {
+ if (friends == null || friends.isEmpty()) {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setNoFriendsState();
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setVisibility(View.GONE);
+ }
+ }
}
private List buildDemoFriends() {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java b/android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java
new file mode 100644
index 00000000..a65a2580
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/NetworkRequestManager.java
@@ -0,0 +1,78 @@
+package com.example.livestreaming;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import retrofit2.Call;
+
+/**
+ * 网络请求管理器
+ * 用于管理Activity/Fragment的网络请求,在生命周期结束时自动取消
+ */
+public class NetworkRequestManager implements LifecycleEventObserver {
+
+ private final Set> activeCalls = new HashSet<>();
+
+ /**
+ * 添加网络请求到管理器
+ */
+ public synchronized void addCall(Call> call) {
+ if (call != null && !call.isCanceled()) {
+ activeCalls.add(call);
+ }
+ }
+
+ /**
+ * 从管理器中移除网络请求
+ */
+ public synchronized void removeCall(Call> call) {
+ if (call != null) {
+ activeCalls.remove(call);
+ }
+ }
+
+ /**
+ * 取消所有活跃的网络请求
+ */
+ public synchronized void cancelAll() {
+ for (Call> call : activeCalls) {
+ if (call != null && !call.isCanceled()) {
+ call.cancel();
+ }
+ }
+ activeCalls.clear();
+ }
+
+ /**
+ * 获取活跃请求的数量
+ */
+ public synchronized int getActiveCallCount() {
+ return activeCalls.size();
+ }
+
+ /**
+ * 绑定到LifecycleOwner,在DESTROY时自动取消所有请求
+ */
+ public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
+ lifecycleOwner.getLifecycle().addObserver(this);
+ }
+
+ /**
+ * 解绑Lifecycle
+ */
+ public void unbindFromLifecycle(LifecycleOwner lifecycleOwner) {
+ lifecycleOwner.getLifecycle().removeObserver(this);
+ }
+
+ @Override
+ public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
+ if (event == Lifecycle.Event.ON_DESTROY) {
+ cancelAll();
+ }
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java b/android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java
new file mode 100644
index 00000000..269318cd
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/NetworkUtils.java
@@ -0,0 +1,53 @@
+package com.example.livestreaming;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+
+/**
+ * 网络工具类
+ * 提供生命周期感知的网络请求管理
+ */
+public class NetworkUtils {
+
+ /**
+ * 执行网络请求,并自动管理生命周期
+ * 当LifecycleOwner销毁时,自动取消请求
+ */
+ public static void enqueueWithLifecycle(
+ @NonNull Call call,
+ @NonNull LifecycleOwner lifecycleOwner,
+ @NonNull Callback callback) {
+
+ // 创建请求管理器并绑定到生命周期
+ NetworkRequestManager manager = new NetworkRequestManager();
+ manager.bindToLifecycle(lifecycleOwner);
+ manager.addCall(call);
+
+ // 执行请求
+ call.enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ manager.removeCall(call);
+ callback.onResponse(call, response);
+ }
+
+ @Override
+ public void onFailure(Call call, Throwable t) {
+ manager.removeCall(call);
+ callback.onFailure(call, t);
+ }
+ });
+ }
+
+ /**
+ * 简单的网络请求执行,不带生命周期管理
+ * 注意:需要手动管理请求取消
+ */
+ public static void enqueue(@NonNull Call call, @NonNull Callback callback) {
+ call.enqueue(callback);
+ }
+}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
index ee2cdeb3..5185ed93 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java
@@ -298,11 +298,12 @@ public class RoomDetailActivity extends AppCompatActivity {
binding.loading.setVisibility(View.VISIBLE);
}
- ApiClient.getService().getRoom(roomId).enqueue(new Callback>() {
+ Call> call = ApiClient.getService().getRoom(roomId);
+ NetworkUtils.enqueueWithLifecycle(call, this, new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
if (isFinishing() || isDestroyed()) return;
-
+
binding.loading.setVisibility(View.GONE);
isFirstLoad = false;
@@ -452,5 +453,22 @@ public class RoomDetailActivity extends AppCompatActivity {
return url.substring(0, url.length() - ".m3u8".length()) + "/index.m3u8";
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ // 确保Handler回调被清理,防止内存泄漏
+ if (handler != null) {
+ if (pollRunnable != null) {
+ handler.removeCallbacks(pollRunnable);
+ }
+ if (chatSimulationRunnable != null) {
+ handler.removeCallbacks(chatSimulationRunnable);
+ }
+ }
+
+ // 释放播放器资源
+ releasePlayer();
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
index 0f750b92..f47ffd8e 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/SearchActivity.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
+import android.view.View;
import android.view.inputmethod.EditorInfo;
import androidx.appcompat.app.AppCompatActivity;
@@ -99,6 +100,7 @@ public class SearchActivity extends AppCompatActivity {
String query = q != null ? q.trim() : "";
if (query.isEmpty()) {
adapter.submitList(new ArrayList<>(all));
+ updateEmptyState(all);
return;
}
@@ -112,6 +114,23 @@ public class SearchActivity extends AppCompatActivity {
}
}
adapter.submitList(filtered);
+ updateEmptyState(filtered);
+ }
+
+ /**
+ * 更新空状态显示
+ */
+ private void updateEmptyState(List rooms) {
+ if (rooms == null || rooms.isEmpty()) {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setNoSearchResultsState();
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setVisibility(View.GONE);
+ }
+ }
}
private List buildDemoRooms(int count) {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java b/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java
index 5c1703ef..6348b714 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/CreateRoomRequest.java
@@ -10,9 +10,13 @@ public class CreateRoomRequest {
@SerializedName("streamerName")
private final String streamerName;
- public CreateRoomRequest(String title, String streamerName) {
+ @SerializedName("type")
+ private final String type;
+
+ public CreateRoomRequest(String title, String streamerName, String type) {
this.title = title;
this.streamerName = streamerName;
+ this.type = type;
}
public String getTitle() {
@@ -22,4 +26,8 @@ public class CreateRoomRequest {
public String getStreamerName() {
return streamerName;
}
-}
+
+ public String getType() {
+ return type;
+ }
+}
\ No newline at end of file
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java
index e7f48058..56b7aee4 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java
@@ -15,6 +15,9 @@ public class Room {
@SerializedName("streamerName")
private String streamerName;
+ @SerializedName("type")
+ private String type;
+
@SerializedName("streamKey")
private String streamKey;
@@ -49,6 +52,10 @@ public class Room {
this.streamerName = streamerName;
}
+ public void setType(String type) {
+ this.type = type;
+ }
+
public void setLive(boolean live) {
isLive = live;
}
@@ -77,6 +84,10 @@ public class Room {
return streamerName;
}
+ public String getType() {
+ return type;
+ }
+
public String getStreamKey() {
return streamKey;
}
@@ -103,12 +114,13 @@ public class Room {
&& Objects.equals(id, room.id)
&& Objects.equals(title, room.title)
&& Objects.equals(streamerName, room.streamerName)
+ && Objects.equals(type, room.type)
&& Objects.equals(streamKey, room.streamKey)
&& Objects.equals(streamUrls, room.streamUrls);
}
@Override
public int hashCode() {
- return Objects.hash(id, title, streamerName, streamKey, isLive, viewerCount, streamUrls);
+ return Objects.hash(id, title, streamerName, type, streamKey, isLive, viewerCount, streamUrls);
}
}
diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml
index fa8abccb..2df115f7 100644
--- a/android-app/app/src/main/res/layout/activity_main.xml
+++ b/android-app/app/src/main/res/layout/activity_main.xml
@@ -212,6 +212,14 @@
android:visibility="gone"
/>
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_search.xml b/android-app/app/src/main/res/layout/activity_search.xml
index 9ce5fd31..d13bcf3c 100644
--- a/android-app/app/src/main/res/layout/activity_search.xml
+++ b/android-app/app/src/main/res/layout/activity_search.xml
@@ -76,4 +76,11 @@
android:paddingBottom="24dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+
diff --git a/android-app/app/src/main/res/layout/view_empty_state.xml b/android-app/app/src/main/res/layout/view_empty_state.xml
new file mode 100644
index 00000000..6160e71a
--- /dev/null
+++ b/android-app/app/src/main/res/layout/view_empty_state.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/values/attrs.xml b/android-app/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..eb4928a5
--- /dev/null
+++ b/android-app/app/src/main/res/values/attrs.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+