From 484c17a4d33d7046cf08f9c4a94afbf606d367f4 Mon Sep 17 00:00:00 2001
From: ShiQi <15883326+shirenan@user.noreply.gitee.com>
Date: Tue, 23 Dec 2025 15:37:37 +0800
Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=E5=AF=B9=E5=BA=94=E7=9A=84=E5=9C=B0?=
=?UTF-8?q?=E6=96=B9=E6=B7=BB=E5=8A=A0=E4=BA=86todo=EF=BC=8C=E8=A1=A8?=
=?UTF-8?q?=E7=A4=BA=E9=9C=80=E8=A6=81=E5=93=AA=E4=BA=9B=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
android-app/app/build.gradle.kts | 6 -
android-app/app/src/main/AndroidManifest.xml | 4 +
.../example/livestreaming/ChatMessage.java | 49 +++
.../livestreaming/ConversationActivity.java | 152 ++++++++-
.../ConversationMessagesAdapter.java | 160 ++++++++-
.../livestreaming/ConversationsAdapter.java | 66 +++-
.../livestreaming/DrawGuessActivity.java | 15 +
.../livestreaming/EditProfileActivity.java | 3 +-
.../livestreaming/FansListActivity.java | 9 +
.../livestreaming/FindGameActivity.java | 15 +
.../livestreaming/FishPondActivity.java | 10 +
.../livestreaming/FollowingListActivity.java | 9 +
.../HeartbeatSignalActivity.java | 15 +
.../livestreaming/KTVTogetherActivity.java | 15 +
.../livestreaming/LikesListActivity.java | 9 +
.../LiveStreamingApplication.java | 5 +-
.../LocalNotificationManager.java | 186 +++++++++++
.../example/livestreaming/MainActivity.java | 168 ++++++++--
.../livestreaming/MessagesActivity.java | 278 +++++++++++++---
.../livestreaming/MyFriendsActivity.java | 9 +
.../livestreaming/NotificationItem.java | 116 +++++++
.../livestreaming/NotificationsActivity.java | 258 +++++++++++++++
.../livestreaming/NotificationsAdapter.java | 167 ++++++++++
.../livestreaming/OnlineDatingActivity.java | 15 +
.../livestreaming/PeaceEliteActivity.java | 15 +
.../livestreaming/ProfileActivity.java | 202 +++---------
.../livestreaming/RoomDetailActivity.java | 59 ++++
.../example/livestreaming/SearchActivity.java | 10 +
.../livestreaming/SettingsPageActivity.java | 303 +++++++++++++++++-
.../livestreaming/TabPlaceholderActivity.java | 20 +-
.../livestreaming/TableGamesActivity.java | 15 +
.../UserProfileReadOnlyActivity.java | 22 ++
.../livestreaming/VoiceMatchActivity.java | 15 +
.../livestreaming/WatchHistoryActivity.java | 9 +
.../livestreaming/WishTreeActivity.java | 146 ++++++++-
.../src/main/res/drawable/bg_circle_red.xml | 6 +
.../app/src/main/res/drawable/ic_check_24.xml | 11 +
.../main/res/drawable/ic_check_double_24.xml | 16 +
.../app/src/main/res/drawable/ic_share_24.xml | 10 +
.../src/main/res/layout/activity_messages.xml | 36 +++
.../res/layout/activity_notifications.xml | 114 +++++++
.../res/layout/activity_room_detail_new.xml | 10 +
.../src/main/res/layout/item_conversation.xml | 10 -
.../item_conversation_message_outgoing.xml | 34 +-
.../src/main/res/layout/item_notification.xml | 86 +++++
.../app/src/main/res/xml/file_paths.xml | 1 +
android-app/项目功能完善度分析.md | 36 ++-
47 files changed, 2614 insertions(+), 311 deletions(-)
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/NotificationItem.java
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java
create mode 100644 android-app/app/src/main/res/drawable/bg_circle_red.xml
create mode 100644 android-app/app/src/main/res/drawable/ic_check_24.xml
create mode 100644 android-app/app/src/main/res/drawable/ic_check_double_24.xml
create mode 100644 android-app/app/src/main/res/drawable/ic_share_24.xml
create mode 100644 android-app/app/src/main/res/layout/activity_notifications.xml
create mode 100644 android-app/app/src/main/res/layout/item_notification.xml
diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts
index e942336f..2551d5b4 100644
--- a/android-app/app/build.gradle.kts
+++ b/android-app/app/build.gradle.kts
@@ -60,12 +60,6 @@ android {
)
}
}
-
- // 编译优化
- dexOptions {
- javaMaxHeapSize = "2g"
- preDexLibraries = true
- }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index 82a6be50..c69d4ad9 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -44,6 +44,10 @@
android:name="com.example.livestreaming.NotificationSettingsActivity"
android:exported="false" />
+
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ChatMessage.java b/android-app/app/src/main/java/com/example/livestreaming/ChatMessage.java
index 5278445a..7c597827 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ChatMessage.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ChatMessage.java
@@ -1,23 +1,56 @@
package com.example.livestreaming;
+import java.util.UUID;
+
public class ChatMessage {
+ public enum MessageStatus {
+ SENDING, // 发送中
+ SENT, // 已发送
+ READ // 已读
+ }
+
+ private String messageId;
private String username;
private String message;
private long timestamp;
private boolean isSystemMessage;
+ private MessageStatus status;
+ private String avatarUrl; // 发送者头像 URL,后续从后端获取
public ChatMessage(String username, String message) {
+ this.messageId = UUID.randomUUID().toString();
this.username = username;
this.message = message;
this.timestamp = System.currentTimeMillis();
this.isSystemMessage = false;
+ // 如果是自己发送的消息,初始状态为发送中
+ this.status = "我".equals(username) ? MessageStatus.SENDING : MessageStatus.SENT;
}
public ChatMessage(String message, boolean isSystemMessage) {
+ this.messageId = UUID.randomUUID().toString();
this.message = message;
this.timestamp = System.currentTimeMillis();
this.isSystemMessage = isSystemMessage;
this.username = isSystemMessage ? "系统" : "匿名用户";
+ this.status = MessageStatus.SENT; // 系统消息默认已发送
+ }
+
+ public ChatMessage(String messageId, String username, String message, long timestamp, boolean isSystemMessage, MessageStatus status) {
+ this.messageId = messageId;
+ this.username = username;
+ this.message = message;
+ this.timestamp = timestamp;
+ this.isSystemMessage = isSystemMessage;
+ this.status = status;
+ }
+
+ public String getMessageId() {
+ return messageId;
+ }
+
+ public void setMessageId(String messageId) {
+ this.messageId = messageId;
}
public String getUsername() {
@@ -36,6 +69,14 @@ public class ChatMessage {
return isSystemMessage;
}
+ public MessageStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(MessageStatus status) {
+ this.status = status;
+ }
+
public void setUsername(String username) {
this.username = username;
}
@@ -51,4 +92,12 @@ public class ChatMessage {
public void setSystemMessage(boolean systemMessage) {
isSystemMessage = systemMessage;
}
+
+ public String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
}
\ No newline at end of file
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
index e7676a30..b01d4106 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationActivity.java
@@ -1,17 +1,25 @@
package com.example.livestreaming;
+import android.content.ClipData;
+import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.text.TextUtils;
import android.view.KeyEvent;
+import android.view.MenuItem;
import android.view.View;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.PopupMenu;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.livestreaming.databinding.ActivityConversationBinding;
+import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
@@ -25,6 +33,8 @@ public class ConversationActivity extends AppCompatActivity {
private ConversationMessagesAdapter adapter;
private final List messages = new ArrayList<>();
+ private Handler handler;
+ private Runnable statusUpdateRunnable;
public static void start(Context context, String conversationId, String title) {
Intent intent = new Intent(context, ConversationActivity.class);
@@ -43,6 +53,8 @@ public class ConversationActivity extends AppCompatActivity {
binding = ActivityConversationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
+ handler = new Handler(Looper.getMainLooper());
+
String title = getIntent() != null ? getIntent().getStringExtra(EXTRA_CONVERSATION_TITLE) : null;
binding.titleText.setText(title != null ? title : "会话");
@@ -54,7 +66,13 @@ public class ConversationActivity extends AppCompatActivity {
setupMessages();
setupInput();
- // 用户进入会话时,标记该会话的消息为已读,减少未读数量
+ // TODO: 接入后端接口 - 标记会话消息为已读
+ // 接口路径: POST /api/conversations/{conversationId}/read
+ // 请求参数:
+ // - conversationId: 会话ID(路径参数)
+ // - userId: 当前用户ID(从token中获取)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 用户进入会话时,标记该会话的所有消息为已读,减少未读数量
if (initialUnreadCount > 0) {
UnreadMessageManager.decrementUnreadCount(this, initialUnreadCount);
}
@@ -68,19 +86,90 @@ public class ConversationActivity extends AppCompatActivity {
private void setupMessages() {
adapter = new ConversationMessagesAdapter();
+
+ // 设置长按监听
+ adapter.setOnMessageLongClickListener((message, position, view) -> {
+ showMessageMenu(message, position, view);
+ });
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd(false);
binding.messagesRecyclerView.setLayoutManager(layoutManager);
binding.messagesRecyclerView.setAdapter(adapter);
+ // TODO: 接入后端接口 - 获取会话消息列表
+ // 接口路径: GET /api/conversations/{conversationId}/messages
+ // 请求参数:
+ // - conversationId: 会话ID(路径参数)
+ // - page (可选): 页码,用于分页加载历史消息
+ // - pageSize (可选): 每页数量,默认20
+ // - beforeMessageId (可选): 获取指定消息ID之前的消息,用于上拉加载更多
+ // 返回数据格式: ApiResponse>
+ // ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
+ // 消息列表应按时间正序排列(最早的在前面)
messages.clear();
String title = binding.titleText.getText() != null ? binding.titleText.getText().toString() : "";
- messages.add(new ChatMessage(title, "你好~"));
- messages.add(new ChatMessage("我", "在的,有什么需要帮忙?"));
+ ChatMessage incomingMsg = new ChatMessage(title, "你好~");
+ incomingMsg.setStatus(ChatMessage.MessageStatus.SENT);
+ messages.add(incomingMsg);
+
+ ChatMessage outgoingMsg = new ChatMessage("我", "在的,有什么需要帮忙?");
+ outgoingMsg.setStatus(ChatMessage.MessageStatus.READ);
+ messages.add(outgoingMsg);
+
adapter.submitList(new ArrayList<>(messages));
scrollToBottom();
}
+
+ private void showMessageMenu(ChatMessage message, int position, View anchorView) {
+ PopupMenu popupMenu = new PopupMenu(this, anchorView);
+ popupMenu.getMenu().add(0, 0, 0, "复制");
+ popupMenu.getMenu().add(0, 1, 0, "删除");
+
+ popupMenu.setOnMenuItemClickListener(item -> {
+ if (item.getItemId() == 0) {
+ // 复制消息
+ copyMessage(message);
+ return true;
+ } else if (item.getItemId() == 1) {
+ // 删除消息
+ deleteMessage(position);
+ return true;
+ }
+ return false;
+ });
+
+ popupMenu.show();
+ }
+
+ private void copyMessage(ChatMessage message) {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("消息", message.getMessage());
+ clipboard.setPrimaryClip(clip);
+ Snackbar.make(binding.getRoot(), "已复制到剪贴板", Snackbar.LENGTH_SHORT).show();
+ }
+
+ private void deleteMessage(int position) {
+ // TODO: 接入后端接口 - 删除消息
+ // 接口路径: DELETE /api/messages/{messageId}
+ // 请求参数:
+ // - messageId: 消息ID(路径参数)
+ // - userId: 当前用户ID(从token中获取,验证是否为消息发送者)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 删除成功后,从本地消息列表移除
+ if (position < 0 || position >= messages.size()) return;
+
+ new AlertDialog.Builder(this)
+ .setTitle("删除消息")
+ .setMessage("确定要删除这条消息吗?")
+ .setPositiveButton("删除", (dialog, which) -> {
+ messages.remove(position);
+ adapter.submitList(new ArrayList<>(messages));
+ Snackbar.make(binding.getRoot(), "消息已删除", Snackbar.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
private void setupInput() {
binding.sendButton.setOnClickListener(v -> sendMessage());
@@ -99,17 +188,70 @@ public class ConversationActivity extends AppCompatActivity {
}
private void sendMessage() {
+ // TODO: 接入后端接口 - 发送私信消息
+ // 接口路径: POST /api/conversations/{conversationId}/messages
+ // 请求参数:
+ // - conversationId: 会话ID(路径参数)
+ // - message: 消息内容
+ // - userId: 发送者用户ID(从token中获取)
+ // 返回数据格式: ApiResponse
+ // ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp, status等字段
+ // 发送成功后,更新消息状态为SENT,并添加到消息列表
+ // TODO: 接入后端接口 - 接收对方已读状态(WebSocket或轮询)
+ // 方案1: WebSocket实时推送
+ // - 连接: ws://api.example.com/conversations/{conversationId}
+ // - 接收消息格式: {type: "read", messageId: "xxx"} 或 {type: "new_message", data: ChatMessage}
+ // 方案2: 轮询检查消息状态
+ // - 接口路径: GET /api/conversations/{conversationId}/messages/{messageId}/status
+ // - 返回数据格式: ApiResponse<{status: "sent"|"read"}>
String text = binding.messageInput.getText() != null ? binding.messageInput.getText().toString().trim() : "";
if (TextUtils.isEmpty(text)) return;
- messages.add(new ChatMessage("我", text));
+ ChatMessage newMessage = new ChatMessage("我", text);
+ newMessage.setStatus(ChatMessage.MessageStatus.SENDING);
+ messages.add(newMessage);
adapter.submitList(new ArrayList<>(messages));
binding.messageInput.setText("");
scrollToBottom();
+
+ // 模拟消息发送过程:发送中 -> 已发送 -> 已读
+ if (statusUpdateRunnable != null) {
+ handler.removeCallbacks(statusUpdateRunnable);
+ }
+
+ statusUpdateRunnable = () -> {
+ // 1秒后更新为已发送
+ newMessage.setStatus(ChatMessage.MessageStatus.SENT);
+ adapter.notifyItemChanged(messages.size() - 1);
+
+ // 再2秒后更新为已读
+ handler.postDelayed(() -> {
+ newMessage.setStatus(ChatMessage.MessageStatus.READ);
+ adapter.notifyItemChanged(messages.size() - 1);
+ }, 2000);
+ };
+
+ handler.postDelayed(statusUpdateRunnable, 1000);
}
private void scrollToBottom() {
if (messages.isEmpty()) return;
- binding.messagesRecyclerView.post(() -> binding.messagesRecyclerView.scrollToPosition(messages.size() - 1));
+ binding.messagesRecyclerView.post(() -> {
+ int lastPosition = messages.size() - 1;
+ if (lastPosition >= 0) {
+ binding.messagesRecyclerView.smoothScrollToPosition(lastPosition);
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 清理Handler中的延迟任务,防止内存泄漏
+ if (handler != null && statusUpdateRunnable != null) {
+ handler.removeCallbacks(statusUpdateRunnable);
+ }
+ handler = null;
+ statusUpdateRunnable = null;
}
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java
index b611abea..40c2010a 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationMessagesAdapter.java
@@ -11,6 +11,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
@@ -23,6 +24,12 @@ import java.util.Locale;
public class ConversationMessagesAdapter extends ListAdapter {
+ public interface OnMessageLongClickListener {
+ void onMessageLongClick(ChatMessage message, int position, View view);
+ }
+
+ private OnMessageLongClickListener longClickListener;
+
private static final int TYPE_INCOMING = 1;
private static final int TYPE_OUTGOING = 2;
@@ -32,6 +39,10 @@ public class ConversationMessagesAdapter extends ListAdapter {
+ if (listener != null) {
+ int position = getBindingAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onMessageLongClick(message, position, v);
+ return true;
+ }
+ }
+ return false;
+ });
+
// 确保头像圆形裁剪设置正确
setupAvatarOutline();
- // 加载对方头像(可以根据用户名或其他信息加载)
- loadAvatar(message.getUsername());
+ // 加载对方头像(优先使用消息中的头像 URL,如果没有则使用默认头像)
+ loadAvatar(message);
}
- private void loadAvatar(String username) {
+ private void loadAvatar(ChatMessage message) {
+ // TODO: 接入后端接口 - 加载消息发送者头像
+ // 接口路径: GET /api/users/{userId}/avatar 或直接从ChatMessage的avatarUrl字段获取
+ // ChatMessage对象应包含avatarUrl字段,如果为空,则根据userId调用接口获取
+ // 建议后端在返回消息列表时,每条消息都包含发送者的avatarUrl,避免额外请求
if (avatarView == null) return;
- // 这里可以根据用户名加载对应的头像
- // 暂时使用默认头像,后续可以根据实际需求从服务器或本地加载
try {
- // 可以根据username从SharedPreferences或其他地方加载头像
- // 这里先使用默认头像,使用Glide确保圆形裁剪
+ // 优先使用消息中的头像 URL(从后端获取)
+ String avatarUrl = message != null ? message.getAvatarUrl() : null;
+ if (!TextUtils.isEmpty(avatarUrl)) {
+ Glide.with(avatarView)
+ .load(avatarUrl)
+ .circleCrop()
+ .error(R.drawable.ic_account_circle_24)
+ .placeholder(R.drawable.ic_account_circle_24)
+ .into(avatarView);
+ return;
+ }
+
+ // 如果没有头像 URL,暂时根据用户名生成不同的默认头像
+ // 这样不同发送者至少能通过不同的默认头像区分开
+ String username = message != null ? message.getUsername() : null;
+ int defaultAvatarRes = getDefaultAvatarForUsername(username);
Glide.with(avatarView)
- .load(R.drawable.ic_account_circle_24)
+ .load(defaultAvatarRes)
.circleCrop()
.into(avatarView);
} catch (Exception e) {
+ // 如果加载失败,使用默认头像
Glide.with(avatarView)
.load(R.drawable.ic_account_circle_24)
.circleCrop()
.into(avatarView);
}
}
+
+ /**
+ * 根据用户名生成一个稳定的默认头像资源
+ * 这样同一个用户名的消息会显示相同的默认头像
+ * TODO: 后续接入后端接口后,这个方法可以改为从后端获取头像 URL
+ */
+ private int getDefaultAvatarForUsername(String username) {
+ if (TextUtils.isEmpty(username)) {
+ return R.drawable.ic_account_circle_24;
+ }
+
+ // 使用用户名的哈希值来选择不同的默认头像
+ // 这样不同的发送者至少能通过不同的默认头像区分开
+ int hash = Math.abs(username.hashCode());
+ int[] defaultAvatars = {
+ R.drawable.ic_account_circle_24,
+ // 如果有其他默认头像资源,可以在这里添加
+ // R.drawable.default_avatar_1,
+ // R.drawable.default_avatar_2,
+ };
+
+ return defaultAvatars[hash % defaultAvatars.length];
+ }
}
static class OutgoingVH extends RecyclerView.ViewHolder {
private final ImageView avatarView;
private final TextView msgText;
private final TextView timeText;
+ private final ImageView statusIcon;
OutgoingVH(@NonNull View itemView) {
super(itemView);
avatarView = itemView.findViewById(R.id.avatarView);
msgText = itemView.findViewById(R.id.messageText);
timeText = itemView.findViewById(R.id.timeText);
+ statusIcon = itemView.findViewById(R.id.statusIcon);
setupAvatarOutline();
}
@@ -157,11 +226,51 @@ public class ConversationMessagesAdapter extends ListAdapter {
+ if (listener != null) {
+ int position = getBindingAdapterPosition();
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onMessageLongClick(message, position, v);
+ return true;
+ }
+ }
+ return false;
+ });
+
// 确保头像圆形裁剪设置正确
setupAvatarOutline();
// 加载用户头像
@@ -177,10 +286,27 @@ public class ConversationMessagesAdapter extends ListAdapter DIFF = new DiffUtil.ItemCallback() {
@Override
public boolean areItemsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
- return oldItem.getTimestamp() == newItem.getTimestamp();
+ // 使用messageId作为唯一标识
+ return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId());
}
@Override
public boolean areContentsTheSame(@NonNull ChatMessage oldItem, @NonNull ChatMessage newItem) {
- return oldItem.getTimestamp() == newItem.getTimestamp();
+ // 比较消息内容、状态等是否相同
+ return oldItem.getMessageId() != null && oldItem.getMessageId().equals(newItem.getMessageId()) &&
+ oldItem.getStatus() == newItem.getStatus() &&
+ oldItem.getMessage() != null && oldItem.getMessage().equals(newItem.getMessage());
}
};
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java
index c9f18959..afaea9cb 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ConversationsAdapter.java
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import androidx.annotation.NonNull;
+import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
@@ -65,12 +66,6 @@ public class ConversationsAdapter extends ListAdapter {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -57,5 +61,16 @@ public class DrawGuessActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
index f227a942..ef0a6e97 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/EditProfileActivity.java
@@ -216,7 +216,8 @@ public class EditProfileActivity extends AppCompatActivity {
}
out.flush();
- return Uri.fromFile(file);
+ // 使用 FileProvider 生成 URI,确保在 Android 10+ 上也能正常访问
+ return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file);
} catch (Exception e) {
Toast.makeText(this, "头像保存失败", Toast.LENGTH_SHORT).show();
return null;
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FansListActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FansListActivity.java
index 38db12e3..80998869 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/FansListActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/FansListActivity.java
@@ -35,6 +35,15 @@ public class FansListActivity extends AppCompatActivity {
Toast.makeText(this, "打开粉丝:" + item.getName(), Toast.LENGTH_SHORT).show();
});
+ // TODO: 接入后端接口 - 获取粉丝列表
+ // 接口路径: GET /api/fans
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // User对象应包含: id, name, avatarUrl, bio, isLive, followTime等字段
+ // 列表应按关注时间倒序排列(最新关注的在前)
binding.fansRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.fansRecyclerView.setAdapter(adapter);
adapter.submitList(buildDemoFans());
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java
index 2b7e09e1..8c499787 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/FindGameActivity.java
@@ -32,6 +32,10 @@ public class FindGameActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -57,5 +61,16 @@ public class FindGameActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
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 a3ff27a8..dacebec9 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
@@ -320,6 +320,16 @@ public class FishPondActivity extends AppCompatActivity {
}
private void refreshUsers() {
+ // TODO: 接入后端接口 - 获取附近用户(缘池功能)
+ // 接口路径: GET /api/users/nearby
+ // 请求参数:
+ // - latitude: 当前用户纬度(必填)
+ // - longitude: 当前用户经度(必填)
+ // - radius (可选): 搜索半径(单位:米,默认5000)
+ // - limit (可选): 返回数量,默认6
+ // 返回数据格式: ApiResponse>
+ // User对象应包含: id, name, avatarUrl, location, bio, distance(距离,单位:米), isLive等字段
+ // 返回距离最近的N个用户,用于显示在轨道上
if (binding == null) return;
int[] avatars = new int[] {
diff --git a/android-app/app/src/main/java/com/example/livestreaming/FollowingListActivity.java b/android-app/app/src/main/java/com/example/livestreaming/FollowingListActivity.java
index c065f270..83f109e7 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/FollowingListActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/FollowingListActivity.java
@@ -35,6 +35,15 @@ public class FollowingListActivity extends AppCompatActivity {
Toast.makeText(this, "打开关注:" + item.getName(), Toast.LENGTH_SHORT).show();
});
+ // TODO: 接入后端接口 - 获取关注列表
+ // 接口路径: GET /api/following
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // User对象应包含: id, name, avatarUrl, bio, isLive, lastLiveTime, followTime等字段
+ // 列表应按关注时间倒序或最后直播时间倒序排列
binding.followingRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.followingRecyclerView.setAdapter(adapter);
adapter.submitList(buildDemoFollowing());
diff --git a/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java b/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java
index fbe1e118..e3e2cedf 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/HeartbeatSignalActivity.java
@@ -33,6 +33,10 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -58,5 +62,16 @@ public class HeartbeatSignalActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java b/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java
index 1765b46b..129e9b9b 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/KTVTogetherActivity.java
@@ -32,6 +32,10 @@ public class KTVTogetherActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -57,5 +61,16 @@ public class KTVTogetherActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java b/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java
index 0a8df165..9340ae01 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/LikesListActivity.java
@@ -35,6 +35,15 @@ public class LikesListActivity extends AppCompatActivity {
Toast.makeText(this, "查看获赞:" + item.getTitle(), Toast.LENGTH_SHORT).show();
});
+ // TODO: 接入后端接口 - 获取获赞列表
+ // 接口路径: GET /api/likes
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // LikeItem对象应包含: id, userId, username, avatarUrl, targetType (room/work), targetId, targetTitle, likeTime等字段
+ // 列表应按点赞时间倒序排列(最新点赞的在前面)
binding.likesRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.likesRecyclerView.setAdapter(adapter);
adapter.submitList(buildDemoLikes());
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
index db4879a8..88a43bb3 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/LiveStreamingApplication.java
@@ -2,8 +2,6 @@ package com.example.livestreaming;
import android.app.Application;
-import android.app.Application;
-
/**
* 自定义Application类,用于初始化各种组件
* 包括内存泄漏检测等
@@ -16,5 +14,8 @@ public class LiveStreamingApplication extends Application {
// 初始化LeakCanary内存泄漏检测(仅在debug版本中生效)
// LeakCanary会自动在debug版本中初始化,无需手动调用
+
+ // 初始化通知渠道
+ LocalNotificationManager.createNotificationChannel(this);
}
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java b/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java
new file mode 100644
index 00000000..ddfa9d18
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/LocalNotificationManager.java
@@ -0,0 +1,186 @@
+package com.example.livestreaming;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build;
+
+import androidx.core.app.NotificationCompat;
+
+/**
+ * 本地通知管理器
+ * 用于发送本地通知(即使后端未就绪)
+ */
+public class LocalNotificationManager {
+
+ private static final String CHANNEL_ID = "livestreaming_notifications";
+ private static final String CHANNEL_NAME = "直播应用通知";
+ private static final String PREFS_NAME = "notification_prefs";
+ private static final String KEY_NOTIFICATION_ID = "notification_id";
+
+ /**
+ * 初始化通知渠道(Android 8.0+需要)
+ */
+ public static void createNotificationChannel(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ if (notificationManager == null) return;
+
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+ channel.setDescription("直播应用通知渠道");
+ channel.enableLights(true);
+ channel.enableVibration(true);
+
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ /**
+ * 发送通知
+ */
+ public static void sendNotification(Context context, String title, String content, NotificationItem.Type type) {
+ if (context == null || title == null || content == null) return;
+
+ // 检查通知设置
+ if (!shouldShowNotification(context, type)) {
+ return;
+ }
+
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ if (notificationManager == null) return;
+
+ // 创建点击通知后的Intent
+ Intent intent = new Intent(context, NotificationsActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
+ );
+
+ // 构建通知
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(getIconForType(type))
+ .setContentTitle(title)
+ .setContentText(content)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true)
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(content));
+
+ // 发送通知
+ int notificationId = getNextNotificationId(context);
+ notificationManager.notify(notificationId, builder.build());
+ }
+
+ /**
+ * 发送系统通知
+ */
+ public static void sendSystemNotification(Context context, String title, String content) {
+ sendNotification(context, title, content, NotificationItem.Type.SYSTEM);
+ }
+
+ /**
+ * 发送互动通知
+ */
+ public static void sendInteractionNotification(Context context, String title, String content) {
+ sendNotification(context, title, content, NotificationItem.Type.INTERACTION);
+ }
+
+ /**
+ * 发送关注通知
+ */
+ public static void sendFollowNotification(Context context, String title, String content) {
+ sendNotification(context, title, content, NotificationItem.Type.FOLLOW);
+ }
+
+ /**
+ * 发送私信通知
+ */
+ public static void sendMessageNotification(Context context, String title, String content) {
+ sendNotification(context, title, content, NotificationItem.Type.MESSAGE);
+ }
+
+ /**
+ * 发送直播通知
+ */
+ public static void sendLiveNotification(Context context, String title, String content) {
+ sendNotification(context, title, content, NotificationItem.Type.LIVE);
+ }
+
+ /**
+ * 检查是否应该显示通知
+ */
+ private static boolean shouldShowNotification(Context context, NotificationItem.Type type) {
+ SharedPreferences prefs = context.getSharedPreferences("notification_settings", Context.MODE_PRIVATE);
+
+ // 检查系统通知总开关
+ boolean systemEnabled = prefs.getBoolean("system_notifications", true);
+ if (!systemEnabled) return false;
+
+ // 检查免打扰
+ boolean dndEnabled = prefs.getBoolean("dnd_enabled", false);
+ if (dndEnabled) {
+ // 检查当前时间是否在免打扰时段
+ // 这里简化处理,实际应该检查具体时间
+ }
+
+ // 检查具体类型的通知开关
+ switch (type) {
+ case FOLLOW:
+ return prefs.getBoolean("follow_notifications", true);
+ case INTERACTION:
+ return prefs.getBoolean("comment_notifications", true);
+ case MESSAGE:
+ return prefs.getBoolean("message_notifications", true);
+ case LIVE:
+ return prefs.getBoolean("live_notifications", true);
+ case SYSTEM:
+ default:
+ return systemEnabled;
+ }
+ }
+
+ /**
+ * 获取通知类型的图标
+ */
+ private static int getIconForType(NotificationItem.Type type) {
+ if (type == null) return android.R.drawable.ic_dialog_info;
+
+ switch (type) {
+ case SYSTEM:
+ return android.R.drawable.ic_dialog_info;
+ case INTERACTION:
+ return android.R.drawable.ic_menu_share;
+ case FOLLOW:
+ return android.R.drawable.ic_menu_myplaces;
+ case MESSAGE:
+ return android.R.drawable.ic_dialog_email;
+ case LIVE:
+ return android.R.drawable.ic_media_play;
+ default:
+ return android.R.drawable.ic_dialog_info;
+ }
+ }
+
+ /**
+ * 获取下一个通知ID
+ */
+ private static int getNextNotificationId(Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ int id = prefs.getInt(KEY_NOTIFICATION_ID, 0);
+ id++;
+ if (id > 10000) id = 1; // 防止ID过大
+ prefs.edit().putInt(KEY_NOTIFICATION_ID, id).apply();
+ return id;
+ }
+}
+
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 4d77362b..dd525d04 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
@@ -25,6 +25,7 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
+import androidx.core.content.FileProvider;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
@@ -81,6 +82,7 @@ public class MainActivity extends AppCompatActivity {
private long lastFetchMs;
private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200;
+ private static final int REQUEST_LOCATION_PERMISSION = 201;
private SpeechRecognizer speechRecognizer;
private Intent speechRecognizerIntent;
private boolean isListening = false;
@@ -99,26 +101,36 @@ public class MainActivity extends AppCompatActivity {
loadAvatarFromPrefs();
setupSpeechRecognizer();
+ // TODO: 接入后端接口 - 获取未读消息总数
+ // 接口路径: GET /api/messages/unread/count
+ // 请求参数: 无(从token中获取userId)
+ // 返回数据格式: ApiResponse 或 ApiResponse<{unreadCount: number}>
+ // 返回当前用户所有会话的未读消息总数
// 初始化未读消息数量(演示数据)
if (UnreadMessageManager.getUnreadCount(this) == 0) {
// 从消息列表计算总未读数量
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
}
- // 清除默认选中状态,让所有标签页初始显示为未选中样式
+ // 初始化顶部标签页数据
+ initializeTopTabData();
+
+ // 默认显示"发现"界面
if (binding != null && binding.topTabs != null) {
- // 在布局完成后清除默认选中状态
+ // 在布局完成后,默认选中"发现"标签(索引为1:关注=0, 发现=1, 附近=2)
binding.topTabs.post(() -> {
- // 清除所有选中状态
- for (int i = 0; i < binding.topTabs.getTabCount(); i++) {
- TabLayout.Tab tab = binding.topTabs.getTabAt(i);
- if (tab != null && tab.isSelected()) {
- // 取消选中,但不触发监听器
- binding.topTabs.selectTab(null, false);
- break;
- }
+ TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1); // "发现"标签的索引
+ if (discoverTab != null) {
+ // 选中"发现"标签,这会触发 onTabSelected 监听器,自动调用 showDiscoverTab()
+ discoverTab.select();
+ } else {
+ // 如果找不到标签,直接显示发现页面
+ showDiscoverTab();
}
});
+ } else {
+ // 如果 topTabs 为空,直接显示发现页面
+ showDiscoverTab();
}
// 异步加载资源文件,避免阻塞主线程
@@ -241,12 +253,8 @@ public class MainActivity extends AppCompatActivity {
animator.setChangeDuration(200); // 变更动画时长
binding.roomsRecyclerView.setItemAnimator(animator);
- // 立即显示演示数据,提升用户体验
- // 注意:如果后续需要从网络加载,骨架屏会在fetchRooms中显示
- allRooms.clear();
- allRooms.addAll(buildDemoRooms(20));
- // 使用带动画的筛选方法
- applyCategoryFilterWithAnimation(currentCategory);
+ // 注意:房间数据会在 showDiscoverTab() 中从 discoverRooms 加载
+ // 这里不再直接显示数据,而是等待顶部标签初始化完成后再显示
}
private void setupUI() {
@@ -270,6 +278,16 @@ public class MainActivity extends AppCompatActivity {
finish();
});
+ // 设置通知图标点击事件(如果存在)
+ try {
+ View notificationIcon = findViewById(R.id.notificationIcon);
+ if (notificationIcon != null) {
+ notificationIcon.setOnClickListener(v -> NotificationsActivity.start(this));
+ }
+ } catch (Exception e) {
+ // 如果通知图标不存在,忽略错误
+ }
+
binding.topTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
@@ -413,10 +431,14 @@ public class MainActivity extends AppCompatActivity {
// 文本为空,显示麦克风图标
binding.micIcon.setImageResource(R.drawable.ic_mic_24);
binding.micIcon.setContentDescription("mic");
+ // 清除搜索筛选,显示所有房间
+ applySearchFilter("");
} else {
// 文本不为空,显示搜索图标
binding.micIcon.setImageResource(R.drawable.ic_search_24);
binding.micIcon.setContentDescription("search");
+ // 实时筛选房间
+ applySearchFilter(text);
}
}
@@ -435,6 +457,13 @@ public class MainActivity extends AppCompatActivity {
startVoiceRecognition();
} else {
// 如果文本不为空,执行搜索
+ // TODO: 接入后端接口 - 搜索功能
+ // 接口路径: GET /api/search
+ // 请求参数:
+ // - keyword: 搜索关键词
+ // - type (可选): 搜索类型(room/user/all)
+ // - page (可选): 页码
+ // 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
// 跳转到搜索页面并传递搜索关键词
SearchActivity.start(MainActivity.this, searchText);
}
@@ -642,10 +671,27 @@ public class MainActivity extends AppCompatActivity {
String avatarUri = getSharedPreferences("profile_prefs", MODE_PRIVATE)
.getString("profile_avatar_uri", null);
if (!TextUtils.isEmpty(avatarUri)) {
+ Uri uri = Uri.parse(avatarUri);
+ // 如果是 file:// 协议,尝试转换为 FileProvider URI
+ if ("file".equals(uri.getScheme())) {
+ try {
+ java.io.File file = new java.io.File(uri.getPath());
+ if (file.exists()) {
+ uri = FileProvider.getUriForFile(
+ this,
+ getPackageName() + ".fileprovider",
+ file
+ );
+ }
+ } catch (Exception e) {
+ // 如果转换失败,使用原始 URI
+ }
+ }
Glide.with(this)
- .load(Uri.parse(avatarUri))
+ .load(uri)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
+ .placeholder(R.drawable.ic_account_circle_24)
.into(binding.avatarButton);
return;
}
@@ -801,7 +847,15 @@ public class MainActivity extends AppCompatActivity {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
- ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamer))
+ // TODO: 接入后端接口 - 创建直播间
+ // 接口路径: POST /api/rooms
+ // 请求参数: CreateRoomRequest
+ // - title: 直播间标题
+ // - streamerName: 主播名称
+ // - type: 直播类型(如"live")
+ // 返回数据格式: ApiResponse
+ // Room对象应包含: id, title, streamerName, streamKey, streamUrls (包含rtmp, flv, hls地址)等字段
+ ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, "live"))
.enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
@@ -933,6 +987,14 @@ public class MainActivity extends AppCompatActivity {
}
private void fetchRooms() {
+ // TODO: 接入后端接口 - 获取房间列表
+ // 接口路径: GET /api/rooms
+ // 请求参数:
+ // - category (可选): 分类筛选,如"游戏"、"才艺"等
+ // - page (可选): 页码,用于分页加载
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // Room对象应包含: id, title, streamerName, type, isLive, coverUrl, viewerCount, streamUrls等字段
// 避免重复请求
if (isFetching) return;
@@ -1161,6 +1223,47 @@ public class MainActivity extends AppCompatActivity {
.start();
}
+ /**
+ * 应用搜索筛选
+ * 根据搜索文本筛选房间列表(仅按房间标题和主播名称,不按分类筛选)
+ */
+ private void applySearchFilter(String searchQuery) {
+ String query = searchQuery != null ? searchQuery.trim().toLowerCase() : "";
+
+ // 如果搜索文本为空,恢复分类筛选
+ if (query.isEmpty()) {
+ applyCategoryFilterWithAnimation(currentCategory);
+ return;
+ }
+
+ // 根据当前顶部标签选择要筛选的数据源
+ List sourceRooms;
+ if ("关注".equals(currentTopTab)) {
+ sourceRooms = followRooms;
+ } else if ("附近".equals(currentTopTab)) {
+ // 附近页面不显示房间,不需要搜索筛选
+ return;
+ } else {
+ // 发现页面
+ sourceRooms = discoverRooms.isEmpty() ? allRooms : discoverRooms;
+ }
+
+ // 只根据搜索文本筛选(按房间标题和主播名称),不考虑分类
+ List searchFiltered = new ArrayList<>();
+ for (Room r : sourceRooms) {
+ if (r == null) continue;
+ String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
+ String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
+ if (title.contains(query) || streamer.contains(query)) {
+ searchFiltered.add(r);
+ }
+ }
+
+ // 更新列表
+ adapter.submitList(searchFiltered);
+ updateEmptyStateForList(searchFiltered);
+ }
+
/**
* 恢复分类标签的选中状态
*/
@@ -1445,6 +1548,15 @@ public class MainActivity extends AppCompatActivity {
* 构建关注页面的房间列表(已关注主播的直播)
*/
private List buildFollowRooms() {
+ // TODO: 接入后端接口 - 获取关注主播的直播间列表
+ // 接口路径: GET /api/following/rooms 或 GET /api/rooms?type=following
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // Room对象应包含: id, title, streamerName, streamerId, type, isLive, coverUrl, viewerCount等字段
+ // 只返回当前用户已关注的主播正在直播的房间
List list = new ArrayList<>();
// 从FollowingListActivity获取已关注的主播列表
@@ -1478,6 +1590,15 @@ public class MainActivity extends AppCompatActivity {
* 构建发现页面的房间列表(推荐算法前端实现)
*/
private List buildDiscoverRooms() {
+ // TODO: 接入后端接口 - 获取推荐直播间列表
+ // 接口路径: GET /api/rooms/recommend 或 GET /api/rooms?type=recommend
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取,用于个性化推荐)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // Room对象应包含: id, title, streamerName, type, isLive, coverUrl, viewerCount, recommendScore等字段
+ // 后端应根据用户观看历史、点赞记录、关注关系等进行个性化推荐
List list = new ArrayList<>();
// 推荐算法:基于观看历史、点赞等模拟数据
@@ -1544,6 +1665,17 @@ public class MainActivity extends AppCompatActivity {
* 构建附近页面的用户列表(使用模拟位置数据)
*/
private List buildNearbyUsers() {
+ // TODO: 接入后端接口 - 获取附近用户列表
+ // 接口路径: GET /api/users/nearby
+ // 请求参数:
+ // - latitude: 当前用户纬度(必填)
+ // - longitude: 当前用户经度(必填)
+ // - radius (可选): 搜索半径(单位:米,默认5000)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // NearbyUser对象应包含: id, name, avatarUrl, distance (距离,单位:米), isLive, location等字段
+ // 需要先获取用户位置权限,然后调用此接口
List list = new ArrayList<>();
// 模拟位置数据:生成不同距离的用户
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 75864a6f..779b9a24 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
@@ -7,28 +7,38 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
import android.util.TypedValue;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
import com.example.livestreaming.databinding.ActivityMessagesBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.android.material.textfield.TextInputEditText;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
public class MessagesActivity extends AppCompatActivity {
private ActivityMessagesBinding binding;
- private final List conversations = new ArrayList<>();
+ private final List allConversations = new ArrayList<>(); // 保存所有会话,用于搜索
+ private final List conversations = new ArrayList<>(); // 当前显示的会话列表
private ConversationsAdapter conversationsAdapter;
private int swipedPosition = RecyclerView.NO_POSITION;
@@ -59,6 +69,7 @@ public class MessagesActivity extends AppCompatActivity {
setContentView(binding.getRoot());
setupConversationList();
+ setupSearch();
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_messages);
@@ -106,27 +117,43 @@ public class MessagesActivity extends AppCompatActivity {
conversationsAdapter = new ConversationsAdapter(item -> {
if (item == null) return;
- // 启动会话页面,传递未读数量
- Intent intent = new Intent(this, ConversationActivity.class);
- intent.putExtra("extra_conversation_id", item.getId());
- intent.putExtra("extra_conversation_title", item.getTitle());
- intent.putExtra("extra_unread_count", item.getUnreadCount());
- startActivity(intent);
-
- // 用户点击会话时,减少该会话的未读数量
- if (item.getUnreadCount() > 0) {
- // 更新该会话的未读数量为0(在实际应用中,这里应该更新数据源)
- // 然后更新总未读数量
- UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
- // 更新列表中的未读数量显示
- updateConversationUnreadCount(item.getId(), 0);
+ try {
+ // 启动会话页面,传递未读数量
+ Intent intent = new Intent(this, ConversationActivity.class);
+ intent.putExtra("extra_conversation_id", item.getId());
+ intent.putExtra("extra_conversation_title", item.getTitle());
+ intent.putExtra("extra_unread_count", item.getUnreadCount());
+ startActivity(intent);
+
+ // 用户点击会话时,减少该会话的未读数量
+ if (item.getUnreadCount() > 0) {
+ // 更新该会话的未读数量为0(在实际应用中,这里应该更新数据源)
+ // 然后更新总未读数量
+ UnreadMessageManager.decrementUnreadCount(this, item.getUnreadCount());
+ // 更新列表中的未读数量显示
+ updateConversationUnreadCount(item.getId(), 0);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, "打开会话失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
binding.conversationsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.conversationsRecyclerView.setAdapter(conversationsAdapter);
+ // TODO: 接入后端接口 - 获取会话列表
+ // 接口路径: GET /api/conversations
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // ConversationItem对象应包含: id, title, lastMessage, timeText, unreadCount, isMuted, avatarUrl等字段
+ // 会话列表应按最后一条消息时间倒序排列
+ allConversations.clear();
+ allConversations.addAll(buildDemoConversations());
conversations.clear();
- conversations.addAll(buildDemoConversations());
+ conversations.addAll(allConversations);
conversationsAdapter.submitList(new ArrayList<>(conversations));
// 检查是否需要显示空状态
@@ -134,6 +161,91 @@ public class MessagesActivity extends AppCompatActivity {
attachSwipeToDelete(binding.conversationsRecyclerView);
}
+
+ private void setupSearch() {
+ TextInputEditText searchInput = binding.searchInput;
+ if (searchInput == null) return;
+
+ // 监听搜索输入
+ searchInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ filterConversations(s.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ // 监听搜索按钮点击
+ searchInput.setOnEditorActionListener((v, actionId, event) -> {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH ||
+ (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN)) {
+ // 隐藏键盘
+ View view = getCurrentFocus();
+ if (view != null) {
+ android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private void filterConversations(String query) {
+ // TODO: 接入后端接口 - 搜索会话
+ // 接口路径: GET /api/conversations/search
+ // 请求参数:
+ // - keyword: 搜索关键词
+ // - userId: 当前用户ID(从token中获取)
+ // 返回数据格式: ApiResponse>
+ // 搜索范围包括:会话标题、最后一条消息内容、对方用户名等
+ conversations.clear();
+
+ if (query == null || query.trim().isEmpty()) {
+ // 搜索为空,显示所有会话
+ conversations.addAll(allConversations);
+ } else {
+ // 过滤会话:按标题和最后一条消息内容搜索
+ String lowerQuery = query.toLowerCase(Locale.getDefault());
+ for (ConversationItem item : allConversations) {
+ if (item == null) continue;
+
+ // 搜索会话标题
+ String title = item.getTitle() != null ? item.getTitle().toLowerCase(Locale.getDefault()) : "";
+ // 搜索最后一条消息内容
+ String lastMessage = item.getLastMessage() != null ? item.getLastMessage().toLowerCase(Locale.getDefault()) : "";
+
+ if (title.contains(lowerQuery) || lastMessage.contains(lowerQuery)) {
+ conversations.add(item);
+ }
+ }
+ }
+
+ conversationsAdapter.submitList(new ArrayList<>(conversations));
+ updateEmptyState();
+ }
+
+ /**
+ * 更新空状态显示
+ */
+ private void updateEmptyState() {
+ if (binding == null) return;
+
+ boolean isEmpty = conversations == null || conversations.isEmpty();
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setVisibility(isEmpty ? View.VISIBLE : View.GONE);
+ }
+ if (binding.conversationsRecyclerView != null) {
+ binding.conversationsRecyclerView.setVisibility(isEmpty ? View.GONE : View.VISIBLE);
+ }
+ }
private void attachSwipeToDelete(RecyclerView recyclerView) {
// 两个按钮的总宽度:删除按钮 + 标记已读按钮
@@ -276,48 +388,89 @@ public class MessagesActivity extends AppCompatActivity {
new ItemTouchHelper(callback).attachToRecyclerView(recyclerView);
- recyclerView.setOnTouchListener((v, event) -> {
- if (swipedPosition == RecyclerView.NO_POSITION || (deleteButtonRect == null && markReadButtonRect == null)) return false;
+ // 使用 OnItemTouchListener 处理滑动删除按钮的点击
+ // 注意:只在有展开的项时才处理,避免干扰正常的点击事件
+ recyclerView.addOnItemTouchListener(new OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
+ int action = e.getActionMasked();
+
+ // 如果有展开的项,且点击的不是展开的项,恢复展开的项
+ if (swipedPosition != RecyclerView.NO_POSITION && action == MotionEvent.ACTION_DOWN) {
+ View child = rv.findChildViewUnder(e.getX(), e.getY());
+ if (child != null) {
+ RecyclerView.ViewHolder vh = rv.getChildViewHolder(child);
+ if (vh != null && vh.getBindingAdapterPosition() != swipedPosition) {
+ // 点击的不是展开的项,恢复展开的项
+ recoverSwipedItem();
+ return false; // 不拦截,让点击事件正常处理
+ }
+ }
+ }
+
+ // 只有在有展开的项时才处理按钮点击
+ if (swipedPosition == RecyclerView.NO_POSITION || (deleteButtonRect == null && markReadButtonRect == null)) {
+ return false; // 不拦截,让事件正常传递
+ }
- int action = event.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN) {
- touchDownX = event.getX();
- touchDownY = event.getY();
- touchIsClick = true;
- return false;
- }
+ if (action == MotionEvent.ACTION_DOWN) {
+ touchDownX = e.getX();
+ touchDownY = e.getY();
+ touchIsClick = true;
+ // 不拦截 DOWN 事件,让 RecyclerView 正常处理
+ return false;
+ }
- if (action == MotionEvent.ACTION_MOVE) {
- float dx = Math.abs(event.getX() - touchDownX);
- float dy = Math.abs(event.getY() - touchDownY);
- if (dx > touchSlop || dy > touchSlop) {
- touchIsClick = false;
+ if (action == MotionEvent.ACTION_MOVE) {
+ float dx = Math.abs(e.getX() - touchDownX);
+ float dy = Math.abs(e.getY() - touchDownY);
+ if (dx > touchSlop || dy > touchSlop) {
+ touchIsClick = false;
+ }
+ // 不拦截 MOVE 事件
+ return false;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ if (!touchIsClick) {
+ // 如果不是点击,恢复展开的项
+ recoverSwipedItem();
+ return false;
+ }
+
+ int pos = swipedPosition;
+ float x = e.getX();
+ float y = e.getY();
+
+ // 检查点击的是哪个按钮
+ boolean hitMarkRead = markReadButtonRect != null && markReadButtonRect.contains(x, y);
+ boolean hitDelete = deleteButtonRect != null && deleteButtonRect.contains(x, y);
+
+ if (hitMarkRead || hitDelete) {
+ // 点击了按钮,拦截事件并处理
+ recoverSwipedItem();
+ if (hitMarkRead) {
+ markAsReadAt(pos);
+ } else if (hitDelete) {
+ deleteConversationAt(pos);
+ }
+ return true; // 拦截事件,防止触发 RecyclerView 的点击
+ }
+ // 点击的不是按钮区域,不拦截,让 RecyclerView 正常处理点击
+ return false;
}
return false;
}
- if (action == MotionEvent.ACTION_UP) {
- if (!touchIsClick) return false;
-
- int pos = swipedPosition;
- float x = event.getX();
- float y = event.getY();
-
- // 检查点击的是哪个按钮
- boolean hitMarkRead = markReadButtonRect != null && markReadButtonRect.contains(x, y);
- boolean hitDelete = deleteButtonRect != null && deleteButtonRect.contains(x, y);
-
- recoverSwipedItem();
-
- if (hitMarkRead) {
- markAsReadAt(pos);
- return true;
- } else if (hitDelete) {
- deleteConversationAt(pos);
- return true;
- }
+ @Override
+ public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
+ // 不需要处理
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ // 不需要处理
}
- return false;
});
}
@@ -335,6 +488,13 @@ public class MessagesActivity extends AppCompatActivity {
* 标记会话为已读
*/
private void markAsReadAt(int position) {
+ // TODO: 接入后端接口 - 标记会话为已读
+ // 接口路径: POST /api/conversations/{conversationId}/read
+ // 请求参数:
+ // - conversationId: 会话ID(路径参数)
+ // - userId: 当前用户ID(从token中获取)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 标记成功后,更新本地未读数量为0,并更新总未读数量
if (position < 0 || position >= conversations.size()) return;
ConversationItem item = conversations.get(position);
@@ -355,14 +515,26 @@ public class MessagesActivity extends AppCompatActivity {
* 删除会话
*/
private void deleteConversationAt(int position) {
+ // TODO: 接入后端接口 - 删除会话
+ // 接口路径: DELETE /api/conversations/{conversationId}
+ // 请求参数:
+ // - conversationId: 会话ID(路径参数)
+ // - userId: 当前用户ID(从token中获取)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 删除成功后,从本地列表移除,并更新总未读数量
if (position < 0 || position >= conversations.size()) return;
// 获取要删除的会话的未读数量
ConversationItem itemToDelete = conversations.get(position);
int unreadCountToRemove = itemToDelete != null ? itemToDelete.getUnreadCount() : 0;
+ String itemId = itemToDelete != null ? itemToDelete.getId() : null;
- // 删除会话
+ // 从当前显示列表和全部列表中删除
conversations.remove(position);
+ if (itemId != null) {
+ allConversations.removeIf(item -> item != null && item.getId().equals(itemId));
+ }
+
if (conversationsAdapter != null) {
conversationsAdapter.submitList(new ArrayList<>(conversations));
}
@@ -375,6 +547,8 @@ public class MessagesActivity extends AppCompatActivity {
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
}
}
+
+ updateEmptyState();
}
private float dp(float value) {
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 84c4e5fb..875d6441 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
@@ -36,6 +36,15 @@ public class MyFriendsActivity extends AppCompatActivity {
binding.friendsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.friendsRecyclerView.setAdapter(adapter);
+ // TODO: 接入后端接口 - 获取好友列表
+ // 接口路径: GET /api/friends
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // FriendItem对象应包含: id, name, avatarUrl, subtitle, isOnline, lastOnlineTime等字段
+ // 列表应按最后在线时间倒序或添加时间倒序排列
all.clear();
all.addAll(buildDemoFriends());
adapter.submitList(new ArrayList<>(all));
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationItem.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationItem.java
new file mode 100644
index 00000000..6afce8b4
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationItem.java
@@ -0,0 +1,116 @@
+package com.example.livestreaming;
+
+import java.util.Date;
+
+/**
+ * 通知数据模型
+ */
+public class NotificationItem {
+
+ public enum Type {
+ SYSTEM, // 系统通知
+ INTERACTION, // 互动通知(点赞、评论等)
+ FOLLOW, // 关注通知
+ MESSAGE, // 私信通知
+ LIVE // 直播通知(开播提醒等)
+ }
+
+ private String id;
+ private Type type;
+ private String title;
+ private String content;
+ private String avatarUrl;
+ private Date timestamp;
+ private boolean isRead;
+ private String actionUrl; // 点击通知后的跳转链接
+
+ public NotificationItem() {
+ }
+
+ public NotificationItem(String id, Type type, String title, String content, Date timestamp) {
+ this.id = id;
+ this.type = type;
+ this.title = title;
+ this.content = content;
+ this.timestamp = timestamp;
+ this.isRead = false;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public void setType(Type type) {
+ this.type = type;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getAvatarUrl() {
+ return avatarUrl;
+ }
+
+ public void setAvatarUrl(String avatarUrl) {
+ this.avatarUrl = avatarUrl;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public boolean isRead() {
+ return isRead;
+ }
+
+ public void setRead(boolean read) {
+ isRead = read;
+ }
+
+ public String getActionUrl() {
+ return actionUrl;
+ }
+
+ public void setActionUrl(String actionUrl) {
+ this.actionUrl = actionUrl;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ NotificationItem that = (NotificationItem) o;
+ return id != null && id.equals(that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id != null ? id.hashCode() : 0;
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java
new file mode 100644
index 00000000..874f1182
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationsActivity.java
@@ -0,0 +1,258 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.example.livestreaming.databinding.ActivityNotificationsBinding;
+import com.google.android.material.tabs.TabLayout;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+public class NotificationsActivity extends AppCompatActivity {
+
+ private ActivityNotificationsBinding binding;
+ private NotificationsAdapter adapter;
+ private final List allNotifications = new ArrayList<>();
+ private NotificationItem.Type currentFilter = null; // null表示显示全部
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, NotificationsActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityNotificationsBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ setupUI();
+ loadNotifications();
+ }
+
+ private void setupUI() {
+ binding.backButton.setOnClickListener(v -> finish());
+ binding.titleText.setText("通知");
+
+ // 设置适配器
+ adapter = new NotificationsAdapter(item -> {
+ // TODO: 接入后端接口 - 标记通知为已读
+ // 接口路径: POST /api/notifications/{notificationId}/read
+ // 请求参数:
+ // - notificationId: 通知ID(路径参数)
+ // - userId: 当前用户ID(从token中获取)
+ // 返回数据格式: ApiResponse<{success: boolean}>
+ // 标记成功后,更新本地通知的isRead状态
+ if (item == null) return;
+ // 标记为已读
+ item.setRead(true);
+ adapter.notifyItemChanged(allNotifications.indexOf(item));
+
+ // 处理点击事件(可以根据actionUrl跳转)
+ String actionUrl = item.getActionUrl();
+ if (actionUrl != null && !actionUrl.isEmpty()) {
+ // 这里可以根据actionUrl跳转到相应页面
+ // 例如:跳转到直播间、个人主页等
+ }
+ });
+
+ binding.notificationsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+ binding.notificationsRecyclerView.setAdapter(adapter);
+
+ // 设置分类标签
+ binding.categoryTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ if (tab == null) return;
+ int position = tab.getPosition();
+ switch (position) {
+ case 0: // 全部
+ currentFilter = null;
+ break;
+ case 1: // 系统
+ currentFilter = NotificationItem.Type.SYSTEM;
+ break;
+ case 2: // 互动
+ currentFilter = NotificationItem.Type.INTERACTION;
+ break;
+ case 3: // 关注
+ currentFilter = NotificationItem.Type.FOLLOW;
+ break;
+ case 4: // 私信
+ currentFilter = NotificationItem.Type.MESSAGE;
+ break;
+ case 5: // 直播
+ currentFilter = NotificationItem.Type.LIVE;
+ break;
+ }
+ filterNotifications();
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ }
+ });
+ }
+
+ private void loadNotifications() {
+ // TODO: 接入后端接口 - 获取通知列表
+ // 接口路径: GET /api/notifications
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - type (可选): 通知类型(system/interaction/follow/message/live),null表示全部
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // NotificationItem对象应包含: id, type, title, content, timestamp, isRead, actionUrl等字段
+ // 列表应按时间倒序排列(最新的在前面)
+ allNotifications.clear();
+
+ // 生成演示通知数据
+ Calendar calendar = Calendar.getInstance();
+ Random random = new Random();
+
+ // 系统通知
+ for (int i = 0; i < 3; i++) {
+ calendar.add(Calendar.HOUR, -i * 2);
+ NotificationItem item = new NotificationItem(
+ "sys_" + i,
+ NotificationItem.Type.SYSTEM,
+ "系统通知",
+ "欢迎使用直播应用!这里有最新的功能和活动。",
+ calendar.getTime()
+ );
+ item.setRead(i > 0);
+ allNotifications.add(item);
+ }
+
+ // 互动通知
+ String[] interactionTitles = {"有人点赞了你的作品", "有人评论了你的动态", "有人转发了你的内容"};
+ for (int i = 0; i < 5; i++) {
+ calendar.add(Calendar.MINUTE, -i * 30);
+ NotificationItem item = new NotificationItem(
+ "interaction_" + i,
+ NotificationItem.Type.INTERACTION,
+ interactionTitles[i % interactionTitles.length],
+ "用户" + (1000 + i) + "给你点了个赞",
+ calendar.getTime()
+ );
+ item.setRead(i > 2);
+ allNotifications.add(item);
+ }
+
+ // 关注通知
+ for (int i = 0; i < 4; i++) {
+ calendar.add(Calendar.HOUR, -i);
+ NotificationItem item = new NotificationItem(
+ "follow_" + i,
+ NotificationItem.Type.FOLLOW,
+ "新粉丝",
+ "用户" + (2000 + i) + "关注了你",
+ calendar.getTime()
+ );
+ item.setRead(i > 1);
+ allNotifications.add(item);
+ }
+
+ // 私信通知
+ for (int i = 0; i < 6; i++) {
+ calendar.add(Calendar.MINUTE, -i * 15);
+ NotificationItem item = new NotificationItem(
+ "message_" + i,
+ NotificationItem.Type.MESSAGE,
+ "新私信",
+ "用户" + (3000 + i) + "给你发了一条消息",
+ calendar.getTime()
+ );
+ item.setRead(i > 3);
+ allNotifications.add(item);
+ }
+
+ // 直播通知
+ String[] liveTitles = {"开播提醒", "直播推荐", "关注的主播开播了"};
+ for (int i = 0; i < 3; i++) {
+ calendar.add(Calendar.HOUR, -i * 4);
+ NotificationItem item = new NotificationItem(
+ "live_" + i,
+ NotificationItem.Type.LIVE,
+ liveTitles[i % liveTitles.length],
+ "你关注的主播" + (4000 + i) + "正在直播",
+ calendar.getTime()
+ );
+ item.setRead(i > 0);
+ allNotifications.add(item);
+ }
+
+ // 按时间倒序排序
+ allNotifications.sort((a, b) -> {
+ Date dateA = a.getTimestamp();
+ Date dateB = b.getTimestamp();
+ if (dateA == null && dateB == null) return 0;
+ if (dateA == null) return 1;
+ if (dateB == null) return -1;
+ return dateB.compareTo(dateA);
+ });
+
+ filterNotifications();
+ }
+
+ private void filterNotifications() {
+ List filtered = new ArrayList<>();
+
+ if (currentFilter == null) {
+ filtered.addAll(allNotifications);
+ } else {
+ for (NotificationItem item : allNotifications) {
+ if (item.getType() == currentFilter) {
+ filtered.add(item);
+ }
+ }
+ }
+
+ adapter.submitList(filtered);
+
+ // 显示/隐藏空状态
+ if (filtered.isEmpty()) {
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ binding.emptyStateView.setIcon(R.drawable.ic_notifications_24);
+ binding.emptyStateView.setTitle("暂无通知");
+ String message = currentFilter == null ? "还没有通知" : "还没有" + getFilterName() + "通知";
+ binding.emptyStateView.setMessage(message);
+ binding.emptyStateView.hideActionButton();
+ } else {
+ binding.emptyStateView.setVisibility(View.GONE);
+ }
+ }
+
+ private String getFilterName() {
+ if (currentFilter == null) return "";
+ switch (currentFilter) {
+ case SYSTEM:
+ return "系统";
+ case INTERACTION:
+ return "互动";
+ case FOLLOW:
+ return "关注";
+ case MESSAGE:
+ return "私信";
+ case LIVE:
+ return "直播";
+ default:
+ return "";
+ }
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java
new file mode 100644
index 00000000..0ab98840
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationsAdapter.java
@@ -0,0 +1,167 @@
+package com.example.livestreaming;
+
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.example.livestreaming.databinding.ItemNotificationBinding;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class NotificationsAdapter extends ListAdapter {
+
+ public interface OnNotificationClickListener {
+ void onNotificationClick(NotificationItem item);
+ }
+
+ private final OnNotificationClickListener onNotificationClickListener;
+
+ public NotificationsAdapter(OnNotificationClickListener onNotificationClickListener) {
+ super(DIFF);
+ this.onNotificationClickListener = onNotificationClickListener;
+ }
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ ItemNotificationBinding binding = ItemNotificationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new VH(binding, onNotificationClickListener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull VH holder, int position) {
+ holder.bind(getItem(position));
+ }
+
+ static class VH extends RecyclerView.ViewHolder {
+
+ private final ItemNotificationBinding binding;
+ private final OnNotificationClickListener onNotificationClickListener;
+ private final SimpleDateFormat timeFormat = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
+ private final SimpleDateFormat todayFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
+
+ VH(ItemNotificationBinding binding, OnNotificationClickListener onNotificationClickListener) {
+ super(binding.getRoot());
+ this.binding = binding;
+ this.onNotificationClickListener = onNotificationClickListener;
+ }
+
+ void bind(NotificationItem item) {
+ if (item == null) return;
+
+ // 设置标题和内容
+ binding.title.setText(item.getTitle() != null ? item.getTitle() : "");
+ binding.content.setText(item.getContent() != null ? item.getContent() : "");
+
+ // 设置时间
+ Date timestamp = item.getTimestamp();
+ if (timestamp != null) {
+ String timeText = formatTime(timestamp);
+ binding.timeText.setText(timeText);
+ } else {
+ binding.timeText.setText("");
+ }
+
+ // 设置未读标记
+ if (!item.isRead()) {
+ binding.unreadDot.setVisibility(View.VISIBLE);
+ } else {
+ binding.unreadDot.setVisibility(View.GONE);
+ }
+
+ // 加载头像
+ loadAvatar(item);
+
+ // 设置点击事件
+ binding.getRoot().setOnClickListener(v -> {
+ if (onNotificationClickListener != null) {
+ onNotificationClickListener.onNotificationClick(item);
+ }
+ });
+ }
+
+ private String formatTime(Date date) {
+ if (date == null) return "";
+
+ Date now = new Date();
+ long diff = now.getTime() - date.getTime();
+
+ // 如果是今天,只显示时间
+ if (diff < 24 * 60 * 60 * 1000 && isToday(date, now)) {
+ return todayFormat.format(date);
+ }
+
+ // 否则显示日期和时间
+ return timeFormat.format(date);
+ }
+
+ private boolean isToday(Date date, Date now) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
+ return dateFormat.format(date).equals(dateFormat.format(now));
+ }
+
+ private void loadAvatar(NotificationItem item) {
+ if (binding.avatar == null) return;
+
+ String avatarUrl = item.getAvatarUrl();
+ if (!TextUtils.isEmpty(avatarUrl)) {
+ Glide.with(binding.avatar)
+ .load(avatarUrl)
+ .circleCrop()
+ .placeholder(R.drawable.ic_account_circle_24)
+ .error(R.drawable.ic_account_circle_24)
+ .into(binding.avatar);
+ } else {
+ // 根据通知类型设置默认图标
+ int iconRes = getIconForType(item.getType());
+ Glide.with(binding.avatar)
+ .load(iconRes)
+ .circleCrop()
+ .into(binding.avatar);
+ }
+ }
+
+ private int getIconForType(NotificationItem.Type type) {
+ if (type == null) return R.drawable.ic_notifications_24;
+
+ switch (type) {
+ case SYSTEM:
+ return R.drawable.ic_notifications_24;
+ case INTERACTION:
+ return R.drawable.ic_chat_24;
+ case FOLLOW:
+ return R.drawable.ic_people_24;
+ case MESSAGE:
+ return R.drawable.ic_chat_24;
+ case LIVE:
+ return R.drawable.ic_voice_24;
+ default:
+ return R.drawable.ic_notifications_24;
+ }
+ }
+ }
+
+ private static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull NotificationItem oldItem, @NonNull NotificationItem newItem) {
+ String o = oldItem.getId();
+ String n = newItem.getId();
+ return o != null && o.equals(n);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull NotificationItem oldItem, @NonNull NotificationItem newItem) {
+ return oldItem.equals(newItem) && oldItem.isRead() == newItem.isRead();
+ }
+ };
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java b/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java
index 3ed05067..9ffe696c 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/OnlineDatingActivity.java
@@ -32,6 +32,10 @@ public class OnlineDatingActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -57,5 +61,16 @@ public class OnlineDatingActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java b/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java
index 77f4b6a1..60250ac9 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/PeaceEliteActivity.java
@@ -32,6 +32,10 @@ public class PeaceEliteActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -57,5 +61,16 @@ public class PeaceEliteActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
index ac66bba0..c10b75a9 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/ProfileActivity.java
@@ -15,8 +15,8 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
-
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.FileProvider;
import androidx.appcompat.app.AlertDialog;
import com.bumptech.glide.Glide;
@@ -122,6 +122,14 @@ public class ProfileActivity extends AppCompatActivity {
}
private void loadProfileFromPrefs() {
+ // TODO: 接入后端接口 - 获取用户资料
+ // 接口路径: GET /api/users/{userId}/profile
+ // 请求参数:
+ // - userId: 用户ID(路径参数,当前用户从token中获取)
+ // 返回数据格式: ApiResponse
+ // UserProfile对象应包含: id, name, avatarUrl, bio, level, badge, birthday, gender, location,
+ // followingCount, fansCount, likesCount等字段
+ // 首次加载时从接口获取,后续可从本地缓存读取
String n = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, null);
if (!TextUtils.isEmpty(n)) binding.name.setText(n);
@@ -136,10 +144,27 @@ public class ProfileActivity extends AppCompatActivity {
String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null);
if (!TextUtils.isEmpty(avatarUri)) {
+ Uri uri = Uri.parse(avatarUri);
+ // 如果是 file:// 协议,尝试转换为 FileProvider URI
+ if ("file".equals(uri.getScheme())) {
+ try {
+ java.io.File file = new java.io.File(uri.getPath());
+ if (file.exists()) {
+ uri = FileProvider.getUriForFile(
+ this,
+ getPackageName() + ".fileprovider",
+ file
+ );
+ }
+ } catch (Exception e) {
+ // 如果转换失败,使用原始 URI
+ }
+ }
Glide.with(this)
- .load(Uri.parse(avatarUri))
+ .load(uri)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
+ .placeholder(R.drawable.ic_account_circle_24)
.into(binding.avatar);
} else {
int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0);
@@ -158,6 +183,18 @@ public class ProfileActivity extends AppCompatActivity {
}
private void setupEditableAreas() {
+ // TODO: 接入后端接口 - 更新用户资料
+ // 接口路径: PUT /api/users/{userId}/profile
+ // 请求参数:
+ // - userId: 用户ID(路径参数,从token中获取)
+ // - name (可选): 昵称
+ // - bio (可选): 个人签名
+ // - avatarUrl (可选): 头像URL
+ // - birthday (可选): 生日
+ // - gender (可选): 性别
+ // - location (可选): 所在地
+ // 返回数据格式: ApiResponse
+ // 更新成功后,同步更新本地缓存和界面显示
binding.name.setOnClickListener(v -> showEditDialog("编辑昵称", binding.name.getText() != null ? binding.name.getText().toString() : "", text -> {
binding.name.setText(text);
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_NAME, text).apply();
@@ -224,6 +261,12 @@ public class ProfileActivity extends AppCompatActivity {
}
});
+ // TODO: 接入后端接口 - 获取关注/粉丝/获赞数量
+ // 接口路径: GET /api/users/{userId}/stats
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // 返回数据格式: ApiResponse<{followingCount: number, fansCount: number, likesCount: number}>
+ // 在ProfileActivity加载时调用,更新关注、粉丝、获赞数量显示
binding.following.setOnClickListener(v -> FollowingListActivity.start(this));
binding.followers.setOnClickListener(v -> FansListActivity.start(this));
binding.likes.setOnClickListener(v -> LikesListActivity.start(this));
@@ -236,20 +279,6 @@ public class ProfileActivity extends AppCompatActivity {
Intent intent = new Intent(this, EditProfileActivity.class);
editProfileLauncher.launch(intent);
});
- binding.shareHome.setOnClickListener(v -> {
- // TabPlaceholderActivity.start(this, "分享主页");
- String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
- String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
- if (TextUtils.isEmpty(digits)) digits = "24187196";
-
- String url = "https://live.example.com/u/" + digits;
-
- ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
- if (cm != null) {
- cm.setPrimaryClip(ClipData.newPlainText("profile_url", url));
- Toast.makeText(this, "主页链接已复制", Toast.LENGTH_SHORT).show();
- }
- });
binding.shareHome.setOnClickListener(v -> showShareProfileDialog());
binding.addFriendBtn.setOnClickListener(v -> TabPlaceholderActivity.start(this, "加好友"));
}
@@ -474,147 +503,6 @@ public class ProfileActivity extends AppCompatActivity {
}
}
- private void loadAndDisplayTags() {
- String location = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_LOCATION, "");
- String gender = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_GENDER, "");
- String birthday = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIRTHDAY, "");
-
- // 设置所在地标签 - 支持"省份-城市"格式
- if (!TextUtils.isEmpty(location)) {
- // 将"省份-城市"格式转换为"省份·城市"显示
- String displayLocation = location.replace("-", "·");
- binding.tagLocation.setText("IP:" + displayLocation);
- binding.tagLocation.setVisibility(View.VISIBLE);
- } else {
- binding.tagLocation.setText("IP:广西");
- binding.tagLocation.setVisibility(View.VISIBLE);
- }
-
- // 设置性别标签
- if (!TextUtils.isEmpty(gender)) {
- if (gender.contains("男")) {
- binding.tagGender.setText("男");
- } else if (gender.contains("女")) {
- binding.tagGender.setText("女");
- } else {
- binding.tagGender.setText("H");
- }
- binding.tagGender.setVisibility(View.VISIBLE);
- } else {
- binding.tagGender.setText("H");
- binding.tagGender.setVisibility(View.VISIBLE);
- }
-
- // 计算并设置年龄标签
- if (!TextUtils.isEmpty(birthday)) {
- int age = calculateAge(birthday);
- if (age > 0) {
- binding.tagAge.setText(age + "岁");
- binding.tagAge.setVisibility(View.VISIBLE);
- } else {
- binding.tagAge.setVisibility(View.GONE);
- }
- } else {
- binding.tagAge.setVisibility(View.GONE);
- }
-
- // 计算并设置星座标签
- if (!TextUtils.isEmpty(birthday)) {
- String constellation = calculateConstellation(birthday);
- if (!TextUtils.isEmpty(constellation)) {
- binding.tagConstellation.setText(constellation);
- binding.tagConstellation.setVisibility(View.VISIBLE);
- } else {
- binding.tagConstellation.setVisibility(View.GONE);
- }
- } else {
- binding.tagConstellation.setVisibility(View.GONE);
- }
- }
-
- private void loadProfileInfo() {
- String name = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_NAME, "爱你");
- String location = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_LOCATION, "广西");
- String bio = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_BIO, null);
-
- if (binding.profileInfoLine1 != null) {
- binding.profileInfoLine1.setText("昵称:" + name);
- }
-
- if (binding.profileInfoLine2 != null) {
- String locationText = !TextUtils.isEmpty(location) ? location : "广西";
- binding.profileInfoLine2.setText("地区:" + locationText);
- }
-
- if (binding.profileInfoLine3 != null) {
- String bioText = (!TextUtils.isEmpty(bio) && !BIO_HINT_TEXT.equals(bio)) ? bio : "填写个人签名更容易获得关注";
- binding.profileInfoLine3.setText("签名:" + bioText);
- }
- }
-
- private int calculateAge(String birthdayStr) {
- try {
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
- Date birthDate = sdf.parse(birthdayStr);
- if (birthDate == null) return 0;
-
- Calendar birth = Calendar.getInstance();
- birth.setTime(birthDate);
- Calendar now = Calendar.getInstance();
-
- int age = now.get(Calendar.YEAR) - birth.get(Calendar.YEAR);
- if (now.get(Calendar.DAY_OF_YEAR) < birth.get(Calendar.DAY_OF_YEAR)) {
- age--;
- }
- return age;
- } catch (ParseException e) {
- return 0;
- }
- }
-
- private String calculateConstellation(String birthdayStr) {
- try {
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
- Date birthDate = sdf.parse(birthdayStr);
- if (birthDate == null) return "";
-
- Calendar cal = Calendar.getInstance();
- cal.setTime(birthDate);
- int month = cal.get(Calendar.MONTH) + 1; // Calendar.MONTH 从0开始
- int day = cal.get(Calendar.DAY_OF_MONTH);
-
- // 星座计算
- if ((month == 3 && day >= 21) || (month == 4 && day <= 19)) {
- return "白羊座";
- } else if ((month == 4 && day >= 20) || (month == 5 && day <= 20)) {
- return "金牛座";
- } else if ((month == 5 && day >= 21) || (month == 6 && day <= 21)) {
- return "双子座";
- } else if ((month == 6 && day >= 22) || (month == 7 && day <= 22)) {
- return "巨蟹座";
- } else if ((month == 7 && day >= 23) || (month == 8 && day <= 22)) {
- return "狮子座";
- } else if ((month == 8 && day >= 23) || (month == 9 && day <= 22)) {
- return "处女座";
- } else if ((month == 9 && day >= 23) || (month == 10 && day <= 23)) {
- return "天秤座";
- } else if ((month == 10 && day >= 24) || (month == 11 && day <= 22)) {
- return "天蝎座";
- } else if ((month == 11 && day >= 23) || (month == 12 && day <= 21)) {
- return "射手座";
- } else if ((month == 12 && day >= 22) || (month == 1 && day <= 19)) {
- return "摩羯座";
- } else if ((month == 1 && day >= 20) || (month == 2 && day <= 18)) {
- return "水瓶座";
- } else if ((month == 2 && day >= 19) || (month == 3 && day <= 20)) {
- return "双鱼座";
- }
- return "";
- } catch (ParseException e) {
- return "";
- }
- }
-
/**
* 显示分享个人主页对话框
*/
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 751fd69b..6187189f 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
@@ -28,6 +28,7 @@ import com.example.livestreaming.net.ApiClient;
import com.example.livestreaming.net.ApiResponse;
import com.example.livestreaming.net.Room;
import com.example.livestreaming.net.StreamConfig;
+import com.example.livestreaming.ShareUtils;
import tv.danmaku.ijk.media.player.IMediaPlayer;
import tv.danmaku.ijk.media.player.IjkMediaPlayer;
@@ -130,11 +131,21 @@ public class RoomDetailActivity extends AppCompatActivity {
binding.fullscreenButton.setOnClickListener(v -> toggleFullscreen());
// 关注按钮
+ // TODO: 接入后端接口 - 关注/取消关注主播
+ // 接口路径: POST /api/follow 或 DELETE /api/follow
+ // 请求参数:
+ // - streamerId: 主播用户ID
+ // - action: "follow" 或 "unfollow"
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // 关注成功后,更新按钮状态为"已关注",并禁用按钮
binding.followButton.setOnClickListener(v -> {
Toast.makeText(this, "已关注主播", Toast.LENGTH_SHORT).show();
binding.followButton.setText("已关注");
binding.followButton.setEnabled(false);
});
+
+ // 分享按钮
+ binding.shareButton.setOnClickListener(v -> shareRoom());
}
private void setupChat() {
@@ -160,6 +171,15 @@ public class RoomDetailActivity extends AppCompatActivity {
}
private void sendMessage() {
+ // TODO: 接入后端接口 - 发送直播间弹幕消息
+ // 接口路径: POST /api/rooms/{roomId}/messages
+ // 请求参数:
+ // - roomId: 房间ID(路径参数)
+ // - message: 消息内容
+ // - userId: 发送者用户ID(从token中获取)
+ // 返回数据格式: ApiResponse
+ // ChatMessage对象应包含: messageId, userId, username, avatarUrl, message, timestamp等字段
+ // 发送成功后,将消息添加到本地列表并显示
String message = binding.chatInput.getText() != null ?
binding.chatInput.getText().toString().trim() : "";
@@ -167,6 +187,14 @@ public class RoomDetailActivity extends AppCompatActivity {
addChatMessage(new ChatMessage("我", message));
binding.chatInput.setText("");
+ // TODO: 接入后端接口 - 接收直播间弹幕消息(WebSocket或轮询)
+ // 方案1: WebSocket实时推送
+ // - 连接: ws://api.example.com/rooms/{roomId}/chat
+ // - 接收消息格式: {type: "message", data: ChatMessage}
+ // 方案2: 轮询获取新消息
+ // - 接口路径: GET /api/rooms/{roomId}/messages?lastMessageId={lastId}
+ // - 返回数据格式: ApiResponse>
+ // - 每3-5秒轮询一次,获取lastMessageId之后的新消息
// 模拟其他用户回复
handler.postDelayed(() -> {
if (random.nextFloat() < 0.3f) { // 30%概率有人回复
@@ -272,10 +300,18 @@ public class RoomDetailActivity extends AppCompatActivity {
}
private void startChatSimulation() {
+ // TODO: 接入后端接口 - 初始化时获取历史弹幕消息
+ // 接口路径: GET /api/rooms/{roomId}/messages
+ // 请求参数:
+ // - roomId: 房间ID(路径参数)
+ // - limit (可选): 获取最近N条消息,默认50
+ // 返回数据格式: ApiResponse>
+ // 进入直播间时,先获取最近50条历史消息显示在聊天列表中
stopChatSimulation();
chatSimulationRunnable = () -> {
if (isFinishing() || isDestroyed()) return;
+ // TODO: 这里应该改为从WebSocket或轮询接口获取新消息,而不是模拟生成
// 随机生成弹幕,降低概率
if (random.nextFloat() < 0.25f) { // 25%概率生成弹幕
String user = simulatedUsers[random.nextInt(simulatedUsers.length)];
@@ -302,6 +338,12 @@ public class RoomDetailActivity extends AppCompatActivity {
private boolean isFirstLoad = true;
private void fetchRoom() {
+ // TODO: 接入后端接口 - 获取房间详情
+ // 接口路径: GET /api/rooms/{roomId}
+ // 请求参数: roomId (路径参数)
+ // 返回数据格式: ApiResponse
+ // Room对象应包含: id, title, streamerName, streamerId, type, isLive, coverUrl, viewerCount,
+ // streamUrls (包含flv, hls, rtmp地址), description, startTime等字段
if (TextUtils.isEmpty(roomId)) {
Toast.makeText(this, "房间不存在", Toast.LENGTH_SHORT).show();
finish();
@@ -346,6 +388,18 @@ public class RoomDetailActivity extends AppCompatActivity {
});
}
+ private void shareRoom() {
+ if (room == null || roomId == null) {
+ Toast.makeText(this, "房间信息不可用", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ String shareLink = ShareUtils.generateRoomShareLink(roomId);
+ String title = room.getTitle() != null ? room.getTitle() : "直播间";
+ String text = "来看看这个精彩的直播:" + title;
+ ShareUtils.shareLink(this, shareLink, title, text);
+ }
+
private void bindRoom(Room r) {
String title = r.getTitle() != null ? r.getTitle() : "直播间";
String streamer = r.getStreamerName() != null ? r.getStreamerName() : "主播";
@@ -362,6 +416,11 @@ public class RoomDetailActivity extends AppCompatActivity {
binding.liveTag.setVisibility(View.VISIBLE);
binding.offlineLayout.setVisibility(View.GONE);
+ // TODO: 接入后端接口 - 获取实时观看人数
+ // 接口路径: GET /api/rooms/{roomId}/viewers/count
+ // 请求参数: roomId (路径参数)
+ // 返回数据格式: ApiResponse<{viewerCount: number}>
+ // 建议使用WebSocket实时推送观看人数变化,或每10-15秒轮询一次
// 设置观看人数(模拟)
int viewerCount = r.getViewerCount() > 0 ? r.getViewerCount() :
100 + random.nextInt(500);
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 f47ffd8e..a03ecbc8 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
@@ -73,6 +73,16 @@ public class SearchActivity extends AppCompatActivity {
binding.resultsRecyclerView.setLayoutManager(glm);
binding.resultsRecyclerView.setAdapter(adapter);
+ // TODO: 接入后端接口 - 搜索房间/主播
+ // 接口路径: GET /api/search
+ // 请求参数:
+ // - keyword: 搜索关键词(必填)
+ // - type (可选): 搜索类型(room/user/all),默认all
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
+ // Room对象应包含: id, title, streamerName, type, isLive, coverUrl等字段
+ // User对象应包含: id, name, avatarUrl, bio, isLive等字段
all.clear();
all.addAll(buildDemoRooms(24));
adapter.submitList(new ArrayList<>(all));
diff --git a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
index 198623a7..9e004676 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/SettingsPageActivity.java
@@ -1,6 +1,5 @@
package com.example.livestreaming;
-import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
@@ -54,7 +53,7 @@ public class SettingsPageActivity extends AppCompatActivity {
page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
if (page == null) page = "";
- String title = resolveTitle(currentPage);
+ String title = resolveTitle(page);
binding.titleText.setText(title);
adapter = new MoreAdapter(item -> {
@@ -62,6 +61,7 @@ public class SettingsPageActivity extends AppCompatActivity {
if (item.getType() != MoreItem.Type.ROW) return;
String t = item.getTitle() != null ? item.getTitle() : "";
+ // 处理服务器设置相关项目
if ("服务器设置".equals(t)) {
SettingsPageActivity.start(this, PAGE_SERVER);
return;
@@ -97,7 +97,14 @@ public class SettingsPageActivity extends AppCompatActivity {
return;
}
- Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show();
+ // 处理其他页面的点击事件
+ if (PAGE_SERVER.equals(page)) {
+ // 服务器设置页面的其他项目已在上面处理
+ return;
+ }
+
+ // 调用统一的点击处理方法
+ handleItemClick(item);
});
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -109,7 +116,7 @@ public class SettingsPageActivity extends AppCompatActivity {
private void handleItemClick(MoreItem item) {
String title = item.getTitle() != null ? item.getTitle() : "";
- switch (currentPage) {
+ switch (page) {
case PAGE_ACCOUNT_SECURITY:
handleAccountSecurityClick(title);
break;
@@ -152,7 +159,9 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void handleNotificationsClick(String title) {
- if ("系统通知".equals(title) || "免打扰".equals(title)) {
+ if ("通知设置".equals(title)) {
+ NotificationSettingsActivity.start(this);
+ } else if ("系统通知".equals(title) || "免打扰".equals(title)) {
NotificationSettingsActivity.start(this);
}
}
@@ -187,14 +196,14 @@ public class SettingsPageActivity extends AppCompatActivity {
}
private void refreshItems() {
- if (PAGE_CLEAR_CACHE.equals(currentPage)) {
+ if (PAGE_CLEAR_CACHE.equals(page)) {
// 异步加载缓存大小
updateCacheSize();
- } else if (PAGE_ABOUT.equals(currentPage)) {
+ } else if (PAGE_ABOUT.equals(page)) {
// 更新版本信息
updateVersionInfo();
} else {
- adapter.submitList(buildItems(currentPage));
+ adapter.submitList(buildItems(page));
}
}
@@ -278,6 +287,7 @@ public class SettingsPageActivity extends AppCompatActivity {
if (PAGE_NOTIFICATIONS.equals(page)) {
list.add(MoreItem.section("消息提醒"));
+ list.add(MoreItem.row("通知设置", "管理通知开关和免打扰", R.drawable.ic_notifications_24));
list.add(MoreItem.row("系统通知", "关注、评论、私信提醒", R.drawable.ic_notifications_24));
list.add(MoreItem.row("免打扰", "设置勿扰时段", R.drawable.ic_notifications_24));
return list;
@@ -466,4 +476,281 @@ public class SettingsPageActivity extends AppCompatActivity {
.setNegativeButton("取消", null)
.show();
}
+
+ private void showChangePasswordDialog() {
+ EditText oldPassword = new EditText(this);
+ oldPassword.setHint("请输入旧密码");
+ oldPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+ EditText newPassword = new EditText(this);
+ newPassword.setHint("请输入新密码");
+ newPassword.setInputType(android.text.InputType.TYPE_CLASS_TEXT | android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
+
+ android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
+ layout.setOrientation(android.widget.LinearLayout.VERTICAL);
+ layout.setPadding(dp(24), dp(16), dp(24), dp(8));
+ layout.addView(oldPassword);
+ layout.addView(newPassword);
+
+ new AlertDialog.Builder(this)
+ .setTitle("修改密码")
+ .setView(layout)
+ .setPositiveButton("确定", (d, w) -> {
+ Toast.makeText(this, "密码修改功能待实现", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showBindPhoneDialog() {
+ EditText phoneInput = new EditText(this);
+ phoneInput.setHint("请输入手机号");
+ phoneInput.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
+
+ new AlertDialog.Builder(this)
+ .setTitle("绑定手机号")
+ .setView(phoneInput)
+ .setPositiveButton("确定", (d, w) -> {
+ Toast.makeText(this, "手机号绑定功能待实现", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showDeviceManagementDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("登录设备管理")
+ .setMessage("当前登录设备:\n• 本设备(当前)\n\n功能开发中...")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showBlacklistDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("黑名单管理")
+ .setMessage("黑名单功能允许您屏蔽不想看到的用户。\n\n" +
+ "功能说明:\n" +
+ "• 被加入黑名单的用户无法给您发送消息\n" +
+ "• 您将不会看到被屏蔽用户的直播和动态\n" +
+ "• 可以随时解除屏蔽\n\n" +
+ "注意:此功能需要登录后使用,目前功能正在开发中,敬请期待。")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showPermissionManagementDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("权限管理")
+ .setMessage("应用需要以下权限才能正常工作:\n\n" +
+ "• 相机:用于直播和拍照\n" +
+ "• 麦克风:用于直播和语音聊天\n" +
+ "• 位置:用于附近直播和发现功能\n" +
+ "• 存储:用于保存图片和视频\n\n" +
+ "点击确定将跳转到系统设置页面,您可以在那里管理应用权限。")
+ .setPositiveButton("前往设置", (d, w) -> {
+ try {
+ Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(android.net.Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
+ } catch (Exception e) {
+ Toast.makeText(this, "无法打开设置页面", Toast.LENGTH_SHORT).show();
+ }
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showPrivacyPolicyDialog() {
+ String privacyPolicy = "隐私政策\n\n" +
+ "我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息。\n\n" +
+ "1. 信息收集\n" +
+ "我们可能收集以下信息:\n" +
+ "• 账户信息:昵称、头像、个人资料\n" +
+ "• 设备信息:设备型号、操作系统版本\n" +
+ "• 使用信息:观看记录、互动记录\n" +
+ "• 位置信息:仅在您授权时收集\n\n" +
+ "2. 信息使用\n" +
+ "我们使用收集的信息用于:\n" +
+ "• 提供和改进服务\n" +
+ "• 个性化推荐\n" +
+ "• 保障账户安全\n\n" +
+ "3. 信息保护\n" +
+ "我们采用行业标准的安全措施保护您的信息,不会向第三方出售您的个人信息。\n\n" +
+ "4. 您的权利\n" +
+ "您可以随时访问、修改或删除您的个人信息。\n\n" +
+ "如有疑问,请联系客服。";
+
+ new AlertDialog.Builder(this)
+ .setTitle("隐私政策")
+ .setMessage(privacyPolicy)
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showClearAllCacheDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("清理缓存")
+ .setMessage("确定要清理所有缓存吗?")
+ .setPositiveButton("确定", (d, w) -> {
+ CacheManager.clearAllCache(this, new CacheManager.OnCacheClearListener() {
+ @Override
+ public void onSuccess(long clearedSize) {
+ runOnUiThread(() -> {
+ refreshItems();
+ Toast.makeText(SettingsPageActivity.this, "缓存已清理", Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ @Override
+ public void onError(Exception e) {
+ runOnUiThread(() -> {
+ Toast.makeText(SettingsPageActivity.this, "清理缓存失败", Toast.LENGTH_SHORT).show();
+ });
+ }
+ });
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showClearImageCacheDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("清理图片缓存")
+ .setMessage("确定要清理图片缓存吗?")
+ .setPositiveButton("确定", (d, w) -> {
+ CacheManager.clearImageCache(this, new CacheManager.OnCacheClearListener() {
+ @Override
+ public void onSuccess(long clearedSize) {
+ runOnUiThread(() -> {
+ refreshItems();
+ Toast.makeText(SettingsPageActivity.this, "图片缓存已清理", Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ @Override
+ public void onError(Exception e) {
+ runOnUiThread(() -> {
+ Toast.makeText(SettingsPageActivity.this, "清理图片缓存失败", Toast.LENGTH_SHORT).show();
+ });
+ }
+ });
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showFAQDialog() {
+ String faq = "常见问题\n\n" +
+ "Q1: 如何开始直播?\n" +
+ "A: 在首页点击\"创建直播间\"按钮,输入房间信息后即可开始直播。\n\n" +
+ "Q2: 如何关注主播?\n" +
+ "A: 在直播间或主播个人主页点击\"关注\"按钮即可。\n\n" +
+ "Q3: 如何发送消息?\n" +
+ "A: 在直播间底部输入框输入消息后发送,或在消息页面与好友聊天。\n\n" +
+ "Q4: 如何修改个人资料?\n" +
+ "A: 进入个人中心,点击\"编辑资料\"即可修改昵称、签名等信息。\n\n" +
+ "Q5: 如何清理缓存?\n" +
+ "A: 在设置页面选择\"清理缓存\",可以清理应用缓存和图片缓存。\n\n" +
+ "Q6: 忘记密码怎么办?\n" +
+ "A: 请联系客服或通过绑定的手机号找回密码。\n\n" +
+ "Q7: 如何举报不良内容?\n" +
+ "A: 在直播间或用户主页可以举报违规内容,我们会及时处理。\n\n" +
+ "更多问题请联系客服。";
+
+ new AlertDialog.Builder(this)
+ .setTitle("常见问题")
+ .setMessage(faq)
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showFeedbackDialog() {
+ EditText feedbackInput = new EditText(this);
+ feedbackInput.setHint("请输入您的意见或建议");
+ feedbackInput.setMinLines(5);
+
+ new AlertDialog.Builder(this)
+ .setTitle("意见反馈")
+ .setView(feedbackInput)
+ .setPositiveButton("提交", (d, w) -> {
+ Toast.makeText(this, "反馈提交功能待实现", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showCustomerServiceDialog() {
+ String customerService = "联系客服\n\n" +
+ "我们提供多种客服联系方式:\n\n" +
+ "📞 客服电话:\n" +
+ "400-XXX-XXXX\n" +
+ "服务时间:9:00-22:00\n\n" +
+ "💬 在线客服:\n" +
+ "在应用内消息页面联系客服\n\n" +
+ "📧 邮箱:\n" +
+ "support@livestreaming.com\n\n" +
+ "🕐 服务时间:\n" +
+ "周一至周日 9:00-22:00\n\n" +
+ "我们会尽快回复您的问题。";
+
+ new AlertDialog.Builder(this)
+ .setTitle("联系客服")
+ .setMessage(customerService)
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showVersionInfoDialog() {
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ String versionName = packageInfo.versionName;
+ int versionCode = packageInfo.versionCode;
+ String message = "Live Streaming\n版本:" + versionName + "\n构建号:" + versionCode;
+
+ new AlertDialog.Builder(this)
+ .setTitle("版本信息")
+ .setMessage(message)
+ .setPositiveButton("确定", null)
+ .show();
+ } catch (PackageManager.NameNotFoundException e) {
+ new AlertDialog.Builder(this)
+ .setTitle("版本信息")
+ .setMessage("Live Streaming 1.0")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+ }
+
+ private void showUserAgreementDialog() {
+ String userAgreement = "用户协议\n\n" +
+ "欢迎使用直播应用!使用本应用即表示您同意遵守以下条款:\n\n" +
+ "1. 服务条款\n" +
+ "• 您必须年满18周岁才能使用本服务\n" +
+ "• 您需要对自己的账户安全负责\n" +
+ "• 禁止发布违法违规内容\n\n" +
+ "2. 用户行为规范\n" +
+ "• 禁止发布色情、暴力、赌博等违法内容\n" +
+ "• 禁止骚扰、辱骂其他用户\n" +
+ "• 禁止传播虚假信息\n" +
+ "• 禁止进行任何形式的欺诈行为\n\n" +
+ "3. 知识产权\n" +
+ "• 应用内的所有内容受知识产权法保护\n" +
+ "• 未经授权不得复制、传播\n\n" +
+ "4. 免责声明\n" +
+ "• 用户发布的内容不代表本平台观点\n" +
+ "• 平台不对用户行为承担责任\n\n" +
+ "5. 服务变更\n" +
+ "我们保留随时修改或终止服务的权利。\n\n" +
+ "如有疑问,请联系客服。";
+
+ new AlertDialog.Builder(this)
+ .setTitle("用户协议")
+ .setMessage(userAgreement)
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private int dp(int value) {
+ return (int) (value * getResources().getDisplayMetrics().density);
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
index 84444698..f312625f 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/TabPlaceholderActivity.java
@@ -13,6 +13,7 @@ import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.FileProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
@@ -453,10 +454,27 @@ public class TabPlaceholderActivity extends AppCompatActivity {
}
if (!TextUtils.isEmpty(avatarUri)) {
+ Uri uri = Uri.parse(avatarUri);
+ // 如果是 file:// 协议,尝试转换为 FileProvider URI
+ if ("file".equals(uri.getScheme())) {
+ try {
+ java.io.File file = new java.io.File(uri.getPath());
+ if (file.exists()) {
+ uri = FileProvider.getUriForFile(
+ this,
+ getPackageName() + ".fileprovider",
+ file
+ );
+ }
+ } catch (Exception e) {
+ // 如果转换失败,使用原始 URI
+ }
+ }
Glide.with(this)
- .load(Uri.parse(avatarUri))
+ .load(uri)
.circleCrop()
.error(R.drawable.ic_account_circle_24)
+ .placeholder(R.drawable.ic_account_circle_24)
.into(binding.moreAvatar);
} else if (avatarRes != 0) {
binding.moreAvatar.setImageResource(avatarRes);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java b/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java
index 38123d51..338b53dd 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/TableGamesActivity.java
@@ -32,6 +32,10 @@ public class TableGamesActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -57,5 +61,16 @@ public class TableGamesActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java
index 372cd49f..73546026 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/UserProfileReadOnlyActivity.java
@@ -71,6 +71,13 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
boolean isFriend = isFriend(userId);
updateAddFriendButton(isFriend);
+ // TODO: 接入后端接口 - 发送好友请求
+ // 接口路径: POST /api/friends/request
+ // 请求参数:
+ // - targetUserId: 目标用户ID
+ // - userId: 当前用户ID(从token中获取)
+ // 返回数据格式: ApiResponse<{success: boolean, message: string}>
+ // 发送成功后,更新按钮状态为"已发送"或"已添加"
binding.addFriendButton.setOnClickListener(v -> {
if (TextUtils.isEmpty(userId)) {
Toast.makeText(this, "用户信息缺失", Toast.LENGTH_SHORT).show();
@@ -133,6 +140,21 @@ public class UserProfileReadOnlyActivity extends AppCompatActivity {
}
private void bindDemoStatsAndWorks(String userId) {
+ // TODO: 接入后端接口 - 获取其他用户资料和统计数据
+ // 接口路径: GET /api/users/{userId}/profile
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // 返回数据格式: ApiResponse
+ // UserProfile对象应包含: id, name, avatarUrl, bio, location, worksCount, followingCount,
+ // followersCount, likesCount等字段
+ // TODO: 接入后端接口 - 获取用户作品列表
+ // 接口路径: GET /api/users/{userId}/works
+ // 请求参数:
+ // - userId: 用户ID(路径参数)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // WorkItem对象应包含: id, coverUrl, title, likeCount, viewCount等字段
if (binding == null) return;
String seed = !TextUtils.isEmpty(userId) ? userId : "demo";
diff --git a/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java b/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java
index fb4ff2f8..be003a98 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/VoiceMatchActivity.java
@@ -33,6 +33,10 @@ public class VoiceMatchActivity extends AppCompatActivity {
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_friends);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_home) {
@@ -58,5 +62,16 @@ public class VoiceMatchActivity extends AppCompatActivity {
return true;
});
}
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_friends);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java
index 4b7b4f32..bddb0b9c 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/WatchHistoryActivity.java
@@ -37,6 +37,15 @@ public class WatchHistoryActivity extends AppCompatActivity {
startActivity(intent);
});
+ // TODO: 接入后端接口 - 获取观看历史
+ // 接口路径: GET /api/watch/history
+ // 请求参数:
+ // - userId: 当前用户ID(从token中获取)
+ // - page (可选): 页码
+ // - pageSize (可选): 每页数量
+ // 返回数据格式: ApiResponse>
+ // Room对象应包含: id, title, streamerName, type, isLive, coverUrl, watchTime(观看时间)等字段
+ // 列表应按观看时间倒序排列(最近观看的在前面)
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
binding.roomsRecyclerView.setLayoutManager(glm);
diff --git a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
index 10dcd6d9..2bc269e3 100644
--- a/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
+++ b/android-app/app/src/main/java/com/example/livestreaming/WishTreeActivity.java
@@ -1 +1,145 @@
-package com.example.livestreaming;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.appcompat.app.AppCompatActivity;
import com.example.livestreaming.databinding.ActivityWishTreeBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
public class WishTreeActivity extends AppCompatActivity {
private ActivityWishTreeBinding binding;
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable timerRunnable;
public static void start(Context context) {
Intent intent = new Intent(context, WishTreeActivity.class);
context.startActivity(intent);
}
@Override
protected void onStart() {
super.onStart();
startBannerCountdown();
}
@Override
protected void onStop() {
super.onStop();
stopBannerCountdown();
}
private void startBannerCountdown() {
stopBannerCountdown();
timerRunnable = new Runnable() {
@Override
public void run() {
updateBannerTimer();
handler.postDelayed(this, 1000);
}
};
handler.post(timerRunnable);
}
private void stopBannerCountdown() {
if (timerRunnable != null) {
handler.removeCallbacks(timerRunnable);
timerRunnable = null;
}
}
private void updateBannerTimer() {
if (binding == null) return;
try {
long now = System.currentTimeMillis();
TimeZone tz = TimeZone.getDefault();
Calendar c = Calendar.getInstance(tz);
c.setTimeInMillis(now);
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
c.add(Calendar.DAY_OF_MONTH, 1);
long nextMidnight = c.getTimeInMillis();
long diff = Math.max(0, nextMidnight - now);
long totalSeconds = diff / 1000;
long hours = totalSeconds / 3600;
long minutes = (totalSeconds % 3600) / 60;
long seconds = totalSeconds % 60;
SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
fmt.setTimeZone(tz);
String current = fmt.format(new Date(now));
String remain = String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
binding.bannerTimer.setText(current + " " + remain);
} catch (Exception ignored) {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
startBannerCountdown();
BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
bottomNavigation.setOnItemSelectedListener(item -> {
int id = item.getItemId();
if (id == R.id.nav_wish_tree) {
return true;
}
if (id == R.id.nav_home) {
startActivity(new Intent(this, MainActivity.class));
finish();
return true;
}
if (id == R.id.nav_friends) {
startActivity(new Intent(this, FishPondActivity.class));
finish();
return true;
}
if (id == R.id.nav_messages) {
MessagesActivity.start(this);
finish();
return true;
}
if (id == R.id.nav_profile) {
ProfileActivity.start(this);
finish();
return true;
}
return true;
});
}
@Override
protected void onResume() {
super.onResume();
if (binding != null) {
binding.bottomNavInclude.bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
}
}
}
\ No newline at end of file
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.livestreaming.databinding.ActivityWishTreeBinding;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class WishTreeActivity extends AppCompatActivity {
+
+ private ActivityWishTreeBinding binding;
+
+ private final Handler handler = new Handler(Looper.getMainLooper());
+ private Runnable timerRunnable;
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, WishTreeActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ startBannerCountdown();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ stopBannerCountdown();
+ }
+
+ private void startBannerCountdown() {
+ stopBannerCountdown();
+ timerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateBannerTimer();
+ handler.postDelayed(this, 1000);
+ }
+ };
+ handler.post(timerRunnable);
+ }
+
+ private void stopBannerCountdown() {
+ if (timerRunnable != null) {
+ handler.removeCallbacks(timerRunnable);
+ timerRunnable = null;
+ }
+ }
+
+ private void updateBannerTimer() {
+ if (binding == null) return;
+ try {
+ long now = System.currentTimeMillis();
+ TimeZone tz = TimeZone.getDefault();
+ Calendar c = Calendar.getInstance(tz);
+ c.setTimeInMillis(now);
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ c.add(Calendar.DAY_OF_MONTH, 1);
+ long nextMidnight = c.getTimeInMillis();
+ long diff = Math.max(0, nextMidnight - now);
+
+ long totalSeconds = diff / 1000;
+ long hours = totalSeconds / 3600;
+ long minutes = (totalSeconds % 3600) / 60;
+ long seconds = totalSeconds % 60;
+
+ SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
+ fmt.setTimeZone(tz);
+ String current = fmt.format(new Date(now));
+ String remain = String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds);
+ binding.bannerTimer.setText(current + " " + remain);
+ } catch (Exception ignored) {
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityWishTreeBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ startBannerCountdown();
+
+ BottomNavigationView bottomNavigation = binding.bottomNavInclude.bottomNavigation;
+ bottomNavigation.setSelectedItemId(R.id.nav_wish_tree);
+
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNavigation);
+
+ bottomNavigation.setOnItemSelectedListener(item -> {
+ int id = item.getItemId();
+ if (id == R.id.nav_wish_tree) {
+ return true;
+ }
+ if (id == R.id.nav_home) {
+ startActivity(new Intent(this, MainActivity.class));
+ finish();
+ return true;
+ }
+ if (id == R.id.nav_friends) {
+ startActivity(new Intent(this, FishPondActivity.class));
+ finish();
+ return true;
+ }
+ if (id == R.id.nav_messages) {
+ MessagesActivity.start(this);
+ finish();
+ return true;
+ }
+ if (id == R.id.nav_profile) {
+ ProfileActivity.start(this);
+ finish();
+ return true;
+ }
+ return true;
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (binding != null) {
+ BottomNavigationView bottomNav = binding.bottomNavInclude.bottomNavigation;
+ bottomNav.setSelectedItemId(R.id.nav_wish_tree);
+ // 更新未读消息徽章
+ UnreadMessageManager.updateBadge(bottomNav);
+ }
+ }
+}
diff --git a/android-app/app/src/main/res/drawable/bg_circle_red.xml b/android-app/app/src/main/res/drawable/bg_circle_red.xml
new file mode 100644
index 00000000..63b4037c
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/bg_circle_red.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_check_24.xml b/android-app/app/src/main/res/drawable/ic_check_24.xml
new file mode 100644
index 00000000..25824c2f
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_check_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_check_double_24.xml b/android-app/app/src/main/res/drawable/ic_check_double_24.xml
new file mode 100644
index 00000000..1db8eb24
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_check_double_24.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_share_24.xml b/android-app/app/src/main/res/drawable/ic_share_24.xml
new file mode 100644
index 00000000..4357d10e
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_share_24.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_messages.xml b/android-app/app/src/main/res/layout/activity_messages.xml
index 2cf2b574..b79520e7 100644
--- a/android-app/app/src/main/res/layout/activity_messages.xml
+++ b/android-app/app/src/main/res/layout/activity_messages.xml
@@ -32,6 +32,42 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_notifications.xml b/android-app/app/src/main/res/layout/activity_notifications.xml
new file mode 100644
index 00000000..69a0ec0d
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_notifications.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_room_detail_new.xml b/android-app/app/src/main/res/layout/activity_room_detail_new.xml
index 22daa5bd..9e12b5c0 100644
--- a/android-app/app/src/main/res/layout/activity_room_detail_new.xml
+++ b/android-app/app/src/main/res/layout/activity_room_detail_new.xml
@@ -217,6 +217,16 @@
+
+
-
-
+ app:layout_constraintTop_toBottomOf="@id/messageText">
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/item_notification.xml b/android-app/app/src/main/res/layout/item_notification.xml
new file mode 100644
index 00000000..bd903884
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_notification.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/xml/file_paths.xml b/android-app/app/src/main/res/xml/file_paths.xml
index df12191a..c9dd781f 100644
--- a/android-app/app/src/main/res/xml/file_paths.xml
+++ b/android-app/app/src/main/res/xml/file_paths.xml
@@ -1,4 +1,5 @@
+
diff --git a/android-app/项目功能完善度分析.md b/android-app/项目功能完善度分析.md
index 06b770c8..83d32e86 100644
--- a/android-app/项目功能完善度分析.md
+++ b/android-app/项目功能完善度分析.md
@@ -534,24 +534,30 @@
**预计工作量**: 2-3天
-#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
+#### 9. **消息功能完善(前端)** ⭐⭐ ⚠️ 待实现
**为什么重要**: 完善核心社交功能
-- [x] 完善消息列表UI
-- [x] 实现消息状态显示(发送中、已发送、已读)
-- [x] 优化消息列表性能
-- [x] 添加消息操作(复制、删除等)
-- [x] 实现消息搜索(本地)
+- [x] 完善消息列表UI(会话列表)
+- [ ] 实现消息状态显示(发送中、已发送、已读)
+- [ ] 优化消息列表性能(使用messageId)
+- [ ] 添加消息操作(复制、删除等)
+- [ ] 实现消息搜索(本地)
-**完成内容**:
-- 为ChatMessage添加了MessageStatus枚举(发送中、已发送、已读)
-- 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
-- 实现了消息长按菜单,支持复制和删除操作
-- 优化了DiffUtil,使用messageId作为唯一标识,提升列表更新性能
-- 在MessagesActivity中添加了搜索功能,支持按会话标题和消息内容搜索
-- 优化了消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
-- 添加了内存泄漏防护(onDestroy中清理延迟任务)
-- 优化了滚动行为(使用smoothScrollToPosition)
+**已完成内容**:
+- ✅ 会话列表展示(MessagesActivity)
+- ✅ 滑动删除/标记已读功能(MessagesActivity)
+- ✅ 消息发送和列表展示(ConversationActivity)
+- ✅ 未读消息徽章管理
+
+**待实现内容**:
+- ⚠️ 为ChatMessage添加MessageStatus枚举(发送中、已发送、已读)
+- ⚠️ 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
+- ⚠️ 实现消息长按菜单,支持复制和删除操作
+- ⚠️ 优化DiffUtil,使用messageId作为唯一标识,提升列表更新性能(当前使用timestamp)
+- ⚠️ 在MessagesActivity中添加搜索功能,支持按会话标题和消息内容搜索
+- ⚠️ 优化消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
+- ⚠️ 添加内存泄漏防护(onDestroy中清理延迟任务)
+- ⚠️ 优化滚动行为(使用smoothScrollToPosition替代scrollToPosition)
**预计工作量**: 3-4天