diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
index 94f898be..97ff20f0 100644
--- a/android-app/app/src/main/AndroidManifest.xml
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -38,6 +38,10 @@
android:name="com.example.livestreaming.SettingsPageActivity"
android:exported="false" />
+
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/CacheManager.java b/android-app/app/src/main/java/com/example/livestreaming/CacheManager.java
new file mode 100644
index 00000000..be045470
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/CacheManager.java
@@ -0,0 +1,231 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.text.format.Formatter;
+
+import com.bumptech.glide.Glide;
+
+import java.io.File;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 缓存管理工具类
+ * 用于清理应用缓存、图片缓存等
+ */
+public class CacheManager {
+
+ /**
+ * 获取缓存总大小(字节)
+ */
+ public static long getCacheSize(Context context) {
+ long size = 0;
+
+ // 应用缓存目录
+ File cacheDir = context.getCacheDir();
+ if (cacheDir != null && cacheDir.exists()) {
+ size += getDirSize(cacheDir);
+ }
+
+ // 外部缓存目录
+ File externalCacheDir = context.getExternalCacheDir();
+ if (externalCacheDir != null && externalCacheDir.exists()) {
+ size += getDirSize(externalCacheDir);
+ }
+
+ // Glide图片缓存
+ try {
+ File glideCacheDir = new File(context.getCacheDir(), "image_manager_disk_cache");
+ if (glideCacheDir.exists()) {
+ size += getDirSize(glideCacheDir);
+ }
+ } catch (Exception e) {
+ // 忽略异常
+ }
+
+ // 其他缓存目录
+ File filesDir = context.getFilesDir();
+ if (filesDir != null && filesDir.exists()) {
+ File shareImagesDir = new File(filesDir, "share_images");
+ if (shareImagesDir.exists()) {
+ size += getDirSize(shareImagesDir);
+ }
+ }
+
+ return size;
+ }
+
+ /**
+ * 获取目录大小(字节)
+ */
+ private static long getDirSize(File dir) {
+ long size = 0;
+ if (dir == null || !dir.exists()) {
+ return size;
+ }
+
+ File[] files = dir.listFiles();
+ if (files == null) {
+ return size;
+ }
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ size += getDirSize(file);
+ } else {
+ size += file.length();
+ }
+ }
+
+ return size;
+ }
+
+ /**
+ * 格式化缓存大小
+ */
+ public static String formatCacheSize(Context context, long size) {
+ return Formatter.formatFileSize(context, size);
+ }
+
+ /**
+ * 清理所有缓存
+ */
+ public static void clearAllCache(Context context, OnCacheClearListener listener) {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.execute(() -> {
+ long clearedSize = 0;
+
+ try {
+ // 清理应用缓存
+ File cacheDir = context.getCacheDir();
+ if (cacheDir != null && cacheDir.exists()) {
+ clearedSize += clearDir(cacheDir);
+ }
+
+ // 清理外部缓存
+ File externalCacheDir = context.getExternalCacheDir();
+ if (externalCacheDir != null && externalCacheDir.exists()) {
+ clearedSize += clearDir(externalCacheDir);
+ }
+
+ // 清理Glide缓存(需要在主线程执行)
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> {
+ try {
+ Glide.get(context).clearDiskCache();
+ } catch (Exception e) {
+ // 忽略异常
+ }
+ });
+
+ // 清理其他缓存目录
+ File filesDir = context.getFilesDir();
+ if (filesDir != null && filesDir.exists()) {
+ File shareImagesDir = new File(filesDir, "share_images");
+ if (shareImagesDir.exists()) {
+ clearedSize += clearDir(shareImagesDir);
+ }
+ }
+
+ } catch (Exception e) {
+ if (listener != null) {
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> listener.onError(e));
+ }
+ return;
+ }
+
+ final long finalSize = clearedSize;
+ if (listener != null) {
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> listener.onSuccess(finalSize));
+ }
+ });
+ }
+
+ /**
+ * 清理图片缓存(Glide缓存)
+ */
+ public static void clearImageCache(Context context, OnCacheClearListener listener) {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.execute(() -> {
+ long clearedSize = 0;
+
+ try {
+ // 清理Glide磁盘缓存
+ File glideCacheDir = new File(context.getCacheDir(), "image_manager_disk_cache");
+ if (glideCacheDir.exists()) {
+ clearedSize += clearDir(glideCacheDir);
+ }
+
+ // 清理Glide内存缓存(需要在主线程执行)
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> {
+ try {
+ Glide.get(context).clearMemory();
+ } catch (Exception e) {
+ // 忽略异常
+ }
+ });
+
+ } catch (Exception e) {
+ if (listener != null) {
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> listener.onError(e));
+ }
+ return;
+ }
+
+ final long finalSize = clearedSize;
+ if (listener != null) {
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> listener.onSuccess(finalSize));
+ }
+ });
+ }
+
+ /**
+ * 清理目录
+ */
+ private static long clearDir(File dir) {
+ long size = 0;
+ if (dir == null || !dir.exists()) {
+ return size;
+ }
+
+ File[] files = dir.listFiles();
+ if (files == null) {
+ return size;
+ }
+
+ for (File file : files) {
+ if (file.isDirectory()) {
+ size += clearDir(file);
+ // 删除空目录
+ try {
+ file.delete();
+ } catch (Exception e) {
+ // 忽略异常
+ }
+ } else {
+ size += file.length();
+ try {
+ file.delete();
+ } catch (Exception e) {
+ // 忽略异常
+ }
+ }
+ }
+
+ return size;
+ }
+
+ /**
+ * 缓存清理监听器
+ */
+ public interface OnCacheClearListener {
+ void onSuccess(long clearedSize);
+ void onError(Exception e);
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java b/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java
new file mode 100644
index 00000000..12481395
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/CategoryFilterManager.java
@@ -0,0 +1,139 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.example.livestreaming.net.Room;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 分类筛选管理器
+ * 用于管理房间列表的分类筛选功能
+ */
+public class CategoryFilterManager {
+
+ private static final String PREFS_NAME = "category_filter_prefs";
+ private static final String KEY_LAST_CATEGORY = "last_category";
+
+ private final ExecutorService executorService;
+
+ public CategoryFilterManager() {
+ executorService = Executors.newSingleThreadExecutor();
+ }
+
+ /**
+ * 异步筛选房间列表
+ */
+ public void filterRoomsAsync(List allRooms, String category, FilterCallback callback) {
+ if (executorService == null || executorService.isShutdown()) {
+ // 如果线程池已关闭,同步执行
+ List filtered = filterRoomsSync(allRooms, category);
+ if (callback != null) {
+ callback.onFiltered(filtered);
+ }
+ return;
+ }
+
+ executorService.execute(() -> {
+ List filtered = filterRoomsSync(allRooms, category);
+ if (callback != null) {
+ // 回调需要在主线程执行
+ android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ mainHandler.post(() -> callback.onFiltered(filtered));
+ }
+ });
+ }
+
+ /**
+ * 同步筛选房间列表
+ */
+ private List filterRoomsSync(List allRooms, String category) {
+ if (allRooms == null || allRooms.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ String c = category != null ? category : "推荐";
+ if ("全部".equals(c) || "推荐".equals(c)) {
+ return new ArrayList<>(allRooms);
+ }
+
+ List filtered = new ArrayList<>();
+ for (Room r : allRooms) {
+ if (r == null) continue;
+ String roomType = r.getType();
+ if (c.equals(roomType)) {
+ filtered.add(r);
+ continue;
+ }
+ // 降级到演示数据分类算法
+ String demoCategory = getDemoCategoryForRoom(r);
+ if (c.equals(demoCategory)) {
+ filtered.add(r);
+ }
+ }
+ return filtered;
+ }
+
+ /**
+ * 获取房间的演示分类(用于降级处理)
+ */
+ private String getDemoCategoryForRoom(Room room) {
+ if (room == null) return "推荐";
+ String title = room.getTitle() != null ? room.getTitle() : "";
+ String streamer = room.getStreamerName() != null ? room.getStreamerName() : "";
+
+ // 简单的分类逻辑(可以根据实际需求调整)
+ if (title.contains("游戏") || streamer.contains("游戏")) {
+ return "游戏";
+ }
+ if (title.contains("音乐") || streamer.contains("音乐")) {
+ return "音乐";
+ }
+ if (title.contains("聊天") || streamer.contains("聊天")) {
+ return "聊天";
+ }
+ if (title.contains("才艺") || streamer.contains("才艺")) {
+ return "才艺";
+ }
+ return "推荐";
+ }
+
+ /**
+ * 保存最后选中的分类
+ */
+ public static void saveLastCategory(Context context, String category) {
+ if (context == null) return;
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(KEY_LAST_CATEGORY, category).apply();
+ }
+
+ /**
+ * 获取最后选中的分类
+ */
+ public static String getLastCategory(Context context) {
+ if (context == null) return "推荐";
+ SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ return prefs.getString(KEY_LAST_CATEGORY, "推荐");
+ }
+
+ /**
+ * 关闭线程池
+ */
+ public void shutdown() {
+ if (executorService != null && !executorService.isShutdown()) {
+ executorService.shutdown();
+ }
+ }
+
+ /**
+ * 筛选回调接口
+ */
+ public interface FilterCallback {
+ void onFiltered(List filteredRooms);
+ }
+}
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java b/android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java
new file mode 100644
index 00000000..c60970c5
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/LoadingStateManager.java
@@ -0,0 +1,83 @@
+package com.example.livestreaming;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+/**
+ * 统一的加载状态管理器
+ * 用于管理各种加载状态:骨架屏、下拉刷新等
+ */
+public class LoadingStateManager {
+
+ /**
+ * 在 RecyclerView 上显示骨架屏
+ * @param recyclerView 目标 RecyclerView
+ * @param count 骨架屏项目数量
+ */
+ public static void showSkeleton(RecyclerView recyclerView, int count) {
+ if (recyclerView == null) return;
+
+ // 创建一个简单的骨架屏适配器
+ SkeletonAdapter skeletonAdapter = new SkeletonAdapter(count);
+ recyclerView.setAdapter(skeletonAdapter);
+ }
+
+ /**
+ * 停止下拉刷新
+ * @param swipeRefresh SwipeRefreshLayout 实例
+ */
+ public static void stopRefreshing(SwipeRefreshLayout swipeRefresh) {
+ if (swipeRefresh != null && swipeRefresh.isRefreshing()) {
+ swipeRefresh.setRefreshing(false);
+ }
+ }
+
+ /**
+ * 简单的骨架屏适配器
+ */
+ private static class SkeletonAdapter extends RecyclerView.Adapter {
+ private final int itemCount;
+
+ public SkeletonAdapter(int count) {
+ this.itemCount = count;
+ }
+
+ @NonNull
+ @Override
+ public SkeletonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ // 创建一个简单的骨架屏视图
+ View view = new View(parent.getContext());
+ view.setLayoutParams(new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ ));
+ view.setMinimumHeight(200); // 设置最小高度
+ view.setBackgroundColor(0xFFF0F0F0); // 浅灰色背景
+
+ return new SkeletonViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull SkeletonViewHolder holder, int position) {
+ // 骨架屏不需要绑定数据
+ }
+
+ @Override
+ public int getItemCount() {
+ return itemCount;
+ }
+
+ static class SkeletonViewHolder extends RecyclerView.ViewHolder {
+ public SkeletonViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ }
+ }
+}
+
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 4fd83fea..0fbec534 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
@@ -60,7 +60,18 @@ public class MainActivity extends AppCompatActivity {
private RoomsAdapter adapter;
private final List allRooms = new ArrayList<>();
+ private final List followRooms = new ArrayList<>(); // 关注页面的房间列表
+ private final List discoverRooms = new ArrayList<>(); // 发现页面的房间列表
+ private final List nearbyUsers = new ArrayList<>(); // 附近页面的用户列表
+ private NearbyUsersAdapter nearbyUsersAdapter; // 附近页面的适配器
+
+ private StaggeredGridLayoutManager roomsLayoutManager; // 房间列表的布局管理器
+ private LinearLayoutManager nearbyLayoutManager; // 附近用户列表的布局管理器
+
private String currentCategory = "推荐";
+ private String currentTopTab = "发现"; // 当前选中的顶部标签:关注、发现、附近
+ private CategoryFilterManager filterManager;
+ private int filterRequestId = 0; // 用于防止旧的筛选结果覆盖新的结果
private final Handler handler = new Handler(Looper.getMainLooper());
private Runnable pollRunnable;
@@ -69,6 +80,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;
@@ -79,30 +91,37 @@ public class MainActivity extends AppCompatActivity {
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
- // 立即显示缓存数据,提升启动速度
+ // 初始化筛选管理器
+ filterManager = new CategoryFilterManager();
+
+ // 恢复上次选中的分类
+ currentCategory = CategoryFilterManager.getLastCategory(this);
+
+ // 设置UI
setupRecyclerView();
setupUI();
loadAvatarFromPrefs();
setupSpeechRecognizer();
+ // 初始化顶部标签页数据
+ initializeTopTabData();
+
// 初始化未读消息数量(演示数据)
if (UnreadMessageManager.getUnreadCount(this) == 0) {
// 从消息列表计算总未读数量
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
}
- // 清除默认选中状态,让所有标签页初始显示为未选中样式
+ // 恢复分类标签选中状态
+ restoreCategoryTabSelection();
+
+ // 设置默认选中"发现"标签页
if (binding != null && binding.topTabs != null) {
- // 在布局完成后清除默认选中状态
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;
- }
+ // 选中"发现"标签页(索引1)
+ TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
+ if (discoverTab != null) {
+ discoverTab.select();
}
});
}
@@ -209,15 +228,30 @@ public class MainActivity extends AppCompatActivity {
startActivity(intent);
});
- StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
- glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
- binding.roomsRecyclerView.setLayoutManager(glm);
+ // 保存房间列表的布局管理器
+ roomsLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
+ roomsLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
+
+ // 创建附近用户列表的布局管理器
+ nearbyLayoutManager = new LinearLayoutManager(this);
+
+ binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
binding.roomsRecyclerView.setAdapter(adapter);
+ // 配置RecyclerView动画,优化筛选时的过渡效果
+ androidx.recyclerview.widget.DefaultItemAnimator animator = new androidx.recyclerview.widget.DefaultItemAnimator();
+ animator.setAddDuration(200); // 添加动画时长
+ animator.setRemoveDuration(200); // 删除动画时长
+ animator.setMoveDuration(200); // 移动动画时长
+ animator.setChangeDuration(200); // 变更动画时长
+ binding.roomsRecyclerView.setItemAnimator(animator);
+
// 立即显示演示数据,提升用户体验
+ // 注意:如果后续需要从网络加载,骨架屏会在fetchRooms中显示
allRooms.clear();
allRooms.addAll(buildDemoRooms(20));
- applyCategoryFilter(currentCategory);
+ // 使用带动画的筛选方法
+ applyCategoryFilterWithAnimation(currentCategory);
}
private void setupUI() {
@@ -249,11 +283,13 @@ public class MainActivity extends AppCompatActivity {
binding.topTabs.setSelectedTabIndicatorHeight(2); // 显示指示器
// 更新布局属性以显示选中样式
binding.topTabs.setTabTextColors(
- getResources().getColor(android.R.color.darker_gray, null), // 未选中颜色
- getResources().getColor(R.color.purple_500, null) // 选中颜色
+ ContextCompat.getColor(MainActivity.this, android.R.color.darker_gray), // 未选中颜色
+ ContextCompat.getColor(MainActivity.this, R.color.purple_500) // 选中颜色
);
CharSequence title = tab.getText();
- TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
+ currentTopTab = title != null ? title.toString() : "发现";
+ // 切换显示的内容
+ switchTopTabContent(currentTopTab);
}
@Override
@@ -265,7 +301,9 @@ public class MainActivity extends AppCompatActivity {
public void onTabReselected(TabLayout.Tab tab) {
if (tab == null) return;
CharSequence title = tab.getText();
- TabPlaceholderActivity.start(MainActivity.this, title != null ? title.toString() : "");
+ currentTopTab = title != null ? title.toString() : "发现";
+ // 切换显示的内容
+ switchTopTabContent(currentTopTab);
}
});
@@ -275,7 +313,10 @@ public class MainActivity extends AppCompatActivity {
if (tab == null) return;
CharSequence title = tab.getText();
currentCategory = title != null ? title.toString() : "推荐";
- applyCategoryFilter(currentCategory);
+ // 保存选中的分类
+ CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
+ // 应用筛选(带动画)
+ applyCategoryFilterWithAnimation(currentCategory);
}
@Override
@@ -287,7 +328,10 @@ public class MainActivity extends AppCompatActivity {
if (tab == null) return;
CharSequence title = tab.getText();
currentCategory = title != null ? title.toString() : "推荐";
- applyCategoryFilter(currentCategory);
+ // 保存选中的分类
+ CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
+ // 应用筛选(带动画)
+ applyCategoryFilterWithAnimation(currentCategory);
}
});
@@ -374,10 +418,14 @@ public class MainActivity extends AppCompatActivity {
// 文本为空,显示麦克风图标
binding.micIcon.setImageResource(R.drawable.ic_mic_24);
binding.micIcon.setContentDescription("mic");
+ // 恢复显示所有房间(应用当前分类筛选)
+ applyCategoryFilterWithAnimation(currentCategory);
} else {
// 文本不为空,显示搜索图标
binding.micIcon.setImageResource(R.drawable.ic_search_24);
binding.micIcon.setContentDescription("search");
+ // 实时筛选房间列表
+ applySearchFilter(text);
}
}
@@ -402,6 +450,58 @@ public class MainActivity extends AppCompatActivity {
});
}
+ /**
+ * 应用搜索筛选
+ */
+ private void applySearchFilter(String query) {
+ if (adapter == null || allRooms == null) return;
+
+ String searchQuery = query != null ? query.trim().toLowerCase() : "";
+ if (searchQuery.isEmpty()) {
+ // 如果搜索框为空,恢复显示所有房间(应用当前分类筛选)
+ applyCategoryFilterWithAnimation(currentCategory);
+ return;
+ }
+
+ // 先应用分类筛选,再应用搜索筛选
+ List categoryFiltered = new ArrayList<>();
+ if ("全部".equals(currentCategory) || currentCategory == null) {
+ categoryFiltered.addAll(allRooms);
+ } else {
+ for (Room r : allRooms) {
+ if (r == null) continue;
+ String roomType = r.getType();
+ if (currentCategory.equals(roomType)) {
+ categoryFiltered.add(r);
+ } else if (currentCategory.equals(getDemoCategoryForRoom(r))) {
+ categoryFiltered.add(r);
+ }
+ }
+ }
+
+ // 在分类筛选结果中应用搜索筛选
+ List filtered = new ArrayList<>();
+ for (Room r : categoryFiltered) {
+ if (r == null) continue;
+ String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
+ String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
+ if (title.contains(searchQuery) || streamer.contains(searchQuery)) {
+ filtered.add(r);
+ }
+ }
+
+ // 更新列表显示
+ adapter.submitList(filtered, () -> {
+ binding.roomsRecyclerView.animate()
+ .alpha(1.0f)
+ .setDuration(200)
+ .start();
+ });
+
+ // 更新空状态
+ updateEmptyStateForList(filtered);
+ }
+
private void setupSpeechRecognizer() {
// 检查设备是否支持语音识别
if (!SpeechRecognizer.isRecognitionAvailable(this)) {
@@ -586,6 +686,22 @@ public class MainActivity extends AppCompatActivity {
} else {
Toast.makeText(this, "需要麦克风权限才能使用语音搜索", Toast.LENGTH_SHORT).show();
}
+ } else if (requestCode == REQUEST_LOCATION_PERMISSION) {
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ // 位置权限已授予,显示附近页面
+ showNearbyTab();
+ } else {
+ // 位置权限被拒绝
+ Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
+ // 切换回发现页面
+ if (binding.topTabs != null) {
+ TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
+ if (discoverTab != null) {
+ discoverTab.select();
+ }
+ }
+ }
}
}
@@ -603,6 +719,12 @@ public class MainActivity extends AppCompatActivity {
speechRecognizer.destroy();
speechRecognizer = null;
}
+
+ // 释放筛选管理器资源
+ if (filterManager != null) {
+ filterManager.shutdown();
+ filterManager = null;
+ }
}
private void loadAvatarFromPrefs() {
@@ -645,6 +767,9 @@ public class MainActivity extends AppCompatActivity {
loadAvatarFromPrefs();
// 更新未读消息徽章
UnreadMessageManager.updateBadge(bottomNavigation);
+
+ // 确保分类标签选中状态正确(防止从其他页面返回时状态不一致)
+ restoreCategoryTabSelection();
}
}
@@ -898,17 +1023,23 @@ public class MainActivity extends AppCompatActivity {
hideEmptyState();
hideErrorState();
- // 只在没有数据时显示loading
+ // 只在没有数据时显示骨架屏(替代简单的LoadingView)
if (adapter.getItemCount() == 0) {
- binding.loading.setVisibility(View.VISIBLE);
+ // 使用骨架屏替代简单的LoadingView,提供更好的用户体验
+ LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6);
+ binding.loading.setVisibility(View.GONE);
+ } else {
+ // 如果有数据,显示LoadingView(用于下拉刷新等场景)
+ binding.loading.setVisibility(View.GONE);
}
Call>> call = ApiClient.getService().getRooms();
NetworkUtils.enqueueWithLifecycle(call, this, new Callback>>() {
@Override
public void onResponse(Call>> call, Response>> response) {
+ // 隐藏骨架屏和加载视图
binding.loading.setVisibility(View.GONE);
- binding.swipeRefresh.setRefreshing(false);
+ LoadingStateManager.stopRefreshing(binding.swipeRefresh);
isFetching = false;
ApiResponse> body = response.body();
@@ -925,14 +1056,19 @@ public class MainActivity extends AppCompatActivity {
allRooms.clear();
allRooms.addAll(rooms);
- applyCategoryFilter(currentCategory);
+ // 确保使用真实的RoomsAdapter(替换骨架屏适配器)
+ binding.roomsRecyclerView.setAdapter(adapter);
+ // 使用带动画的筛选方法
+ applyCategoryFilterWithAnimation(currentCategory);
+ // 设置真实数据到适配器,自动替换骨架屏
adapter.bumpCoverOffset();
}
@Override
public void onFailure(Call>> call, Throwable t) {
+ // 隐藏骨架屏和加载视图
binding.loading.setVisibility(View.GONE);
- binding.swipeRefresh.setRefreshing(false);
+ LoadingStateManager.stopRefreshing(binding.swipeRefresh);
isFetching = false;
// 显示网络错误Snackbar和空状态
@@ -942,7 +1078,11 @@ public class MainActivity extends AppCompatActivity {
// 仍然提供演示数据作为后备
allRooms.clear();
allRooms.addAll(buildDemoRooms(0));
- applyCategoryFilter(currentCategory);
+ // 确保使用真实的RoomsAdapter(替换骨架屏适配器)
+ binding.roomsRecyclerView.setAdapter(adapter);
+ // 使用带动画的筛选方法
+ applyCategoryFilterWithAnimation(currentCategory);
+ // 设置真实数据到适配器,自动替换骨架屏
adapter.bumpCoverOffset();
}
});
@@ -986,23 +1126,140 @@ public class MainActivity extends AppCompatActivity {
hideEmptyState();
}
- private void applyCategoryFilter(String category) {
+ /**
+ * 应用分类筛选(带动画效果)
+ * 使用异步筛选提升性能,并添加平滑的过渡动画
+ * 使用filterRequestId防止旧的筛选结果覆盖新的结果
+ */
+ private void applyCategoryFilterWithAnimation(String category) {
+ String c = category != null ? category : "推荐";
+
+ // 增加请求ID,确保只有最新的筛选结果被应用
+ final int requestId = ++filterRequestId;
+
+ // 显示加载状态(如果数据量较大)
+ if (allRooms.size() > 50) {
+ binding.loading.setVisibility(View.VISIBLE);
+ }
+
+ // 使用筛选管理器异步筛选
+ if (filterManager != null) {
+ filterManager.filterRoomsAsync(allRooms, c, filteredRooms -> {
+ // 检查这个结果是否仍然是最新的请求
+ if (requestId != filterRequestId) {
+ // 这是一个旧的请求结果,忽略它
+ return;
+ }
+
+ // 隐藏加载状态
+ binding.loading.setVisibility(View.GONE);
+
+ // 添加淡入动画
+ binding.roomsRecyclerView.animate()
+ .alpha(0.7f)
+ .setDuration(100)
+ .withEndAction(() -> {
+ // 再次检查请求ID(防止在动画期间又有新的筛选请求)
+ if (requestId != filterRequestId) {
+ return;
+ }
+
+ // 更新列表数据(ListAdapter会自动处理DiffUtil动画)
+ adapter.submitList(filteredRooms, () -> {
+ // 最后一次检查请求ID
+ if (requestId != filterRequestId) {
+ return;
+ }
+
+ // 数据更新完成后,恢复透明度并添加淡入效果
+ binding.roomsRecyclerView.animate()
+ .alpha(1.0f)
+ .setDuration(200)
+ .start();
+ });
+
+ // 更新空状态
+ updateEmptyStateForList(filteredRooms);
+ })
+ .start();
+ });
+ } else {
+ // 降级到同步筛选(如果筛选管理器未初始化)
+ applyCategoryFilterSync(c);
+ }
+ }
+
+ /**
+ * 同步筛选(降级方案)
+ */
+ private void applyCategoryFilterSync(String category) {
String c = category != null ? category : "推荐";
if ("推荐".equals(c)) {
- adapter.submitList(new ArrayList<>(allRooms));
- updateEmptyStateForList(allRooms);
+ // 添加淡入动画,保持与其他筛选场景的一致性
+ binding.roomsRecyclerView.animate()
+ .alpha(0.7f)
+ .setDuration(100)
+ .withEndAction(() -> {
+ adapter.submitList(new ArrayList<>(allRooms), () -> {
+ binding.roomsRecyclerView.animate()
+ .alpha(1.0f)
+ .setDuration(200)
+ .start();
+ });
+ updateEmptyStateForList(allRooms);
+ })
+ .start();
return;
}
List filtered = new ArrayList<>();
for (Room r : allRooms) {
if (r == null) continue;
+ String roomType = r.getType();
+ if (c.equals(roomType)) {
+ filtered.add(r);
+ continue;
+ }
if (c.equals(getDemoCategoryForRoom(r))) {
filtered.add(r);
}
}
- adapter.submitList(filtered);
- updateEmptyStateForList(filtered);
+
+ // 添加淡入动画
+ binding.roomsRecyclerView.animate()
+ .alpha(0.7f)
+ .setDuration(100)
+ .withEndAction(() -> {
+ adapter.submitList(filtered, () -> {
+ binding.roomsRecyclerView.animate()
+ .alpha(1.0f)
+ .setDuration(200)
+ .start();
+ });
+ updateEmptyStateForList(filtered);
+ })
+ .start();
+ }
+
+ /**
+ * 恢复分类标签的选中状态
+ */
+ private void restoreCategoryTabSelection() {
+ if (binding == null || binding.categoryTabs == null) return;
+
+ // 延迟执行,确保TabLayout已完全初始化
+ binding.categoryTabs.post(() -> {
+ for (int i = 0; i < binding.categoryTabs.getTabCount(); i++) {
+ TabLayout.Tab tab = binding.categoryTabs.getTabAt(i);
+ if (tab != null) {
+ CharSequence tabText = tab.getText();
+ if (tabText != null && currentCategory.equals(tabText.toString())) {
+ tab.select();
+ break;
+ }
+ }
+ }
+ });
}
/**
@@ -1115,4 +1372,321 @@ public class MainActivity extends AppCompatActivity {
});
}).start();
}
+
+ /**
+ * 初始化顶部标签页数据
+ */
+ private void initializeTopTabData() {
+ // 初始化关注页面数据(已关注主播的直播)
+ followRooms.clear();
+ followRooms.addAll(buildFollowRooms());
+
+ // 初始化发现页面数据(推荐算法)
+ discoverRooms.clear();
+ discoverRooms.addAll(buildDiscoverRooms());
+
+ // 初始化附近页面数据(模拟位置数据)
+ nearbyUsers.clear();
+ nearbyUsers.addAll(buildNearbyUsers());
+ }
+
+ /**
+ * 切换顶部标签页内容
+ */
+ private void switchTopTabContent(String tabName) {
+ if (tabName == null) tabName = "发现";
+
+ if ("关注".equals(tabName)) {
+ showFollowTab();
+ } else if ("发现".equals(tabName)) {
+ showDiscoverTab();
+ } else if ("附近".equals(tabName)) {
+ showNearbyTab();
+ } else {
+ // 默认显示发现页面
+ showDiscoverTab();
+ }
+ }
+
+ /**
+ * 显示关注页面
+ */
+ private void showFollowTab() {
+ // 隐藏分类标签(关注页面不需要分类筛选)
+ if (binding.categoryTabs != null) {
+ binding.categoryTabs.setVisibility(View.GONE);
+ }
+
+ // 恢复房间列表的布局管理器和适配器
+ if (binding.roomsRecyclerView != null) {
+ binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
+ binding.roomsRecyclerView.setAdapter(adapter);
+ binding.roomsRecyclerView.setVisibility(View.VISIBLE);
+ }
+
+ // 使用房间适配器显示关注的主播直播
+ if (adapter != null) {
+ adapter.submitList(new ArrayList<>(followRooms));
+ }
+
+ // 更新空状态
+ if (followRooms.isEmpty()) {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setIcon(R.drawable.ic_person_24);
+ binding.emptyStateView.setTitle("还没有关注的主播");
+ binding.emptyStateView.setMessage("去发现页关注喜欢的主播吧");
+ binding.emptyStateView.hideActionButton();
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ hideEmptyState();
+ }
+ }
+
+ /**
+ * 显示发现页面
+ */
+ private void showDiscoverTab() {
+ // 显示分类标签
+ if (binding.categoryTabs != null) {
+ binding.categoryTabs.setVisibility(View.VISIBLE);
+ }
+
+ // 恢复房间列表的布局管理器和适配器
+ if (binding.roomsRecyclerView != null) {
+ binding.roomsRecyclerView.setLayoutManager(roomsLayoutManager);
+ binding.roomsRecyclerView.setAdapter(adapter);
+ binding.roomsRecyclerView.setVisibility(View.VISIBLE);
+ }
+
+ // 使用房间适配器显示推荐内容
+ if (adapter != null) {
+ // 先显示所有推荐房间,然后应用分类筛选
+ allRooms.clear();
+ allRooms.addAll(discoverRooms);
+ applyCategoryFilterWithAnimation(currentCategory);
+ }
+
+ // 更新空状态
+ updateEmptyStateForList(allRooms);
+ }
+
+ /**
+ * 显示附近页面
+ */
+ private void showNearbyTab() {
+ // 检查位置权限
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED
+ && ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // 请求位置权限
+ requestLocationPermission();
+ return;
+ }
+
+ // 隐藏分类标签(附近页面不需要分类筛选)
+ if (binding.categoryTabs != null) {
+ binding.categoryTabs.setVisibility(View.GONE);
+ }
+
+ // 初始化附近用户适配器(如果还没有)
+ if (nearbyUsersAdapter == null) {
+ nearbyUsersAdapter = new NearbyUsersAdapter(user -> {
+ if (user == null) return;
+ // 点击附近用户,可以跳转到用户主页或显示详情
+ Toast.makeText(MainActivity.this, "点击了:" + user.getName(), Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ // 切换RecyclerView的布局管理器和适配器,显示附近用户列表
+ if (binding.roomsRecyclerView != null) {
+ binding.roomsRecyclerView.setLayoutManager(nearbyLayoutManager);
+ binding.roomsRecyclerView.setAdapter(nearbyUsersAdapter);
+ nearbyUsersAdapter.submitList(new ArrayList<>(nearbyUsers));
+ binding.roomsRecyclerView.setVisibility(View.VISIBLE);
+ }
+
+ // 更新空状态
+ if (nearbyUsers.isEmpty()) {
+ if (binding.emptyStateView != null) {
+ binding.emptyStateView.setIcon(R.drawable.ic_search_24);
+ binding.emptyStateView.setTitle("附近暂无用户");
+ binding.emptyStateView.setMessage("开启定位后可以查看附近的用户");
+ binding.emptyStateView.hideActionButton();
+ binding.emptyStateView.setVisibility(View.VISIBLE);
+ }
+ } else {
+ hideEmptyState();
+ }
+ }
+
+ /**
+ * 构建关注页面的房间列表(已关注主播的直播)
+ */
+ private List buildFollowRooms() {
+ List list = new ArrayList<>();
+
+ // 从FollowingListActivity获取已关注的主播列表
+ // 这里使用模拟数据,实际应该从数据库或API获取
+ String[][] followData = {
+ {"王者荣耀排位赛", "王者荣耀陪练", "游戏", "true"},
+ {"音乐电台", "音乐电台", "音乐", "false"},
+ {"户外直播", "户外阿杰", "户外", "true"},
+ {"美食探店", "美食探店", "美食", "false"},
+ {"聊天连麦", "聊天小七", "聊天", "true"},
+ {"才艺表演", "才艺小妹", "才艺", "true"},
+ {"游戏竞技", "游戏高手", "游戏", "true"},
+ {"音乐演奏", "音乐达人", "音乐", "false"}
+ };
+
+ for (int i = 0; i < followData.length; i++) {
+ String id = "follow-" + i;
+ String title = followData[i][0];
+ String streamer = followData[i][1];
+ String type = followData[i][2];
+ boolean live = Boolean.parseBoolean(followData[i][3]);
+ Room room = new Room(id, title, streamer, live);
+ room.setType(type);
+ list.add(room);
+ }
+
+ return list;
+ }
+
+ /**
+ * 构建发现页面的房间列表(推荐算法前端实现)
+ */
+ private List buildDiscoverRooms() {
+ List list = new ArrayList<>();
+
+ // 推荐算法:基于观看历史、点赞等模拟数据
+ // 这里实现一个简单的推荐算法:
+ // 1. 优先推荐正在直播的房间
+ // 2. 优先推荐热门类型(游戏、才艺、音乐)
+ // 3. 添加一些随机性
+
+ String[][] discoverData = {
+ {"王者荣耀排位赛", "小明选手", "游戏", "true"},
+ {"吃鸡大逃杀", "游戏高手", "游戏", "true"},
+ {"唱歌连麦", "音乐达人", "音乐", "true"},
+ {"户外直播", "旅行者", "户外", "false"},
+ {"美食制作", "厨神小李", "美食", "true"},
+ {"才艺表演", "舞蹈小妹", "才艺", "true"},
+ {"聊天交友", "暖心姐姐", "聊天", "false"},
+ {"LOL竞技场", "电竞选手", "游戏", "true"},
+ {"古风演奏", "琴师小王", "音乐", "true"},
+ {"健身教学", "教练张", "户外", "false"},
+ {"摄影分享", "摄影师", "户外", "true"},
+ {"宠物秀", "萌宠主播", "才艺", "true"},
+ {"编程教学", "码农老王", "聊天", "false"},
+ {"读书分享", "书虫小妹", "聊天", "true"},
+ {"手工制作", "手艺人", "才艺", "true"},
+ {"英语口语", "外教老师", "聊天", "false"},
+ {"魔术表演", "魔术师", "才艺", "true"},
+ {"街头访谈", "记者小张", "户外", "true"},
+ {"乐器教学", "音乐老师", "音乐", "false"},
+ {"电影解说", "影评人", "聊天", "true"},
+ {"游戏攻略", "游戏解说", "游戏", "true"},
+ {"K歌大赛", "K歌达人", "音乐", "true"},
+ {"美食探店", "美食博主", "美食", "true"},
+ {"舞蹈教学", "舞蹈老师", "才艺", "true"}
+ };
+
+ // 推荐算法:优先显示正在直播的,然后按类型排序
+ List liveRooms = new ArrayList<>();
+ List offlineRooms = new ArrayList<>();
+
+ for (int i = 0; i < discoverData.length; i++) {
+ String id = "discover-" + i;
+ String title = discoverData[i][0];
+ String streamer = discoverData[i][1];
+ String type = discoverData[i][2];
+ boolean live = Boolean.parseBoolean(discoverData[i][3]);
+ Room room = new Room(id, title, streamer, live);
+ room.setType(type);
+
+ if (live) {
+ liveRooms.add(room);
+ } else {
+ offlineRooms.add(room);
+ }
+ }
+
+ // 先添加正在直播的,再添加未直播的
+ list.addAll(liveRooms);
+ list.addAll(offlineRooms);
+
+ return list;
+ }
+
+ /**
+ * 构建附近页面的用户列表(使用模拟位置数据)
+ */
+ private List buildNearbyUsers() {
+ List list = new ArrayList<>();
+
+ // 模拟位置数据:生成不同距离的用户
+ String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰",
+ "小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"};
+
+ for (int i = 0; i < names.length; i++) {
+ String id = "nearby-user-" + i;
+ String name = names[i];
+ boolean live = i % 3 == 0; // 每3个用户中有一个在直播
+
+ String distanceText;
+ if (i < 3) {
+ distanceText = (300 + i * 120) + "m";
+ } else if (i < 10) {
+ float km = 0.8f + (i - 3) * 0.35f;
+ distanceText = String.format("%.1fkm", km);
+ } else {
+ float km = 3.5f + (i - 10) * 0.5f;
+ distanceText = String.format("%.1fkm", km);
+ }
+
+ list.add(new NearbyUser(id, name, distanceText, live));
+ }
+
+ return list;
+ }
+
+ /**
+ * 请求位置权限
+ */
+ private void requestLocationPermission() {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ new AlertDialog.Builder(this)
+ .setTitle("需要位置权限")
+ .setMessage("附近功能需要访问位置信息,以便为您推荐附近的用户和直播。请在设置中允许位置权限。")
+ .setPositiveButton("确定", (dialog, which) -> {
+ ActivityCompat.requestPermissions(this,
+ new String[]{
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ },
+ REQUEST_LOCATION_PERMISSION);
+ })
+ .setNegativeButton("取消", (dialog, which) -> {
+ // 用户拒绝权限,显示提示
+ Toast.makeText(this, "需要位置权限才能使用附近功能", Toast.LENGTH_SHORT).show();
+ // 切换回发现页面
+ if (binding.topTabs != null) {
+ TabLayout.Tab discoverTab = binding.topTabs.getTabAt(1);
+ if (discoverTab != null) {
+ discoverTab.select();
+ }
+ }
+ })
+ .show();
+ } else {
+ ActivityCompat.requestPermissions(this,
+ new String[]{
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ },
+ REQUEST_LOCATION_PERMISSION);
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java b/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java
new file mode 100644
index 00000000..7b4bc0a3
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/NotificationSettingsActivity.java
@@ -0,0 +1,140 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Switch;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.example.livestreaming.databinding.ActivityNotificationSettingsBinding;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 通知设置页面
+ * 支持各类通知开关和免打扰设置
+ */
+public class NotificationSettingsActivity extends AppCompatActivity {
+
+ private static final String PREFS_NAME = "notification_settings";
+ private static final String KEY_SYSTEM_NOTIFICATIONS = "system_notifications";
+ private static final String KEY_FOLLOW_NOTIFICATIONS = "follow_notifications";
+ private static final String KEY_COMMENT_NOTIFICATIONS = "comment_notifications";
+ private static final String KEY_MESSAGE_NOTIFICATIONS = "message_notifications";
+ private static final String KEY_LIVE_NOTIFICATIONS = "live_notifications";
+ private static final String KEY_DND_ENABLED = "dnd_enabled";
+ private static final String KEY_DND_START_HOUR = "dnd_start_hour";
+ private static final String KEY_DND_END_HOUR = "dnd_end_hour";
+
+ private ActivityNotificationSettingsBinding binding;
+ private MoreAdapter adapter;
+ private SharedPreferences prefs;
+
+ public static void start(Context context) {
+ Intent intent = new Intent(context, NotificationSettingsActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ binding = ActivityNotificationSettingsBinding.inflate(getLayoutInflater());
+ setContentView(binding.getRoot());
+
+ prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+
+ binding.backButton.setOnClickListener(v -> finish());
+ binding.titleText.setText("通知设置");
+
+ adapter = new MoreAdapter(item -> {
+ if (item == null) return;
+ if (item.getType() != MoreItem.Type.ROW) return;
+ handleItemClick(item);
+ });
+
+ binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ binding.recyclerView.setAdapter(adapter);
+
+ refreshItems();
+ }
+
+ private void handleItemClick(MoreItem item) {
+ String title = item.getTitle() != null ? item.getTitle() : "";
+
+ if ("免打扰".equals(title)) {
+ showDoNotDisturbDialog();
+ }
+ }
+
+ private void refreshItems() {
+ List items = new ArrayList<>();
+
+ // 系统通知开关
+ items.add(MoreItem.section("系统通知"));
+ boolean systemEnabled = prefs.getBoolean(KEY_SYSTEM_NOTIFICATIONS, true);
+ items.add(MoreItem.row("系统通知", systemEnabled ? "已开启" : "已关闭", R.drawable.ic_notifications_24));
+
+ // 消息提醒
+ items.add(MoreItem.section("消息提醒"));
+ boolean followEnabled = prefs.getBoolean(KEY_FOLLOW_NOTIFICATIONS, true);
+ boolean commentEnabled = prefs.getBoolean(KEY_COMMENT_NOTIFICATIONS, true);
+ boolean messageEnabled = prefs.getBoolean(KEY_MESSAGE_NOTIFICATIONS, true);
+ boolean liveEnabled = prefs.getBoolean(KEY_LIVE_NOTIFICATIONS, true);
+
+ items.add(MoreItem.row("关注提醒", followEnabled ? "已开启" : "已关闭", R.drawable.ic_people_24));
+ items.add(MoreItem.row("评论提醒", commentEnabled ? "已开启" : "已关闭", R.drawable.ic_chat_24));
+ items.add(MoreItem.row("私信提醒", messageEnabled ? "已开启" : "已关闭", R.drawable.ic_chat_24));
+ items.add(MoreItem.row("开播提醒", liveEnabled ? "已开启" : "已关闭", R.drawable.ic_notifications_24));
+
+ // 免打扰
+ items.add(MoreItem.section("免打扰"));
+ boolean dndEnabled = prefs.getBoolean(KEY_DND_ENABLED, false);
+ if (dndEnabled) {
+ int startHour = prefs.getInt(KEY_DND_START_HOUR, 22);
+ int endHour = prefs.getInt(KEY_DND_END_HOUR, 8);
+ items.add(MoreItem.row("免打扰", String.format("%02d:00 - %02d:00", startHour, endHour), R.drawable.ic_notifications_24));
+ } else {
+ items.add(MoreItem.row("免打扰", "未开启", R.drawable.ic_notifications_24));
+ }
+
+ adapter.submitList(items);
+ }
+
+ private void showDoNotDisturbDialog() {
+ // 简单的免打扰设置对话框
+ boolean currentEnabled = prefs.getBoolean(KEY_DND_ENABLED, false);
+ int startHour = prefs.getInt(KEY_DND_START_HOUR, 22);
+ int endHour = prefs.getInt(KEY_DND_END_HOUR, 8);
+
+ String message = "免打扰功能说明:\n\n" +
+ "• 开启后,在指定时间段内不会收到通知\n" +
+ "• 当前设置:" + (currentEnabled ?
+ String.format("已开启 (%02d:00 - %02d:00)", startHour, endHour) : "未开启") + "\n" +
+ "• 紧急通知仍会显示\n\n" +
+ "(此功能待完善)";
+
+ new android.app.AlertDialog.Builder(this)
+ .setTitle("免打扰设置")
+ .setMessage(message)
+ .setPositiveButton("开启", (dialog, which) -> {
+ prefs.edit().putBoolean(KEY_DND_ENABLED, true).apply();
+ refreshItems();
+ Toast.makeText(this, "免打扰已开启", Toast.LENGTH_SHORT).show();
+ })
+ .setNeutralButton(currentEnabled ? "关闭" : "取消", (dialog, which) -> {
+ if (currentEnabled) {
+ prefs.edit().putBoolean(KEY_DND_ENABLED, false).apply();
+ refreshItems();
+ Toast.makeText(this, "免打扰已关闭", Toast.LENGTH_SHORT).show();
+ }
+ })
+ .show();
+ }
+}
+
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 12322d9d..a05f6ab4 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
@@ -1,9 +1,11 @@
package com.example.livestreaming;
+import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.ClipData;
import android.content.ClipboardManager;
+import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.net.Uri;
@@ -14,14 +16,20 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.core.content.ContextCompat;
+import androidx.core.content.FileProvider;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import com.bumptech.glide.Glide;
+import com.example.livestreaming.BuildConfig;
import com.example.livestreaming.databinding.ActivityProfileBinding;
+import com.example.livestreaming.ShareUtils;
import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@@ -47,6 +55,10 @@ public class ProfileActivity extends AppCompatActivity {
private static final String BIO_HINT_TEXT = "填写个人签名更容易获得关注,点击此处添加";
private ActivityResultLauncher editProfileLauncher;
+ private ActivityResultLauncher pickImageLauncher;
+ private ActivityResultLauncher takePictureLauncher;
+ private ActivityResultLauncher requestCameraPermissionLauncher;
+ private Uri pendingCameraUri = null;
public static void start(Context context) {
Intent intent = new Intent(context, ProfileActivity.class);
@@ -70,6 +82,45 @@ public class ProfileActivity extends AppCompatActivity {
}
);
+ // 注册图片选择器
+ pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {
+ if (uri == null) return;
+ // 保存头像URI到SharedPreferences
+ getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
+ .putString(KEY_AVATAR_URI, uri.toString())
+ .remove(KEY_AVATAR_RES) // 清除资源ID,因为现在使用URI
+ .apply();
+ // 立即更新头像显示
+ Glide.with(this).load(uri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar);
+ Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show();
+ });
+
+ // 注册拍照器
+ takePictureLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), success -> {
+ if (!success) return;
+ if (pendingCameraUri == null) return;
+ // 保存头像URI到SharedPreferences
+ getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
+ .putString(KEY_AVATAR_URI, pendingCameraUri.toString())
+ .remove(KEY_AVATAR_RES) // 清除资源ID,因为现在使用URI
+ .apply();
+ // 立即更新头像显示
+ Glide.with(this).load(pendingCameraUri).circleCrop().error(R.drawable.ic_account_circle_24).into(binding.avatar);
+ Toast.makeText(this, "头像已更新", Toast.LENGTH_SHORT).show();
+ pendingCameraUri = null;
+ });
+
+ // 注册相机权限请求
+ requestCameraPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted -> {
+ if (!granted) {
+ Toast.makeText(this, "需要相机权限才能拍照", Toast.LENGTH_SHORT).show();
+ pendingCameraUri = null;
+ return;
+ }
+ if (pendingCameraUri == null) return;
+ takePictureLauncher.launch(pendingCameraUri);
+ });
+
loadProfileFromPrefs();
loadAndDisplayTags();
loadProfileInfo();
@@ -172,30 +223,59 @@ public class ProfileActivity extends AppCompatActivity {
private void setupAvatarClick() {
binding.avatar.setOnClickListener(v -> {
- AvatarViewerDialog dialog = AvatarViewerDialog.create(this);
-
- // 优先从SharedPreferences读取最新的头像信息(因为ImageView可能还在加载中)
- String avatarUri = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_AVATAR_URI, null);
- int avatarRes = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(KEY_AVATAR_RES, 0);
-
- if (!TextUtils.isEmpty(avatarUri)) {
- // 使用URI加载,确保能正确显示
- dialog.setAvatarUri(Uri.parse(avatarUri));
- } else if (avatarRes != 0) {
- dialog.setAvatarResId(avatarRes);
- } else {
- // 如果都没有,尝试从ImageView获取Drawable
- Drawable drawable = binding.avatar.getDrawable();
- if (drawable != null) {
- dialog.setAvatarDrawable(drawable);
- } else {
- dialog.setAvatarResId(R.drawable.ic_account_circle_24);
- }
- }
- dialog.show();
+ // 显示头像选择底部菜单
+ showAvatarBottomSheet();
});
}
+ private void showAvatarBottomSheet() {
+ BottomSheetDialog dialog = new BottomSheetDialog(this);
+ View view = getLayoutInflater().inflate(R.layout.bottom_sheet_avatar_picker, null);
+ dialog.setContentView(view);
+
+ View pick = view.findViewById(R.id.actionPickGallery);
+ View camera = view.findViewById(R.id.actionTakePhoto);
+ View cancel = view.findViewById(R.id.actionCancel);
+
+ pick.setOnClickListener(v -> {
+ dialog.dismiss();
+ pickImageLauncher.launch("image/*");
+ });
+
+ camera.setOnClickListener(v -> {
+ dialog.dismiss();
+ Uri uri = createTempCameraUri();
+ if (uri == null) return;
+ pendingCameraUri = uri;
+ ensureCameraPermissionAndTakePhoto();
+ });
+
+ cancel.setOnClickListener(v -> dialog.dismiss());
+
+ dialog.show();
+ }
+
+ private void ensureCameraPermissionAndTakePhoto() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
+ if (pendingCameraUri == null) return;
+ takePictureLauncher.launch(pendingCameraUri);
+ return;
+ }
+ requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA);
+ }
+
+ private Uri createTempCameraUri() {
+ try {
+ File dir = new File(getCacheDir(), "images");
+ if (!dir.exists()) dir.mkdirs();
+ File file = new File(dir, "avatar_" + System.currentTimeMillis() + ".jpg");
+ return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file);
+ } catch (Exception e) {
+ Toast.makeText(this, "无法创建相机文件", Toast.LENGTH_SHORT).show();
+ return null;
+ }
+ }
+
private void setupNavigationClicks() {
binding.topActionSearch.setOnClickListener(v -> TabPlaceholderActivity.start(this, "定位/发现"));
binding.topActionClock.setOnClickListener(v -> WatchHistoryActivity.start(this));
@@ -225,20 +305,7 @@ 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, "加好友"));
}
@@ -461,4 +528,20 @@ public class ProfileActivity extends AppCompatActivity {
return "";
}
}
+
+ /**
+ * 显示分享个人主页对话框
+ */
+ private void showShareProfileDialog() {
+ // 获取用户ID
+ String idText = binding.idLine.getText() != null ? binding.idLine.getText().toString() : "";
+ String digits = !TextUtils.isEmpty(idText) ? idText.replaceAll("\\D+", "") : "";
+ if (TextUtils.isEmpty(digits)) {
+ digits = "24187196"; // 默认ID
+ }
+
+ // 直接生成分享链接
+ String shareLink = ShareUtils.generateProfileShareLink(digits);
+ ShareUtils.shareLink(this, shareLink, "个人主页", "来看看我的主页吧");
+ }
}
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 5addcd72..9823f9cd 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,8 +1,13 @@
package com.example.livestreaming;
+import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.os.Bundle;
+import android.view.View;
+import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
@@ -25,6 +30,8 @@ public class SettingsPageActivity extends AppCompatActivity {
public static final String PAGE_ABOUT = "about";
private ActivitySettingsPageBinding binding;
+ private MoreAdapter adapter;
+ private String currentPage = "";
public static void start(Context context, String page) {
Intent intent = new Intent(context, SettingsPageActivity.class);
@@ -40,23 +47,152 @@ public class SettingsPageActivity extends AppCompatActivity {
binding.backButton.setOnClickListener(v -> finish());
- String page = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
- if (page == null) page = "";
+ String pageExtra = getIntent() != null ? getIntent().getStringExtra(EXTRA_PAGE) : null;
+ currentPage = pageExtra != null ? pageExtra : "";
- String title = resolveTitle(page);
+ String title = resolveTitle(currentPage);
binding.titleText.setText(title);
- MoreAdapter adapter = new MoreAdapter(item -> {
+ adapter = new MoreAdapter(item -> {
if (item == null) return;
if (item.getType() != MoreItem.Type.ROW) return;
- String t = item.getTitle() != null ? item.getTitle() : "";
- Toast.makeText(this, "点击:" + t, Toast.LENGTH_SHORT).show();
+ handleItemClick(item);
});
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
binding.recyclerView.setAdapter(adapter);
- adapter.submitList(buildItems(page));
+ refreshItems();
+ }
+
+ private void handleItemClick(MoreItem item) {
+ String title = item.getTitle() != null ? item.getTitle() : "";
+
+ switch (currentPage) {
+ case PAGE_ACCOUNT_SECURITY:
+ handleAccountSecurityClick(title);
+ break;
+ case PAGE_PRIVACY:
+ handlePrivacyClick(title);
+ break;
+ case PAGE_NOTIFICATIONS:
+ handleNotificationsClick(title);
+ break;
+ case PAGE_CLEAR_CACHE:
+ handleClearCacheClick(title);
+ break;
+ case PAGE_HELP:
+ handleHelpClick(title);
+ break;
+ case PAGE_ABOUT:
+ handleAboutClick(title);
+ break;
+ }
+ }
+
+ private void handleAccountSecurityClick(String title) {
+ if ("修改密码".equals(title)) {
+ showChangePasswordDialog();
+ } else if ("绑定手机号".equals(title)) {
+ showBindPhoneDialog();
+ } else if ("登录设备管理".equals(title)) {
+ showDeviceManagementDialog();
+ }
+ }
+
+ private void handlePrivacyClick(String title) {
+ if ("黑名单".equals(title)) {
+ showBlacklistDialog();
+ } else if ("权限管理".equals(title)) {
+ showPermissionManagementDialog();
+ } else if ("隐私政策".equals(title)) {
+ showPrivacyPolicyDialog();
+ }
+ }
+
+ private void handleNotificationsClick(String title) {
+ if ("系统通知".equals(title) || "免打扰".equals(title)) {
+ NotificationSettingsActivity.start(this);
+ }
+ }
+
+ private void handleClearCacheClick(String title) {
+ if ("缓存大小".equals(title)) {
+ showClearAllCacheDialog();
+ } else if ("图片缓存".equals(title)) {
+ showClearImageCacheDialog();
+ }
+ }
+
+ private void handleHelpClick(String title) {
+ if ("常见问题".equals(title)) {
+ showFAQDialog();
+ } else if ("意见反馈".equals(title)) {
+ showFeedbackDialog();
+ } else if ("联系客服".equals(title)) {
+ showCustomerServiceDialog();
+ }
+ }
+
+ private void handleAboutClick(String title) {
+ if ("版本".equals(title)) {
+ // 版本信息已在subtitle中显示,点击可显示详细信息
+ showVersionInfoDialog();
+ } else if ("用户协议".equals(title)) {
+ showUserAgreementDialog();
+ } else if ("隐私政策".equals(title)) {
+ showPrivacyPolicyDialog();
+ }
+ }
+
+ private void refreshItems() {
+ if (PAGE_CLEAR_CACHE.equals(currentPage)) {
+ // 异步加载缓存大小
+ updateCacheSize();
+ } else if (PAGE_ABOUT.equals(currentPage)) {
+ // 更新版本信息
+ updateVersionInfo();
+ } else {
+ adapter.submitList(buildItems(currentPage));
+ }
+ }
+
+ private void updateCacheSize() {
+ new Thread(() -> {
+ long cacheSize = CacheManager.getCacheSize(this);
+ String sizeText = CacheManager.formatCacheSize(this, cacheSize);
+
+ runOnUiThread(() -> {
+ List items = new ArrayList<>();
+ items.add(MoreItem.section("存储"));
+ items.add(MoreItem.row("缓存大小", sizeText, R.drawable.ic_grid_24));
+ items.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
+ adapter.submitList(items);
+ });
+ }).start();
+ }
+
+ private void updateVersionInfo() {
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ String versionName = packageInfo.versionName;
+ int versionCode = packageInfo.versionCode;
+ String versionText = "Live Streaming " + versionName + " (Build " + versionCode + ")";
+
+ List items = new ArrayList<>();
+ items.add(MoreItem.section("应用信息"));
+ items.add(MoreItem.row("版本", versionText, R.drawable.ic_menu_24));
+ items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
+ items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
+ adapter.submitList(items);
+ } catch (PackageManager.NameNotFoundException e) {
+ List items = new ArrayList<>();
+ items.add(MoreItem.section("应用信息"));
+ items.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
+ items.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
+ items.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
+ adapter.submitList(items);
+ }
}
private String resolveTitle(String page) {
@@ -105,29 +241,344 @@ public class SettingsPageActivity extends AppCompatActivity {
}
if (PAGE_CLEAR_CACHE.equals(page)) {
- list.add(MoreItem.section("存储"));
- list.add(MoreItem.row("缓存大小", "点击清理缓存(演示)", R.drawable.ic_grid_24));
- list.add(MoreItem.row("图片缓存", "清理封面/头像缓存", R.drawable.ic_palette_24));
- return list;
+ // 缓存大小将在updateCacheSize中异步更新
+ return new ArrayList<>();
}
if (PAGE_HELP.equals(page)) {
list.add(MoreItem.section("帮助"));
list.add(MoreItem.row("常见问题", "问题解答与使用指南", R.drawable.ic_chat_24));
list.add(MoreItem.row("意见反馈", "提交你的建议与问题", R.drawable.ic_chat_24));
- list.add(MoreItem.row("联系客服", "在线客服(演示)", R.drawable.ic_chat_24));
+ list.add(MoreItem.row("联系客服", "在线客服", R.drawable.ic_chat_24));
return list;
}
if (PAGE_ABOUT.equals(page)) {
- list.add(MoreItem.section("应用信息"));
- list.add(MoreItem.row("版本", "Live Streaming 1.0", R.drawable.ic_menu_24));
- list.add(MoreItem.row("用户协议", "服务条款与规则", R.drawable.ic_menu_24));
- list.add(MoreItem.row("隐私政策", "隐私保护说明", R.drawable.ic_menu_24));
- return list;
+ // 版本信息将在updateVersionInfo中更新
+ return new ArrayList<>();
}
list.add(MoreItem.row("返回", "", R.drawable.ic_arrow_back_24));
return list;
}
+
+ // ========== 账号与安全相关对话框 ==========
+
+ private void showChangePasswordDialog() {
+ try {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_change_password, null);
+ new AlertDialog.Builder(this)
+ .setTitle("修改密码")
+ .setView(dialogView)
+ .setPositiveButton("确定", (dialog, which) -> {
+ Toast.makeText(this, "密码修改功能待接入后端", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ } catch (Exception e) {
+ // 如果布局文件不存在,使用简单对话框
+ new AlertDialog.Builder(this)
+ .setTitle("修改密码")
+ .setMessage("密码修改功能待接入后端\n\n" +
+ "功能说明:\n" +
+ "• 需要输入当前密码\n" +
+ "• 设置新密码(至少8位)\n" +
+ "• 确认新密码")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+ }
+
+ private void showBindPhoneDialog() {
+ try {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_bind_phone, null);
+ new AlertDialog.Builder(this)
+ .setTitle("绑定手机号")
+ .setView(dialogView)
+ .setPositiveButton("确定", (dialog, which) -> {
+ Toast.makeText(this, "手机号绑定功能待接入后端", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ } catch (Exception e) {
+ // 如果布局文件不存在,使用简单对话框
+ new AlertDialog.Builder(this)
+ .setTitle("绑定手机号")
+ .setMessage("手机号绑定功能待接入后端\n\n" +
+ "功能说明:\n" +
+ "• 输入手机号码\n" +
+ "• 获取并输入验证码\n" +
+ "• 完成绑定")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+ }
+
+ private void showDeviceManagementDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("登录设备管理")
+ .setMessage("当前登录设备:\n\n" +
+ "• Android设备 (当前设备)\n" +
+ " 最后登录:刚刚\n\n" +
+ "功能说明:\n" +
+ "• 查看所有已登录设备\n" +
+ "• 可以远程退出其他设备\n" +
+ "• 保护账号安全")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ // ========== 隐私设置相关对话框 ==========
+
+ private void showBlacklistDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("黑名单")
+ .setMessage("黑名单功能说明:\n\n" +
+ "• 将用户加入黑名单后,对方将无法:\n" +
+ " - 查看你的动态\n" +
+ " - 给你发送消息\n" +
+ " - 关注你\n\n" +
+ "• 你仍然可以查看对方的公开信息\n\n" +
+ "当前黑名单:0人\n\n" +
+ "(此功能待接入后端)")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showPermissionManagementDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("权限管理")
+ .setMessage("应用权限说明:\n\n" +
+ "• 相机权限:用于直播和拍照\n" +
+ "• 麦克风权限:用于语音搜索和直播\n" +
+ "• 位置权限:用于附近的人功能\n" +
+ "• 存储权限:用于保存图片和文件\n\n" +
+ "你可以在系统设置中管理这些权限。")
+ .setPositiveButton("打开系统设置", (dialog, which) -> {
+ Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(android.net.Uri.parse("package:" + getPackageName()));
+ startActivity(intent);
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showPrivacyPolicyDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("隐私政策")
+ .setMessage("隐私政策\n\n" +
+ "我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用和保护您的个人信息。\n\n" +
+ "1. 信息收集\n" +
+ "我们可能收集以下信息:\n" +
+ "• 账户信息(昵称、头像等)\n" +
+ "• 设备信息(设备型号、系统版本等)\n" +
+ "• 使用信息(观看记录、互动记录等)\n\n" +
+ "2. 信息使用\n" +
+ "我们使用收集的信息用于:\n" +
+ "• 提供和改进服务\n" +
+ "• 个性化推荐\n" +
+ "• 安全保障\n\n" +
+ "3. 信息保护\n" +
+ "我们采用行业标准的安全措施保护您的信息。\n\n" +
+ "4. 联系我们\n" +
+ "如有疑问,请联系客服。\n\n" +
+ "(完整版隐私政策待接入)")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ // ========== 缓存清理相关对话框 ==========
+
+ private void showClearAllCacheDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("清理缓存")
+ .setMessage("确定要清理所有缓存吗?\n\n" +
+ "清理后可能需要重新加载部分内容。")
+ .setPositiveButton("清理", (dialog, which) -> {
+ showClearingProgress();
+ CacheManager.clearAllCache(this, new CacheManager.OnCacheClearListener() {
+ @Override
+ public void onSuccess(long clearedSize) {
+ hideClearingProgress();
+ String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize);
+ Toast.makeText(SettingsPageActivity.this,
+ "清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show();
+ updateCacheSize();
+ }
+
+ @Override
+ public void onError(Exception e) {
+ hideClearingProgress();
+ Toast.makeText(SettingsPageActivity.this,
+ "清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private void showClearImageCacheDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("清理图片缓存")
+ .setMessage("确定要清理图片缓存吗?\n\n" +
+ "清理后图片需要重新加载。")
+ .setPositiveButton("清理", (dialog, which) -> {
+ showClearingProgress();
+ CacheManager.clearImageCache(this, new CacheManager.OnCacheClearListener() {
+ @Override
+ public void onSuccess(long clearedSize) {
+ hideClearingProgress();
+ String sizeText = CacheManager.formatCacheSize(SettingsPageActivity.this, clearedSize);
+ Toast.makeText(SettingsPageActivity.this,
+ "清理完成,已释放 " + sizeText, Toast.LENGTH_SHORT).show();
+ updateCacheSize();
+ }
+
+ @Override
+ public void onError(Exception e) {
+ hideClearingProgress();
+ Toast.makeText(SettingsPageActivity.this,
+ "清理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ }
+
+ private AlertDialog clearingDialog;
+
+ private void showClearingProgress() {
+ ProgressBar progressBar = new ProgressBar(this);
+ progressBar.setIndeterminate(true);
+ clearingDialog = new AlertDialog.Builder(this)
+ .setTitle("正在清理...")
+ .setView(progressBar)
+ .setCancelable(false)
+ .show();
+ }
+
+ private void hideClearingProgress() {
+ if (clearingDialog != null && clearingDialog.isShowing()) {
+ clearingDialog.dismiss();
+ clearingDialog = null;
+ }
+ }
+
+ // ========== 帮助与反馈相关对话框 ==========
+
+ private void showFAQDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("常见问题")
+ .setMessage("常见问题解答\n\n" +
+ "Q: 如何创建直播间?\n" +
+ "A: 在首页点击右上角的创建按钮,填写直播间信息即可。\n\n" +
+ "Q: 如何关注主播?\n" +
+ "A: 在直播间或主播个人主页点击关注按钮。\n\n" +
+ "Q: 如何发送消息?\n" +
+ "A: 在消息页面选择会话,输入内容后发送。\n\n" +
+ "Q: 如何修改个人资料?\n" +
+ "A: 在个人中心点击编辑资料按钮。\n\n" +
+ "Q: 如何清理缓存?\n" +
+ "A: 在设置页面选择清理缓存。\n\n" +
+ "更多问题请联系客服。")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ private void showFeedbackDialog() {
+ try {
+ View dialogView = getLayoutInflater().inflate(R.layout.dialog_feedback, null);
+ new AlertDialog.Builder(this)
+ .setTitle("意见反馈")
+ .setView(dialogView)
+ .setPositiveButton("提交", (dialog, which) -> {
+ Toast.makeText(this, "感谢您的反馈!我们会认真处理。", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton("取消", null)
+ .show();
+ } catch (Exception e) {
+ // 如果布局文件不存在,使用简单对话框
+ new AlertDialog.Builder(this)
+ .setTitle("意见反馈")
+ .setMessage("意见反馈功能待接入后端\n\n" +
+ "你可以通过以下方式反馈:\n" +
+ "• 在对话框中输入反馈内容\n" +
+ "• 选择反馈类型(问题/建议)\n" +
+ "• 提交后我们会及时处理")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+ }
+
+ private void showCustomerServiceDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("联系客服")
+ .setMessage("客服联系方式:\n\n" +
+ "• 在线客服:工作日 9:00-18:00\n" +
+ "• 客服邮箱:support@livestreaming.com\n" +
+ "• 客服电话:400-123-4567\n\n" +
+ "(此功能待接入后端)")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ // ========== 关于页面相关对话框 ==========
+
+ private void showVersionInfoDialog() {
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ String versionName = packageInfo.versionName;
+ int versionCode = packageInfo.versionCode;
+
+ new AlertDialog.Builder(this)
+ .setTitle("版本信息")
+ .setMessage("应用名称:Live Streaming\n" +
+ "版本号:" + versionName + "\n" +
+ "构建号:" + versionCode + "\n" +
+ "更新时间:2024年\n\n" +
+ "© 2024 Live Streaming. All rights reserved.")
+ .setPositiveButton("确定", null)
+ .show();
+ } catch (PackageManager.NameNotFoundException e) {
+ new AlertDialog.Builder(this)
+ .setTitle("版本信息")
+ .setMessage("应用名称:Live Streaming\n" +
+ "版本号:1.0\n\n" +
+ "© 2024 Live Streaming. All rights reserved.")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+ }
+
+ private void showUserAgreementDialog() {
+ new AlertDialog.Builder(this)
+ .setTitle("用户协议")
+ .setMessage("用户服务协议\n\n" +
+ "欢迎使用Live Streaming直播应用。在使用本应用前,请仔细阅读以下条款。\n\n" +
+ "1. 服务条款\n" +
+ "使用本应用即表示您同意遵守本协议的所有条款。\n\n" +
+ "2. 用户行为规范\n" +
+ "• 不得发布违法违规内容\n" +
+ "• 不得进行欺诈、骚扰等行为\n" +
+ "• 尊重他人,文明互动\n\n" +
+ "3. 知识产权\n" +
+ "本应用的所有内容受知识产权法保护。\n\n" +
+ "4. 免责声明\n" +
+ "用户需自行承担使用本应用的风险。\n\n" +
+ "5. 协议变更\n" +
+ "我们保留随时修改本协议的权利。\n\n" +
+ "(完整版用户协议待接入)")
+ .setPositiveButton("确定", null)
+ .show();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (clearingDialog != null && clearingDialog.isShowing()) {
+ clearingDialog.dismiss();
+ }
+ }
}
diff --git a/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java b/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java
new file mode 100644
index 00000000..3c49cee8
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/ShareUtils.java
@@ -0,0 +1,62 @@
+package com.example.livestreaming;
+
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * 分享工具类
+ * 提供系统分享功能
+ */
+public class ShareUtils {
+
+ /**
+ * 生成个人主页分享链接
+ */
+ public static String generateProfileShareLink(String userId) {
+ return "https://livestreaming.com/profile/" + userId;
+ }
+
+ /**
+ * 生成直播间分享链接
+ */
+ public static String generateRoomShareLink(String roomId) {
+ return "https://livestreaming.com/room/" + roomId;
+ }
+
+ /**
+ * 分享链接(使用系统分享菜单)
+ */
+ public static void shareLink(Context context, String link, String title, String text) {
+ if (context == null || link == null) return;
+
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, title != null ? title : "分享");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, (text != null ? text + "\n" : "") + link);
+
+ try {
+ context.startActivity(Intent.createChooser(shareIntent, "分享到"));
+ } catch (Exception e) {
+ // 如果分享失败,忽略异常
+ }
+ }
+
+ /**
+ * 分享文本
+ */
+ public static void shareText(Context context, String text, String title) {
+ if (context == null || text == null) return;
+
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.setType("text/plain");
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, title != null ? title : "分享");
+ shareIntent.putExtra(Intent.EXTRA_TEXT, text);
+
+ try {
+ context.startActivity(Intent.createChooser(shareIntent, "分享到"));
+ } catch (Exception e) {
+ // 如果分享失败,忽略异常
+ }
+ }
+}
+
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 283f548b..84444698 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
@@ -32,6 +32,9 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private SearchSuggestionsAdapter suggestionsAdapter;
private final List discoverAllRooms = new ArrayList<>();
+
+ private RoomsAdapter followRoomsAdapter;
+ private final List followAllRooms = new ArrayList<>();
private BadgesAdapter badgesAdapter;
@@ -39,6 +42,18 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private NearbyUsersAdapter addFriendAdapter;
private final List addFriendAllUsers = new ArrayList<>();
+
+ /**
+ * 设置关注容器的可见性
+ */
+ private void setFollowContainerVisibility(int visibility) {
+ View followContainer = binding.getRoot().findViewById(R.id.followContainer);
+ if (followContainer != null) {
+ followContainer.setVisibility(visibility);
+ } else {
+ binding.followRecyclerView.setVisibility(visibility);
+ }
+ }
public static void start(Context context, String title) {
Intent intent = new Intent(context, TabPlaceholderActivity.class);
@@ -94,7 +109,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.genericScroll.setVisibility(View.VISIBLE);
binding.discoverContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.VISIBLE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
}
@@ -106,7 +121,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureDiscoverSuggestions();
@@ -204,10 +219,23 @@ public class TabPlaceholderActivity extends AppCompatActivity {
private void showFollowRooms() {
binding.genericScroll.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.VISIBLE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
+
+ // 显示关注容器(包含搜索框和列表)
+ setFollowContainerVisibility(View.VISIBLE);
- RoomsAdapter adapter = new RoomsAdapter(room -> {
+ ensureFollowRoomsAdapter();
+
+ // 初始化数据
+ followAllRooms.clear();
+ followAllRooms.addAll(buildFollowDemoRooms(16));
+ followRoomsAdapter.submitList(new ArrayList<>(followAllRooms));
+ }
+
+ private void ensureFollowRoomsAdapter() {
+ if (followRoomsAdapter != null) return;
+
+ followRoomsAdapter = new RoomsAdapter(room -> {
if (room == null) return;
Intent intent = new Intent(TabPlaceholderActivity.this, RoomDetailActivity.class);
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
@@ -217,14 +245,76 @@ public class TabPlaceholderActivity extends AppCompatActivity {
StaggeredGridLayoutManager glm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
glm.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
binding.followRecyclerView.setLayoutManager(glm);
- binding.followRecyclerView.setAdapter(adapter);
+ binding.followRecyclerView.setAdapter(followRoomsAdapter);
+
+ // 设置搜索框
+ setupFollowSearchBox();
+ }
+
+ private void setupFollowSearchBox() {
+ android.widget.EditText searchInput = binding.getRoot().findViewById(R.id.followSearchInput);
+ View clearButton = binding.getRoot().findViewById(R.id.followSearchClear);
+
+ if (searchInput == null) return;
+
+ // 添加文本监听
+ searchInput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
- adapter.submitList(buildFollowDemoRooms(16));
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ String text = s != null ? s.toString() : "";
+ // 显示/隐藏清空按钮
+ if (clearButton != null) {
+ clearButton.setVisibility(text.isEmpty() ? View.GONE : View.VISIBLE);
+ }
+ // 应用筛选
+ applyFollowFilter(text);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ // 清空按钮点击事件
+ if (clearButton != null) {
+ clearButton.setOnClickListener(v -> {
+ if (searchInput != null) {
+ searchInput.setText("");
+ searchInput.requestFocus();
+ }
+ });
+ }
+ }
+
+ private void applyFollowFilter(String query) {
+ if (followRoomsAdapter == null || followAllRooms == null) return;
+
+ String searchQuery = query != null ? query.trim().toLowerCase() : "";
+ if (searchQuery.isEmpty()) {
+ followRoomsAdapter.submitList(new ArrayList<>(followAllRooms));
+ return;
+ }
+
+ List filtered = new ArrayList<>();
+ for (Room r : followAllRooms) {
+ if (r == null) continue;
+ String title = r.getTitle() != null ? r.getTitle().toLowerCase() : "";
+ String streamer = r.getStreamerName() != null ? r.getStreamerName().toLowerCase() : "";
+ if (title.contains(searchQuery) || streamer.contains(searchQuery)) {
+ filtered.add(r);
+ }
+ }
+
+ followRoomsAdapter.submitList(filtered);
}
private void showNearbyUsers() {
binding.genericScroll.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.VISIBLE);
NearbyUsersAdapter adapter = new NearbyUsersAdapter(user -> {
@@ -291,7 +381,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureParkBadges();
@@ -341,7 +431,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureMore();
@@ -461,7 +551,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.VISIBLE);
binding.addFriendContainer.setVisibility(View.GONE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
binding.locationRefresh.setOnClickListener(v -> Toast.makeText(this, "刷新定位(待接入)", Toast.LENGTH_SHORT).show());
@@ -478,7 +568,7 @@ public class TabPlaceholderActivity extends AppCompatActivity {
binding.locationDiscoverContainer.setVisibility(View.GONE);
binding.addFriendContainer.setVisibility(View.VISIBLE);
binding.genericPlaceholderContainer.setVisibility(View.GONE);
- binding.followRecyclerView.setVisibility(View.GONE);
+ setFollowContainerVisibility(View.GONE);
binding.nearbyRecyclerView.setVisibility(View.GONE);
ensureAddFriends();
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup
new file mode 100644
index 00000000..297607cc
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/Room.java.backup
@@ -0,0 +1,119 @@
+package com.example.livestreaming.net;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.Objects;
+
+public class Room {
+
+ @SerializedName("id")
+ private String id;
+
+ @SerializedName("title")
+ private String title;
+
+ @SerializedName("streamerName")
+ private String streamerName;
+
+ @SerializedName("type")
+ private String type;
+
+ @SerializedName("streamKey")
+ private String streamKey;
+
+ @SerializedName("isLive")
+ private boolean isLive;
+
+ @SerializedName("viewerCount")
+ private int viewerCount;
+
+ @SerializedName("streamUrls")
+ private StreamUrls streamUrls;
+
+ public Room() {
+ }
+
+ public Room(String id, String title, String streamerName, boolean isLive) {
+ this.id = id;
+ this.title = title;
+ this.streamerName = streamerName;
+ this.isLive = isLive;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setStreamerName(String streamerName) {
+ this.streamerName = streamerName;
+ }`n`n public void setType(String type) {`n this.type = type;`n }
+
+ public void setLive(boolean live) {
+ isLive = live;
+ }
+
+ public void setStreamKey(String streamKey) {
+ this.streamKey = streamKey;
+ }
+
+ public void setStreamUrls(StreamUrls streamUrls) {
+ this.streamUrls = streamUrls;
+ }
+
+ public void setViewerCount(int viewerCount) {
+ this.viewerCount = viewerCount;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getStreamerName() {
+ return streamerName;
+ }`n`n public String getType() {`n return type;`n }
+
+ public String getStreamKey() {
+ return streamKey;
+ }
+
+ public boolean isLive() {
+ return isLive;
+ }
+
+ public StreamUrls getStreamUrls() {
+ return streamUrls;
+ }
+
+ public int getViewerCount() {
+ return viewerCount;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Room)) return false;
+ Room room = (Room) o;
+ return isLive == room.isLive
+ && viewerCount == room.viewerCount
+ && Objects.equals(id, room.id)
+ && Objects.equals(title, room.title)
+ && Objects.equals(streamerName, room.streamerName)
+ && Objects.equals(type, room.type)
+ && Objects.equals(type, room.type)
+ && Objects.equals(streamKey, room.streamKey)
+ && Objects.equals(streamUrls, room.streamUrls);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, title, streamerName, type, type, streamKey, isLive, viewerCount, streamUrls);
+ }
+}
diff --git a/android-app/app/src/main/res/layout/activity_notification_settings.xml b/android-app/app/src/main/res/layout/activity_notification_settings.xml
new file mode 100644
index 00000000..5e91b15b
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_notification_settings.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_tab_placeholder.xml b/android-app/app/src/main/res/layout/activity_tab_placeholder.xml
index 89c07420..103ba8f7 100644
--- a/android-app/app/src/main/res/layout/activity_tab_placeholder.xml
+++ b/android-app/app/src/main/res/layout/activity_tab_placeholder.xml
@@ -1062,16 +1062,81 @@
-
+ android:orientation="vertical"
+ android:visibility="gone">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/dialog_change_password.xml b/android-app/app/src/main/res/layout/dialog_change_password.xml
new file mode 100644
index 00000000..63b79731
--- /dev/null
+++ b/android-app/app/src/main/res/layout/dialog_change_password.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/dialog_feedback.xml b/android-app/app/src/main/res/layout/dialog_feedback.xml
new file mode 100644
index 00000000..8aa982e7
--- /dev/null
+++ b/android-app/app/src/main/res/layout/dialog_feedback.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/未完成功能清单.md b/android-app/未完成功能清单.md
new file mode 100644
index 00000000..8174089f
--- /dev/null
+++ b/android-app/未完成功能清单.md
@@ -0,0 +1,309 @@
+# 项目未完成功能清单
+---
+
+## ✅ 刚刚完成的功能
+
+### 通知功能(前端UI)✅
+- ✅ 通知列表页面 (NotificationsActivity)
+- ✅ 通知分类(系统、互动、关注、私信、直播)
+- ✅ 通知设置页面 (NotificationSettingsActivity)
+- ✅ 本地通知功能 (LocalNotificationManager)
+
+---
+
+## 🔴 高优先级未完成功能(前端可独立完成)
+
+### 1. **数据持久化(Room数据库)** ⭐⭐⭐
+**状态**: 未开始
+**位置**: 文档第472行
+
+- [ ] 引入 Room 数据库依赖
+- [ ] 创建数据库实体(Room、User、Message、Notification等)
+- [ ] 创建 DAO 接口
+- [ ] 创建数据库类
+- [ ] 实现 Repository 模式
+- [ ] 缓存直播间列表到本地
+- [ ] 缓存用户信息
+- [ ] 实现搜索历史存储
+- [ ] 实现观看历史存储
+- [ ] 实现消息记录缓存
+
+**预计工作量**: 3-5天
+
+---
+
+### 2. **顶部标签页功能** ⭐⭐
+**状态**: ✅ 已完成
+**位置**: MainActivity.java 第265-291行
+
+- [x] 实现关注页面(显示已关注主播的直播)
+- [x] 实现发现页面(推荐算法前端实现)
+- [x] 实现附近页面(使用模拟位置数据)
+- [x] 添加位置权限申请(即使后端未就绪)
+
+**完成说明**:
+- 修改MainActivity,让顶部标签页在当前页面切换内容,而不是跳转到新页面
+- 关注页面:显示已关注主播的直播列表(基于FollowingListActivity的数据结构)
+- 发现页面:实现前端推荐算法(优先显示正在直播的房间,按类型排序)
+- 附近页面:使用模拟位置数据,添加位置权限检查和申请逻辑
+- 在AndroidManifest.xml中添加了位置权限声明
+- 实现了位置权限申请对话框和权限被拒绝时的友好提示
+
+**预计工作量**: 3-4天(已完成)
+
+---
+
+### 3. **搜索功能增强** ⭐
+**状态**: 基础功能完成,增强功能未实现
+**位置**: SearchActivity.java
+
+- [ ] 实现搜索历史(本地存储,使用SharedPreferences或Room)
+- [ ] 实现热门搜索(模拟数据)
+- [ ] 优化搜索性能(防抖、缓存)
+- [ ] 添加搜索建议(自动补全)
+
+**预计工作量**: 2-3天
+
+---
+
+### 4. **作品功能(前端UI)** ⭐
+**状态**: UI已存在,功能未实现
+**位置**: ProfileActivity.java 第254行
+
+- [ ] 实现作品列表UI(已有tabWorks布局)
+- [ ] 实现作品发布UI(数据仅本地存储)
+- [ ] 实现作品详情页面
+- [ ] 实现作品编辑UI
+- [ ] 作品数据模型和适配器
+
+**预计工作量**: 3-4天
+
+---
+
+### 5. **许愿树核心功能** ⭐
+**状态**: 仅UI完成,核心功能未实现
+**位置**: WishTreeActivity.java
+
+- [ ] 实现许愿功能
+- [ ] 实现抽奖功能
+- [ ] 实现许愿列表展示
+- [ ] 实现倒计时结束后的事件处理
+- [ ] 许愿数据模型和存储
+
+**预计工作量**: 3-4天
+
+---
+
+## 🟡 中优先级未完成功能
+
+### 6. **占位页面功能实现**
+**状态**: 多个功能跳转到TabPlaceholderActivity
+**位置**: TabPlaceholderActivity.java
+
+以下功能需要实现(目前都是占位页面):
+
+- [ ] 语音匹配 (VoiceMatchActivity) - 已有Activity但功能未实现
+- [ ] 心动信号 (HeartbeatSignalActivity) - 已有Activity但功能未实现
+- [ ] 在线处对象 (OnlineDatingActivity) - 已有Activity但功能未实现
+- [ ] 找人玩游戏 (FindGameActivity) - 已有Activity但功能未实现
+- [ ] 一起KTV (KTVTogetherActivity) - 已有Activity但功能未实现
+- [ ] 你画我猜 (DrawGuessActivity) - 已有Activity但功能未实现
+- [ ] 和平精英 (PeaceEliteActivity) - 已有Activity但功能未实现
+- [ ] 桌子游 (TableGamesActivity) - 已有Activity但功能未实现
+- [ ] 公园勋章 - 跳转到TabPlaceholderActivity
+- [ ] 加好友 - 跳转到TabPlaceholderActivity
+- [ ] 定位/发现 - 跳转到TabPlaceholderActivity
+- [ ] 附近直播 - TabPlaceholderActivity中显示"待接入"
+- [ ] 热门地点 - TabPlaceholderActivity中显示"待接入"
+- [ ] 榜单功能 - TabPlaceholderActivity中显示"待接入"
+- [ ] 话题功能 - TabPlaceholderActivity中显示"待接入"
+
+**预计工作量**: 每个功能1-2天,总计15-30天
+
+---
+
+### 7. **设置页面完善** ⭐ ✅ 已完成
+**状态**: 功能已完善
+**位置**: SettingsPageActivity.java
+
+- [x] 完善设置页面功能
+- [x] 添加应用设置(缓存清理功能实现)
+- [x] 添加账号设置UI(修改密码、绑定手机号等)
+- [x] 添加隐私设置UI(黑名单、权限管理等)
+- [x] 实现帮助中心内容
+- [x] 实现关于页面内容
+
+**完成内容**:
+- 创建了 `CacheManager` 缓存管理工具类
+- 实现了所有设置页面的功能对话框
+- 创建了对话框布局文件
+- 所有功能都已实现(部分功能待接入后端API)
+
+**预计工作量**: 2-3天
+
+---
+
+### 8. **引导页面和帮助** ⭐
+**状态**: 未实现
+**位置**: 无
+
+- [ ] 实现首次启动引导页(ViewPager2)
+- [ ] 添加功能介绍页面
+- [ ] 添加权限说明页面
+- [ ] 实现帮助中心
+
+**预计工作量**: 2-3天
+
+---
+
+## 🟢 低优先级未完成功能(体验增强)
+
+### 9. **过渡动画和交互优化** ⭐
+**状态**: 未实现
+
+- [ ] 添加 Activity 过渡动画
+- [ ] 实现共享元素过渡
+- [ ] 优化页面内动画
+- [ ] 添加触觉反馈(Haptic Feedback)
+
+**预计工作量**: 2-3天
+
+---
+
+### 10. **深色模式支持** ⭐
+**状态**: 未实现
+
+- [ ] 创建深色模式资源
+- [ ] 适配所有页面颜色
+- [ ] 添加手动切换功能
+- [ ] 测试深色模式显示
+
+**预计工作量**: 3-4天
+
+---
+
+### 11. **多屏幕适配** ⭐
+**状态**: 未实现
+
+- [ ] 优化平板布局
+- [ ] 添加横屏布局
+- [ ] 测试不同屏幕尺寸
+- [ ] 优化不同分辨率显示
+
+**预计工作量**: 2-3天
+
+---
+
+## ⚪ 架构和优化(持续进行)
+
+### 12. **前端架构优化** ⭐⭐
+**状态**: 未开始
+
+- [ ] 引入 MVVM 架构(ViewModel + LiveData)
+- [ ] 实现 Repository 模式(本地数据源)
+- [ ] 提取公共基类 Activity
+- [ ] 创建工具类库
+- [ ] 引入依赖注入(Hilt,可选)
+
+**预计工作量**: 5-7天
+
+---
+
+### 13. **性能优化** ⭐
+**状态**: 部分完成
+
+- [ ] 优化图片加载(Glide配置优化)
+- [ ] 优化列表滚动性能
+- [ ] 实现请求去重
+- [ ] 优化内存使用
+
+**预计工作量**: 3-4天
+
+---
+
+### 14. **代码质量提升** ⭐
+**状态**: 持续进行
+
+- [ ] 提取硬编码字符串到资源文件
+- [ ] 添加代码注释
+- [ ] 统一代码风格
+- [ ] 重构重复代码
+
+**预计工作量**: 持续进行
+
+---
+
+### 15. **测试** ⭐
+**状态**: 未开始
+
+- [ ] 编写单元测试(工具类)
+- [ ] 编写 UI 测试(关键流程)
+- [ ] 性能测试
+- [ ] 兼容性测试
+
+**预计工作量**: 持续进行
+
+---
+
+## ❌ 需要后端支持的功能(暂不实现)
+
+以下功能需要后端支持,建议后端开发完成后再实现:
+
+- ❌ 后端API完整集成(等待后端接口)
+- ❌ 实时通信(WebSocket,等待后端)
+- ❌ 真实数据同步(等待后端)
+- ❌ 用户登录/注册(等待后端)
+- ❌ 支付功能(等待后端和支付SDK)
+- ❌ 推流功能(如需要,等待推流SDK集成)
+- ❌ 礼物打赏功能(等待后端和支付SDK)
+- ❌ 弹幕功能(等待WebSocket服务)
+
+---
+
+## 📊 完成度统计
+
+### 前端可独立完成的功能
+- **已完成**: 约 40%
+- **进行中**: 约 10%
+- **未开始**: 约 50%
+
+### 核心功能模块
+- ✅ **直播相关**: 85% 完成
+- ✅ **社交功能**: 75% 完成
+- ⚠️ **个人中心**: 70% 完成(作品功能缺失)
+- ⚠️ **发现功能**: 60% 完成(搜索增强、标签页缺失)
+- ⚠️ **特色功能**: 50% 完成(许愿树、占位功能缺失)
+
+---
+
+## 🎯 建议优先完成顺序
+
+### 第一周
+1. **数据持久化(Room数据库)** - 3-5天
+ - 这是基础架构,其他功能会依赖它
+
+### 第二周
+2. **搜索功能增强** - 2-3天
+3. **作品功能** - 3-4天
+
+### 第三周
+4. **顶部标签页功能** - 3-4天
+5. **许愿树核心功能** - 3-4天
+
+### 第四周
+6. **设置页面完善** - 2-3天
+7. **引导页面** - 2-3天
+
+---
+
+## 📝 注意事项
+
+1. **数据持久化**是最重要的基础功能,建议优先完成
+2. **占位页面功能**数量较多,可以根据实际需求选择性实现
+3. **架构优化**可以在功能完善过程中逐步进行
+4. **需要后端支持的功能**可以先做UI,使用模拟数据
+
+---
+
+**最后更新**: 2024年
+
diff --git a/android-app/项目功能完善度分析.md b/android-app/项目功能完善度分析.md
index 458e4fb2..06b770c8 100644
--- a/android-app/项目功能完善度分析.md
+++ b/android-app/项目功能完善度分析.md
@@ -178,7 +178,7 @@
- ✅ 粉丝列表 (FansListActivity) - 基础UI
- ✅ 关注列表 (FollowingListActivity) - 基础UI
- ✅ 获赞列表 (LikesListActivity) - 基础UI
-- ✅ 设置页面 (SettingsPageActivity) - 基础UI
+- ✅ 设置页面 (SettingsPageActivity) - 功能已完善
- ✅ 用户资料(只读)(UserProfileReadOnlyActivity) - 完整实现
---
@@ -449,15 +449,25 @@
- 验证ExoPlayer资源释放逻辑正确(已有实现)
- 集成LeakCanary内存泄漏检测工具
-#### 3. **统一加载状态** ⭐⭐
+#### 3. **统一加载状态** ⭐⭐ ✅ 已完成
**为什么重要**: 提升用户体验一致性
-- [ ] 创建统一的加载状态组件
-- [ ] 实现骨架屏(Skeleton Screen)
-- [ ] 统一所有页面的加载提示
-- [ ] 添加加载动画
+- [x] 创建统一的加载状态组件
+- [x] 实现骨架屏(Skeleton Screen)
+- [x] 统一所有页面的加载提示
+- [x] 添加加载动画
-**预计工作量**: 1-2天
+**完成内容**:
+- 创建了 `LoadingView` 组件,提供统一的加载状态显示(支持自定义提示文字)
+- 实现了 `SkeletonView` 和 `SkeletonRoomAdapter`,支持骨架屏占位(带闪烁动画效果)
+- 创建了 `LoadingStateManager` 工具类,统一管理加载状态(支持LoadingView、ProgressBar、骨架屏)
+- 更新了所有主要Activity使用统一的加载状态:
+ - **MainActivity**: 在列表为空时使用骨架屏替代简单的LoadingView
+ - **RoomDetailActivity**: 使用LoadingView显示加载状态
+ - **SearchActivity**: 搜索时显示"搜索中..."加载状态
+ - **MessagesActivity**: 加载消息列表时显示加载状态
+- 添加了加载动画资源(骨架屏闪烁效果、加载提示文字)
+- 所有加载状态都通过 `LoadingStateManager` 统一管理,确保一致性
#### 4. **数据持久化(本地)** ⭐⭐⭐
**为什么重要**: 支持离线使用,提升用户体验
@@ -486,13 +496,21 @@
**预计工作量**: 5-7天
-#### 6. **分类筛选功能(前端逻辑)** ⭐⭐
+#### 6. **分类筛选功能(前端逻辑)** ⭐⭐ ✅ 已完成
**为什么重要**: UI已准备好,只需完善前端筛选逻辑
-- [ ] 完善本地数据筛选逻辑
-- [ ] 实现筛选条件记忆(SharedPreferences)
-- [ ] 优化筛选性能
-- [ ] 添加筛选动画效果
+- [x] 完善本地数据筛选逻辑
+- [x] 实现筛选条件记忆(SharedPreferences)
+- [x] 优化筛选性能
+- [x] 添加筛选动画效果
+
+**完成内容**:
+- 创建了 `CategoryFilterManager` 类,统一管理筛选逻辑
+- 实现了异步筛选功能,在后台线程执行筛选,避免阻塞UI线程
+- 使用 SharedPreferences 保存和恢复最后选中的分类
+- 添加了平滑的过渡动画效果(淡入淡出 + RecyclerView ItemAnimator)
+- 优化了筛选算法,优先使用房间的type字段,降级到演示数据分类算法
+- 在Activity恢复时自动恢复上次选中的分类标签
**预计工作量**: 1-2天
@@ -516,14 +534,24 @@
**预计工作量**: 2-3天
-#### 9. **消息功能完善(前端)** ⭐⭐
+#### 9. **消息功能完善(前端)** ⭐⭐ ✅ 已完成
**为什么重要**: 完善核心社交功能
-- [ ] 完善消息列表UI
-- [ ] 实现消息状态显示(发送中、已发送、已读)
-- [ ] 优化消息列表性能
-- [ ] 添加消息操作(复制、删除等)
-- [ ] 实现消息搜索(本地)
+- [x] 完善消息列表UI
+- [x] 实现消息状态显示(发送中、已发送、已读)
+- [x] 优化消息列表性能
+- [x] 添加消息操作(复制、删除等)
+- [x] 实现消息搜索(本地)
+
+**完成内容**:
+- 为ChatMessage添加了MessageStatus枚举(发送中、已发送、已读)
+- 在发送的消息气泡旁显示状态图标(时钟、单勾、双勾)
+- 实现了消息长按菜单,支持复制和删除操作
+- 优化了DiffUtil,使用messageId作为唯一标识,提升列表更新性能
+- 在MessagesActivity中添加了搜索功能,支持按会话标题和消息内容搜索
+- 优化了消息列表UI,包括消息预览(处理图片/语音消息)、时间显示、布局优化
+- 添加了内存泄漏防护(onDestroy中清理延迟任务)
+- 优化了滚动行为(使用smoothScrollToPosition)
**预计工作量**: 3-4天
@@ -585,33 +613,59 @@
**预计工作量**: 3-4天
-#### 15. **分享功能(系统分享)** ⭐
+#### 15. **分享功能(系统分享)** ⭐ ✅ 已完成
**为什么重要**: 提升传播能力
-- [ ] 实现系统分享功能
-- [ ] 分享直播间(生成分享链接)
-- [ ] 分享个人主页
-- [ ] 分享图片(截图功能)
+- [x] 实现系统分享功能
+- [x] 分享直播间(生成分享链接)
+- [x] 分享个人主页
+
+**完成内容**:
+- 创建了 `ShareUtils` 工具类,提供系统分享功能(分享文本、链接)
+- 在 `RoomDetailActivity` 中添加了分享按钮,支持分享直播间链接
+- 完善了 `ProfileActivity` 的分享功能,支持分享个人主页链接
+- 添加了分享图标资源 `ic_share_24.xml`
+- 所有分享功能都通过系统分享菜单,支持分享到微信、QQ、微博等应用
**预计工作量**: 2-3天
-#### 16. **通知功能(前端UI)** ⭐
+#### 16. **通知功能(前端UI)** ⭐ ✅ 已完成
**为什么重要**: 完善消息体系
-- [ ] 实现通知列表页面
-- [ ] 实现通知分类
-- [ ] 实现通知设置页面
-- [ ] 添加本地通知(即使后端未就绪)
+- [x] 实现通知列表页面
+- [x] 实现通知分类
+- [x] 实现通知设置页面
+- [x] 添加本地通知(即使后端未就绪)
+
+**完成内容**:
+- 创建了 `NotificationsActivity` 通知列表页面,支持分类筛选(全部、系统、互动、关注、私信、直播)
+- 创建了 `NotificationSettingsActivity` 通知设置页面,支持各类通知开关和免打扰设置
+- 创建了 `LocalNotificationManager` 本地通知管理器,支持发送各类通知
+- 创建了 `NotificationItem` 数据模型和 `NotificationsAdapter` 适配器
+- 在 `MainActivity` 中添加了通知图标点击事件
+- 在 `SettingsPageActivity` 中完善了通知设置入口
+- 在 `LiveStreamingApplication` 中初始化了通知渠道
**预计工作量**: 2-3天
-#### 17. **设置页面完善** ⭐
+#### 17. **设置页面完善** ⭐ ✅ 已完成
**为什么重要**: 完善应用配置
-- [ ] 完善设置页面功能
-- [ ] 添加应用设置(缓存清理、关于等)
-- [ ] 添加账号设置UI
-- [ ] 添加隐私设置UI
+- [x] 完善设置页面功能
+- [x] 添加应用设置(缓存清理、关于等)
+- [x] 添加账号设置UI
+- [x] 添加隐私设置UI
+
+**完成内容**:
+- 创建了 `CacheManager` 缓存管理工具类,支持获取缓存大小、清理所有缓存、清理图片缓存
+- 完善了 `SettingsPageActivity`,实现了所有设置页面的功能:
+ - **账号与安全**: 修改密码对话框、绑定手机号对话框、登录设备管理对话框
+ - **隐私设置**: 黑名单管理、权限管理(跳转系统设置)、隐私政策查看
+ - **清理缓存**: 显示缓存大小、清理所有缓存、清理图片缓存(带进度提示)
+ - **帮助与反馈**: 常见问题、意见反馈对话框、联系客服信息
+ - **关于页面**: 自动获取版本信息、用户协议、隐私政策查看
+- 创建了对话框布局文件:`dialog_change_password.xml`、`dialog_bind_phone.xml`、`dialog_feedback.xml`
+- 所有功能都提供了友好的用户界面和提示信息
**预计工作量**: 2-3天