Compare commits

...

95 Commits

Author SHA1 Message Date
1613fe00ed feat:社区新增关注页面—客户端 https://jira.shanqu.cc/browse/GHZSCY-5098 2024-06-04 15:58:10 +08:00
e955920f7e feat:社区新增关注页面—客户端 https://jira.shanqu.cc/browse/GHZSCY-5098 2024-06-04 14:22:29 +08:00
d7f307603b fix:社区新增关注页面—06/03测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5610 2024-06-04 10:00:09 +08:00
25c60c61d8 fix:社区新增关注页面—0603UI测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5602 2024-06-03 15:24:13 +08:00
7a30967ec6 fix:社区新增关注页面—0529UI测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5530 2024-05-31 18:04:01 +08:00
33aecc1f71 fix: 社区新增关注页面—0528产品测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5498 2024-05-31 17:07:57 +08:00
c7b3b98ed0 fix:社区新增关注页面—0529UI测试—客户端 https://jira.shanqu.cc/projects/GHZSCY/issues/GHZSCY-5530?filter=myopenissues 2024-05-31 10:30:19 +08:00
883e250538 fix:社区新增关注页面—0529测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5521 2024-05-30 10:45:03 +08:00
0b8cc539da fix:社区新增关注页面—0529测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5521 2024-05-29 14:57:13 +08:00
1b63ae871a fix:社区新增关注页面—0528UI测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5509 2024-05-29 14:16:07 +08:00
37d37374b6 fix:社区新增关注页面—0528测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5502 2024-05-28 17:35:46 +08:00
23abf14e32 fix:社区新增关注页面—0528测试—客户端 https://jira.shanqu.cc/browse/GHZSCY-5498 2024-05-28 16:06:29 +08:00
fd114f13aa fix:社区新增关注页面(数据类放在entity包下,防止混淆)https://jira.shanqu.cc/browse/GHZSCY-5098 2024-05-28 10:06:52 +08:00
b13b196639 fix:社区新增关注页面(禁止group相关的viewId混淆) https://jira.shanqu.cc/browse/GHZSCY-5098 2024-05-28 09:24:40 +08:00
5302bc7972 fix:禁止group相关的viewId混淆 https://jira.shanqu.cc/browse/GHZSCY-5098 2024-05-27 17:50:22 +08:00
61bab61ccf feat:社区新增关注页面—客户端 https://jira.shanqu.cc/browse/GHZSCY-5098 2024-05-27 15:35:22 +08:00
60635e748f feat:社区新增关注页面 https://jira.shanqu.cc/browse/GHZSCY-5098 2024-05-27 10:36:23 +08:00
44a2e616ab Merge branch 'feat/log_all_redirected_host' into 'dev'
feat: 游戏下载host上报 https://jira.shanqu.cc/browse/GHZSCY-5476

See merge request halo/android/assistant-android!1680
2024-05-24 15:54:35 +08:00
fffd3ef789 feat: 游戏下载host上报 https://jira.shanqu.cc/browse/GHZSCY-5476 2024-05-24 15:18:22 +08:00
d53782b8de Merge branch 'fix/GHZSCY-5231' into 'dev'
fix:【光环助手】轮播banner刷新时蒙层消失 https://jira.shanqu.cc/browse/GHZSCY-5231

See merge request halo/android/assistant-android!1679
2024-05-22 15:01:47 +08:00
b16c5d723b fix:【光环助手】轮播banner刷新时蒙层消失 https://jira.shanqu.cc/browse/GHZSCY-5231 2024-05-22 14:44:00 +08:00
1dfd636283 Merge branch 'feature-GHZS-5385' into 'dev'
feat: 【光环助手】QQ小游戏支付唤起问题 https://jira.shanqu.cc/browse/GHZSCY-5385

See merge request halo/android/assistant-android!1668
2024-05-22 11:02:38 +08:00
65feed01f4 feat: 【光环助手】QQ小游戏支付唤起问题 https://jira.shanqu.cc/browse/GHZSCY-5385 2024-05-22 11:02:38 +08:00
2a596bd2a0 Merge branch 'fix/vivo_crash' into 'dev'
fix: 修复在 vivo 的 Android 14  设备上点击剪贴板功能时的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/277509

See merge request halo/android/assistant-android!1677
2024-05-21 11:03:28 +08:00
16c4f1016c fix: 修复在 vivo 的 Android 14 设备上点击剪贴板功能时的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/277509 2024-05-20 18:19:23 +08:00
1ced82c1f6 Merge branch 'fix/GHZSCY-5372' into 'dev'
fix:【光环助手】开服表bug https://jira.shanqu.cc/browse/GHZSCY-5372

See merge request halo/android/assistant-android!1676
2024-05-20 11:43:37 +08:00
078c9204af fix:【光环助手】开服表bug https://jira.shanqu.cc/browse/GHZSCY-5372 2024-05-20 11:43:37 +08:00
68818b3409 Merge branch 'feat/GHZSCY-5080' into 'dev'
feat:【光环助手】工具新增跳转类型配置 https://jira.shanqu.cc/browse/GHZSCY-4618

See merge request halo/android/assistant-android!1675
2024-05-17 17:53:13 +08:00
7d4aa34d12 feat:【光环助手】工具新增跳转类型配置 https://jira.shanqu.cc/browse/GHZSCY-4618 2024-05-17 17:53:13 +08:00
2a27e0f467 Merge remote-tracking branch 'origin/release' into dev
# Conflicts:
#	dependencies.gradle
2024-05-16 16:01:28 +08:00
54ee8a9c69 chore: 版本更新至 5.35.5 2024-05-16 11:36:57 +08:00
2b44efd6b3 Merge branch 'hotfix/v5.35.4-1054/wrong_install_status' into 'release'
fix: 修复安装游戏回到光环时的安装状态更新问题

See merge request halo/android/assistant-android!1673
2024-05-16 11:32:46 +08:00
d62c8beb30 fix: 修复安装游戏回到光环时的安装状态更新问题 2024-05-16 11:22:04 +08:00
155ec08280 Merge branch 'fix/image_display_issue' into 'dev'
fix: 修复图片显示问题

See merge request halo/android/assistant-android!1672
2024-05-16 10:54:14 +08:00
d46aa81dbe chore: 版本更新至 5.35.4 2024-05-15 16:30:39 +08:00
51c0bf27cf Merge branch 'hotfix/v5.35.3-1053/wrong_update_status' into 'release'
fix: 修复使用跳转落地页下载安装的游戏更新时下载按钮的更新问题;修复安装同包名同版本不同游戏 id 的游戏时下载按钮的更新问题

See merge request halo/android/assistant-android!1670
2024-05-15 16:29:48 +08:00
43eb4c88c9 fix: 修复普通图片显示的锯齿问题 (还原为加载 View 大小的图片),移除冗余的图片加载高度入参 2024-05-15 16:21:15 +08:00
06b2d2b416 Merge branch 'hotfix/v5.35.3-1053/packagename_null_crash' into 'release'
fix: 修复部分位置跳转安装因为包名为空而产生的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/385367

See merge request halo/android/assistant-android!1671
2024-05-15 16:00:11 +08:00
af7580a6a6 fix: 修复部分位置跳转安装因为包名为空而产生的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/385367 2024-05-15 15:57:37 +08:00
1e4375ec8a fix: 修复使用跳转落地页下载安装的游戏更新时下载按钮的更新问题;修复安装同包名同版本不同游戏 id 的游戏时下载按钮的更新问题 2024-05-15 15:31:41 +08:00
195247a5c3 fix: 修复重登录时,用户头像不能正常显示的问题 2024-05-14 18:10:58 +08:00
dd65bc8bc2 Merge remote-tracking branch 'origin/release' into dev
# Conflicts:
#	dependencies.gradle
2024-05-14 17:38:18 +08:00
196e719358 chore: 版本更新至 5.35.3 2024-05-14 17:13:10 +08:00
b1c267b179 Merge branch 'fix/rank_loading_issue' into 'dev'
fix: 修复进入排行榜页面时全量加载所有页面导致页面卡顿的问题

See merge request halo/android/assistant-android!1669
2024-05-14 16:18:34 +08:00
a2679c8dbd fix: 修复进入排行榜页面时全量加载所有页面导致页面卡顿的问题
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-14 16:14:36 +08:00
c9e1816be0 Merge branch 'feat/remove_useless_legacy_code' into 'dev'
feat: 移除部分冗余的历史代码,关闭部分意义不大的日志输出(有需要时再手动开启)

See merge request halo/android/assistant-android!1667
2024-05-14 15:07:00 +08:00
a1f0455d5a feat: 移除部分冗余的历史代码,关闭部分意义不大的日志输出(有需要时再手动开启)
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-14 15:02:00 +08:00
5b3f4b6104 Merge branch 'chen/202405/GHZSCY-5378' into 'dev'
fix:【光环助手】游戏专题点击神策埋点问题 https://jira.shanqu.cc/browse/GHZSCY-5378

See merge request halo/android/assistant-android!1666
2024-05-14 11:02:42 +08:00
fe4e9d9d3b fix:【光环助手】游戏专题点击神策埋点问题 https://jira.shanqu.cc/browse/GHZSCY-5378 2024-05-14 10:58:57 +08:00
dd051b4d13 Merge branch 'hotfix/v5.35.2-1052/logout_crash' into 'release'
fix: 修复退出登录出现的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/385028/?project=22

See merge request halo/android/assistant-android!1665
2024-05-14 09:45:26 +08:00
e7f756555c fix: 修复退出登录出现的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/385028/?project=22 2024-05-14 09:39:57 +08:00
9488837e9e Merge remote-tracking branch 'origin/release' into dev
# Conflicts:
#	dependencies.gradle
2024-05-10 14:14:17 +08:00
b8c4f1403b chore: 版本更新至 5.35.2 2024-05-10 11:18:37 +08:00
b88698c2a3 Merge branch 'hotfix/v5.35.1-1051/crashes' into 'release'
fix: 修复 5.35.1 线上闪退问题

See merge request halo/android/assistant-android!1662
2024-05-10 11:17:13 +08:00
c897d5ad0f fix: 修复多线程更新已安装列表时的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/384726 https://sentry.shanqu.cc/organizations/lightgame/issues/384682
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-10 10:53:41 +08:00
037f453a75 fix: 修复小米设备安装 APKS 格式 XAPK 包时候的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/384710
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-10 10:51:38 +08:00
ad3a3c1341 Merge branch 'hotfix/v5.35.1-1051/hot_launch_splash_ad' into 'release'
fix: 修复移除第三方广告SDK后错误展示热启动广告的问题

See merge request halo/android/assistant-android!1661
2024-05-09 17:45:06 +08:00
ba320f7740 fix: 修复移除第三方广告SDK后错误展示热启动广告的问题 2024-05-09 17:19:33 +08:00
f3dbc0b779 Merge branch 'hotfix/v5.35.1-1051/update_notify_issue' into 'release'
fix: 修复更新应用时遗漏模拟卸载监听导致的状态变更异常问题

See merge request halo/android/assistant-android!1660
2024-05-09 15:40:05 +08:00
0c518ac40e fix: 修复更新应用时遗漏模拟卸载监听导致的状态变更异常问题 2024-05-09 15:37:45 +08:00
bf57118900 Merge branch 'hotfix/v5.35.1-1051/init_crash' into 'release'
fix: 修复部分 downloadEntity versionName 而导致的启动闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/384612

See merge request halo/android/assistant-android!1659
2024-05-09 13:42:04 +08:00
b80a14f2b1 fix: 修复部分 downloadEntity versionName 而导致的启动闪退问题
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-09 11:48:43 +08:00
1450064640 chore: 版本更新至 5.35.1 2024-05-09 10:26:59 +08:00
90e19d5099 Merge branch 'feat/GHZSCY-5342' into 'release'
feat: 关于移除监听广播后的相关优化 https://jira.shanqu.cc/browse/GHZSCY-5342

See merge request halo/android/assistant-android!1657
2024-05-09 10:25:59 +08:00
383124dc36 feat: 关于移除监听广播后的相关优化(处理错误注释) https://jira.shanqu.cc/browse/GHZSCY-5342
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-09 10:21:28 +08:00
6d6ce2613a feat: 关于移除监听广播后的相关优化 https://jira.shanqu.cc/browse/GHZSCY-5342 2024-05-09 09:21:34 +08:00
8569264b82 feat: 合规调整-已安装列表权限-2024/4/22 https://jira.shanqu.cc/browse/GHZSCY-5250 2024-05-09 09:21:34 +08:00
59667abf09 feat: 合规调整-2024/4/30-后台静默监听行为 https://jira.shanqu.cc/browse/GHZSCY-5331 2024-05-09 09:21:34 +08:00
5201637326 Merge branch 'docs/GHZSCY-5350' into 'dev'
docs: 补充 Activity 类名注释 https://jira.shanqu.cc/browse/GHZSCY-5350

See merge request halo/android/assistant-android!1656
2024-05-08 17:04:41 +08:00
417c41c8a0 docs: 补充 Activity 类名注释 https://jira.shanqu.cc/browse/GHZSCY-5350 2024-05-08 16:59:53 +08:00
b651ef8617 Merge branch 'hotfix/v5.35.0-1050/GHZSCY-5329' into 'release'
fix: 优化 gid 的获取逻辑,避免部分接口请求时因为 gid 为空供导致异常 https://jira.shanqu.cc/browse/GHZSCY-5329

See merge request halo/android/assistant-android!1654
2024-05-08 11:14:46 +08:00
af67894d4e Merge branch 'fix/GHZSCY-5339' into 'dev'
fix: 游戏专题-游戏替换问题 https://jira.shanqu.cc/browse/GHZSCY-5329

See merge request halo/android/assistant-android!1655
2024-05-08 11:10:23 +08:00
76e17eddd7 fix: 优化 gid 的获取逻辑,避免部分接口请求时因为 gid 为空供导致异常 https://jira.shanqu.cc/browse/GHZSCY-5329 2024-05-08 10:03:02 +08:00
f8c9c41eb0 Merge branch 'feat/GHZSCY-5281' into 'dev'
feat: 搜索业务-游戏搜索结果页面补充神策埋点—客户端 https://jira.shanqu.cc/browse/GHZSCY-5281

See merge request halo/android/assistant-android!1653
2024-05-07 18:01:46 +08:00
40ac173389 feat: 搜索业务-游戏搜索结果页面补充神策埋点—客户端 https://jira.shanqu.cc/browse/GHZSCY-5281 2024-05-07 18:01:46 +08:00
c6bc7ca6cc fix: 游戏专题-游戏替换问题 https://jira.shanqu.cc/browse/GHZSCY-5329
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-07 11:51:50 +08:00
f01e08aec9 Merge branch 'hotfix/v5.35.0-1050/crashes' into 'release'
fix: 修复线上闪退问题

See merge request halo/android/assistant-android!1652
2024-05-07 11:02:11 +08:00
faddf5d7b6 fix: 收到卸载广播时移除调用 MutableCollections.removeAll() 方法引起的数组越界异常 (MutableCollections.removeAll()线程不安全) https://sentry.shanqu.cc/organizations/lightgame/issues/242447
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-07 10:38:55 +08:00
81cf2f0ddc fix: 部分镜像游戏开关切换时由于不同接口更新不及时导致的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/378659
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-07 09:56:29 +08:00
198561d15a fix: 收到卸载广播时移除调用 MutableCollections.removeAll() 方法引起的数组越界异常 (MutableCollections.removeAll()线程不安全) https://sentry.shanqu.cc/organizations/lightgame/issues/242447
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-07 09:42:44 +08:00
1e721b699c fix: 修复存储空间不足时进行数据库删除操作引起的闪退问题 https://sentry.shanqu.cc/organizations/lightgame/issues/381076
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-05-06 17:58:50 +08:00
8f48dfd347 Merge branch 'fix-community_browsing_duration_error' into 'release'
fix: 使用ViewPager2导致CommunityBrowsingDuration埋点上报异常的问题

See merge request halo/android/assistant-android!1651
2024-04-30 15:04:50 +08:00
bc811a2882 fix: 使用ViewPager2导致CommunityBrowsingDuration埋点上报异常的问题 2024-04-30 14:56:42 +08:00
bdccbc4c28 chore: 版本更新至 5.36.0 2024-04-29 14:51:06 +08:00
bfd986fdfd Merge remote-tracking branch 'origin/release' into dev 2024-04-28 10:59:46 +08:00
a2bd5e01e0 Merge branch 'hotfix/v5.35.0-1050/jpush_clear_badge' into 'release'
fix: 修复极光推送清除角标提前获取信息的问题

See merge request halo/android/assistant-android!1649
2024-04-28 10:43:11 +08:00
844d227f19 fix: 修复极光推送清除角标提前获取信息的问题 2024-04-28 10:26:20 +08:00
8b1f92e9c4 Merge branch 'chen/202404/GHZSCY-5293' into 'dev'
fix:【光环助手】新游开测右上角显示 https://jira.shanqu.cc/browse/GHZSCY-5293

See merge request halo/android/assistant-android!1648
2024-04-28 10:12:25 +08:00
f5834d440b fix:【光环助手】新游开测右上角显示 https://jira.shanqu.cc/browse/GHZSCY-5293 2024-04-28 10:09:20 +08:00
f982bf6478 Merge branch 'hotfix/v5.35.0-1050/disable_jpush_auto_init' into 'release'
fix: 移除极光推送通过 ContentProvider 自动初始化的问题

See merge request halo/android/assistant-android!1647
2024-04-26 14:01:09 +08:00
33d7afec71 fix: 移除极光推送通过 ContentProvider 自动初始化相关代码 2024-04-26 13:40:16 +08:00
37ef50f323 Merge branch 'feat/GHZSCY-5243' into 'dev'
feat: 穿山甲广告SDK更换新版本与信息流广告优化 https://jira.shanqu.cc/browse/GHZSCY-5243

See merge request halo/android/assistant-android!1641
2024-04-26 10:10:33 +08:00
4c6acdee3a Merge branch 'fix/GHZSCY-5261' into 'dev'
fix: 【光环助手】开屏广告神策埋点数据上报问题 https://jira.shanqu.cc/browse/GHZSCY-5261

See merge request halo/android/assistant-android!1646
2024-04-25 10:36:50 +08:00
6beb060e63 fix: 【光环助手】开屏广告神策埋点数据上报问题 https://jira.shanqu.cc/browse/GHZSCY-5261 2024-04-25 10:31:22 +08:00
d11ccba0b7 feat: 穿山甲广告SDK更换新版本与信息流广告优化 https://jira.shanqu.cc/browse/GHZSCY-5243
Signed-off-by: chenjuntao <chenjuntao@ghzhushou.com>
2024-04-22 16:29:42 +08:00
367 changed files with 13585 additions and 3376 deletions

View File

@ -72,6 +72,7 @@ android_build:
only:
- dev
- release
- feat/GHZSCY-5098
# 代码检查
sonarqube_analysis:
@ -103,6 +104,7 @@ sonarqube_analysis:
only:
- dev
- release
- feat/GHZSCY-5098
## 发送简易检测结果报告
send_sonar_report:
@ -121,6 +123,7 @@ send_sonar_report:
only:
- dev
- release
- feat/GHZSCY-5098
oss-upload&send-email:
tags:
@ -152,4 +155,5 @@ oss-upload&send-email:
- /usr/local/bin/python /ci-android-mail-jira-comment.py
only:
- dev
- release
- release
- feat/GHZSCY-5098

View File

@ -401,7 +401,7 @@ dependencies {
implementation(project(':feature:pkg'))
implementation(project(':feature:oaid'))
implementation(project(':feature:floating-window'))
implementation(project(':feature:csj_ad'))
// implementation(project(':feature:csj_ad'))
// implementation(project(':feature:beizi_startup_ad'))
implementation(project(':feature:xapk-installer'))
implementation(project(':feature:qq_game')) {
@ -409,12 +409,13 @@ dependencies {
}
internalImplementation(project(':module_internal_test'))
// 根据BUILD_PUSH_TYPE决定使用哪个推送SDK目前默认使用阿里云推送
def pushProject = findProperty('BUILD_PUSH_TYPE') == 'jg'
? project(':feature:jg_push') : project(':feature:acloud_push')
implementation(pushProject) {
exclude group: 'androidx.swiperefreshlayout'
}
// def pushProperty = findProperty('BUILD_PUSH_TYPE')
// // 根据BUILD_PUSH_TYPE决定使用哪个推送SDK目前默认使用极光推送
// def pushProject = (pushProperty == null || pushProperty == 'jg')
// ? project(':feature:jg_push') : project(':feature:acloud_push')
// implementation(pushProject) {
// exclude group: 'androidx.swiperefreshlayout'
// }
}
File propFile = file('sign.properties')
@ -568,7 +569,16 @@ andResGuard {
"R.id.cardMask",
"R.id.cardGradientMask",
"R.id.gameIconIv",
"R.id.titleContainer"
"R.id.titleContainer",
"R.id.v_login_background",
"R.id.tv_login_tips",
"R.id.tv_login",
"R.id.v_code_background",
"R.id.tv_code_tips",
"R.id.tv_code",
"R.id.tv_copy",
"R.id.v_indicator_background",
"R.id.v_indicator"
]
compressFilePattern = [
"*.png",

View File

@ -770,8 +770,19 @@
android:name="com.gh.gamecenter.wrapper.ToolbarWrapperActivity"
android:screenOrientation="portrait" />
<activity android:name=".forum.home.CommunityActivity"
android:screenOrientation="portrait"/>
<activity
android:name=".forum.home.CommunityActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.home.follow.FollowDynamicActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Transparent" />
<activity
android:name=".forum.home.follow.AllFollowedActivity"
android:screenOrientation="portrait"
android:theme="@style/AppCompatTheme.APP" />
<!-- <activity-->
<!-- android:name="${applicationId}.douyinapi.DouYinEntryActivity"-->

View File

@ -191,10 +191,11 @@ object AdDelegateHelper {
}
/**
* 热启动是否需要显示开屏广告
* 热启动是否需要显示开屏广告(目前只展示第三方广告)
*/
private fun shouldShowStartUpAdWhenHotLaunch() =
mSplashAd?.displayRule?.hotStartSplashAd?.type == AD_TYPE_SDK && mSplashAd?.hotStartThirdPartyAd != null
private fun shouldShowStartUpAdWhenHotLaunch() = (mCsjAdImpl != null || mBeiziAdImpl != null)
&& mSplashAd?.displayRule?.hotStartSplashAd?.type == AD_TYPE_SDK
&& mSplashAd?.hotStartThirdPartyAd != null
/**
* 是否需要显示下载管理广告

View File

@ -45,6 +45,14 @@ class GlobalActivityLifecycleObserver : Application.ActivityLifecycleCallbacks {
}
isFromBackgroundToForeground = false
}
if (activityCount == 1) {
// 清除桌面角标
if (activity !is SplashScreenActivity && activity !is AuthorizationActivity) {
val pushProvider = ARouter.getInstance().build(RouteConsts.provider.push).navigation() as? IPushProvider
pushProvider?.cleanBadgeNumber(activity.applicationContext)
}
}
}
override fun onActivityResumed(activity: Activity) {
@ -84,10 +92,6 @@ class GlobalActivityLifecycleObserver : Application.ActivityLifecycleCallbacks {
}
XapkInstaller.updateCurrentInstallStatus()
// 清除桌面角标
val pushProvider = ARouter.getInstance().build(RouteConsts.provider.push).navigation() as? IPushProvider
pushProvider?.cleanBadgeNumber(activity.applicationContext)
}
override fun onActivityPaused(activity: Activity) {

View File

@ -1,6 +1,7 @@
package com.gh.common.constant;
import android.annotation.SuppressLint;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
@ -28,6 +29,7 @@ import com.gh.gamecenter.entity.VSetting;
import com.gh.gamecenter.feature.entity.SettingsEntity;
import com.gh.gamecenter.feature.entity.SimulatorEntity;
import com.gh.gamecenter.feature.utils.ContentBlockedHelper;
import com.gh.gamecenter.receiver.PackageChangeBroadcastReceiver;
import com.gh.gamecenter.retrofit.RetrofitManager;
import com.gh.vspace.VHelper;
import com.halo.assistant.HaloApp;
@ -94,8 +96,7 @@ public class Config {
getPreferences().edit().putString(SETTINGS_KEY, GsonUtils.toJson(settingsEntity)).apply();
mSettingsEntity = settingsEntity;
// 加载完设置后刷新下
PackageHelper.initList();
PackageHelper.refreshList();
}
@Nullable
@ -332,8 +333,31 @@ public class Config {
if (mNewApiSettingsEntity.getGameShieldContents() != null) {
ContentBlockedHelper.INSTANCE.init(mNewApiSettingsEntity.getGameShieldContents());
}
// 更新安装列表是否开启的配置
// if (mNewApiSettingsEntity.getInstalledComplianceSwitch() != null) {
// PackageHelper.INSTANCE.updateIsGetInstalledPackagesApiAgreedRequired(mNewApiSettingsEntity.getInstalledComplianceSwitch());
// } else {
// PackageHelper.INSTANCE.updateIsGetInstalledPackagesApiAgreedRequired(false);
// }
// 更新包名监听是否开启
if (mNewApiSettingsEntity.isPackageObserveEnable()) {
observePackageChange(mNewApiSettingsEntity.getPackageObserveActions());
}
}
});
}
}
public static void observePackageChange(NewApiSettingsEntity.PackageObserveActions packageObserveActions) {
PackageChangeBroadcastReceiver receiver = new PackageChangeBroadcastReceiver(packageObserveActions);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(packageObserveActions.getAdd());
intentFilter.addAction(packageObserveActions.getRem());
intentFilter.addAction(packageObserveActions.getRep());
intentFilter.addDataScheme("package");
HaloApp.getInstance().registerReceiver(receiver, intentFilter);
}
}

View File

@ -19,6 +19,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.gh.common.util.DirectUtils
import com.gh.common.util.LogUtils
import com.gh.common.util.PackageHelper
import com.gh.common.util.PackageUtils
import com.gh.download.DownloadManager
import com.gh.gamecenter.R
@ -59,7 +60,7 @@ class PackageCheckDialogFragment : BaseDialogFragment() {
private val mDuration = 3000
private var mDisposable: Disposable? = null
private var mAdapter: PackageCheckAdapter? = null
private var mAllInstalledPackages = PackageUtils.getInstalledPackages(HaloApp.getInstance().application, 0)
private var mAllInstalledPackages = PackageHelper.getInstalledPackages(HaloApp.getInstance().application, 0)
var gameEntity: GameEntity? = null
var callBack: ConfirmListener? = null
@ -325,7 +326,7 @@ class PackageCheckDialogFragment : BaseDialogFragment() {
override fun onResume() {
super.onResume()
mAllInstalledPackages = PackageUtils.getInstalledPackages(HaloApp.getInstance().application, 0)
mAllInstalledPackages = PackageHelper.getInstalledPackages(HaloApp.getInstance().application, 0)
gameEntity?.packageDialog?.let {
if (isAllPackageInstalled(mAllInstalledPackages, it)) {
callBack?.onConfirm()
@ -363,7 +364,7 @@ class PackageCheckDialogFragment : BaseDialogFragment() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(busFour: EBPackage) {
if (busFour.isInstalledOrUninstalled()) {
mAllInstalledPackages = PackageUtils.getInstalledPackages(HaloApp.getInstance().application, 0)
mAllInstalledPackages = PackageHelper.getInstalledPackages(HaloApp.getInstance().application, 0)
mAdapter?.notifyDataSetChanged()
}
}
@ -416,7 +417,7 @@ class PackageCheckDialogFragment : BaseDialogFragment() {
return
}
val allInstalledPackages = PackageUtils.getInstalledPackages(HaloApp.getInstance().application, 0)
val allInstalledPackages = PackageHelper.getInstalledPackages(HaloApp.getInstance().application, 0)
if (isAllPackageInstalled(allInstalledPackages, packageDialogEntity)) {
callBack.onConfirm()
return

View File

@ -9,10 +9,12 @@ import androidx.fragment.app.FragmentActivity
import com.gh.common.iinterface.ISuperiorChain
import com.gh.common.util.CheckLoginUtils
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.SplashAdActivity
import com.gh.gamecenter.SplashScreenActivity
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.entity.SimpleGameEntity
import com.gh.gamecenter.common.retrofit.BiResponse
import com.gh.gamecenter.core.AppExecutor
import com.gh.gamecenter.core.utils.SPUtils
import com.gh.gamecenter.entity.DialogEntity
import com.gh.gamecenter.feature.entity.WelcomeDialogEntity
@ -42,12 +44,13 @@ object GlobalPriorityChainHelper : ISuperiorChain {
return activity is FragmentActivity
&& !activity.isFinishing
&& activity !is SplashScreenActivity
&& activity !is SplashAdActivity
}
/**
* 预启动所有的优先级弹窗管理链
*/
fun preStart() {
fun preStart(withSpecialDelay: Boolean) {
val launchRedirectHandler = LaunchRedirectHandler(-101)
val updateDialogHandler = UpdateDialogHandler(-100)
val privacyPolicyDialogHandler = PrivacyPolicyDialogHandler(-99)
@ -64,8 +67,13 @@ object GlobalPriorityChainHelper : ISuperiorChain {
launchRedirectHandler.doPreProcess()
updateDialogHandler.doPreProcess()
requestOpeningDialogData(welcomeDialogHandler, privacyPolicyDialogHandler)
requestReserveDialogData(reserveDialogHandler)
// 首次启动延迟 300ms保证请求首次启动时已经获取到了 GID 、 OAID 等标记
val requestDelay = if (withSpecialDelay) 300L else 0L
AppExecutor.uiExecutor.executeWithDelay({
requestOpeningDialogData(welcomeDialogHandler, privacyPolicyDialogHandler)
requestReserveDialogData(reserveDialogHandler)
}, requestDelay)
}
/**
@ -123,6 +131,17 @@ object GlobalPriorityChainHelper : ISuperiorChain {
mainChain.resume()
}
/**
* 添加新的 handler 到优先级弹窗管理链 (插队!)
*/
fun queueNewHandler(handler: PriorityChainHandler) {
if (mainChain.isHandlerQueueEmpty()) {
observeLifecycle()
}
mainChain.addHandler(handler)
}
/**
* 请求首页启动弹窗相关的数据并执行相关 handler 的 preProcess
*/

View File

@ -0,0 +1,28 @@
package com.gh.common.prioritychain
import androidx.fragment.app.FragmentActivity
import com.gh.common.util.PackageHelper
import com.gh.gamecenter.common.base.GlobalActivityManager
class RequestInstalledListPermissionHandler : PriorityChainHandler(-1000) {
init {
updateStatus(STATUS_VALID)
}
override fun onProcess(): Boolean {
val currentActivity = GlobalActivityManager.currentActivity ?: return false
if (currentActivity !is FragmentActivity) return false
PackageHelper.showGetInstallAppsListDialogAndRequestPermissionIfNeeded(
activity = currentActivity,
ignorePermanentlyDenied = true
) {
processNext()
}
return true
}
}

View File

@ -32,5 +32,6 @@ class BuildConfigImpl : IBuildConfigProvider {
override fun getVApiHost(): String = BuildConfig.VAPI_HOST
override fun getVDevApiHost(): String = BuildConfig.DEV_VAPI_HOST
override fun getLogProducerProject(): String = BuildConfig.LOG_HUB_PROJECT
}

View File

@ -0,0 +1,103 @@
package com.gh.common.provider
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.viewbinding.ViewBinding
import com.alibaba.android.arouter.facade.annotation.Route
import com.facebook.drawee.view.SimpleDraweeView
import com.gh.common.util.ConcernContentUtils
import com.gh.common.view.ImageContainerView
import com.gh.gamecenter.ImageViewerActivity.Companion.getIntent
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.common.utils.toArrayList
import com.gh.gamecenter.core.provider.IConcernArticleUtilsProvider
import com.gh.gamecenter.databinding.RecyclerGameArticleBinding
import com.gh.gamecenter.message.R
@Route(path = RouteConsts.provider.concernContentUtils, name = "ConcernContentUtils暴露服务")
class ConcernArticleUtilsProviderImpl : IConcernArticleUtilsProvider {
override fun addContentPic(
context: Context,
linearLayout: LinearLayout,
list: List<String>,
entrance: String,
width: Int
) {
ConcernContentUtils.addContentPic(context, linearLayout, list, entrance, width)
}
override fun createArticleBinding(parent: ViewGroup): ViewBinding {
val inflater = LayoutInflater.from(parent.context)
return RecyclerGameArticleBinding.inflate(inflater, parent, false)
}
override fun initArticleStyle(binding: ViewBinding) {
if (binding is RecyclerGameArticleBinding) {
val context = binding.root.context
binding.root
.setBackground(ContextCompat.getDrawable(context, R.drawable.reuse_listview_item_style))
binding.tvGameName.setTextColor(ContextCompat.getColor(context, R.color.text_primary))
binding.tvTime.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
binding.tvTitle.setTextColor(ContextCompat.getColor(context, R.color.text_primary))
binding.tvDescription.setTextColor(ContextCompat.getColor(context, R.color.text_secondary))
binding.tvComment.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
binding.tvShare.setTextColor(ContextCompat.getColor(context, R.color.text_tertiary))
}
}
override fun getIvIcon(binding: ViewBinding): View =
(binding as RecyclerGameArticleBinding).ivIcon
override fun getTvGameName(binding: ViewBinding): TextView =
(binding as RecyclerGameArticleBinding).tvGameName
override fun getTvTime(binding: ViewBinding): TextView = (binding as RecyclerGameArticleBinding).tvTime
override fun getTvTitle(binding: ViewBinding): TextView = (binding as RecyclerGameArticleBinding).tvTitle
override fun getTvDescription(binding: ViewBinding): TextView =
(binding as RecyclerGameArticleBinding).tvDescription
override fun bindImgs(binding: ViewBinding, img: List<String>) {
if (binding is RecyclerGameArticleBinding) {
val images = img.map {
ImageContainerView.ImageContainerData.ImageInfo(it, 3, 2)
}
val imageContainerData =
ImageContainerView.ImageContainerData("", false, images, show = images.isNotEmpty())
binding.ivImagesContainer.bindData(imageContainerData,
object : ImageContainerView.OnImageContainerEventListener {
override fun onImageClick(
images: List<String>,
position: Int,
imageViewList: ArrayList<SimpleDraweeView>
) {
val checkIntent = getIntent(
binding.root.context,
images.toArrayList(),
position,
imageViewList,
""
)
binding.root.context.startActivity(checkIntent)
}
override fun onVideoCLick(videoId: String) = Unit
})
}
}
override fun getTvComment(binding: ViewBinding): TextView = (binding as RecyclerGameArticleBinding).tvComment
override fun getTvShare(binding: ViewBinding): TextView = (binding as RecyclerGameArticleBinding).tvShare
override fun init(context: Context?) {
// Do nothing
}
}

View File

@ -1,26 +0,0 @@
package com.gh.common.provider
import android.content.Context
import android.widget.LinearLayout
import com.alibaba.android.arouter.facade.annotation.Route
import com.gh.common.util.ConcernContentUtils
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.core.provider.IConcernContentUtilsProvider
@Route(path = RouteConsts.provider.concernContentUtils, name = "ConcernContentUtils暴露服务")
class ConcernContentUtilsProviderImpl : IConcernContentUtilsProvider {
override fun addContentPic(
context: Context,
linearLayout: LinearLayout,
list: List<String>,
entrance: String,
width: Int
) {
ConcernContentUtils.addContentPic(context, linearLayout, list, entrance, width)
}
override fun init(context: Context?) {
// Do nothing
}
}

View File

@ -0,0 +1,55 @@
package com.gh.common.provider
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.viewbinding.ViewBinding
import com.alibaba.android.arouter.facade.annotation.Route
import com.gh.gamecenter.R
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.common.utils.goneIf
import com.gh.gamecenter.common.utils.toColor
import com.gh.gamecenter.core.provider.IConcernGiftPackUtilsProvider
import com.gh.gamecenter.databinding.RecyclerGiftPackBinding
@Route(path = RouteConsts.provider.concernGiftPackUtils, name = "ConcernGiftPackUtils暴露服务")
class ConcernGiftPackUtilsProviderImpl : IConcernGiftPackUtilsProvider {
override fun createBinding(parent: ViewGroup): ViewBinding {
val inflater = LayoutInflater.from(parent.context)
return RecyclerGiftPackBinding.inflate(inflater, parent, false)
.also {
it.gCode.goneIf(true)
}
}
override fun initStyle(viewBinding: ViewBinding) {
with(viewBinding as RecyclerGiftPackBinding) {
val context = root.context
tvGameName.setTextColor(R.color.text_primary.toColor(context))
tvTime.setTextColor(R.color.text_tertiary.toColor(context))
tvGiftPackName.setTextColor(R.color.text_primary.toColor(context))
tvGiftPackContent.setTextColor(R.color.text_secondary.toColor(context))
}
}
override fun getIvGameIcon(viewBinding: ViewBinding) =
(viewBinding as RecyclerGiftPackBinding).ivIcon
override fun getTvGameName(viewBinding: ViewBinding) =
(viewBinding as RecyclerGiftPackBinding).tvGameName
override fun getTvTime(viewBinding: ViewBinding) =
(viewBinding as RecyclerGiftPackBinding).tvTime
override fun getTvGiftPackName(viewBinding: ViewBinding) =
(viewBinding as RecyclerGiftPackBinding).tvGiftPackName
override fun getTvGiftPackContent(viewBinding: ViewBinding) =
(viewBinding as RecyclerGiftPackBinding).tvGiftPackContent
override fun init(context: Context?) {
// Do Nothing
}
}

View File

@ -0,0 +1,19 @@
package com.gh.common.provider
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import com.alibaba.android.arouter.facade.annotation.Route
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.core.provider.IConcernShareNewsProvider
import com.gh.gamecenter.newsdetail.NewsShareDialog
@Route(path = RouteConsts.provider.concernShareNews, name = "ConcernShareNews暴露服务")
class ConcernShareNewsProviderImpl : IConcernShareNewsProvider {
override fun share(activity: AppCompatActivity, shortId: String?, id: String?, gameIcon: String?, title: String?) {
NewsShareDialog.show(activity, shortId, id, gameIcon, title)
}
override fun init(context: Context?) {
// Do Nothing
}
}

View File

@ -3,6 +3,7 @@ package com.gh.common.provider
import android.content.Context
import android.content.pm.PackageInfo
import com.alibaba.android.arouter.facade.annotation.Route
import com.gh.common.util.PackageHelper
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.core.provider.IPackageUtilsProvider
@ -23,7 +24,7 @@ class PackageUtilsProviderImpl : IPackageUtilsProvider {
}
override fun getInstalledPackages(context: Context, flag: Int): List<PackageInfo> {
return PackageUtils.getInstalledPackages(context, flag)
return PackageHelper.getInstalledPackages(context, flag)
}
override fun getApkSignatureByPackageName(context: Context, packageName: String): Array<String> {
@ -38,6 +39,10 @@ class PackageUtilsProviderImpl : IPackageUtilsProvider {
return PackageUtils.isSignedByGh(context, packageName)
}
override fun isInstalledWithLauncherIcon(context: Context, packageName: String): Boolean {
return PackageUtils.isInstalled(context, packageName)
}
override fun getInstalledTime(context: Context, packageName: String): Long {
return PackageUtils.getInstalledTime(context, packageName)
}

View File

@ -166,162 +166,160 @@ class SimulatorDownloadManager private constructor() {
this.gameName = gameName
this.gameType = gameCategoryChinese
PermissionHelper.checkGetInstalledAppsListBeforeAction(context, object : EmptyCallback {
override fun onCallback() {
val isInstalledNewSimulator =
SimulatorGameManager.isNewSimulatorInstalled(HaloApp.getInstance().application)
//当没有安装新版本模拟器时候 判断是否隐藏
if (simulator?.active == false && !isInstalledNewSimulator) {
showNoneEmulatorDialog(context)
return
}
var isInstalled = PackageUtils.isInstalledFromAllPackage(
context,
simulator?.apk?.packageName
)
//模拟器管理界面还是用之前的逻辑
if (isInstalledNewSimulator && location != SimulatorLocation.SIMULATOR_MANAGE) {
isInstalled = isInstalledNewSimulator
}
// val versionFromInstalledApp = PackageUtils.getVersionNameByPackageName(simulator?.apk?.packageName)
val shouldShowUpdate =
PackageUtils.isInstalledApkMatchedMd5(simulator?.apk?.packageName, simulator?.apk?.md5)
val showAlertTag = SPUtils.getString(SimulatorGameManager.SIMULATOR_UPDATE_SHOW_ALERT_TAG, "") //当天是否弹过
val todayIsShow = showAlertTag == TimeUtils.getToday()
downloadType = if (shouldShowUpdate && isInstalled) "update" else "download"
if (downloadType == "update" && todayIsShow && location != SimulatorLocation.SIMULATOR_MANAGE) {
return
}
if (downloadType == "download" && isInstalled) {
return
}
val title = if (shouldShowUpdate && isInstalled) "更新模拟器" else "安装模拟器"
val message =
if (shouldShowUpdate && isInstalled) "检测到模拟器存在更高版本,是否前往更新" else "模拟器游戏需要先下载安装对应的模拟器,才可以运行"
val positiveText =
if (shouldShowUpdate && isInstalled) "更新(${simulator?.apk?.size}" else "下载(${simulator?.apk?.size}"
val negativeText = if (shouldShowUpdate && isInstalled) "下次再说" else "取消"
val trackableEntity = TrackableEntity(
"模拟器下载",
key = if (shouldShowUpdate && isInstalled) "更新弹窗" else "下载弹窗",
logShowEvent = true
)
if (shouldShowUpdate && isInstalled) {
NewFlatLogUtils.logSimulatorUpdateAlertShow()
}
if (shouldShowUpdate && isInstalled) {
SensorsBridge.trackSimulatorUpdateDialogShow(
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogShow(
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
DialogHelper.showDialog(
context,
title,
message,
positiveText,
negativeText,
trackMtaEvent = true,
cancelClickCallback = {
if (shouldShowUpdate && isInstalled) {
cancelCallback?.invoke()
NewFlatLogUtils.logSimulatorUpdateAlertClick("取消")
MtaHelper.onEvent(trackableEntity.event, trackableEntity.key, "点击下次再说")
SensorsBridge.trackSimulatorUpdateDialogClick(
buttonName = negativeText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogClick(
buttonName = negativeText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
},
confirmClickCallback = {
showDownloadingDialog(context, simulator, gameId, gameName, gameCategoryChinese)
NewFlatLogUtils.logSimulatorUpdateAlertClick("更新")
MtaHelper.onEvent(
trackableEntity.event,
trackableEntity.key,
if (shouldShowUpdate && isInstalled) "点击更新" else "点击下载"
)
if (shouldShowUpdate && isInstalled) {
SensorsBridge.trackSimulatorUpdateDialogClick(
buttonName = positiveText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogClick(
buttonName = positiveText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
},
touchOutsideCallback = {
if (shouldShowUpdate && isInstalled) {
SensorsBridge.trackSimulatorUpdateDialogClick(
buttonName = "关闭弹窗",
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogClick(
buttonName = "关闭弹窗",
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
},
mtaEvent = trackableEntity.event, mtaKey = trackableEntity.key,
extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true)
)
if (downloadType == "update" && location != SimulatorLocation.SIMULATOR_MANAGE) {
SPUtils.setString(SimulatorGameManager.SIMULATOR_UPDATE_SHOW_ALERT_TAG, TimeUtils.getToday())
}
PermissionHelper.checkGetInstalledAppsListBeforeAction(context) { _ ->
val isInstalledNewSimulator =
SimulatorGameManager.isNewSimulatorInstalled(HaloApp.getInstance().application)
//当没有安装新版本模拟器时候 判断是否隐藏
if (simulator?.active == false && !isInstalledNewSimulator) {
showNoneEmulatorDialog(context)
return@checkGetInstalledAppsListBeforeAction
}
})
var isInstalled = PackageUtils.isInstalledFromAllPackage(
context,
simulator?.apk?.packageName
)
//模拟器管理界面还是用之前的逻辑
if (isInstalledNewSimulator && location != SimulatorLocation.SIMULATOR_MANAGE) {
isInstalled = isInstalledNewSimulator
}
// val versionFromInstalledApp = PackageUtils.getVersionNameByPackageName(simulator?.apk?.packageName)
val shouldShowUpdate =
PackageUtils.isInstalledApkMatchedMd5(simulator?.apk?.packageName, simulator?.apk?.md5)
val showAlertTag = SPUtils.getString(SimulatorGameManager.SIMULATOR_UPDATE_SHOW_ALERT_TAG, "") //当天是否弹过
val todayIsShow = showAlertTag == TimeUtils.getToday()
downloadType = if (shouldShowUpdate && isInstalled) "update" else "download"
if (downloadType == "update" && todayIsShow && location != SimulatorLocation.SIMULATOR_MANAGE) {
return@checkGetInstalledAppsListBeforeAction
}
if (downloadType == "download" && isInstalled) {
return@checkGetInstalledAppsListBeforeAction
}
val title = if (shouldShowUpdate && isInstalled) "更新模拟器" else "安装模拟器"
val message =
if (shouldShowUpdate && isInstalled) "检测到模拟器存在更高版本,是否前往更新" else "模拟器游戏需要先下载安装对应的模拟器,才可以运行"
val positiveText =
if (shouldShowUpdate && isInstalled) "更新(${simulator?.apk?.size}" else "下载(${simulator?.apk?.size}"
val negativeText = if (shouldShowUpdate && isInstalled) "下次再说" else "取消"
val trackableEntity = TrackableEntity(
"模拟器下载",
key = if (shouldShowUpdate && isInstalled) "更新弹窗" else "下载弹窗",
logShowEvent = true
)
if (shouldShowUpdate && isInstalled) {
NewFlatLogUtils.logSimulatorUpdateAlertShow()
}
if (shouldShowUpdate && isInstalled) {
SensorsBridge.trackSimulatorUpdateDialogShow(
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogShow(
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
DialogHelper.showDialog(
context,
title,
message,
positiveText,
negativeText,
trackMtaEvent = true,
cancelClickCallback = {
if (shouldShowUpdate && isInstalled) {
cancelCallback?.invoke()
NewFlatLogUtils.logSimulatorUpdateAlertClick("取消")
MtaHelper.onEvent(trackableEntity.event, trackableEntity.key, "点击下次再说")
SensorsBridge.trackSimulatorUpdateDialogClick(
buttonName = negativeText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogClick(
buttonName = negativeText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
},
confirmClickCallback = {
showDownloadingDialog(context, simulator, gameId, gameName, gameCategoryChinese)
NewFlatLogUtils.logSimulatorUpdateAlertClick("更新")
MtaHelper.onEvent(
trackableEntity.event,
trackableEntity.key,
if (shouldShowUpdate && isInstalled) "点击更新" else "点击下载"
)
if (shouldShowUpdate && isInstalled) {
SensorsBridge.trackSimulatorUpdateDialogClick(
buttonName = positiveText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogClick(
buttonName = positiveText,
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
},
touchOutsideCallback = {
if (shouldShowUpdate && isInstalled) {
SensorsBridge.trackSimulatorUpdateDialogClick(
buttonName = "关闭弹窗",
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
} else {
SensorsBridge.trackSimulatorInstallDialogClick(
buttonName = "关闭弹窗",
gameId = gameId,
gameName = gameName,
gameType = gameCategoryChinese,
lastPageId = GlobalActivityManager.getLastPageEntity().pageId,
lastPageName = GlobalActivityManager.getLastPageEntity().pageName,
lastPageBusinessId = GlobalActivityManager.getLastPageEntity().pageBusinessId
)
}
},
mtaEvent = trackableEntity.event, mtaKey = trackableEntity.key,
extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true)
)
if (downloadType == "update" && location != SimulatorLocation.SIMULATOR_MANAGE) {
SPUtils.setString(SimulatorGameManager.SIMULATOR_UPDATE_SHOW_ALERT_TAG, TimeUtils.getToday())
}
}
}
fun showDownloadingDialog(

View File

@ -7,8 +7,8 @@ import android.widget.LinearLayout;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.view.SimpleDraweeView;
import com.gh.gamecenter.ImageViewerActivity;
import com.gh.gamecenter.core.utils.DisplayUtils;
import com.gh.gamecenter.common.utils.ImageUtils;
import com.gh.gamecenter.core.utils.DisplayUtils;
import java.util.ArrayList;
import java.util.List;
@ -121,5 +121,4 @@ public class ConcernContentUtils {
}
return imageView;
}
}

View File

@ -1,17 +1,14 @@
package com.gh.common.util;
import android.content.Context;
import android.os.Build;
import com.gh.gamecenter.common.constant.Constants;
import com.gh.gamecenter.common.constant.EntranceConsts;
import com.gh.gamecenter.common.exposure.meta.MetaUtil;
import com.gh.gamecenter.common.utils.NetworkUtils;
import com.gh.gamecenter.feature.entity.GameEntity;
import com.gh.gamecenter.entity.NewsDetailEntity;
import com.gh.gamecenter.feature.utils.PlatformUtils;
import com.gh.gamecenter.manager.DataCollectionManager;
import com.gh.gamecenter.manager.PackagesManager;
import com.lightgame.download.DownloadEntity;
import java.util.HashMap;
@ -32,7 +29,7 @@ public class DataCollectionUtils {
map.put("type", android.os.Build.MODEL);
map.put("system", android.os.Build.VERSION.SDK_INT + "=" + android.os.Build.VERSION.RELEASE);
// WIFI实时
DataCollectionManager.onEvent(context, "error", map, NetworkUtils.isWifiConnected(context));
DataCollectionManager.onEvent(context, "error", map, true);
}
// 上传下载数据(开始、完成)

View File

@ -14,12 +14,10 @@ import com.gh.gamecenter.BuildConfig;
import com.gh.gamecenter.common.base.GlobalActivityManager;
import com.gh.gamecenter.common.base.activity.BaseActivity;
import com.gh.gamecenter.common.constant.Constants;
import com.gh.gamecenter.common.eventbus.EBReuse;
import com.gh.gamecenter.common.exposure.meta.MetaUtil;
import com.gh.gamecenter.common.retrofit.BiResponse;
import com.gh.gamecenter.common.utils.ExtensionsKt;
import com.gh.gamecenter.core.AppExecutor;
import com.gh.gamecenter.core.utils.GsonUtils;
import com.gh.gamecenter.core.utils.MtaHelper;
import com.gh.gamecenter.core.utils.SPUtils;
import com.gh.gamecenter.core.utils.SentryHelper;
import com.gh.gamecenter.login.entity.IdCardEntity;
@ -32,8 +30,6 @@ import com.halo.assistant.HaloApp;
import com.lightgame.config.CommonDebug;
import com.lightgame.utils.Utils;
import org.greenrobot.eventbus.EventBus;
import io.reactivex.schedulers.Schedulers;
import io.sentry.Sentry;
import io.sentry.android.core.SentryAndroid;
@ -108,45 +104,51 @@ public class DataUtils {
}
public static void getGid() {
GidHelper.getInstance().registerDevice(HaloApp.getInstance().getApplication(), new GidCallback() {
@Override
public void onSuccess(String gid) {
Utils.log("Gid", gid);
PreferenceManager.getDefaultSharedPreferences(HaloApp.getInstance().getApplication()).edit().putString(Constants.DEVICE_KEY, gid).apply();
// 默认用 APP 级已存储的 GID 来使用,不使用外部 GID
String savedGid = SPUtils.getString(Constants.GID);
if (!TextUtils.isEmpty(savedGid)) {
HaloApp.getInstance().setGid(savedGid);
onGidReceived(savedGid);
} else {
GidHelper.getInstance().registerDevice(HaloApp.getInstance().getApplication(), new GidCallback() {
@Override
public void onSuccess(String gid) {
Utils.log("Gid", gid);
PreferenceManager.getDefaultSharedPreferences(HaloApp.getInstance().getApplication()).edit().putString(Constants.DEVICE_KEY, gid).apply();
// 默认用 APP 级已存储的 GID 来使用,不使用外部 GID
String savedGid = SPUtils.getString(Constants.GID);
if (!TextUtils.isEmpty(savedGid)) {
gid = savedGid;
} else {
SPUtils.setString(Constants.GID, gid);
onGidReceived(gid);
}
HaloApp.getInstance().setGid(gid);
// 更新广告配置
AdDelegateHelper.INSTANCE.requestAdConfig(false, "", null);
getDeviceCertification(gid);
// 避免初始化顺序问题导致 MetaUtil 一直持有空的 gid
MetaUtil.INSTANCE.refreshMeta();
ContentValues values = new ContentValues();
values.put(GhContentProvider.KEY_GID, gid);
values.put(GhContentProvider.KEY_ANDROID_ID, MetaUtil.getBase64EncodedAndroidId());
try {
HaloApp.getInstance().getContentResolver().insert(Uri.parse("content://com.gh.gamecenter.provider/device"), values);
} catch (Exception exception) {
SentryHelper.INSTANCE.onEvent("DEVICE_INSERT_ERROR", "exception_digest", exception.getLocalizedMessage());
exception.printStackTrace();
@Override
public void onFailure(String s) {
// 更新广告配置
AdDelegateHelper.INSTANCE.requestAdConfig(false, "", null);
}
}
});
}
}
@Override
public void onFailure(String s) {
// 更新广告配置
AdDelegateHelper.INSTANCE.requestAdConfig(false, "", null);
private static void onGidReceived(String gid) {
HaloApp.getInstance().setGid(gid);
// 更新广告配置
AdDelegateHelper.INSTANCE.requestAdConfig(false, "", null);
getDeviceCertification(gid);
// 避免初始化顺序问题导致 MetaUtil 一直持有空的 gid
MetaUtil.INSTANCE.refreshMeta();
AppExecutor.getIoExecutor().execute(() -> {
ContentValues values = new ContentValues();
values.put(GhContentProvider.KEY_GID, gid);
values.put(GhContentProvider.KEY_ANDROID_ID, MetaUtil.getBase64EncodedAndroidId());
try {
HaloApp.getInstance().getContentResolver().insert(Uri.parse("content://com.gh.gamecenter.provider/device"), values);
} catch (Exception exception) {
SentryHelper.INSTANCE.onEvent("DEVICE_INSERT_ERROR", "exception_digest", exception.getLocalizedMessage());
exception.printStackTrace();
}
});
}

View File

@ -1988,7 +1988,7 @@ object DirectUtils {
val qGameProvider = ARouter
.getInstance()
.build(RouteConsts.provider.qGame)
.navigation() as IQGameProvider<*>
.navigation() as IQGameProvider
qGameProvider.setLoginInfo(activity, userId, userName, userToken)
qGameProvider.launchGame(activity, qqGameId) { _, _ ->
RetrofitManager

View File

@ -50,7 +50,7 @@ public class InstallUtils {
public void handleMessage(Message msg) {
if (msg.what == INSTALL_WHAT && packageManager != null) {
ArrayList<String> list = new ArrayList<>();
List<PackageInfo> packageInfos = PackageUtils.getInstalledPackages(context, 0);
List<PackageInfo> packageInfos = PackageHelper.INSTANCE.getInstalledPackages(context, 0);
for (PackageInfo packageInfo : packageInfos) {
if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
list.add(packageInfo.packageName);

View File

@ -768,7 +768,7 @@ public class LibaoUtils {
}
public static boolean isAppInstalled(Context context, String packageName) {
List<PackageInfo> pinfo = PackageUtils.getInstalledPackages(context, 0);
List<PackageInfo> pinfo = PackageHelper.INSTANCE.getInstalledPackages(context, 0);
if (pinfo != null) {
for (int i = 0; i < pinfo.size(); i++) {
String pn = pinfo.get(i).packageName;

View File

@ -0,0 +1,161 @@
package com.gh.common.util
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.gh.download.DownloadManager
import com.gh.download.PackageObserver
import com.gh.gamecenter.common.utils.NewFlatLogUtils
import com.gh.gamecenter.common.utils.SensorsBridge
import com.gh.gamecenter.eventbus.EBPackage
import com.gh.gamecenter.manager.PackagesManager
import com.gh.gamecenter.packagehelper.PackageRepository
import com.lightgame.utils.Utils
import org.greenrobot.eventbus.EventBus
object PackageChangeHelper : DefaultLifecycleObserver {
private const val TAG = "PackageChangeHelper"
private const val INSTALL_PENDING = 1
private const val UNINSTALL_PENDING = 2
private const val UPDATE_PENDING = 3
// <包名pending 类型,应用版本> Triple
private var pendingPackagePair: Triple<String, Int, String>? = null
private var pendingGhId: String ? = null
/**
* 添加一个等待中,待确定是否已成功安装的应用
*/
fun addInstallPendingPackage(packageName: String) {
val installData = PackagesManager.getInstalledData(packageName)
if (installData == null) {
Utils.log(TAG, "添加了: $packageName 包名等待安装成功")
pendingPackagePair = Triple(packageName, INSTALL_PENDING, "")
} else {
Utils.log(TAG, "添加了: $packageName 包名等待安装更新成功")
val ghId = PackageUtils.getGhId(packageName)
// 记录光环插件相关信息,用于安装成功后的处理
if (ghId != null) {
pendingGhId = ghId.toString()
}
pendingPackagePair = Triple(packageName, UPDATE_PENDING, installData.version)
}
}
/**
* 添加一个等待中,待确定是否已成功卸载的应用
*/
fun addUninstallPendingPackage(packageName: String) {
Utils.log(TAG, "添加了: $packageName 包名等待卸载成功")
pendingPackagePair = Triple(packageName, UNINSTALL_PENDING, "")
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
if (pendingPackagePair != null) {
val packageName = pendingPackagePair?.first ?: return
val isInstallPending = pendingPackagePair?.second == INSTALL_PENDING
val isUninstallPending = pendingPackagePair?.second == UNINSTALL_PENDING
val isUpdatePending = pendingPackagePair?.second == UPDATE_PENDING
val pendingVersion = pendingPackagePair?.third ?: ""
val installedVersionName = PackageUtils.getVersionNameByPackageName(packageName)
val isInstalled = installedVersionName != null
if (isInstallPending && isInstalled) {
pendingPackagePair = null
pendingGhId = null
PackageRepository.addInstalledGame(packageName)
performInstallSuccessAction(packageName)
} else if (isUninstallPending && !isInstalled) {
pendingPackagePair = null
pendingGhId = null
performUninstallSuccessAction(packageName)
} else if (isUpdatePending) {
val isUpdateValid = if (installedVersionName != pendingVersion) {
true
} else {
!pendingGhId.isNullOrEmpty() && pendingGhId != PackageUtils.getGhId(packageName).toString()
}
pendingPackagePair = null
pendingGhId = null
if (isUpdateValid) {
performUninstallSuccessAction(packageName)
performInstallSuccessAction(packageName)
}
}
}
}
fun addInstall(packageName: String) {
performInstallSuccessAction(packageName)
}
fun addUpdate(packageName: String) {
performUninstallSuccessAction(packageName)
performInstallSuccessAction(packageName)
}
/**
* 对应包名安装成功后的操作,继承至 PackageChangeBroadcastObserver
*/
private fun performInstallSuccessAction(packageName: String, withLog: Boolean = true) {
Utils.log(TAG, "安装了: $packageName 包名的程序")
val downloadEntity = DownloadManager.getInstance().getDownloadEntitySnapshotByPackageName(packageName)
val gameId = if (downloadEntity != null && downloadEntity.gameId != null) downloadEntity.gameId else ""
val gameName = if (downloadEntity != null && downloadEntity.name != null) downloadEntity.name else ""
if (withLog) {
NewFlatLogUtils.logGameInstallComplete(gameId, gameName)
SensorsBridge.trackInstallGameFinish(gameId, gameName)
}
InstallUtils.getInstance().removeInstall(packageName)
PackageHelper.refreshLocalPackageList()
val versionName = PackageUtils.getVersionNameByPackageName(packageName)
val installEb = EBPackage(EBPackage.TYPE_INSTALLED, packageName, versionName)
PackageObserver.onPackageChanged(installEb)
EventBus.getDefault().post(installEb)
}
fun addUninstall(packageName: String) {
performUninstallSuccessAction(packageName)
}
/**
* 对应包名卸载成功后的操作,继承至 PackageChangeBroadcastObserver
*/
private fun performUninstallSuccessAction(packageName: String, withLog: Boolean = true) {
Utils.log(TAG, "卸载了: $packageName 包名的程序")
val install = PackagesManager.getInstalledData(packageName)
val gameId = if (install?.id != null) install.id else ""
val gameName = if (install?.name != null) install.name else ""
if (withLog) {
NewFlatLogUtils.logGameUninstallComplete(gameId!!, gameName!!)
SensorsBridge.trackUnloadGameFinish(gameId, gameName)
}
InstallUtils.getInstance().removeUninstall(packageName)
PackageHelper.refreshLocalPackageList()
val uninstallEb = EBPackage(EBPackage.TYPE_UNINSTALLED, packageName, "")
PackageObserver.onPackageChanged(uninstallEb)
EventBus.getDefault().post(uninstallEb)
}
}

View File

@ -1,24 +1,81 @@
package com.gh.common.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PermissionInfo
import android.os.Build
import android.provider.Settings
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.gh.common.constant.Config
import com.gh.common.prioritychain.GlobalPriorityChainHelper
import com.gh.common.prioritychain.RequestInstalledListPermissionHandler
import com.gh.gamecenter.common.retrofit.BiResponse
import com.gh.gamecenter.common.utils.DialogHelper
import com.gh.gamecenter.common.utils.PermissionHelper
import com.gh.gamecenter.common.utils.PermissionHelper.isGetInstalledListPermissionDisabled
import com.gh.gamecenter.common.utils.SensorsBridge
import com.gh.gamecenter.core.runOnIoThread
import com.gh.gamecenter.core.runOnUiThread
import com.gh.gamecenter.core.utils.SPUtils
import com.gh.gamecenter.entity.WhitePackageListEntity
import com.gh.gamecenter.feature.entity.SettingsEntity
import com.gh.gamecenter.manager.PackagesManager
import com.gh.gamecenter.packagehelper.PackageRepository
import com.gh.gamecenter.retrofit.RetrofitManager
import com.halo.assistant.HaloApp
import com.lightgame.utils.Utils
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
object PackageHelper {
private const val TAG = "PackageHelper"
private const val SP_GET_INSTALLED_API_AGREED = "get_installed_api_agreed"
private const val UNKNOWN = -1
private const val UNSUPPORTED = 0
private const val SUPPORTED = 1
private const val ENABLED = 2
private const val DISABLED = 3
private var lastInstalledPackageListTime = 0L
private var cachedInstalledPackagesList: List<PackageInfo> = arrayListOf()
private var isGetInstalledPackagesApiAgreed = true // 用户是否已经同意使用已安装应用列表 API
private var isGetInstalledPackagesApiAgreedRequired = DISABLED // 需要用户手动授权才获取已安装应用列表的功能的开关
private var isGetInstalledPackagesPermissionSupported = UNKNOWN // 设备是否支持禁用获取已安装应用列表
// 评论黑名单包名列表,避免用户安装了 Xposed Installer 这样的工具,也能在包含该安装包的游戏详情页评论
var commentPackageNameBlackList = arrayListOf<String>()
private var _commentPackageNameBlackList = arrayListOf<String>()
val commentPackageNameBlackList: ArrayList<String> = _commentPackageNameBlackList
// 关闭下载的包列表
var downloadPackageNameBlackList = arrayListOf<String>()
private var _downloadPackageNameBlackList = arrayListOf<String>()
val downloadPackageNameBlackList: ArrayList<String> = _downloadPackageNameBlackList
// 本地已安装的包去掉关闭下载的包后的列表
var validLocalPackageNameSet = hashSetOf<String>()
private var _validLocalPackageNameSet = hashSetOf<String>()
val validLocalPackageNameSet: HashSet<String> = _validLocalPackageNameSet
// 游戏包名匹配列表
var relatedPackageList = arrayListOf<SettingsEntity.GameWithPackages>()
private var _relatedPackageList = arrayListOf<SettingsEntity.GameWithPackages>()
val relatedPackageList: ArrayList<SettingsEntity.GameWithPackages> = _relatedPackageList
// 接口控制的已安装应用列表获取开关状态 (UI 显示)
private var _installedPackageApiSwitchStatusLiveData = MutableLiveData<Boolean>()
val installedPackageApiSwitchStatusLiveData: LiveData<Boolean> = _installedPackageApiSwitchStatusLiveData
// 本地已安装包的列表
var localPackageNameSet = hashSetOf<String>()
@ -31,6 +88,22 @@ object PackageHelper {
}
}
/**
* 获取已安装的白名单列表(为了在没有已安装应用列表获取能力的时候也能正常判断更新、插件化)
*/
@SuppressLint("CheckResult")
fun getInstalledWhiteList() {
RetrofitManager.getInstance().newApi.installWhitelist
.subscribeOn(Schedulers.io())
.subscribe(object : BiResponse<WhitePackageListEntity>() {
override fun onSuccess(data: WhitePackageListEntity) {
data.data?.let {
addInstalledButMissingPackages(it)
}
}
})
}
@JvmStatic
fun refreshLocalPackageList() {
localPackageNameSet = getAllPackageName(HaloApp.getInstance().application)
@ -38,33 +111,26 @@ object PackageHelper {
}
@JvmStatic
fun initList() {
Config.getSettings()?.gameCommentBlackList?.let {
commentPackageNameBlackList = ArrayList(it)
}
Config.getSettings()?.gameDownloadBlackList?.let {
downloadPackageNameBlackList = ArrayList(it)
}
Config.getSettings()?.gamePackageMatch?.let {
relatedPackageList = ArrayList(it)
}
fun refreshList() {
Config.getSettings()?.gameCommentBlackList?.let { _commentPackageNameBlackList = ArrayList(it) }
Config.getSettings()?.gameDownloadBlackList?.let { _downloadPackageNameBlackList = ArrayList(it) }
Config.getSettings()?.gamePackageMatch?.let { _relatedPackageList = ArrayList(it) }
Config.getSettings()?.gameDownloadBlackList
updateValidPackageNameList()
}
private fun updateValidPackageNameList() {
validLocalPackageNameSet =
_validLocalPackageNameSet =
localPackageNameSet.filterNot { p -> downloadPackageNameBlackList.contains(p) }.toHashSet()
}
/*
/**
* 获取所有已安装的软件的包名、版本(非系统应用)
*/
private fun getAllPackageName(context: Context): HashSet<String> {
val set = HashSet<String>()
return try {
val packageInfos = PackageUtils.getInstalledPackages(context, 0)
val packageInfos = getInstalledPackages(context, 0)
for (packageInfo in packageInfos) {
if (packageInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0) {
if (context.packageName != packageInfo.packageName) {
@ -79,4 +145,357 @@ object PackageHelper {
}
}
/**
* 弃用已安装列表缓存
*/
fun dumpInstalledListCache() {
lastInstalledPackageListTime = 0
}
/**
* 在超时后,若后台没有开启获取已安装应用列表的功能,默认以接口不控制的方式获取已安装应用列表
*/
fun fallbackInstalledPackageApiSwitchAfterTimeout(timeout: Long) {
CoroutineScope(SupervisorJob()).launch {
delay(timeout)
if (isGetInstalledPackagesApiAgreedRequired == UNKNOWN) {
Utils.log(TAG, "后台没有开启获取已安装应用列表的功能,超时后默认以接口不控制的方式获取已安装应用列表")
updateIsGetInstalledPackagesApiAgreedRequired(false)
}
}
}
/**
* 更新已安装应用列表获取开关状态
*/
fun updateIsGetInstalledPackagesApiAgreedRequired(isEnabled: Boolean) {
// 若状态不为 unknown 或者用户已经同意使用了,无需再更新
if (isGetInstalledPackagesApiAgreedRequired != UNKNOWN || isGetInstalledPackagesApiAgreed()) {
Utils.log(TAG, "installedPackageApiSwitchStatus 不为 UNKNOWN无需再更新")
return
}
if (isEnabled) {
getInstalledWhiteList()
_installedPackageApiSwitchStatusLiveData.postValue(true)
isGetInstalledPackagesApiAgreedRequired = ENABLED
} else {
isGetInstalledPackagesApiAgreedRequired = DISABLED
if (isSupportGetInstalledAppsPermission(HaloApp.getInstance())) {
GlobalPriorityChainHelper.queueNewHandler(RequestInstalledListPermissionHandler())
} else {
agreeOnGetInstalledPackagesApi()
}
}
}
/**
* 用户是否已经允许了调用获取已安装应用列表接口
* 优先用内存的值,没有再从 SP 中获取并更新
*/
fun isGetInstalledPackagesApiAgreed(): Boolean {
return isGetInstalledPackagesApiAgreed
|| (SPUtils.getBoolean(SP_GET_INSTALLED_API_AGREED).also { isGetInstalledPackagesApiAgreed = it })
}
fun isGetInstalledPackagesApiAgreedRequired(): Boolean {
return isGetInstalledPackagesApiAgreedRequired == ENABLED
}
/**
* 同意使用已安装应用列表 API
*/
private fun agreeOnGetInstalledPackagesApi() {
isGetInstalledPackagesApiAgreed = true
SPUtils.setBoolean(SP_GET_INSTALLED_API_AGREED, true)
_installedPackageApiSwitchStatusLiveData.postValue(false)
}
/**
* 获取已安装应用列表
*/
fun getInstalledPackages(context: Context?, flags: Int): List<PackageInfo> {
Utils.log(TAG, "即将获取已安装应用列表")
// 用户未同意使用已安装应用列表 API返回空列表
if (!isGetInstalledPackagesApiAgreed()) {
Utils.log(TAG, "用户未同意使用已安装应用列表 API返回空列表")
return cachedInstalledPackagesList
}
// 简单 debounce 过于频繁的获取已安装应用列表调用
if (System.currentTimeMillis() - lastInstalledPackageListTime < 3000 && cachedInstalledPackagesList.isNotEmpty()) {
Utils.log(TAG, "使用了缓存的已安装应用列表")
return cachedInstalledPackagesList
}
var shouldGetNewInstalledPackagedList = false
// 当前设备是否支持限制获取已安装应用列表的功能
if (isSupportGetInstalledAppsPermission(context!!)) {
Utils.log(TAG, "当前设备支持限制获取已安装应用列表的功能")
// 当前设备是否支持禁用了获取已安装应用列表
if (!isGetInstalledListPermissionDisabled(context)) {
Utils.log(TAG, "当前设备没有限制获取已安装应用列表的功能")
shouldGetNewInstalledPackagedList = true
} else {
Utils.log(TAG, "当前设备已限制获取已安装应用列表的功能")
}
} else {
Utils.log(TAG, "当前设备不支持限制获取已安装应用列表的功能")
shouldGetNewInstalledPackagedList = true
}
if (shouldGetNewInstalledPackagedList) {
lastInstalledPackageListTime = System.currentTimeMillis()
cachedInstalledPackagesList = getInstalledPackagesInternal(context, flags)
}
return cachedInstalledPackagesList
}
/**
* 显示获取已安装应用列表的对话框并请求权限
*/
fun showGetInstallAppsListDialogAndRequestPermissionIfNeeded(
activity: FragmentActivity,
ignorePermanentlyDenied: Boolean = false,
resultClosure: (Boolean) -> Unit
) {
val globalOnPermissionGrantedClosure = {
agreeOnGetInstalledPackagesApi()
// 进行包名初始化相关的操作
PackageRepository.initData()
refreshLocalPackageList()
refreshList()
}
if (isSupportGetInstalledAppsPermission(activity)) {
// 若系统已经授予了获取应用列表的权限,直接进行授权成功回调
if (!isGetInstalledListPermissionDisabled(activity)) {
globalOnPermissionGrantedClosure.invoke()
resultClosure.invoke(true)
return
}
PermissionHelper.showGetInstalledAppsListPermissionDialog(
activity = activity,
requestPermission = true,
ignorePermanentlyDenied = ignorePermanentlyDenied
) { isGranted ->
if (isGranted) {
SensorsBridge.trackInstalledListPermissionsResult("成功")
globalOnPermissionGrantedClosure.invoke()
resultClosure.invoke(true)
trackInstalledListAfterDelay()
} else {
resultClosure.invoke(false)
SensorsBridge.trackInstalledListPermissionsResult("拒绝")
}
}
} else {
val hintDialog = PermissionHelper.showGetInstalledAppsListPermissionDialog(
activity = activity,
requestPermission = false,
) {
// do nothing
}
SensorsBridge.trackInstalledListPermissionsCustomDialogShow()
val noticeDialog = DialogHelper.showGuideDialog(
context = activity,
title = "权限申请",
content = "是否允许“光环助手”获取已安装的应用信息",
confirmText = "开启",
cancelText = "拒绝",
confirmClickCallback = {
SensorsBridge.trackInstalledListPermissionsCustomClick("开启")
globalOnPermissionGrantedClosure.invoke()
resultClosure.invoke(true)
trackInstalledListAfterDelay()
},
cancelClickCallback = {
resultClosure.invoke(false)
SensorsBridge.trackInstalledListPermissionsCustomClick("拒绝")
}
)
noticeDialog?.setOnDismissListener {
hintDialog?.dismiss()
}
}
}
/**
* 延迟5秒后上报已安装应用列表
*/
private fun trackInstalledListAfterDelay() {
CoroutineScope(SupervisorJob()).launch {
delay(5000)
SensorsBridge.trackNumberOfInstalledList(localPackageNameSet.size, localPackageNameSet)
}
}
/**
* 是否支持动态获取已安装应用列表权限
*/
fun isSupportGetInstalledAppsPermission(context: Context): Boolean {
// 若存在缓存,直接返回缓存结果。
if (isGetInstalledPackagesPermissionSupported != UNKNOWN) {
return isGetInstalledPackagesPermissionSupported != UNSUPPORTED
}
try {
// 根据官方提供的方法来判定是否支持限制获取已安装应用列表
val flag =
Settings.Secure.getInt(context.contentResolver, "oem_installed_apps_runtime_permission_enable", 0)
if (flag == 1) {
isGetInstalledPackagesPermissionSupported = SUPPORTED
return true
}
// 部分未升级的手机没有上面配置项,有定义下面危险权限也认为是支持设备软件列表管控
val packageManager = context.packageManager
val permissionInfo = packageManager.getPermissionInfo("com.android.permission.GET_INSTALLED_APPS", 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (permissionInfo.protection == PermissionInfo.PROTECTION_DANGEROUS) {
isGetInstalledPackagesPermissionSupported = SUPPORTED
return true
} else {
isGetInstalledPackagesPermissionSupported = UNSUPPORTED
return false
}
} else {
isGetInstalledPackagesPermissionSupported = UNSUPPORTED
return false
}
} catch (e: PackageManager.NameNotFoundException) {
isGetInstalledPackagesPermissionSupported = UNSUPPORTED
return false
}
}
/**
* 确保指定包名的应用在已安装了的情况下能正常收录
*/
fun addInstalledButMissingPackages(packageNameSet: HashSet<String>) {
Utils.log(TAG, "addInstalledButMissingPackages 检查已安装但未收录的应用")
val installedPackageNameSet: HashSet<String> = hashSetOf()
for (packageName in packageNameSet) {
if (!PackagesManager.isInstalled(packageName)
&& PackageUtils.getVersionNameByPackageName(packageName) != null
) {
installedPackageNameSet.add(packageName)
}
}
Utils.log(TAG, "addInstalledButMissingPackages 需要请求接口获取的包数量为 ${installedPackageNameSet.size}")
PackageRepository.addInstalledGames(
pkgNameList = ArrayList(installedPackageNameSet),
updateInstallStatus = true
)
}
fun refreshWrongInstallStatus(packageNameSet: MutableSet<String>) {
runOnIoThread {
Utils.log(TAG, "refreshWrongInstallStatus 检查安装状态异常的应用")
val installedButKeepingWrongStatusPackageNameSet: HashSet<String> = hashSetOf()
val uninstalledButKeepingWrongStatusPackageNameSet: HashSet<String> = hashSetOf()
val updatedButKeepingWrongStatusPackageNameSet: HashSet<String> = hashSetOf()
for (packageName in packageNameSet) {
val installedVersionName = PackageUtils.getVersionNameByPackageName(packageName)
if (!PackagesManager.isInstalled(packageName)
&& installedVersionName != null
) {
installedButKeepingWrongStatusPackageNameSet.add(packageName)
} else if (PackagesManager.isInstalled(packageName)
&& installedVersionName == null) {
uninstalledButKeepingWrongStatusPackageNameSet.add(packageName)
} else if (PackagesManager.isInstalled(packageName)
&& installedVersionName != null
&& !PackagesManager.isInstalledWithSpecificVersion(packageName, installedVersionName)) {
updatedButKeepingWrongStatusPackageNameSet.add(packageName)
}
}
Utils.log(TAG, "refreshWrongInstallStatus 需要更新已安装状态的包数量为 ${installedButKeepingWrongStatusPackageNameSet.size}")
Utils.log(TAG, "refreshWrongInstallStatus 需要更新已更新状态的包数量为 ${updatedButKeepingWrongStatusPackageNameSet.size}")
Utils.log(TAG, "refreshWrongInstallStatus 需要移除已安装的包数量为 ${uninstalledButKeepingWrongStatusPackageNameSet.size}")
runOnUiThread {
if (installedButKeepingWrongStatusPackageNameSet.isNotEmpty()) {
for (packageName in installedButKeepingWrongStatusPackageNameSet) {
PackageChangeHelper.addInstall(packageName)
}
}
if (uninstalledButKeepingWrongStatusPackageNameSet.isNotEmpty()) {
for (packageName in uninstalledButKeepingWrongStatusPackageNameSet) {
PackageChangeHelper.addUninstall(packageName)
}
}
if (updatedButKeepingWrongStatusPackageNameSet.isNotEmpty()) {
for (packageName in updatedButKeepingWrongStatusPackageNameSet) {
PackageChangeHelper.addUpdate(packageName)
}
}
}
}
}
/**
* 在5.1系统手机使用PackageManager获取已安装应用容易发生Package manager has died异常
* https://stackoverflow.com/questions/13235793/transactiontoolargeeception-when-trying-tÏo-get-a-list-of-applications-installed/30062632#30062632
*/
private fun getInstalledPackagesInternal(context: Context, flags: Int): List<PackageInfo> {
Utils.log(TAG, "调用系统 API 获取已安装应用列表")
val pm = context.packageManager
try {
return pm.getInstalledPackages(flags)
} catch (ignored: java.lang.Exception) {
//we don't care why it didn't succeed. We'll do it using an alternative way instead
}
// use fallback:
val process: Process
val result: MutableList<PackageInfo> = java.util.ArrayList()
var bufferedReader: BufferedReader? = null
try {
process = Runtime.getRuntime().exec("pm list packages")
bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
var line: String
while ((bufferedReader.readLine().also { line = it }) != null) {
val packageName = line.substring(line.indexOf(':') + 1)
val packageInfo = pm.getPackageInfo(packageName, flags)
result.add(packageInfo)
}
process.waitFor()
} catch (e: java.lang.Exception) {
e.printStackTrace()
if (e is InterruptedException) {
Thread.currentThread().interrupt()
}
} finally {
if (bufferedReader != null) try {
bufferedReader.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
return result
}
}

View File

@ -108,6 +108,12 @@ object PackageInstaller {
return
}
val packageName = downloadEntity?.packageName ?: PackageUtils.getPackageNameByPath(context, pkgPath)
packageName?.let {
PackageChangeHelper.addInstallPendingPackage(packageName)
}
try {
// 判断是否需要使用浏览器来进行安装
if (BrowserInstallHelper.isUseBrowserToInstallEnabled()
@ -250,6 +256,8 @@ object PackageInstaller {
fun uninstallForPackageName(context: Context, pkn: String?) {
if (pkn.isNullOrEmpty()) return
PackageChangeHelper.addUninstallPendingPackage(pkn)
val uninstallIntent = Intent()
uninstallIntent.action = Intent.ACTION_DELETE
uninstallIntent.addCategory(Intent.CATEGORY_DEFAULT)

View File

@ -7,14 +7,12 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.content.pm.Signature;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@ -29,8 +27,6 @@ import com.gh.common.xapk.XapkInstaller;
import com.gh.gamecenter.BuildConfig;
import com.gh.gamecenter.common.constant.Constants;
import com.gh.gamecenter.common.utils.ExtensionsKt;
import com.gh.gamecenter.common.utils.PackageFlavorHelper;
import com.gh.gamecenter.common.utils.PermissionHelper;
import com.gh.gamecenter.core.utils.MD5Utils;
import com.gh.gamecenter.core.utils.SentryHelper;
import com.gh.gamecenter.feature.entity.ApkEntity;
@ -49,12 +45,10 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@ -68,20 +62,14 @@ import java.util.zip.ZipFile;
public class PackageUtils {
private static long mLastInstalledPackageListTime = 0L;
private static List<PackageInfo> mInstalledPackageList = null;
private static final String TAG = "PackageUtils";
// 设备是否支持禁用获取已安装应用列表。-1 代表支持情况未知0 代表不支持, 1 代表支持
private static int mIsSupportGetInstalledListPermission = -1;
public static String getInstallPackageInfoSourceDir(String packageName) {
try {
return HaloApp.getInstance().getApplication().getPackageManager().getPackageInfo(packageName,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT).applicationInfo.sourceDir;
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return null;
}
@ -319,7 +307,7 @@ public class PackageUtils {
return new String[]{null, null};
}
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return new String[]{null, null};
}
@ -599,7 +587,7 @@ public class PackageUtils {
.getPackageInfo(packageName, 0);
return packageInfo.firstInstallTime;
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return 0;
}
@ -626,7 +614,7 @@ public class PackageUtils {
return HaloApp.getInstance().getApplication().getPackageManager()
.getPackageInfo(BuildConfig.APPLICATION_ID, 0).lastUpdateTime;
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return 0;
@ -640,7 +628,7 @@ public class PackageUtils {
return HaloApp.getInstance().getApplication().getPackageManager().getPackageInfo(packageName,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT).versionName;
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return null;
}
@ -653,7 +641,7 @@ public class PackageUtils {
return HaloApp.getInstance().getApplication().getPackageManager().getPackageInfo(packageName,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT).versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return 0;
}
@ -667,7 +655,7 @@ public class PackageUtils {
PackageManager packageManager = context.getApplicationContext().getPackageManager();
return packageManager.getApplicationIcon(packageName);
} catch (NameNotFoundException e) {
e.printStackTrace();
// do nothing
}
return null;
}
@ -677,7 +665,7 @@ public class PackageUtils {
*/
public static ArrayList<String> getAllPackageName(Context context) {
ArrayList<String> list = new ArrayList<>();
List<PackageInfo> packageInfos = getInstalledPackages(context, 0);
List<PackageInfo> packageInfos = PackageHelper.INSTANCE.getInstalledPackages(context, 0);
for (PackageInfo packageInfo : packageInfos) {
if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
if (!context.getPackageName().equals(packageInfo.packageName)) {
@ -690,7 +678,7 @@ public class PackageUtils {
public static ArrayList<String> getAllPackageNameIncludeGh(Context context) {
ArrayList<String> list = new ArrayList<>();
List<PackageInfo> packageInfos = getInstalledPackages(context, 0);
List<PackageInfo> packageInfos = PackageHelper.INSTANCE.getInstalledPackages(context, 0);
for (PackageInfo packageInfo : packageInfos) {
if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
list.add(packageInfo.packageName);
@ -704,11 +692,9 @@ public class PackageUtils {
*/
public static ArrayList<String> getAllPackageNameIncludeSystemApps(Context context) {
ArrayList<String> list = new ArrayList<>();
List<PackageInfo> packageInfos = getInstalledPackages(context, 0);
List<PackageInfo> packageInfos = PackageHelper.INSTANCE.getInstalledPackages(context, 0);
for (PackageInfo packageInfo : packageInfos) {
if (!context.getPackageName().equals(packageInfo.packageName)) {
list.add(packageInfo.packageName);
}
list.add(packageInfo.packageName);
}
return list;
}
@ -717,7 +703,7 @@ public class PackageUtils {
JSONArray jsonArray = new JSONArray();
try {
PackageManager pm = context.getPackageManager();
List<PackageInfo> packageInfos = getInstalledPackages(context, 0);
List<PackageInfo> packageInfos = PackageHelper.INSTANCE.getInstalledPackages(context, 0);
for (PackageInfo packageInfo : packageInfos) {
if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
JSONObject jsonObject = new JSONObject();
@ -758,7 +744,6 @@ public class PackageUtils {
}
}
/**
* 启动应用
* 请使用 PackageLauncher.launchApp()
@ -778,22 +763,6 @@ public class PackageUtils {
}
}
/*
* 根据包名,获取软件名称
*/
public static String getNameByPackageName(Context context, String packageName) {
try {
PackageManager pm = context.getApplicationContext().getPackageManager();
ApplicationInfo applicationInfo = pm.getApplicationInfo(
packageName, 0);
return applicationInfo.loadLabel(pm).toString();
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
/**
* todo 统一判断
* <p>
@ -912,8 +881,8 @@ public class PackageUtils {
String packageName = context.getApplicationContext().getPackageName();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
// The name of the process that this object is associated with.
if (appProcess.processName.equals(packageName) && appProcess.importance
== ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
if (appProcess.processName.equals(packageName)
&& appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
@ -924,132 +893,6 @@ public class PackageUtils {
return false;
}
/**
* 弃用已安装列表缓存
*/
public static void dumpInstalledListCache() {
mLastInstalledPackageListTime = 0;
}
public static List<PackageInfo> getInstalledPackages(Context context, int flags) {
Utils.log(TAG, "即将获取已安装应用列表");
// 简单 debounce 掉过于频繁的调用获取已安装列表调用
if (System.currentTimeMillis() - mLastInstalledPackageListTime < 1000
&& mInstalledPackageList != null
&& mInstalledPackageList.size() > 0) {
Utils.log(TAG, "使用了缓存的已安装应用列表");
return new ArrayList<>(mInstalledPackageList);
}
// 是否需要调用系统 API 获取最新的已安装应用列表
boolean shouldGetNewInstalledPackagedList = false;
// 当前设备是否支持限制获取已安装应用列表的功能
if (isSupportGetInstalledAppsPermission(context)) {
Utils.log(TAG, "当前设备支持限制获取已安装应用列表的功能");
// 当前设备是否支持禁用了获取已安装应用列表
if (!PermissionHelper.isGetInstalledListPermissionDisabled(context)) {
Utils.log(TAG, "当前设备没有限制获取已安装应用列表的功能");
shouldGetNewInstalledPackagedList = true;
} else {
Utils.log(TAG, "当前设备已限制获取已安装应用列表的功能");
}
} else {
Utils.log(TAG, "当前设备不支持限制获取已安装应用列表的功能");
shouldGetNewInstalledPackagedList = true;
}
if (shouldGetNewInstalledPackagedList) {
mLastInstalledPackageListTime = System.currentTimeMillis();
mInstalledPackageList = getInstalledPackagesInternal(context, flags);
}
if (mInstalledPackageList == null) {
mInstalledPackageList = new ArrayList<>();
}
return mInstalledPackageList;
}
public static boolean isSupportGetInstalledAppsPermission(Context context) {
// 若存在缓存,直接返回缓存结果。为 0 代表不支持,为 1 代表支持
if (mIsSupportGetInstalledListPermission != -1) {
return mIsSupportGetInstalledListPermission != 0;
}
try {
// 根据官方提供的方法来判定是否支持限制获取已安装应用列表
int flag = Settings.Secure.getInt(context.getContentResolver(), "oem_installed_apps_runtime_permission_enable", 0);
if (flag == 1) {
mIsSupportGetInstalledListPermission = 1;
return true;
}
// 部分未升级的手机没有上面配置项,有定义下面危险权限也认为是支持设备软件列表管控
PackageManager packageManager = context.getPackageManager();
PermissionInfo permissionInfo = packageManager.getPermissionInfo("com.android.permission.GET_INSTALLED_APPS", 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS) {
mIsSupportGetInstalledListPermission = 1;
return true;
} else {
mIsSupportGetInstalledListPermission = 0;
return false;
}
} else {
mIsSupportGetInstalledListPermission = 0;
return false;
}
} catch (NameNotFoundException e) {
mIsSupportGetInstalledListPermission = 0;
return false;
}
}
/**
* 在5.1系统手机使用PackageManager获取已安装应用容易发生Package manager has died异常
* https://stackoverflow.com/questions/13235793/transactiontoolargeeception-when-trying-tÏo-get-a-list-of-applications-installed/30062632#30062632
*/
private static List<PackageInfo> getInstalledPackagesInternal(Context context, int flags) {
Utils.log(TAG, "调用系统 API 获取已安装应用列表");
final PackageManager pm = context.getPackageManager();
try {
return pm.getInstalledPackages(flags);
} catch (Exception ignored) {
//we don't care why it didn't succeed. We'll do it using an alternative way instead
}
// use fallback:
Process process;
List<PackageInfo> result = new ArrayList<>();
BufferedReader bufferedReader = null;
try {
process = Runtime.getRuntime().exec("pm list packages");
bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
final String packageName = line.substring(line.indexOf(':') + 1);
final PackageInfo packageInfo = pm.getPackageInfo(packageName, flags);
result.add(packageInfo);
}
process.waitFor();
} catch (Exception e) {
e.printStackTrace();
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
} finally {
if (bufferedReader != null)
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
public static String getWebviewPath(Context context) {
final PackageInfo webViewPackageInfo = WebViewCompat.getCurrentWebViewPackage(context);
return webViewPackageInfo != null ? webViewPackageInfo.applicationInfo.sourceDir : null;

View File

@ -27,8 +27,8 @@ import com.gh.gamecenter.gamedetail.rating.RatingReplyActivity
object SyncDataBetweenPageHelper {
private const val REQUEST_CODE_TAG = "REQUEST_CODE_TAG"
private const val DATA_POSITION_TAG = "DATA_POSITION_TAG"
const val REQUEST_CODE_TAG = "REQUEST_CODE_TAG"
const val DATA_POSITION_TAG = "DATA_POSITION_TAG"
private const val DEFAULT_NUMBER = -1111
fun startActivityForResult(context: Context, intent: Intent, requestCode: Int, dataPosition: Int) {

View File

@ -6,25 +6,21 @@ import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.core.view.get
import com.facebook.drawee.drawable.ScalingUtils
import com.facebook.drawee.view.SimpleDraweeView
import com.gh.common.util.DirectUtils
import com.gh.gamecenter.ImageViewerActivity
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.ImageUtils
import com.gh.gamecenter.common.utils.debounceActionWithInterval
import com.gh.gamecenter.common.utils.dip2px
import com.gh.gamecenter.common.utils.toResString
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.core.utils.TopCutProcess
import com.gh.gamecenter.databinding.ItemCommunityImageBinding
import com.gh.gamecenter.feature.entity.AnswerEntity
import com.gh.gamecenter.feature.entity.CommunityVideoEntity
import com.gh.gamecenter.feature.entity.ImageInfo
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.video.detail.VideoDetailContainerViewModel
class ImageContainerView : LinearLayout {
private var mAnswerEntity: AnswerEntity? = null
private var data: ImageContainerData? = null
//三图默认宽度
private var mDefaultWidth = 0f
@ -41,9 +37,6 @@ class ImageContainerView : LinearLayout {
//长图比例
private var mLongPictureRatio = 9 / 18f
private var mEntrance = ""
private var mPath = ""
//图片之间的间距
private val mItemSpace = 4f.dip2px()
private var mOffset = 0
@ -51,6 +44,8 @@ class ImageContainerView : LinearLayout {
private val imageViewList = arrayListOf<SimpleDraweeView>()
private var onImageContainerEventListener: OnImageContainerEventListener? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
@ -75,40 +70,34 @@ class ImageContainerView : LinearLayout {
calculateWidth()
}
fun bindData(entity: AnswerEntity, entrance: String = "", path: String = "", imageClick: (() -> Unit)? = null) {
fun bindData(
data: ImageContainerData,
listener: OnImageContainerEventListener? = null
) {
this.data = data
onImageContainerEventListener = listener
imageViewList.clear()
if (entity.id != mAnswerEntity?.id) {
removeAllViews()
}
mAnswerEntity = entity
mEntrance = entrance
mPath = path
removeAllViews()
index = 0
if ((entity.user.id == UserManager.getInstance().userId && entity.videos.isNotEmpty()) ||
(entity.user.id != UserManager.getInstance().userId && entity.getPassVideos().isNotEmpty()) ||
entity.images.isNullOrEmpty()
) {
if (!data.show) {
visibility = View.GONE
return
}
visibility = View.VISIBLE
if (mAnswerEntity?.type == "community_article") {
if (data.isPostCard) {
//若文章内容含有图片及视频,则信息流卡片,仅展示图片,且标题后带有‘有视频’标签
//若文章内容仅含有图片,则信息流卡片,仅展示图片,无标签
//若文章内容仅含有视频,则信息流卡片,仅展示视频,无标签
when {
entity.images.isNotEmpty() -> {
val imagesInfo = entity.imagesInfo
val images = entity.images.take(3)
images.forEachIndexed { index, url ->
val width = if (index <= entity.imagesInfo.size - 1) imagesInfo[index].width else 0
val height = if (index <= entity.imagesInfo.size - 1) imagesInfo[index].height else 0
bindImage(url, width, height, images.size == 1, imageClick)
}
data.images.isNotEmpty() -> {
data.images.take(3)
.forEach {
bindImage(it.url, it.width, it.height, data.images.size == 1)
}
}
entity.getPassVideos().isNotEmpty() -> {
val video = entity.getPassVideos()[0]
data.video != null -> {
val video = data.video
bindVideo(video, video.width, video.height, true)
}
@ -120,28 +109,22 @@ class ImageContainerView : LinearLayout {
//若问答内容含有图片及视频,则信息流卡片,同时展示图片及视频,且参考以往排序逻辑(视频优先放置第一位),无标签
//若问答内容仅含有图片,则信息流卡片,仅展示图片,无标签
//若问答内容仅含有视频,则信息流卡片,仅展示视频,无标签
if (entity.getPassVideos().isNotEmpty()) {
val video = entity.getPassVideos()[0]
bindVideo(video, video.width, video.height, mAnswerEntity?.images.isNullOrEmpty())
entity.images.take(2).forEachIndexed { index, url ->
val imagesInfo = entity.imagesInfo
val width = if (index <= entity.imagesInfo.size - 1) imagesInfo[index].width else 0
val height = if (index <= entity.imagesInfo.size - 1) imagesInfo[index].height else 0
bindImage(url, width, height, false, imageClick)
if (data.video != null) {
val video = data.video
bindVideo(video, video.width, video.height, data.images.isNullOrEmpty())
data.images.take(2).forEach {
bindImage(it.url, it.width, it.height, false)
}
} else {
val images = entity.images.take(3)
images.forEachIndexed { index, url ->
val imagesInfo = entity.imagesInfo
val width = if (index <= entity.imagesInfo.size - 1) imagesInfo[index].width else 0
val height = if (index <= entity.imagesInfo.size - 1) imagesInfo[index].height else 0
bindImage(url, width, height, images.size == 1, imageClick)
}
data.images.take(3)
.forEach {
bindImage(it.url, it.width, it.height, data.images.size == 1)
}
}
}
}
private fun bindVideo(video: CommunityVideoEntity, width: Int, height: Int, isChangeRatio: Boolean) {
private fun bindVideo(video: ImageContainerData.VideoInfo, width: Int, height: Int, isChangeRatio: Boolean) {
val oldView = if (childCount == 0 || index >= childCount) null else getChildAt(index)
val binding = if (oldView != null) {
ItemCommunityImageBinding.bind(oldView)
@ -158,16 +141,7 @@ class ImageContainerView : LinearLayout {
displayImage(binding, video.poster, width.toFloat(), height.toFloat(), isChangeRatio, true)
binding.root.setOnClickListener {
debounceActionWithInterval(it.id, 1000) {
if (mAnswerEntity == null) return@debounceActionWithInterval
val videoEntity = mAnswerEntity!!.getPassVideos().firstOrNull()
DirectUtils.directToVideoDetail(
context,
videoEntity?.id ?: "",
VideoDetailContainerViewModel.Location.VIDEO_HOT.value,
showComment = false,
entrance = mEntrance,
path = mPath
)
onImageContainerEventListener?.onVideoCLick(video.id)
}
}
index++
@ -177,8 +151,7 @@ class ImageContainerView : LinearLayout {
url: String,
width: Int,
height: Int,
isChangeRatio: Boolean,
imageClick: (() -> Unit)?
isChangeRatio: Boolean
) {
val oldView = if (childCount == 0 || index >= childCount) null else getChildAt(index)
val binding = if (oldView != null) {
@ -195,25 +168,21 @@ class ImageContainerView : LinearLayout {
binding.videoPlay.visibility = View.GONE
displayImage(binding, url, width.toFloat(), height.toFloat(), isChangeRatio)
binding.root.setOnClickListener {
if (mAnswerEntity?.status == "pending" || mAnswerEntity?.status == "fail") return@setOnClickListener
imageClick?.invoke()
if (data?.status == "pending" || data?.status == "fail") return@setOnClickListener
debounceActionWithInterval(it.id, 1000) {
if (mAnswerEntity == null) return@debounceActionWithInterval
val position = if (mAnswerEntity?.type == "community_article") {
binding.root.tag as Int
} else {
if (mAnswerEntity!!.getPassVideos()
.isNullOrEmpty()
) binding.root.tag as Int else (binding.root.tag as Int) - 1
data?.run {
val position = if (isPostCard) {
binding.root.tag as Int
} else {
if (video == null) binding.root.tag as Int else (binding.root.tag as Int) - 1
}
onImageContainerEventListener?.onImageClick(
images.map(ImageContainerData.ImageInfo::url),
position,
imageViewList
)
}
if (mAnswerEntity?.communityId.isNullOrEmpty()) {
mAnswerEntity?.communityId = mAnswerEntity?.bbs?.id
}
val intent = ImageViewerActivity.getIntent(
context, mAnswerEntity!!.images as ArrayList<String>, position, imageViewList,
if (mAnswerEntity?.type == "community_article") mAnswerEntity else null, mEntrance, true
)
context.startActivity(intent)
}
}
index++
@ -279,7 +248,7 @@ class ImageContainerView : LinearLayout {
}
binding.pendingView.run {
when (mAnswerEntity?.status) {
when (data?.status) {
"pending" -> {
visibility = View.VISIBLE
text = R.string.pending_status.toResString()
@ -297,7 +266,7 @@ class ImageContainerView : LinearLayout {
}
val imageCount = mAnswerEntity?.images?.size ?: 0
val imageCount = data?.images?.size ?: 0
if (!isVideo && index == 2 && imageCount > 3) {
binding.labelIcon.visibility = View.GONE
binding.durationOrNumTv.visibility = View.VISIBLE
@ -308,4 +277,80 @@ class ImageContainerView : LinearLayout {
if (index != 0) params.leftMargin = mItemSpace
binding.root.layoutParams = params
}
companion object {
private const val COMMUNITY_ARTICLE = "community_article"
fun AnswerEntity.toImageContainerData(): ImageContainerData {
val imageInfoList = arrayListOf<ImageContainerData.ImageInfo>()
images.forEachIndexed { index, url ->
if (index < 3) {
imageInfoList.add(ImageContainerData.ImageInfo(url))
}
}
imageInfoList.forEachIndexed { index, imageInfo ->
val item = imagesInfo.getOrNull(index)
if (item != null) {
imageInfo.width = item.width
imageInfo.height = item.height
}
}
val video =
getPassVideos().firstOrNull()?.let {
ImageContainerData.VideoInfo(
it.id,
it.duration,
it.poster,
it.width,
it.height
)
}
val show = !((user.id == UserManager.getInstance().userId && videos.isNotEmpty())
|| (user.id != UserManager.getInstance().userId && getPassVideos().isNotEmpty())
|| images.isNullOrEmpty())
return ImageContainerData(
status = status,
isPostCard = type == COMMUNITY_ARTICLE,
images = imageInfoList,
video = video,
show
)
}
}
data class ImageContainerData(
val status: String,
val isPostCard: Boolean, // 是否是帖子卡片
val images: List<ImageInfo>,
val video: VideoInfo? = null,
val show: Boolean
) {
data class ImageInfo(
val url: String,
var width: Int = 0,
var height: Int = 0
)
data class VideoInfo(
val id: String,
val duration: String,
val poster: String,
var width: Int = 0,
var height: Int = 0,
)
}
interface OnImageContainerEventListener {
fun onImageClick(
images: List<String>,
position: Int,
imageViewList: ArrayList<SimpleDraweeView>
)
fun onVideoCLick(videoId: String)
}
}

View File

@ -10,8 +10,8 @@ import com.gh.gamecenter.common.exposure.meta.MetaUtil
import com.gh.gamecenter.common.exposure.meta.MetaUtil.getMeta
import com.gh.gamecenter.common.loghub.LoghubUtils
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.utils.SentryHelper
import com.gh.ndownload.NDataChanger
import com.gh.ndownload.NDownloadBridge
import com.halo.assistant.HaloApp
import com.lightgame.download.DownloadConfig
import com.lightgame.download.DownloadEntity
@ -124,6 +124,7 @@ object DownloadDataHelper {
val payloadObject = JSONObject()
payloadObject.put("host", downloadEntity.meta[DownloadEntity.DOWNLOAD_HOST_KEY] ?: "unknown")
payloadObject.put("path", downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown")
payloadObject.put("redirected_host_list", downloadEntity.meta[NDownloadBridge.REDIRECTED_HOST_LIST] ?: "unknown")
payloadObject.put("game_id", downloadEntity.gameId)
payloadObject.put("gameName", downloadEntity.name)
payloadObject.put("platform", downloadEntity.platform)
@ -212,6 +213,7 @@ object DownloadDataHelper {
payloadObject.put("host", downloadEntity.meta[DownloadEntity.DOWNLOAD_HOST_KEY] ?: "unknown")
payloadObject.put("path", downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown")
payloadObject.put("redirected_host_list", downloadEntity.meta[NDownloadBridge.REDIRECTED_HOST_LIST] ?: "unknown")
payloadObject.put("game_id", downloadEntity.gameId)
payloadObject.put("gameName", downloadEntity.name)
payloadObject.put("platform", downloadEntity.platform)
@ -251,6 +253,7 @@ object DownloadDataHelper {
payloadObject.put("host", downloadEntity.meta[DownloadEntity.DOWNLOAD_HOST_KEY] ?: "unknown")
payloadObject.put("path", downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown")
payloadObject.put("redirected_host_list", downloadEntity.meta[NDownloadBridge.REDIRECTED_HOST_LIST] ?: "unknown")
payloadObject.put("game_id", downloadEntity.gameId)
payloadObject.put("gameName", downloadEntity.name)
payloadObject.put("platform", downloadEntity.platform)
@ -315,6 +318,7 @@ object DownloadDataHelper {
payloadObject.put("host", downloadEntity.meta[DownloadEntity.DOWNLOAD_HOST_KEY] ?: "unknown")
payloadObject.put("path", downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown")
payloadObject.put("redirected_host_list", downloadEntity.meta[NDownloadBridge.REDIRECTED_HOST_LIST] ?: "unknown")
payloadObject.put("game_id", downloadEntity.gameId)
payloadObject.put("gameName", downloadEntity.name)
payloadObject.put("platform", downloadEntity.platform)
@ -357,6 +361,7 @@ object DownloadDataHelper {
sheet = JSONObject()
sheet.put("host", downloadEntity.meta[DownloadEntity.DOWNLOAD_HOST_KEY] ?: "unknown")
sheet.put("path", downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown")
sheet.put("redirected_host_list", downloadEntity.meta[NDownloadBridge.REDIRECTED_HOST_LIST] ?: "unknown")
sheet.put("game_id", downloadEntity.gameId)
sheet.put("platform", downloadEntity.platform)
sheet.put("package", downloadEntity.packageName)
@ -376,6 +381,7 @@ object DownloadDataHelper {
"path",
downloadEntity.meta[DownloadEntity.DOWNLOAD_PATH_KEY] ?: "unknown"
) // 初始化记录的 path 为空
sheet.put("redirected_host_list", downloadEntity.meta[NDownloadBridge.REDIRECTED_HOST_LIST] ?: "unknown")
sheet.put("total_size", downloadEntity.size / 1024 / 1024) // 初始化记录的 total_size 有可能为0
sheet.put("progress_size", downloadEntity.progress / 1024 - progressSize)
sheet.put("current_progress_size", downloadEntity.progress / 1024)

View File

@ -22,7 +22,6 @@ import com.gh.gamecenter.core.AppExecutor;
import com.gh.gamecenter.common.constant.Constants;
import com.gh.gamecenter.feature.entity.TagStyleEntity;
import com.gh.gamecenter.feature.entity.CustomPageTrackData;
import com.gh.gamecenter.feature.entity.TagStyleEntity;
import com.gh.gamecenter.feature.exposure.ExposureEvent;
import com.gh.common.exposure.ExposureUtils;
import com.gh.common.history.HistoryHelper;
@ -34,16 +33,10 @@ import com.gh.common.util.LunchType;
import com.gh.common.util.PackageInstaller;
import com.gh.common.util.PackageUtils;
import com.gh.gamecenter.BuildConfig;
import com.gh.gamecenter.common.base.GlobalActivityManager;
import com.gh.gamecenter.common.constant.Constants;
import com.gh.gamecenter.common.exposure.meta.MetaUtil;
import com.gh.gamecenter.common.utils.DeviceUtils;
import com.gh.gamecenter.common.utils.ExtensionsKt;
import com.gh.gamecenter.common.utils.FileUtils;
import com.gh.gamecenter.common.utils.NetworkUtils;
import com.gh.gamecenter.common.utils.SensorsBridge;
import com.gh.gamecenter.core.AppExecutor;
import com.gh.gamecenter.core.utils.AppDebugConfig;
import com.gh.gamecenter.core.utils.GsonUtils;
import com.gh.gamecenter.core.utils.PageSwitchDataHelper;
import com.gh.gamecenter.core.utils.SPUtils;
@ -55,7 +48,6 @@ import com.gh.gamecenter.eventbus.EBDownloadStatus;
import com.gh.gamecenter.feature.entity.ApkEntity;
import com.gh.gamecenter.feature.entity.GameEntity;
import com.gh.gamecenter.feature.entity.PluginLocation;
import com.gh.gamecenter.feature.exposure.ExposureEvent;
import com.gh.gamecenter.login.user.UserManager;
import com.gh.gamecenter.manager.PackagesManager;
import com.gh.gamecenter.packagehelper.PackageRepository;
@ -64,7 +56,6 @@ import com.gh.ndownload.NDownloadBridge;
import com.gh.ndownload.NDownloadService;
import com.gh.vspace.VHelper;
import com.halo.assistant.HaloApp;
import com.lightgame.download.ConnectionUtils;
import com.lightgame.download.DataWatcher;
import com.lightgame.download.DownloadConfig;
import com.lightgame.download.DownloadDao;
@ -179,9 +170,6 @@ public class DownloadManager implements DownloadStatusListener {
mUpdateMarks = SPUtils.getStringSet(UPDATE_IS_READ_MARK);
// 只有下载模块需要这坨东西,因此移动到这里初始化
ConnectionUtils.initHttpsUrlConnection(mContext);
updateDownloadMetaMap();
lastTimeMap = new ArrayMap<>();

View File

@ -28,10 +28,10 @@ import org.json.JSONObject
import java.io.File
import java.net.URLEncoder
import java.util.*
import kotlin.collections.ArrayList
object BrowserInstallHelper {
// 随便选的 32321 居然在部分 vivo 手机上被占用,喷了
private const val PORT = 40705
private const val RESERVE_PORT = 40706
@ -40,15 +40,12 @@ object BrowserInstallHelper {
private val mContext by lazy { HaloApp.getInstance().application }
private var mUseReservePort = false
private val mAllInstalledPackageList: ArrayList<String> by lazy {
PackageUtils.getAllPackageNameIncludeSystemApps(HaloApp.getInstance().applicationContext).apply {
add(HaloApp.getInstance().applicationContext.packageName)
}
}
private var mValidInstalledPackageList: ArrayList<String> = arrayListOf()
private var mValidConditionMatchedCache: Boolean? = null
private fun getServer(port: Int): DownloadServer {
val server = DownloadServer(port)
for (packageName in mAllInstalledPackageList) {
for (packageName in getAllInstalledPackageList()) {
if (packageName.contains("com.freeme") || packageName.contains("com.zhuoyi")) {
server.isBuggyDevice = true
break
@ -57,6 +54,23 @@ object BrowserInstallHelper {
return server
}
private fun getAllInstalledPackageList(): ArrayList<String> {
when {
mValidInstalledPackageList.isNotEmpty() -> {
return mValidInstalledPackageList
}
else -> {
val allInstalledPackageList = PackageUtils.getAllPackageNameIncludeSystemApps(mContext)
if (allInstalledPackageList.isNotEmpty()) {
mValidInstalledPackageList = allInstalledPackageList
}
return mValidInstalledPackageList
}
}
}
fun downloadFile(filePath: String) {
if (!::mServer.isInitialized) mServer = if (mUseReservePort) getServer(RESERVE_PORT) else getServer(PORT)
if (!mServer.isAlive && !startServer()) {
@ -237,32 +251,43 @@ object BrowserInstallHelper {
* 是否满足开启浏览器安装的条件
*/
private fun isConditionMatched(settingsEntity: NewSettingsEntity): Boolean {
if (mValidConditionMatchedCache != null) {
return mValidConditionMatchedCache!!
}
val packageList = getAllInstalledPackageList()
if (packageList.isEmpty()) return false
settingsEntity.installModel?.whiteList?.let {
for (packageName in it) {
if (mAllInstalledPackageList.contains(packageName)) {
return false
if (packageList.contains(packageName)) {
mValidConditionMatchedCache = false
break
}
}
}
settingsEntity.installModel?.packages?.let {
for (packageName in it) {
if (mAllInstalledPackageList.contains(packageName)) {
return true
if (packageList.contains(packageName)) {
mValidConditionMatchedCache = true
break
}
}
}
// 匹配部分字符串即可
settingsEntity.installModel?.regexPackages?.let {
for (packageNamePieces in it) {
for (installedPackageName in mAllInstalledPackageList) {
for (installedPackageName in packageList) {
if (installedPackageName.contains(packageNamePieces)) {
return true
mValidConditionMatchedCache = true
break
}
}
}
}
return false
return mValidConditionMatchedCache ?: false
}
fun onApkInstalled(path: String?) {

View File

@ -10,6 +10,9 @@ import com.gh.gamecenter.common.utils.updateStatusBarColor
import com.gh.gamecenter.entity.SubjectRecommendEntity
import com.gh.gamecenter.game.GameFragment
/**
* 板块
*/
class BlockActivity : DownloadToolbarActivity() {
companion object {

View File

@ -14,6 +14,7 @@ import com.halo.assistant.fragment.ApkCleanerFragment;
/**
* Created by khy on 2017/1/24.
* 清理安装包
*/
@Route(path = RouteConsts.activity.cleanApkActivity)
public class CleanApkActivity extends ToolBarActivity {

View File

@ -16,6 +16,7 @@ import java.util.ArrayList;
/**
* Created by khy on 18/07/17.
* 我的收藏
*/
public class CollectionActivity extends ToolBarActivity {
@Override

View File

@ -23,6 +23,9 @@ import java.lang.ref.SoftReference;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
/**
* 裁剪图片
*/
public class CropImageActivity extends ToolBarActivity {
protected CropImageCustom mCropImageCustom;

View File

@ -24,7 +24,7 @@ import com.halo.assistant.HaloApp
/**
* Created by khy on 2017/3/24.
* 游戏详情适配器
* 游戏详情
*/
class GameDetailActivity : DownloadToolbarActivity() {

View File

@ -16,6 +16,7 @@ import java.util.ArrayList;
/**
* Created by khy on 2016/12/12.
* 礼包中心
*/
@Deprecated
public class LibaoActivity extends ToolBarActivity {

View File

@ -285,9 +285,6 @@ public class MainActivity extends BaseActivity {
// 耗时操作
AppExecutor.getIoExecutor().execute(() -> {
// 上传数据
DataCollectionManager.getInstance().upload();
// 初始化PlatformUtils
PlatformUtils.getInstance(getApplicationContext());
@ -556,7 +553,10 @@ public class MainActivity extends BaseActivity {
} else {
TextView jumpBtn = findViewById(R.id.jumpBtn);
jumpBtn.setText(String.format(Locale.CHINA, "跳过 %d", COUNTDOWN_MAX_COUNT - mCountdownCount));
mBaseHandler.sendEmptyMessageDelayed(COUNTDOWN_AD, 1000);
Message newMsg = Message.obtain();
newMsg.what = COUNTDOWN_AD;
newMsg.obj = msg.obj;
mBaseHandler.sendMessageDelayed(newMsg, 1000);
}
}
}
@ -877,13 +877,6 @@ public class MainActivity extends BaseActivity {
return true;
}
@Override
public void finish() {
// 上传数据
DataCollectionManager.getInstance().statClickData();
super.finish();
}
@Override
protected void onSaveInstanceState(@NotNull Bundle outState) {
super.onSaveInstanceState(outState);

View File

@ -29,6 +29,9 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.concurrent.TimeUnit
/**
* 游戏搜索页
*/
open class SearchActivity : BaseActivity() {
lateinit var searchEt: EditText

View File

@ -48,6 +48,7 @@ import io.reactivex.functions.Consumer;
/**
* Created by khy on 2016/11/7.
* 分享卡片
*/
public class ShareCardPicActivity extends ToolBarActivity {

View File

@ -21,6 +21,7 @@ import com.tencent.tauth.Tencent;
/**
* Created by khy on 2017/2/6.
* 分享光环
*/
public class ShareGhActivity extends ToolBarActivity {

View File

@ -197,7 +197,7 @@ class SplashScreenActivity : BaseActivity() {
// 尝试获取安装应用列表权限并启动首页(不在乎结果)
private fun requestGetInstallListPermissionAndLaunchMainActivity() {
if (PackageUtils.isSupportGetInstalledAppsPermission(this)
if (PackageHelper.isSupportGetInstalledAppsPermission(this)
&& PermissionHelper.isGetInstalledListPermissionDisabled(this)
) {
PermissionHelper.requestGetInstalledAppsListPermission(this, true) {

View File

@ -7,6 +7,9 @@ import com.gh.gamecenter.common.base.activity.ToolBarActivity
import com.gh.gamecenter.common.utils.updateStatusBarColor
import com.halo.assistant.fragment.user.UserInfoFragment
/**
* 编辑资料
*/
class UserInfoActivity : ToolBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -8,6 +8,9 @@ import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.login.user.UserViewModel
import com.halo.assistant.fragment.user.UserInfoEditFragment
/**
* 修改个人信息
*/
class UserInfoEditActivity : ToolBarActivity() {
companion object {
fun getIntent(context: Context, editType: String): Intent {

View File

@ -8,6 +8,7 @@ import com.halo.assistant.fragment.user.SelectRegionFragment;
/**
* Created by khy on 25/09/17.
* 选择地区
*/
public class UserRegionActivity extends ToolBarActivity {

View File

@ -18,6 +18,7 @@ import android.widget.TextView;
import androidx.collection.ArrayMap;
import androidx.core.content.ContextCompat;
import com.gh.common.util.PackageHelper;
import com.gh.common.util.PackageInstaller;
import com.gh.common.util.PackageUtils;
import com.gh.gamecenter.R;
@ -188,7 +189,7 @@ public class CleanApkAdapter extends BaseRecyclerAdapter<KcSelectGameViewHolder>
}
private int doType(String packageName) {
List<PackageInfo> pakageinfos = PackageUtils.getInstalledPackages(mContext, 0);
List<PackageInfo> pakageinfos = PackageHelper.INSTANCE.getInstalledPackages(mContext, 0);
for (PackageInfo pi : pakageinfos) {
if ((pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
String pi_packageName = pi.packageName;

View File

@ -8,6 +8,9 @@ import com.gh.gamecenter.R
import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.common.utils.updateStatusBarColor
/**
* 新分类2.0
*/
class CategoryV2Activity : DownloadToolbarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View File

@ -5,6 +5,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.gh.gamecenter.R
import com.gh.gamecenter.common.baselist.ListAdapter
import com.gh.gamecenter.common.utils.formatTime
import com.gh.gamecenter.common.utils.toBinding
import com.gh.gamecenter.databinding.ItemArchiveLimitBinding
import com.gh.gamecenter.entity.ArchiveEntity
@ -29,10 +30,7 @@ class ArchiveLimitAdapter(context: Context) : ListAdapter<ArchiveLimitAdapter.Ar
if (holder is ArchiveLimitViewHolder) {
val item = mEntityList[position]
holder.binding.tvTitle.text = item.data.name
val timeLong = item.data.time.create
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA)
val date = Date(timeLong)
holder.binding.tvTime.text = sdf.format(date)
holder.binding.tvTime.text = item.data.time.create.formatTime("yyyy-MM-dd HH:mm")
val resId = if (item.isChecked) R.drawable.ic_selector_selected else R.drawable.ic_selector_default
holder.binding.ivSelector.setImageResource(resId)
@ -53,10 +51,10 @@ class ArchiveLimitAdapter(context: Context) : ListAdapter<ArchiveLimitAdapter.Ar
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any?>) {
if(holder is ArchiveLimitViewHolder){
if(payloads.isEmpty()){
if (holder is ArchiveLimitViewHolder) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
}else{
} else {
val item = mEntityList[position]
val resId = if (item.isChecked) R.drawable.ic_selector_selected else R.drawable.ic_selector_default
holder.binding.ivSelector.setImageResource(resId)

View File

@ -41,6 +41,9 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
/**
* 云存档管理
*/
class CloudArchiveManagerActivity : BaseActivity_TabLayout(), ArchiveLimitSelectedListener {
private lateinit var mBinding: ActivityCloudArchiveManagerBinding

View File

@ -1,120 +0,0 @@
package com.gh.gamecenter.db;
import android.content.Context;
import com.gh.gamecenter.db.info.DataCollectionInfo;
import com.j256.ormlite.dao.Dao;
import java.sql.SQLException;
import java.util.List;
// TODO 这个数据库其实没有用了,上传到 loghub 已经有相关的逻辑处理,有空删掉它
public class DataCollectionDao {
private DatabaseHelper helper;
private Dao<DataCollectionInfo, String> dao;
public DataCollectionDao(Context context) {
try {
helper = DatabaseHelper.getHelper(context);
dao = helper.getDao(DataCollectionInfo.class);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查找一个数据
*/
public List<DataCollectionInfo> findByType(String type) {
try {
return dao.queryForEq("type", type);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 添加一个数据
*/
public void add(DataCollectionInfo entity) {
try {
dao.create(entity);
} catch (Exception e) {
// java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase:
e.printStackTrace();
}
}
/**
* 删除一个数据
*/
public void delete(String id) {
try {
dao.deleteById(id);
} catch (Exception e) {
// java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase:
e.printStackTrace();
}
}
/**
* 删除一组数据
*/
public void delete(List<String> ids) {
try {
dao.deleteIds(ids);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据id获取某一个数据
*/
public DataCollectionInfo find(String id) {
try {
return dao.queryForId(id);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取所有的数据
*/
public List<DataCollectionInfo> getAll() {
try {
return dao.queryForAll();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取点击数据
*/
public List<DataCollectionInfo> getClickData() {
try {
return dao.queryForEq("type", "click-item");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 更新数据
*/
public void update(DataCollectionInfo entity) {
try {
dao.update(entity);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -5,9 +5,7 @@ import android.database.sqlite.SQLiteDatabase;
import androidx.collection.ArrayMap;
import com.gh.gamecenter.db.info.DataCollectionInfo;
import com.gh.gamecenter.db.info.GameTrendsInfo;
import com.gh.gamecenter.db.info.PackageInfo;
import com.gh.gamecenter.db.info.SearchHistoryInfo;
import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper;
import com.j256.ormlite.dao.Dao;
@ -52,8 +50,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
try {
Utils.log("DatabaseHelper onCreate");
TableUtils.createTable(connectionSource, SearchHistoryInfo.class);
TableUtils.createTable(connectionSource, DataCollectionInfo.class);
TableUtils.createTable(connectionSource, PackageInfo.class);
TableUtils.createTable(connectionSource, GameTrendsInfo.class);
} catch (SQLException e) {
e.printStackTrace();
@ -65,8 +61,6 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
try {
Utils.log("DatabaseHelper onUpgrade");
TableUtils.dropTable(connectionSource, SearchHistoryInfo.class, true);
TableUtils.dropTable(connectionSource, DataCollectionInfo.class, true);
TableUtils.dropTable(connectionSource, PackageInfo.class, true);
TableUtils.dropTable(connectionSource, GameTrendsInfo.class, true);
onCreate(database, connectionSource);
} catch (SQLException e) {

View File

@ -1,65 +0,0 @@
package com.gh.gamecenter.db.info;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import java.io.Serializable;
@DatabaseTable(tableName = "tb_datacollection")
public class DataCollectionInfo implements Serializable {
/**
*
*/
private static final long serialVersionUID = 3196892351397147390L;
@DatabaseField(id = true, columnName = "id")
private String id;
@DatabaseField(columnName = "type")
private String type;
@DatabaseField(columnName = "data")
private String data;
public DataCollectionInfo() {
}
public DataCollectionInfo(String id, String type, String data) {
this.id = id;
this.type = type;
this.data = data;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "DataCollectionEntity [id=" + id + ", type=" + type + ", data="
+ data + "]";
}
}

View File

@ -1,42 +0,0 @@
package com.gh.gamecenter.db.info;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import java.io.Serializable;
@DatabaseTable(tableName = "tb_package")
public class PackageInfo implements Serializable {
@DatabaseField(id = true, columnName = "packageName")
private String packageName;
@DatabaseField(columnName = "time")
private long time;
public PackageInfo() {
}
public PackageInfo(String packageName, long time) {
this.packageName = packageName;
this.time = time;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
}

View File

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.gh.ad.AdDelegateHelper
import com.gh.common.util.HomePluggableHelper
import com.gh.common.util.NewFlatLogUtils
import com.gh.common.util.PackageHelper
import com.gh.common.util.PackageInstaller
import com.gh.download.DownloadManager
import com.gh.gamecenter.DownloadManagerActivity
@ -276,6 +277,12 @@ class DownloadFragment : BaseFragment_TabLayout() {
if (mBinding.adGameItemContainer.isVisible) {
DownloadManager.getInstance().addObserver(mDataWatcher)
}
refreshInstallStatus()
}
private fun refreshInstallStatus() {
PackageHelper.refreshWrongInstallStatus(PackagesManager.getInstalledSet())
}
override fun onParentActivityFinish() {

View File

@ -10,8 +10,8 @@ import com.ethanhua.skeleton.ViewSkeletonScreen
import com.gh.common.exposure.ExposureListener
import com.gh.common.util.DirectUtils
import com.gh.common.util.DownloadItemUtils
import com.gh.common.util.PackageHelper
import com.gh.download.DownloadManager
import com.gh.gamecenter.MainActivity
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.fragment.ToolbarFragment
import com.gh.gamecenter.common.eventbus.EBReuse
@ -29,7 +29,6 @@ import com.gh.gamecenter.feature.entity.GameInstall
import com.gh.gamecenter.manager.PackagesManager
import com.gh.gamecenter.packagehelper.PackageRepository
import com.gh.gamecenter.packagehelper.PackageViewModel
import com.gh.gamecenter.wrapper.MainWrapperFragment
import com.lightgame.download.DataWatcher
import com.lightgame.download.DownloadEntity
import com.lightgame.download.DownloadStatus
@ -147,15 +146,20 @@ class NewInstalledGameFragment : ToolbarFragment() {
}
mBinding.run {
if (PermissionHelper.isGetInstalledListPermissionDisabled(requireContext())) {
val isGetInstalledListDisagreed = PackageHelper.isGetInstalledPackagesApiAgreedRequired()
&& !PackageHelper.isGetInstalledPackagesApiAgreed()
val isGetInstalledListPermissionDisabled = PermissionHelper.isGetInstalledListPermissionDisabled(requireContext())
if (isGetInstalledListDisagreed || isGetInstalledListPermissionDisabled) {
reuseNoneData.reuseNoneDataIv.visibility = View.GONE
reuseNoneData.reuseNoneDataTv.text = "开启应用列表权限"
reuseNoneData.reuseNoneDataDescTv.text = " 及时获悉游戏最新的更新消息"
reuseNoneData.reuseResetLoadTv.text = "去开启"
reuseNoneData.reuseResetLoadTv.setOnClickListener {
PermissionHelper.requestGetInstalledAppsListPermission(requireActivity()) {
updateNoDataView()
PackageRepository.initData()
PackageHelper.showGetInstallAppsListDialogAndRequestPermissionIfNeeded(requireActivity()) { isGranted ->
if (isGranted) {
updateNoDataView()
}
}
}
} else {

View File

@ -3,8 +3,8 @@ package com.gh.gamecenter.download
import android.view.View
import com.gh.common.exposure.ExposureListener
import com.gh.common.util.DirectUtils
import com.gh.common.util.PackageHelper
import com.gh.download.DownloadManager
import com.gh.gamecenter.MainActivity
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.fragment.LazyFragment
import com.gh.gamecenter.common.constant.EntranceConsts
@ -12,12 +12,10 @@ import com.gh.gamecenter.common.eventbus.EBReuse
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.common.view.FixLinearLayoutManager
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.core.utils.EmptyCallback
import com.gh.gamecenter.databinding.FragmentGameUpdatableBinding
import com.gh.gamecenter.eventbus.EBDownloadStatus
import com.gh.gamecenter.packagehelper.PackageRepository
import com.gh.gamecenter.packagehelper.PackageViewModel
import com.gh.gamecenter.wrapper.MainWrapperFragment
import com.lightgame.download.DataWatcher
import com.lightgame.download.DownloadEntity
import org.greenrobot.eventbus.Subscribe
@ -40,7 +38,6 @@ class UpdatableGameFragment : LazyFragment() {
}
}
override fun getRealLayoutId() = R.layout.fragment_game_updatable
override fun onRealLayoutInflated(inflatedView: View) {
mBinding = FragmentGameUpdatableBinding.bind(inflatedView)
@ -98,21 +95,21 @@ class UpdatableGameFragment : LazyFragment() {
noDataContainer.reuseResetLoadTv.layoutParams = layoutParam
noDataContainer.reuseResetLoadTv.visibility = View.VISIBLE
noDataContainer.reuseNoneDataDescTv.visibility = View.VISIBLE
if (PermissionHelper.isGetInstalledListPermissionDisabled(requireContext())) {
val isGetInstalledListDisagreed = !PackageHelper.isGetInstalledPackagesApiAgreed()
val isGetInstalledListPermissionDisabled = PermissionHelper.isGetInstalledListPermissionDisabled(requireContext())
if (isGetInstalledListDisagreed || isGetInstalledListPermissionDisabled) {
noDataContainer.reuseNoneDataIv.visibility = View.GONE
noDataContainer.reuseNoneDataTv.text = "开启应用列表权限"
noDataContainer.reuseNoneDataDescTv.text = "及时获悉游戏最新的更新消息"
noDataContainer.reuseResetLoadTv.text = "去开启"
noDataContainer.reuseResetLoadTv.setOnClickListener {
PermissionHelper.requestGetInstalledAppsListPermission(
requireActivity(),
false,
object : EmptyCallback {
override fun onCallback() {
updateNoDataView()
PackageRepository.initData()
}
})
PackageHelper.showGetInstallAppsListDialogAndRequestPermissionIfNeeded(requireActivity()) { isGranted ->
if (isGranted) {
updateNoDataView()
}
}
}
} else {
noDataContainer.reuseNoneDataIv.visibility = View.VISIBLE

View File

@ -0,0 +1,23 @@
package com.gh.gamecenter.entity
import com.gh.gamecenter.common.entity.LinkEntity
import com.gh.gamecenter.home.custom.model.CustomPageData
import com.google.gson.annotations.SerializedName
data class FollowCommonContentCollection(
@SerializedName("title")
private val _title: String? = null,
@SerializedName("link")
private val _link: LinkEntity? = null,
@SerializedName("link_common_collection")
private val _linkCommonCollection: CustomPageData.CommonContentCollection? = null
) {
val title: String
get() = _title ?: ""
val link: LinkEntity
get() = _link ?: LinkEntity()
val linkCommonCollection: CustomPageData.CommonContentCollection
get() = _linkCommonCollection ?: CustomPageData.CommonContentCollection()
}

View File

@ -0,0 +1,137 @@
package com.gh.gamecenter.entity
import android.os.Parcelable
import com.gh.gamecenter.feature.entity.*
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class FollowDynamicEntity(
@SerializedName("type")
private val _type: String? = null,
@SerializedName("libao")
val libao: LibaoEntity? = null,
@SerializedName("libao_exchange")
val libaoExchange: LibaoEntity? = null,
@SerializedName("game")
private val _game: GameEntity? = null,
@SerializedName("me")
private val _me: MeEntity? = null,
@SerializedName("time")
private val _time: Long? = null,
@SerializedName("article")
val article: Article? = null,
@SerializedName("user_post")
val userPost: ArticleEntity? = null
) : Parcelable {
val type: String
get() = _type ?: ""
val game: GameEntity
get() = _game ?: GameEntity()
val me: MeEntity
get() = _me ?: MeEntity()
val time: Long
get() = _time ?: 0L
companion object {
const val FOLLOW_UPDATE_TYPE_LIBAO = "libao"
const val FOLLOW_UPDATE_TYPE_LIBAO_EXCHANGE = "libao_exchange"
const val FOLLOW_UPDATE_TYPE_ARTICLE = "article"
const val FOLLOW_UPDATE_TYPE_USER_POST = "user_post"
}
@Parcelize
data class Article(
@SerializedName("_id")
private val _id: String? = null,
@SerializedName("title")
private val _title: String? = null,
@SerializedName("content")
private val _content: String? = null,
@SerializedName("img")
private val _img: List<String>? = null,
@SerializedName("count")
private val _count: Count? = null,
@SerializedName("game")
private val _game: GameEntity? = null,
@SerializedName("time")
private val _time: Long? = null,
@SerializedName("_seq")
private val _shortId: String? = null
) : Parcelable {
val id: String
get() = _id ?: ""
val title: String
get() = _title ?: ""
val content: String
get() = _content ?: ""
val img: List<String>
get() = _img ?: listOf()
val count: Count
get() = _count ?: Count()
val game: GameEntity
get() = _game ?: GameEntity()
val time: Long
get() = _time ?: 0L
val shortId: String
get() = _shortId ?: ""
@Parcelize
data class Count(
@SerializedName("comment")
private val _comment: Int? = null
) : Parcelable {
val comment: Int
get() = _comment ?: 0
}
}
@Parcelize
data class LibaoExchange(
@SerializedName("_id")
private val _id: String? = null,
@SerializedName("libao_id")
private val _libaoId: String? = null,
@SerializedName("libao_code")
private val _libaoCode: String? = null,
@SerializedName("name")
private val _name: String? = null,
@SerializedName("content")
private val _content: String? = null,
@SerializedName("active")
private val _active: Boolean? = null
) : Parcelable {
val id: String
get() = _id ?: ""
val libaoId: String
get() = _libaoId ?: ""
val libaoCode: String
get() = _libaoCode ?: ""
val name: String
get() = _name ?: ""
val content: String
get() = _content ?: ""
val active: Boolean
get() = _active ?: false
}
}

View File

@ -0,0 +1,7 @@
package com.gh.gamecenter.entity
data class FollowOperateTopRequest(
val _id: String,
val order: Int
) {
}

View File

@ -0,0 +1,159 @@
package com.gh.gamecenter.entity
import android.os.Parcelable
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.User
import com.gh.gamecenter.feature.entity.UserEntity
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import java.util.Objects
@Parcelize
data class FollowUserEntity(
@SerializedName("_id")
private val _id: String? = null,
@SerializedName("type")
private val _type: String? = null, // user用户、bbs论坛
@SerializedName("user")
private val _user: UserEntity? = null,
@SerializedName("bbs")
private var _bbs: Bbs? = null,
@SerializedName("is_top")
private val _isTop: Int? = null,
@SerializedName("is_show_tip")
private val _isShowTip: Int? = null,
) : Parcelable {
val id: String
get() = _id ?: ""
val type: String
get() = _type ?: ""
val user: UserEntity
get() = _user ?: UserEntity()
val isTop: Boolean
get() = _isTop == 1
val isShowTip: Boolean
get() = _isShowTip == 1
val bbs: Bbs
get() = _bbs ?: Bbs()
val isUser: Boolean
get() = type == TYPE_USER
val name: String
get() = if (isUser) {
user.name ?: ""
} else {
bbs.name
}
val icon: String
get() = when {
type == TYPE_USER -> user.icon ?: ""
bbs.type == BBS_TYPE_GAME -> bbs.game.icon ?: ""
else -> bbs.icon
}
override fun equals(other: Any?): Boolean {
return other is FollowUserEntity
&& id == other.id
&& type == other.type
&& user.id == other.user.id
&& user.name == other.user.name
&& bbs.id == other.bbs.id
&& bbs.name == other.bbs.name
&& bbs.type == other.bbs.type
&& bbs.icon == other.bbs.icon
&& bbs.game.icon == other.bbs.game.icon
&& isTop == other.isTop
&& isShowTip == other.isShowTip
&& icon == other.icon
}
override fun hashCode(): Int {
return Objects.hash(
_id,
type,
user.id,
user.name,
bbs.id,
bbs.name,
bbs.type,
bbs.icon,
bbs.game.icon,
isTop,
isShowTip,
icon
)
}
companion object {
private const val TYPE_USER = "user"
private const val BBS_TYPE_GAME = "game_bbs"
}
@Parcelize
data class Bbs(
@SerializedName("_id")
private val _id: String? = null,
@SerializedName("name")
private val _name: String? = null,
@SerializedName("type")
private val _type: String? = null,
@SerializedName("icon")
private val _icon: String? = null,
@SerializedName("game")
private val _game: GameEntity? = null
) : Parcelable {
val id: String
get() = _id ?: ""
val name: String
get() = _name ?: ""
val type: String
get() = _type ?: ""
val icon: String
get() = _icon ?: ""
val game: GameEntity
get() = _game ?: GameEntity()
}
@Parcelize
data class Game(
@SerializedName("_id")
private val _id: String? = null,
@SerializedName("name")
private val _name: String? = null,
@SerializedName("icon")
private val _icon: String? = null,
@SerializedName("ori_icon")
private val _oriIcon: String? = null,
@SerializedName("active")
private val _active: Boolean? = null
) : Parcelable {
val id: String
get() = _id ?: ""
val name: String
get() = _name ?: ""
val icon: String
get() = _icon ?: ""
val oriIcon: String
get() = _oriIcon ?: ""
val active: Boolean
get() = _active ?: false
}
}

View File

@ -11,6 +11,12 @@ class NewApiSettingsEntity(
var startup: StartupAdEntity? = null,//启动文案广告
@SerializedName("user_interested_game")
var userInterestedGame: Boolean = false, //偏好设置状态开关
@SerializedName("installed_compliance_switch")
var installedComplianceSwitch: Boolean? = false, //安装合规开关
@SerializedName("listen_switch")
var isPackageObserveEnable: Boolean = false, // 安装包监听开关
@SerializedName("listen_str")
var packageObserveActions: PackageObserveActions? = null, // 安装包监听的三个 action
var install: Install, // 安装相关的
@SerializedName("game_shield_contents")
var gameShieldContents: List<String>? = listOf(),//游戏屏蔽内容
@ -46,4 +52,13 @@ class NewApiSettingsEntity(
val type: String,
val link: LinkEntity
)
class PackageObserveActions(
@SerializedName("ADD")
val add: String,
@SerializedName("REM")
val rem: String,
@SerializedName("REP")
val rep: String
)
}

View File

@ -163,6 +163,14 @@ data class PersonalHistoryEntity(
get() = community.name
set(_) {}
override fun vote(isVote: Boolean) {
_count.vote = if (isVote) {
_count.vote + 1
} else {
_count.vote - 1
}
}
fun getPassVideos(): List<CommunityVideoEntity> {
val passVideos = arrayListOf<CommunityVideoEntity>()
for (video in videos) {

View File

@ -4,10 +4,7 @@ import android.os.Parcelable
import com.alibaba.android.arouter.launcher.ARouter
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.core.provider.IConfigProvider
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.MeEntity
import com.gh.gamecenter.feature.entity.SourceEntity
import com.gh.gamecenter.feature.entity.UserEntity
import com.gh.gamecenter.feature.entity.*
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@ -62,6 +59,16 @@ open class VideoEntity(
@SerializedName("_source")
val ipSource: SourceEntity? = null,
// 关注首页新增字段
@SerializedName("choiceness")
private val _choiceness: Boolean? = null,
@SerializedName("video_info")
private val _videoInfo: VideoInfo? = null,
@SerializedName("show_me_only")
private val _showMeOnly: Boolean? = null,
@SerializedName("count")
private val _count: Count? = null,
//本地数据
@IgnoredOnParcel
var videoIsMuted: Boolean = false,//是否静音标记
@ -73,6 +80,18 @@ open class VideoEntity(
val gameName: String
get() = mGameName.removeSuffix(".")
val choiceness: Boolean
get() = _choiceness ?: false
val videoInfo: VideoInfo
get() = _videoInfo ?: VideoInfo()
val showMeOnly: Boolean
get() = _showMeOnly ?: false
val count: Count
get() = _count ?: Count()
fun getThumb(): String {
val configProvider = ARouter.getInstance().build(RouteConsts.provider.config).navigation() as? IConfigProvider
return if (!configProvider?.getVideoSnapshotSuffix().isNullOrEmpty()) {

View File

@ -0,0 +1,5 @@
package com.gh.gamecenter.entity
class WhitePackageListEntity {
var data: HashSet<String>? = null
}

View File

@ -42,6 +42,10 @@ class ForumArticleAskListAdapter(
private var mVideoOrderList = listOf("推荐", "发布")
private var mFilterPosition = if (path == "视频") 1 else 0
override fun setListData(updateData: MutableList<AnswerEntity>?) {
super.setListData(updateData)
}
override fun areItemsTheSame(oldItem: AnswerEntity?, newItem: AnswerEntity?): Boolean {
return oldItem?.id == newItem?.id
}

View File

@ -10,6 +10,9 @@ import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.common.utils.updateStatusBarColor
/**
* 论坛详情
*/
class ForumDetailActivity : BaseActivity() {
private var mContainerFragment: Fragment? = null

View File

@ -0,0 +1,86 @@
package com.gh.gamecenter.forum.home
import android.app.Activity
import com.gh.gamecenter.common.entity.AdditionalParamsEntity
import com.gh.gamecenter.common.utils.SensorsBridge
import com.gh.gamecenter.common.utils.ShareUtils
import com.gh.gamecenter.common.utils.isPublishEnv
import com.gh.gamecenter.feature.entity.ForumVideoEntity
import com.gh.gamecenter.login.user.UserManager
import com.shuyu.gsyvideoplayer.utils.OrientationUtils
class AnswerArticleVideoViewEventHelper(
private val entity: ForumVideoEntity,
private val orientationUtils: OrientationUtils
) : ArticleItemVideoView.OnArticleItemVideoViewEventListener {
override fun onTrackVideoStartPlaying() {
SensorsBridge.trackVideoStartPlaying(
articleId = entity.id,
bbsId = entity.bbs?.id ?: "",
bbsType = entity.bbs?.typeChinese ?: "综合论坛",
customerType = entity.user.auth?.text ?: "",
videoId = entity.id,
playType = "视频贴",
gameForumType = entity.bbs?.game?.categoryChinese ?: "",
activityTag = entity.tagActivityName,
articleType = entity.typeChinese
)
}
override fun onTrackVideoEndPlaying(maxPlayedProgress: Int) {
SensorsBridge.trackVideoEndPlaying(
articleId = entity.id,
bbsId = entity.bbs?.id ?: "",
bbsType = entity.bbs?.typeChinese ?: "综合论坛",
customerType = entity.user.auth?.text ?: "",
videoId = entity.id,
playType = "视频贴",
gameForumType = entity.bbs?.game?.categoryChinese ?: "",
activityTag = entity.tagActivityName,
articleType = entity.typeChinese,
result = if (maxPlayedProgress >= 95) "" else ""
)
}
override fun onShare(videoView: ArticleItemVideoView) {
val shareIcon = entity.poster
val shareUrl = if (isPublishEnv()) {
"https://m.ghzs666.com/video/${entity.id}"
} else {
"https://resource.ghzs.com/page/video_play/video/video.html?video=${entity.id}"
}
val additionalParams = AdditionalParamsEntity().apply {
contentType = "视频帖"
contentId = entity.id ?: ""
bbsId = entity.bbs?.id ?: ""
bbsType = entity.bbs?.typeChinese ?: "综合论坛"
customerType = entity.user.auth?.text ?: ""
activityTagName = entity.tagActivityName ?: ""
gameForumType = entity.bbs?.game?.categoryChinese ?: ""
refUserId = UserManager.getInstance().userId
}
val activity = (videoView.context as? Activity) ?: return
ShareUtils.getInstance(videoView.context).showShareWindowsCallback(activity,
videoView,
shareUrl,
shareIcon,
entity.title,
entity.des,
ShareUtils.ShareEntrance.video,
entity.id,
additionalParams,
object : ShareUtils.ShareCallBack {
override fun onSuccess(label: String) = Unit
override fun onCancel() = Unit
})
}
override fun onMutedChanged(isMuted: Boolean) {
entity.videoIsMuted = isMuted
}
override fun onStartWindowFullscreen() {
orientationUtils.resolveByClick()
}
}

View File

@ -1,7 +1,8 @@
package com.gh.gamecenter.forum.home
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.Surface
@ -12,11 +13,9 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import com.facebook.drawee.view.SimpleDraweeView
import com.gh.gamecenter.R
import com.gh.gamecenter.common.entity.AdditionalParamsEntity
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.utils.MD5Utils
import com.gh.gamecenter.feature.entity.ForumVideoEntity
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.video.detail.CustomManager
import com.lightgame.utils.Utils
import com.shuyu.gsyvideoplayer.utils.CommonUtil
@ -25,13 +24,12 @@ import com.shuyu.gsyvideoplayer.video.base.GSYVideoView
import com.shuyu.gsyvideoplayer.video.base.GSYVideoViewBridge
import io.reactivex.disposables.Disposable
import java.util.*
import android.os.Handler
import android.os.Looper
class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
StandardGSYVideoPlayer(context, attrs) {
private var mVideoEntity: ForumVideoEntity? = null
var data: ArticleVideoData? = null
private var mMuteDisposable: Disposable? = null
private var mIsAutoPlay = false
var uuid = UUID.randomUUID().toString()
@ -53,6 +51,8 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
private val mTrackHandler = Handler(Looper.getMainLooper())
private var onEventListener: OnArticleItemVideoViewEventListener? = null
override fun getLayoutId(): Int {
return R.layout.layout_article_item_video
}
@ -81,7 +81,7 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
fun startPlayLogic(isAutoPlay: Boolean) {
mIsAutoPlay = isAutoPlay
if (mIsAutoPlay) {
val seekTime = ForumScrollCalculatorHelper.getPlaySchedule(MD5Utils.getContentMD5(mVideoEntity?.url))
val seekTime = ForumScrollCalculatorHelper.getPlaySchedule(MD5Utils.getContentMD5(data?.url))
seekOnStart = seekTime
}
startPlayLogic()
@ -137,7 +137,7 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
}
override fun onClickUiToggle(e: MotionEvent?) {
if (mVideoEntity?.status != "pending" && mVideoEntity?.status != "fail") {
if (data?.status != "pending" && data?.status != "fail") {
if (mCurrentState == CURRENT_STATE_PLAYING) {
if (mStartButton.visibility == View.VISIBLE) {
changeUiToPlayingClear()
@ -156,7 +156,6 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
super.onSurfaceUpdated(surface)
if (mThumbImageViewLayout != null && mThumbImageViewLayout.visibility == View.VISIBLE) {
mThumbImageViewLayout.visibility = View.INVISIBLE
uploadVideoStreamingPlaying("开始播放")
}
}
@ -169,14 +168,9 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
// }
override fun releaseVideos() {
uploadVideoStreamingPlaying("结束播放")
CustomManager.releaseAllVideos(getKey())
}
override fun onVideoPause() {
super.onVideoPause()
uploadVideoStreamingPlaying("暂停播放")
}
// 重载以减少横竖屏切换的时间
override fun checkoutState() {
@ -206,9 +200,7 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
private fun showBackBtn() {
mTopContainer.background = ContextCompat.getDrawable(context, R.drawable.video_title_bg)
back.visibility = View.VISIBLE
mVideoEntity?.run {
titleTv.text = title
}
data?.title?.let(titleTv::setText)
}
private fun hideBackBtn() {
@ -217,19 +209,32 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
titleTv.text = ""
}
fun updateThumb(url: String) {
ImageUtils.display(thumbImage, url)
}
fun updateDurationTv(duration: String) {
durationTv.text = duration
}
fun updateVideoData(video: ForumVideoEntity) {
fun updateVideoData(video: ArticleVideoData, listener: OnArticleItemVideoViewEventListener) {
this.onEventListener = listener
this.mStopTrackRunnable = null
mVideoEntity = video
data = video
titleTv.text = if (mIfCurrentIsFullscreen) video.title else ""
ImageUtils.display(thumbImage, video.poster)
if (!mIfCurrentIsFullscreen) {
durationTv.text = video.duration
setVideoStatus(video.status)
fullscreenButton.setOnClickListener {
val horizontalVideoView =
startWindowFullscreen(context, true, true) as? ArticleItemVideoView
if (horizontalVideoView == null) {
toastInInternalRelease("全屏失败,请向技术人员提供具体的操作步骤")
return@setOnClickListener
}
onEventListener?.onStartWindowFullscreen()
horizontalVideoView.uuid = uuid
horizontalVideoView.updateVideoData(video, listener)
horizontalVideoView.violenceUpdateMuteStatus()
horizontalVideoView.setFullViewStatus()
}
}
}
override fun setViewShowState(view: View?, visibility: Int) {
@ -266,25 +271,16 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
override fun setStateAndUi(state: Int) {
if (state != currentState
&& state == CURRENT_STATE_PLAYING
&& currentState != CURRENT_STATE_PLAYING_BUFFERING_START) {// 上报开始视频播放埋点
&& currentState != CURRENT_STATE_PLAYING_BUFFERING_START
) {// 上报开始视频播放埋点
// 视频停止播放后再恢复播放的间隔时间未超过3秒则取消上报结束视频播放埋点
mStopTrackRunnable?.let { runnable ->
mTrackHandler.removeCallbacks(runnable)
}
mVideoEntity?.let {
data?.let {
val startTrackRunnable = Runnable {
SensorsBridge.trackVideoStartPlaying(
articleId = it.id,
bbsId = it.bbs?.id ?: "",
bbsType = it.bbs?.typeChinese ?: "综合论坛",
customerType = it.user.auth?.text ?: "",
videoId = it.id,
playType = "视频贴",
gameForumType = it.bbs?.game?.categoryChinese ?: "",
activityTag = it.tagActivityName,
articleType = it.typeChinese
)
onEventListener?.onTrackVideoStartPlaying()
mStartTrackRunnable = null
}.also {
mStartTrackRunnable = it
@ -303,20 +299,9 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
mTrackHandler.removeCallbacks(it)
mStartTrackRunnable = null
} ?: let {
mVideoEntity?.let {
data?.let {
val stopTrackRunnable = Runnable {
SensorsBridge.trackVideoEndPlaying(
articleId = it.id,
bbsId = it.bbs?.id ?: "",
bbsType = it.bbs?.typeChinese ?: "综合论坛",
customerType = it.user.auth?.text ?: "",
videoId = it.id,
playType = "视频贴",
gameForumType = it.bbs?.game?.categoryChinese ?: "",
activityTag = it.tagActivityName,
articleType = it.typeChinese,
result = if (mMaxPlayedProgress >= 95) "" else ""
)
onEventListener?.onTrackVideoEndPlaying(mMaxPlayedProgress)
}.also {
this.mStopTrackRunnable = it
}
@ -342,10 +327,9 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
replayTv.setOnClickListener {
startButton.performClick()
violenceUpdateMuteStatus()
uploadVideoStreamingPlaying("重新播放")
}
shareTv.setOnClickListener {
share()
onEventListener?.onShare(this)
}
} else {
setViewShowState(completeContainer, View.GONE)
@ -363,88 +347,33 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
}
private fun toggleMute() {
if (mVideoEntity?.videoIsMuted == true) {
unMute(true)
if (data?.videoIsMuted == true) {
unMute()
} else {
mute(true)
mute()
}
}
fun updateMuteStatus() {
if (mVideoEntity?.videoIsMuted == true) {
private fun updateMuteStatus() {
if (data?.videoIsMuted == true) {
mute()
} else {
unMute()
}
}
fun mute(isManual: Boolean = false) {
mVideoEntity?.videoIsMuted = true
private fun mute() {
onEventListener?.onMutedChanged(true)
data?.videoIsMuted = true
volume.setImageResource(R.drawable.ic_article_video_volume_off)
CustomManager.getCustomManager(getKey()).isNeedMute = true
if (isManual) {
// Utils.toast(context, "当前处于静音状态")
uploadVideoStreamingPlaying("点击静音")
}
}
fun unMute(isManual: Boolean = false) {
mVideoEntity?.videoIsMuted = false
private fun unMute() {
onEventListener?.onMutedChanged(false)
data?.videoIsMuted = false
volume.setImageResource(R.drawable.ic_article_video_volume_on)
CustomManager.getCustomManager(getKey()).isNeedMute = false
if (isManual) {
uploadVideoStreamingPlaying("取消静音")
}
}
override fun onAutoCompletion() {
super.onAutoCompletion()
uploadVideoStreamingPlaying("播放完毕")
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
super.onStopTrackingTouch(seekBar)
uploadVideoStreamingPlaying("拖动")
}
private fun share() {
mVideoEntity?.let {
val shareIcon = it.poster
val shareUrl = if (isPublishEnv()) {
"https://m.ghzs666.com/video/${it.id}"
} else {
"https://dev-and-static.ghzs66.com/page/video_play/video/video.html?video=${it.id}"
}
val additionalParams = AdditionalParamsEntity().apply {
contentType = "视频帖"
contentId = mVideoEntity?.id ?: ""
bbsId = mVideoEntity?.bbs?.id ?: ""
bbsType = mVideoEntity?.bbs?.typeChinese ?: "综合论坛"
customerType = mVideoEntity?.user?.auth?.text ?: ""
activityTagName = mVideoEntity?.tagActivityName ?: ""
gameForumType = mVideoEntity?.bbs?.game?.categoryChinese ?: ""
refUserId = UserManager.getInstance().userId
}
ShareUtils.getInstance(context).showShareWindowsCallback(context as Activity,
this,
shareUrl,
shareIcon,
it.title,
it.des,
ShareUtils.ShareEntrance.video, it.id, additionalParams, object : ShareUtils.ShareCallBack {
override fun onSuccess(label: String) {
// if ("短信" == label || "复制链接" == label) viewModel?.shareVideoStatistics(it)
}
override fun onCancel() {
uploadVideoStreamingPlaying("取消分享")
}
})
}
}
fun uploadVideoStreamingPlaying(action: String) {
}
fun setFullViewStatus() {
@ -456,7 +385,7 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
durationTv.visibility = View.GONE
}
fun setVideoStatus(status: String) {
private fun setVideoStatus(status: String) {
if (status == "pending" || status == "fail") {
pendingView.text = if (status == "pending") "审核中...请耐心等待" else "审核不通过"
pendingView.visibility = View.VISIBLE
@ -494,4 +423,32 @@ class ArticleItemVideoView @JvmOverloads constructor(context: Context, attrs: At
super.onDetachedFromWindow()
mMuteDisposable?.dispose()
}
companion object {
fun ForumVideoEntity.toArticleVideoData() =
ArticleVideoData(url, title, status, poster, duration, videoIsMuted)
}
data class ArticleVideoData(
val url: String,
val title: String,
val status: String,
val poster: String,
val duration: String,
var videoIsMuted: Boolean
)
interface OnArticleItemVideoViewEventListener {
fun onTrackVideoStartPlaying()
fun onTrackVideoEndPlaying(maxPlayedProgress: Int)
fun onShare(videoView: ArticleItemVideoView)
fun onMutedChanged(isMuted: Boolean)
fun onStartWindowFullscreen()
}
}

View File

@ -8,6 +8,9 @@ import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.common.constant.EntranceConsts.IS_DETAIL_PAGE
import com.gh.gamecenter.video.detail.HomeVideoFragment
/**
* 社区首页
*/
class CommunityActivity : BaseActivity() {
override fun getLayoutId(): Int = R.layout.activity_community

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.ColorFilter
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
@ -11,17 +12,20 @@ import android.view.*
import android.view.animation.AnimationUtils
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.graphics.ColorUtils
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.viewpager.widget.ViewPager
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.SimpleColorFilter
import com.airbnb.lottie.model.KeyPath
import com.airbnb.lottie.value.LottieValueCallback
import com.gh.common.browse.BrowseTimer
import com.gh.common.browse.withLifecycle
import com.gh.common.util.NewFlatLogUtils
import com.gh.common.util.DirectUtils
import com.gh.common.util.NewFlatLogUtils
import com.gh.common.util.NewLogUtils
import com.gh.common.util.ViewPagerFragmentHelper
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.TrackableDialog
import com.gh.gamecenter.common.base.adapter.FragmentAdapter
@ -32,6 +36,7 @@ import com.gh.gamecenter.common.constant.EntranceConsts.IS_DETAIL_PAGE
import com.gh.gamecenter.common.retrofit.ApiResponse
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.common.view.AvatarBorderView
import com.gh.gamecenter.core.iinterface.IScrollable
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.core.utils.MtaHelper
import com.gh.gamecenter.core.utils.SPUtils
@ -43,8 +48,11 @@ import com.gh.gamecenter.eventbus.EBSkip
import com.gh.gamecenter.eventbus.EBTypeChange
import com.gh.gamecenter.feature.entity.ArticleEntity
import com.gh.gamecenter.feature.entity.ForumVideoEntity
import com.gh.gamecenter.forum.home.follow.FollowHomeFilterPopWindow
import com.gh.gamecenter.forum.home.follow.fragment.FollowHomeFragment
import com.gh.gamecenter.forum.search.ForumOrUserSearchActivity
import com.gh.gamecenter.login.entity.UserInfoEntity
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.login.user.UserRepository
import com.gh.gamecenter.qa.article.detail.ArticleDetailWebCacheManager
import com.gh.gamecenter.qa.article.edit.ArticleEditActivity
@ -54,6 +62,7 @@ import com.gh.gamecenter.qa.video.publish.VideoPublishActivity
import com.gh.gamecenter.video.detail.VideoDetailContainerViewModel
import com.gh.gamecenter.wrapper.MainWrapperViewModel
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.halo.assistant.HaloApp
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -66,7 +75,7 @@ class CommunityHomeFragment : LazyFragment() {
private var mViewModel: CommunityHomeViewModel? = null
private var mMainWrapperViewModel: MainWrapperViewModel? = null
private var mFragmentList = arrayListOf<Fragment>()
private var mTitleList = arrayListOf("推荐", "论坛", "活动")
private var mTitleList = arrayListOf("关注", "推荐", "论坛", "活动")
private var mTabList = arrayListOf<Any>()
private var mDefaultSelectedTab = -1
private var mNavigationBitmap: Bitmap? = null
@ -74,11 +83,46 @@ class CommunityHomeFragment : LazyFragment() {
private var mBottomTabId = ""
private val browseTimer = BrowseTimer()
.withLifecycle(this)
.withResult {
SensorsBridge.trackCommunityBrowsingDuration(it / 1000.0)
}
private val followFilterPopWindow by lazy {
FollowHomeFilterPopWindow.create(requireContext()).apply {
setOnDismissListener {
resetFollowTab()
}
setFilterListener(object : FollowHomeFilterPopWindow.OnFilteredChangedListener {
override fun filterFollowed(position: Int) {
mViewModel?.filterFollowed(position)
}
})
}
}
private val obTabSelectedListener = object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
if (tab?.position == TAB_FOLLOW_INDEX) {
mViewModel?.updateFilterResId(R.drawable.ic_follow_arrow_down)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
if (tab?.position == TAB_FOLLOW_INDEX) {
mViewModel?.updateFilterResId(null)
}
}
override fun onTabReselected(tab: TabLayout.Tab?) {
if (mViewModel?.followFilterStatus?.value == R.drawable.ic_follow_arrow_down) {
mViewModel?.updateFilterResId(R.drawable.ic_follow_arrow_up)
mBinding?.tabLayout?.let(followFilterPopWindow::showAsDropDown)
}
}
}
override fun getRealLayoutId(): Int {
return R.layout.fragment_community_home
}
@ -97,6 +141,19 @@ class CommunityHomeFragment : LazyFragment() {
super.onFragmentFirstVisible()
initViewPager()
mViewModel?.followFilterStatus?.observe(viewLifecycleOwner, Observer { resId ->
val tabLayout = mBinding?.tabLayout ?: return@Observer
val tab = tabLayout.getTabAt(TAB_FOLLOW_INDEX)
val tvTitle = tab?.customView?.findViewById<TextView>(R.id.tab_title)
if (resId != null) {
tvTitle?.setDrawableEnd(resId, 8F.dip2px(), 8F.dip2px())
} else {
tvTitle?.setDrawableEnd(null, 8F.dip2px(), 8F.dip2px())
}
})
}
override fun initRealView() {
@ -147,7 +204,13 @@ class CommunityHomeFragment : LazyFragment() {
topBg.visibleIf(!mIsDarkModeOn)
videoLottie.setOnClickListener {
DirectUtils.directToLegacyVideoDetail(requireContext(), "", VideoDetailContainerViewModel.Location.VIDEO_ACTIVITY.value, referer = "视频流-社区右上角", isHomeVideo = true)
DirectUtils.directToLegacyVideoDetail(
requireContext(),
"",
VideoDetailContainerViewModel.Location.VIDEO_ACTIVITY.value,
referer = "视频流-社区右上角",
isHomeVideo = true
)
}
searchIconIv.setOnClickListener {
NewLogUtils.logCommunitySearchClick()
@ -202,13 +265,14 @@ class CommunityHomeFragment : LazyFragment() {
mBottomTabId = arguments?.getString(EntranceConsts.KEY_BOTTOM_TAB_ID, "") ?: ""
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
browseTimer.start()
} else {
browseTimer.stop()
}
override fun onResume() {
super.onResume()
browseTimer.start()
}
override fun onPause() {
super.onPause()
browseTimer.stop()
}
override fun onSaveInstanceState(outState: Bundle) {
@ -236,7 +300,12 @@ class CommunityHomeFragment : LazyFragment() {
mTabList.clear()
mFragmentList.clear()
val tag = "android:switcher:${viewPager.id}:"
val forumArticleListFragment = childFragmentManager.findFragmentByTag("${tag}0")
val followFragment = childFragmentManager.findFragmentByTag("$tag$TAB_FOLLOW_INDEX")
?: FollowHomeFragment()
mFragmentList.add(followFragment)
val forumArticleListFragment = childFragmentManager.findFragmentByTag("$tag$TAB_RECOMMEND_INDEX")
?: ForumArticleListFragment().with(
bundleOf(
EntranceConsts.KEY_ENTRANCE to "社区",
@ -245,11 +314,11 @@ class CommunityHomeFragment : LazyFragment() {
)
mFragmentList.add(forumArticleListFragment)
val forumFragment = childFragmentManager.findFragmentByTag("${tag}1")
val forumFragment = childFragmentManager.findFragmentByTag("${tag}$TAB_FORUM_INDEX")
?: ForumFragment().with(bundleOf(EntranceConsts.KEY_ENTRANCE to "社区"))
mFragmentList.add(forumFragment)
val activityFragment = childFragmentManager.findFragmentByTag("${tag}2")
val activityFragment = childFragmentManager.findFragmentByTag("${tag}$TAB_ACTIVITY_INDEX")
?: ForumActivityFragment().with(bundleOf(EntranceConsts.KEY_ENTRANCE to "活动"))
mFragmentList.add(activityFragment)
@ -263,8 +332,16 @@ class CommunityHomeFragment : LazyFragment() {
adapter = FragmentAdapter(childFragmentManager, mFragmentList, mTitleList)
doOnScroll(
onPageSelected = { position ->
communityEditBtn.goneIf(position != 0)
communityEditBtn.goneIf(position != TAB_RECOMMEND_INDEX && position != TAB_FOLLOW_INDEX)
when (position) {
TAB_FOLLOW_INDEX -> {
root.setBackgroundColor(R.color.ui_background.toColor(requireContext()))
topBg.translationY = 0F
changeNavigationBg()
NewLogUtils.logCommunityHomeEvent("click_for_you_tab")
SensorsBridge.trackCommunityTopTabSelected("关注")
}
TAB_RECOMMEND_INDEX -> {
root.setBackgroundColor(R.color.ui_background.toColor(requireContext()))
topBg.translationY = 0F
@ -275,7 +352,7 @@ class CommunityHomeFragment : LazyFragment() {
TAB_FORUM_INDEX -> {
root.setBackgroundColor(R.color.ui_surface.toColor(requireContext()))
(mFragmentList[1] as ForumFragment).translationY.run {
(mFragmentList[2] as ForumFragment).translationY.run {
topBg.translationY = -this.toFloat()
changeNavigationBg(this)
}
@ -285,7 +362,7 @@ class CommunityHomeFragment : LazyFragment() {
TAB_ACTIVITY_INDEX -> {
root.setBackgroundColor(R.color.ui_background.toColor(requireContext()))
(mFragmentList[2] as ForumActivityFragment).translationY.run {
(mFragmentList[3] as ForumActivityFragment).translationY.run {
topBg.translationY = -this.toFloat()
changeNavigationBg(this)
}
@ -297,8 +374,6 @@ class CommunityHomeFragment : LazyFragment() {
onPageScrolled = { position, positionOffset, _ ->
if (position + 1 != mTabList.size) {
(mTabList[position] as TextView).run {
layoutParams.width =
(DEFAULT_TAB_TEXT_WIDTH + ((1 - positionOffset) * 4F.dip2px())).roundToInt()
textSize = (DEFAULT_TAB_TEXT_SIZE + ((1 - positionOffset) * 4)).roundTo(1)
setTextColor(
ColorUtils.blendARGB(
@ -310,8 +385,6 @@ class CommunityHomeFragment : LazyFragment() {
}
if (mTabList[position + 1] is TextView) {
(mTabList[position + 1] as TextView).run {
layoutParams.width =
(DEFAULT_TAB_TEXT_WIDTH + ((positionOffset) * 4F.dip2px())).roundToInt()
textSize = (DEFAULT_TAB_TEXT_SIZE + ((positionOffset) * 4)).roundTo(1)
setTextColor(
ColorUtils.blendARGB(
@ -323,13 +396,12 @@ class CommunityHomeFragment : LazyFragment() {
}
} else {
(mTabList[position + 1] as TabItemCommunityBinding).run {
tabTitle.layoutParams.width =
(DEFAULT_TAB_TEXT_WIDTH + ((positionOffset) * 4F.dip2px())).roundToInt()
tabTitle.textSize = (DEFAULT_TAB_TEXT_SIZE + ((positionOffset) * 4)).roundTo(1)
tabImg.scaleX =
(DEFAULT_TAB_IMG_WIDTH + ((positionOffset) * 8)).roundTo(1) / DEFAULT_TAB_IMG_WIDTH
tabImg.scaleY =
(DEFAULT_TAB_IMG_HEIGHT + ((positionOffset) * 4)).roundTo(1) / DEFAULT_TAB_IMG_HEIGHT
val layoutParams = tabImg.layoutParams
layoutParams.width =
(DEFAULT_TAB_IMG_WIDTH + 8F.dip2px() * positionOffset).roundToInt()
layoutParams.height =
((DEFAULT_TAB_IMG_HEIGHT) + 4F.dip2px() * positionOffset).roundToInt()
tabImg.layoutParams = layoutParams
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tabImg.imageTintList =
ColorStateList.valueOf(
@ -340,6 +412,7 @@ class CommunityHomeFragment : LazyFragment() {
)
)
}
}
}
@ -358,20 +431,26 @@ class CommunityHomeFragment : LazyFragment() {
}
)
}
tabLayout.addOnTabSelectedListener(obTabSelectedListener)
tabLayout.setupWithViewPager(viewPager)
indicatorView.run {
setupWithTabLayout(tabLayout)
setupWithViewPager(viewPager)
setIndicatorWidth(18)
}
val selectedPosition = if (UserManager.getInstance().isLoggedIn) {
TAB_FOLLOW_INDEX
} else {
TAB_RECOMMEND_INDEX
}
for (i in 0 until tabLayout.tabCount) {
val tab = tabLayout.getTabAt(i) ?: continue
val tabTitle = if (tab.text != null) tab.text.toString() else ""
val tabViewBinding = generateTabView(tabTitle, i)
val tabViewBinding = generateTabView(tabTitle, i, selectedPosition)
tab.customView = tabViewBinding.root
tab.view.setPadding(0, 0, 0, 0)
}
viewPager.setCurrentItem(selectedPosition, false)
}
}
@ -394,7 +473,6 @@ class CommunityHomeFragment : LazyFragment() {
if (index == selectedPosition) {
if (positionOffset == 0F) {
titleView.setTextColor(TAB_SELECTED_COLOR.toColor(requireContext()))
titleView.setTypeface(null, Typeface.NORMAL)
titleView.setTypeface(titleView.typeface, Typeface.BOLD)
} else if (positionOffset > 0F) {
titleView.setTypeface(null, Typeface.NORMAL)
@ -428,9 +506,6 @@ class CommunityHomeFragment : LazyFragment() {
} else {
tabImg.setImageResource(R.drawable.ic_tab_activity_active)
}
tabTitle.layoutParams.width = 64F.dip2px()
tabImg.scaleX = SCALE_TAB_IMG_WIDTH / DEFAULT_TAB_IMG_WIDTH
tabImg.scaleY = SCALE_TAB_IMG_HEIGHT / DEFAULT_TAB_IMG_HEIGHT
}
}
}
@ -444,30 +519,22 @@ class CommunityHomeFragment : LazyFragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tabImg.imageTintList = ColorStateList.valueOf(TAB_DEFAULT_COLOR.toColor(requireContext()))
}
tabTitle.layoutParams.width = DEFAULT_TAB_TEXT_WIDTH
tabImg.scaleX = 1F
tabImg.scaleY = 1F
}
}
}
private fun generateTabView(title: String, index: Int): TabItemCommunityBinding {
private fun generateTabView(title: String, index: Int, selectedPosition: Int): TabItemCommunityBinding {
val binding = TabItemCommunityBinding.inflate(LayoutInflater.from(requireContext()))
binding.run {
if (index == TAB_ACTIVITY_INDEX) {
mTabList.add(binding)
tabTitle.visibility = View.INVISIBLE
tabTitle.visibility = View.GONE
tabImg.visibility = View.VISIBLE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
tabImg.setImageResource(R.drawable.ic_tab_activity_svg)
} else {
tabImg.setImageResource(R.drawable.ic_tab_activity_default)
}
tabTitle.run {
text = title
// textSize = DEFAULT_TAB_TEXT_SIZE
// setTextColor(TAB_DEFAULT_COLOR.toColor(requireContext()))
}
} else {
mTabList.add(tabTitle)
tabTitle.visibility = View.VISIBLE
@ -476,13 +543,12 @@ class CommunityHomeFragment : LazyFragment() {
text = title
textSize = DEFAULT_TAB_TEXT_SIZE
setTextColor(TAB_DEFAULT_COLOR.toColor(requireContext()))
if (index == TAB_FOLLOW_INDEX && selectedPosition == TAB_FOLLOW_INDEX) {
mViewModel?.updateFilterResId(R.drawable.ic_follow_arrow_down)
}
}
}
}
// binding.invisibleTabTitle.run {
// text = title
// textSize = DEFAULT_TAB_TEXT_SIZE
// }
return binding
}
@ -570,6 +636,18 @@ class CommunityHomeFragment : LazyFragment() {
}
}
private fun resetFollowTab() {
mBinding?.tabLayout?.run {
if (selectedTabPosition == TAB_FOLLOW_INDEX) {
mViewModel?.updateFilterResId(R.drawable.ic_follow_arrow_down)
} else {
mViewModel?.updateFilterResId(null)
}
}
mBinding?.tabLayout?.removeOnTabSelectedListener(obTabSelectedListener)
mBinding?.tabLayout?.addOnTabSelectedListener(obTabSelectedListener)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
@ -596,13 +674,13 @@ class CommunityHomeFragment : LazyFragment() {
}
private fun insertDataToRecommendTab(entity: ArticleEntity) {
(mFragmentList[0] as? ForumArticleListFragment)?.insertDataToFirstIndex(entity)
(mFragmentList[TAB_RECOMMEND_INDEX] as? ForumArticleListFragment)?.insertDataToFirstIndex(entity)
}
override fun onBackPressed(): Boolean {
mBinding?.viewPager?.run {
if (currentItem == 0) {
return (mFragmentList[0] as ForumArticleListFragment).onBackPressed()
if (currentItem == 1) {
return (mFragmentList[1] as ForumArticleListFragment).onBackPressed()
}
}
return super.onBackPressed()
@ -610,7 +688,10 @@ class CommunityHomeFragment : LazyFragment() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(status: EBTypeChange) {
if (mBinding?.viewPager?.currentItem != 0) return
val currentPosition = mBinding?.viewPager?.currentItem ?: 0
if (currentPosition != TAB_FOLLOW_INDEX && currentPosition != TAB_RECOMMEND_INDEX) {
return
}
if (status.type == EB_SHOW_QUESTION_BUTTON) {
setPutQuestionButtonStatus(View.VISIBLE)
@ -636,8 +717,14 @@ class CommunityHomeFragment : LazyFragment() {
private fun scrollToTop() {
if (mFragmentList.isEmpty()) return
(mFragmentList[0] as ForumArticleListFragment).scrollToTop()
setPutQuestionButtonStatus(View.VISIBLE)
val currentPosition = mBinding?.viewPager?.currentItem ?: 0
val fragment = mFragmentList.getOrNull(currentPosition)
if (fragment is IScrollable) {
fragment.scrollToTop()
}
if (currentPosition == TAB_FOLLOW_INDEX || currentPosition == TAB_RECOMMEND_INDEX) {
setPutQuestionButtonStatus(View.VISIBLE)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -671,7 +758,7 @@ class CommunityHomeFragment : LazyFragment() {
} else if (!mIsDarkModeOn && navigationBg.drawable != null) {
navigationBg.setBackgroundColor(R.color.transparent.toColor(requireContext()))
navigationBg.setImageDrawable(null)
} else if (viewPager.currentItem == TAB_RECOMMEND_INDEX) {
} else if (viewPager.currentItem == TAB_RECOMMEND_INDEX || viewPager.currentItem == TAB_FOLLOW_INDEX) {
navigationBg.setBackgroundColor(
if (mIsDarkModeOn && y > 0) R.color.ui_surface.toColor(requireContext()) else if (mIsDarkModeOn && y == 0) R.color.ui_background.toColor(
requireContext()
@ -692,7 +779,20 @@ class CommunityHomeFragment : LazyFragment() {
for (i in 0 until tabCount) {
val tab: TabLayout.Tab? = getTabAt(i)
if (tab != null) {
updateTabStyle(mBinding?.viewPager?.currentItem ?: 0, 0f)
val tvTitle = tab.customView?.findViewById<TextView>(R.id.tab_title)
if (tvTitle != null && tvTitle.visibility == View.VISIBLE) {
tvTitle.post {
if (i == TAB_FOLLOW_INDEX && tab.isSelected) {
val resId = mViewModel?.followFilterStatus?.value
if (resId != null) {
mViewModel?.updateFilterResId(resId)
}
}
tvTitle.setTextColor(
ColorStateList.valueOf(R.color.text_primary.toColor(requireContext()))
)
}
}
}
}
}
@ -717,7 +817,21 @@ class CommunityHomeFragment : LazyFragment() {
}
}
}
searchIconIv.setImageResource(R.drawable.ic_column_search)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
searchIconIv.imageTintList = ColorStateList.valueOf(R.color.text_primary.toColor(requireContext()))
}
val csl = AppCompatResources.getColorStateList(requireContext(), R.color.text_primary)
val filter = SimpleColorFilter(csl.defaultColor)
val keyPath = KeyPath("**")
val callback = LottieValueCallback<ColorFilter>(filter)
videoLottie.addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback)
}
followFilterPopWindow.onDarkModeChanged()
}
fun getTopBgView() = mBinding?.topBg
@ -725,16 +839,14 @@ class CommunityHomeFragment : LazyFragment() {
companion object {
var TAB_SELECTED_COLOR: Int = R.color.text_primary
var TAB_DEFAULT_COLOR: Int = R.color.community_forum_more
var DEFAULT_TAB_TEXT_SIZE = 18F
var DEFAULT_TAB_TEXT_WIDTH = 60F.dip2px()
var DEFAULT_TAB_IMG_WIDTH = 34F
var DEFAULT_TAB_IMG_HEIGHT = 18F
var SCALE_TAB_IMG_WIDTH = 42F
var SCALE_TAB_IMG_HEIGHT = 22F
var DEFAULT_TAB_TEXT_SIZE = 16F
var DEFAULT_TAB_IMG_WIDTH = 34F.dip2px()
var DEFAULT_TAB_IMG_HEIGHT = 16F.dip2px()
const val EB_TAB = "forum_tab"
const val TAB_RECOMMEND_INDEX = 0
const val TAB_FORUM_INDEX = 1
const val TAB_ACTIVITY_INDEX = 2
const val TAB_FOLLOW_INDEX = 0
const val TAB_RECOMMEND_INDEX = 1
const val TAB_FORUM_INDEX = 2
const val TAB_ACTIVITY_INDEX = 3
const val ARTICLE_REQUEST_CODE = 200
const val QUESTION_REQUEST_CODE = 201
const val VIDEO_REQUEST_CODE = 202

View File

@ -1,19 +1,20 @@
package com.gh.gamecenter.forum.home
import android.app.Application
import androidx.lifecycle.*
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import com.gh.gamecenter.common.entity.CommunityEntity
import com.gh.gamecenter.common.utils.clearHtmlFormatCompletely
import com.gh.gamecenter.common.utils.observableToMain
import com.gh.gamecenter.common.utils.removeInsertedContent
import com.gh.gamecenter.common.utils.removeVideoContent
import com.gh.gamecenter.qa.entity.ArticleDetailEntity
import com.gh.gamecenter.feature.entity.ArticleEntity
import com.gh.gamecenter.qa.entity.QuestionsDetailEntity
import com.gh.gamecenter.feature.entity.TimeEntity
import com.gh.gamecenter.common.retrofit.Response
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.feature.entity.ArticleEntity
import com.gh.gamecenter.feature.entity.ForumVideoEntity
import com.gh.gamecenter.feature.entity.TimeEntity
import com.gh.gamecenter.feature.entity.UserEntity
import com.gh.gamecenter.livedata.Event
import com.gh.gamecenter.qa.entity.ArticleDetailEntity
import com.gh.gamecenter.qa.entity.QuestionsDetailEntity
import com.gh.gamecenter.retrofit.RetrofitManager
class CommunityHomeViewModel(application: Application) : AndroidViewModel(application) {
@ -131,4 +132,64 @@ class CommunityHomeViewModel(application: Application) : AndroidViewModel(applic
return articleEntity
}
private val _hasFollowedUsers = MutableLiveData<Boolean>()
val hasFollowedUser: LiveData<Boolean> = _hasFollowedUsers
fun updateHasFollowUser(show: Boolean) {
_hasFollowedUsers.value = show
}
private val filterDrawableResId = MutableLiveData<Int?>()
fun updateFilterResId(resId: Int?) {
filterDrawableResId.value = resId
}
val followFilterStatus = MediatorLiveData<Int?>().apply {
addSource(hasFollowedUser) {
if (it) {
value = filterDrawableResId.value
} else {
value = null
}
}
addSource(filterDrawableResId) { resId ->
// 当 hasFollowedUser.value == null 时不需要调用setValue
if (hasFollowedUser.value == true) {
value = resId
} else if (hasFollowedUser.value == false) {
value = null
}
when {
resId == null ||
hasFollowedUser.value == false -> {
// 当resId == null 时表示已切换到别的tab 直接setValue
// 当 resId != null && hasFollowedUser,value 为false时说明关注页数据已加载完毕并且没有关注任何用户直接setValue
value = null
}
hasFollowedUser.value == true -> {
// 当 resId = null 且 有关注用户,显示当前 resId
value = resId
}
else -> {
// do Nothing 当resId != null 但是hasFollowedUser.value == null,说明关注页数据正在loading等待无需setValue
}
}
}
}
private val _filterFollowedAction = MutableLiveData<Event<Int>>()
val filterFollowedAction: LiveData<Event<Int>> = _filterFollowedAction
fun filterFollowed(position: Int) {
val filterName = when (position) {
0 -> "全部"
1 -> "关注的人"
else -> "关注的游戏"
}
SensorsBridge.trackFollowTabFilterOptionClick(filterName)
_filterFollowedAction.value = Event(position)
}
}

View File

@ -13,6 +13,7 @@ import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.common.view.SpacingItemDecoration
import com.gh.gamecenter.core.iinterface.IScrollable
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.databinding.FragmentForumActivityBinding
import com.gh.gamecenter.databinding.LayoutForumActivityCategoryItemBinding
@ -20,7 +21,7 @@ import com.gh.gamecenter.entity.ForumActivityCategoryEntity
import com.gh.gamecenter.entity.ForumActivityEntity
import com.google.android.flexbox.FlexboxLayout
class ForumActivityFragment : LazyListFragment<ForumActivityEntity, ForumActivityViewModel>() {
class ForumActivityFragment : LazyListFragment<ForumActivityEntity, ForumActivityViewModel>(), IScrollable {
private var mBinding: FragmentForumActivityBinding? = null
private var mAdapter: ForumActivityAdapter? = null
@ -200,4 +201,12 @@ class ForumActivityFragment : LazyListFragment<ForumActivityEntity, ForumActivit
}
changeCategoryBg()
}
override fun scrollToTop() {
mBinding?.listRv?.let {
if (it.visibility == View.VISIBLE) {
it.scrollToPosition(0)
}
}
}
}

View File

@ -6,13 +6,18 @@ import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.text.SpannableStringBuilder
import android.view.View
import com.facebook.drawee.view.SimpleDraweeView
import com.gh.common.util.*
import com.gh.common.util.DialogUtils
import com.gh.common.util.LogUtils
import com.gh.common.util.NewLogUtils
import com.gh.common.view.ImageContainerView
import com.gh.common.view.ImageContainerView.Companion.toImageContainerData
import com.gh.gamecenter.ImageViewerActivity
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.common.callback.ConfirmListener
import com.gh.gamecenter.common.entity.AdditionalParamsEntity
import com.gh.gamecenter.common.entity.CommunityEntity
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.utils.MtaHelper
@ -28,6 +33,7 @@ import com.gh.gamecenter.qa.article.detail.ArticleDetailActivity
import com.gh.gamecenter.feature.entity.AnswerEntity
import com.gh.gamecenter.feature.entity.ArticleEntity
import com.gh.gamecenter.feature.entity.CommunityItemData
import com.gh.gamecenter.forum.home.ArticleItemVideoView.Companion.toArticleVideoData
import com.gh.gamecenter.forum.search.CommunitySearchEventListener
import com.gh.gamecenter.forum.search.CommunitySearchEventListener.Companion.SEARCH_BUTTON_COMMENT
import com.gh.gamecenter.forum.search.CommunitySearchEventListener.Companion.SEARCH_BUTTON_DISLIKE
@ -42,6 +48,7 @@ import com.gh.gamecenter.qa.entity.QuestionsDetailEntity
import com.gh.gamecenter.qa.questions.invite.QuestionsInviteActivity
import com.gh.gamecenter.qa.questions.newdetail.NewQuestionDetailActivity
import com.gh.gamecenter.qa.video.detail.ForumVideoDetailActivity
import com.gh.gamecenter.video.detail.VideoDetailContainerViewModel
import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder
import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack
import com.shuyu.gsyvideoplayer.utils.OrientationUtils
@ -148,15 +155,46 @@ class ForumArticleAskItemViewHolder(
}
}
binding.imageContainer.bindData(entity, entrance, path) {
clickListener?.onItemChildClick(
entity.id,
entity.type,
htmlToString(entity.questions.title),
position,
SEARCH_BUTTON_VIEW_IMAGE
)
}
binding.imageContainer.bindData(
entity.toImageContainerData(),
object : ImageContainerView.OnImageContainerEventListener {
override fun onImageClick(
images: List<String>,
position: Int,
imageViewList: ArrayList<SimpleDraweeView>,
) {
clickListener?.onItemChildClick(
entity.id,
entity.type,
htmlToString(entity.questions.title),
position,
SEARCH_BUTTON_VIEW_IMAGE,
)
if (entity.communityId.isNullOrEmpty()) {
entity.communityId = entity.bbs.id
}
val intent = ImageViewerActivity.getIntent(
itemView.context, entity.images as ArrayList<String>, position, imageViewList,
if (entity.type == "community_article") entity else null, entrance, true,
)
itemView.context.startActivity(intent)
}
override fun onVideoCLick(videoId: String) {
DirectUtils.directToVideoDetail(
itemView.context,
videoId,
VideoDetailContainerViewModel.Location.VIDEO_HOT.value,
showComment = false,
entrance = entrance,
path = path
)
}
},
)
bindVideoData(entity.transformForumVideoEntity())
val user = entity.user
@ -315,34 +353,13 @@ class ForumArticleAskItemViewHolder(
.setVideoAllCallBack(object : GSYSampleCallBack() {
override fun onQuitFullscreen(url: String?, vararg objects: Any) {
orientationUtils.backToProtVideo()
visibleView.uploadVideoStreamingPlaying("退出全屏")
}
})
.build(visibleView)
visibleView.run {
updateVideoData(entity)
updateThumb(entity.poster)
updateDurationTv(entity.duration)
setVideoStatus(entity.status)
fullscreenButton.setOnClickListener {
val horizontalVideoView =
startWindowFullscreen(itemView.context, true, true) as? ArticleItemVideoView
if (horizontalVideoView == null) {
toastInInternalRelease("全屏失败,请向技术人员提供具体的操作步骤")
return@setOnClickListener
}
orientationUtils.resolveByClick()
horizontalVideoView.uuid = uuid
horizontalVideoView.updateVideoData(entity)
horizontalVideoView.updateThumb(entity.poster)
horizontalVideoView.violenceUpdateMuteStatus()
horizontalVideoView.setFullViewStatus()
uploadVideoStreamingPlaying("开始播放")
uploadVideoStreamingPlaying("点击全屏")
}
}
visibleView.updateVideoData(
entity.toArticleVideoData(),
AnswerArticleVideoViewEventHelper(entity, orientationUtils)
)
}
}
}
@ -382,7 +399,13 @@ class ForumArticleAskItemViewHolder(
SEARCH_BUTTON_VIEW_FORUM_DETAIL
)
MtaHelper.onEvent(getEventId(entrance), getKey(entrance), entity.community.name)
itemView.context.startActivity(ForumDetailActivity.getIntent(itemView.context, entity.community.id, entrance))
itemView.context.startActivity(
ForumDetailActivity.getIntent(
itemView.context,
entity.community.id,
entrance
)
)
LogUtils.uploadAccessToBbs(entity.community.id, "文章外所属论坛")
}

View File

@ -22,6 +22,7 @@ import com.gh.gamecenter.common.base.fragment.LazyFragment
import com.gh.gamecenter.common.constant.Constants
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.common.view.GridSpacingItemColorDecoration
import com.gh.gamecenter.core.iinterface.IScrollable
import com.gh.gamecenter.databinding.ForumBannerIndicatorItemBinding
import com.gh.gamecenter.databinding.FragmentForumBinding
import com.gh.gamecenter.entity.ForumBannerEntity
@ -34,7 +35,7 @@ import com.halo.assistant.HaloApp
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ForumFragment : LazyFragment(), SwipeRefreshLayout.OnRefreshListener {
class ForumFragment : LazyFragment(), SwipeRefreshLayout.OnRefreshListener, IScrollable {
private var mBinding: FragmentForumBinding? = null
private var mViewModel: ForumViewModel? = null
@ -513,4 +514,12 @@ class ForumFragment : LazyFragment(), SwipeRefreshLayout.OnRefreshListener {
super.onDarkModeChanged()
mBinding?.hotForumRv?.adapter?.let { it.notifyItemRangeChanged(0, it.itemCount) }
}
override fun scrollToTop() {
mBinding?.contentContainer?.let {
if (it.visibility == View.VISIBLE) {
it.scrollTo(0, 0)
}
}
}
}

View File

@ -0,0 +1,31 @@
package com.gh.gamecenter.forum.home.follow
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.forum.home.follow.fragment.AllFollowedFragment
class AllFollowedActivity : BaseActivity() {
override fun getLayoutId(): Int = R.layout.activity_all_followed
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DisplayUtils.setLightStatusBar(this, true)
setStatusBarColor(Color.TRANSPARENT)
val containerFragment = supportFragmentManager.findFragmentByTag(AllFollowedFragment::class.java.name)
?: AllFollowedFragment().with(intent.extras)
// 若 placeholder 外层为 RelativeLayout 的话,会出现莫名的偏移
supportFragmentManager.beginTransaction()
.replace(R.id.layout_activity_content, containerFragment, AllFollowedFragment::class.java.name)
.commitAllowingStateLoss()
}
companion object {
fun getIntent(context: Context): Intent = Intent(context, AllFollowedActivity::class.java)
}
}

View File

@ -0,0 +1,50 @@
package com.gh.gamecenter.forum.home.follow
import com.gh.gamecenter.entity.FollowUserEntity
abstract class AllFollowedItem() {
abstract val itemType: Int
open fun areItemsTheSame(other: AllFollowedItem): Boolean {
return other.itemType == itemType && doAreItemsTheSame(other)
}
open fun areContentsTheSame(other: AllFollowedItem): Boolean {
return other.itemType == itemType && doAreContentsTheSame(other)
}
open fun doAreItemsTheSame(other: AllFollowedItem): Boolean = true
open fun doAreContentsTheSame(other: AllFollowedItem): Boolean = this == other
companion object {
const val ALL_FOLLOWED_ITEM_TYPE_NO_TOP = 0
const val ALL_FOLLOWED_ITEM_TYPE_NORMAL = 1
const val ALL_FOLLOWED_ITEM_TYPE_MY_FOLLOW_HEADER = 2
}
}
class AllFollowedNoTopItem : AllFollowedItem() {
override val itemType: Int
get() = ALL_FOLLOWED_ITEM_TYPE_NO_TOP
}
data class AllFollowedMyFollowedHeaderItem(val isTop: Boolean) : AllFollowedItem() {
override val itemType: Int
get() = ALL_FOLLOWED_ITEM_TYPE_MY_FOLLOW_HEADER
}
data class AllFollowedNormalItem(
val data: FollowUserEntity,
var isTop: Boolean
) : AllFollowedItem() {
override val itemType: Int
get() = ALL_FOLLOWED_ITEM_TYPE_NORMAL
override fun doAreItemsTheSame(other: AllFollowedItem): Boolean {
return other is AllFollowedNormalItem && data.id == other.data.id
}
}

View File

@ -0,0 +1,306 @@
package com.gh.gamecenter.forum.home.follow
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.gh.common.util.SyncDataBetweenPageHelper
import com.gh.gamecenter.common.entity.CommunityEntity
import com.gh.gamecenter.entity.PersonalHistoryEntity
import com.gh.gamecenter.feature.entity.AnswerEntity
import com.gh.gamecenter.feature.entity.ArticleEntity
import com.gh.gamecenter.feature.entity.CommentEntity
import com.gh.gamecenter.feature.entity.ForumVideoEntity
import com.gh.gamecenter.personalhome.home.UserHistoryFragment
import com.gh.gamecenter.qa.article.detail.ArticleDetailActivity
import com.gh.gamecenter.qa.entity.ArticleDetailEntity
import com.gh.gamecenter.qa.entity.QuestionsDetailEntity
import com.gh.gamecenter.qa.questions.newdetail.NewQuestionDetailActivity
import com.gh.gamecenter.qa.video.detail.ForumVideoDetailActivity
class FollowActivityResultLauncher(
private val registry: ActivityResultRegistry,
private val onResultListener: OnResultListener
) : DefaultLifecycleObserver {
private lateinit var articleDetailLauncher: ActivityResultLauncher<LauncherDestination>
private lateinit var commentEntityLauncher: ActivityResultLauncher<LauncherDestination>
private lateinit var questionsDetailLauncher: ActivityResultLauncher<LauncherDestination>
private lateinit var forumVideoLauncher: ActivityResultLauncher<LauncherDestination>
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
articleDetailLauncher = registry.register(
KEY_ARTICLE_DETAIL_LAUNCHER,
owner,
object : ActivityResultContract<LauncherDestination, ArticleDetailResult?>() {
override fun createIntent(context: Context, input: LauncherDestination): Intent {
val intent =
if (input.historyEntity != null) {
ArticleDetailActivity.getIntent(
context,
input.historyEntity.community,
input.historyEntity.id,
"",
UserHistoryFragment.PATH_USER_QUESTION_ANSWER,
sourceEntrance = "关注-用户动态"
)
} else {
ArticleDetailActivity.getIntent(
context,
CommunityEntity(input.answerEntity?.bbs?.id ?: ""),
input.answerEntity?.id ?: "",
"",
"",
sourceEntrance = "关注-论坛动态",
)
}
intent.putExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, input.position)
intent.putExtra(SyncDataBetweenPageHelper.REQUEST_CODE_TAG, 101)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): ArticleDetailResult? {
val position = intent?.getIntExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, -1)
val resultData =
intent?.getParcelableExtra<ArticleDetailEntity>(ArticleDetailEntity::class.java.simpleName)
if (position != null && resultData != null) {
return ArticleDetailResult(position, resultData)
} else {
return null
}
}
})
{
it?.run {
onResultListener.onArticleDetailResult(position, articleDetailEntity)
}
}
commentEntityLauncher = registry.register(
KEY_COMMENT_LAUNCHER,
owner,
object : ActivityResultContract<LauncherDestination, CommentResult?>() {
override fun createIntent(context: Context, input: LauncherDestination): Intent {
val intent = NewQuestionDetailActivity.getSpecifiedCommentIntent(
context,
input.historyEntity?.question?.id ?: "",
input.historyEntity?.id ?: "",
"",
UserHistoryFragment.PATH_USER_QUESTION_ANSWER,
sourceEntrance = "关注-动态"
)
intent.putExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, input.position)
intent.putExtra(SyncDataBetweenPageHelper.REQUEST_CODE_TAG, 102)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): CommentResult? {
val position = intent?.getIntExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, -1)
val commentEntity = intent?.getParcelableExtra<CommentEntity>(CommentEntity::class.java.simpleName)
if (position != null && commentEntity != null) {
return CommentResult(position, commentEntity)
} else {
return null
}
}
})
{
it?.run {
onResultListener.onCommentResult(position, commentEntity)
}
}
questionsDetailLauncher = registry.register(
KEY_QUESTION_DETAIL_LAUNCHER,
owner,
object : ActivityResultContract<LauncherDestination, QuestionsDetailResult?>() {
override fun createIntent(context: Context, input: LauncherDestination): Intent {
val intent = if (input.historyEntity != null) {
NewQuestionDetailActivity.getIntent(
context,
input.historyEntity.id,
"",
UserHistoryFragment.PATH_USER_QUESTION_ANSWER
)
} else {
if (input.answerEntity?.type == "question") {
NewQuestionDetailActivity.getIntent(
context, input.answerEntity.id,
"",
"",
sourceEntrance = "关注-论坛动态"
)
} else {
NewQuestionDetailActivity.getCommentIntent(
context,
input.answerEntity?.questions?.id ?: "",
input.answerEntity?.answerId ?: "",
"",
""
)
}
}
intent.putExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, input.position)
intent.putExtra(SyncDataBetweenPageHelper.REQUEST_CODE_TAG, 103)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): QuestionsDetailResult? {
val position = intent?.getIntExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, -1)
val questionsDetailEntity =
intent?.getParcelableExtra<QuestionsDetailEntity>(QuestionsDetailEntity::class.java.simpleName)
return if (position != null && questionsDetailEntity != null) {
QuestionsDetailResult(position, questionsDetailEntity)
} else {
null
}
}
}
) {
it?.run {
onResultListener.onQuestionsDetailResult(position, questionsDetailEntity)
}
}
forumVideoLauncher = registry.register(
KEY_FORUM_VIDEO_LAUNCHER,
owner,
object : ActivityResultContract<LauncherDestination, ForumVideoResult?>() {
override fun createIntent(context: Context, input: LauncherDestination): Intent {
val intent = if (input.historyEntity != null) {
ForumVideoDetailActivity.getIntent(
context,
input.historyEntity.id,
input.historyEntity.community.id,
sourceEntrance = "关注-个人动态"
)
} else {
ForumVideoDetailActivity.getIntent(
context,
input.answerEntity?.id ?: "",
input.answerEntity?.bbs?.id ?: "",
"关注-论坛动态"
)
}
intent.putExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, input.position)
intent.putExtra(SyncDataBetweenPageHelper.REQUEST_CODE_TAG, 104)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?): ForumVideoResult? {
val position = intent?.getIntExtra(SyncDataBetweenPageHelper.DATA_POSITION_TAG, -1)
val forumVideoEntity =
intent?.getParcelableExtra<ForumVideoEntity>(ForumVideoEntity::class.java.simpleName)
return if (position != null && forumVideoEntity != null) {
ForumVideoResult(position, forumVideoEntity)
} else {
null
}
}
}
)
{
it?.run {
onResultListener.onForumVideoResult(position, forumVideoEntity)
}
}
}
fun onPersonItemClick(position: Int, entity: PersonalHistoryEntity) {
when {
entity.type == "community_article"
|| entity.type == "community_article_vote" -> {
articleDetailLauncher.launch(LauncherDestination(position, historyEntity = entity))
}
entity.type.contains("video") -> {
forumVideoLauncher.launch(LauncherDestination(position, historyEntity = entity))
}
entity.type.contains("question") -> {
questionsDetailLauncher.launch(LauncherDestination(position, historyEntity = entity))
}
else -> {
commentEntityLauncher.launch(LauncherDestination(position, historyEntity = entity))
}
}
}
fun onBbsItemClick(position: Int, answerEntity: AnswerEntity) {
when (answerEntity.type) {
"community_article" -> {
articleDetailLauncher.launch(LauncherDestination(position, answerEntity = answerEntity))
}
"video" -> {
forumVideoLauncher.launch(LauncherDestination(position, answerEntity = answerEntity))
}
"question" -> {
questionsDetailLauncher.launch(LauncherDestination(position, answerEntity = answerEntity))
}
"answer" -> {
questionsDetailLauncher.launch(LauncherDestination(position, answerEntity = answerEntity))
}
}
}
companion object {
private const val KEY_ARTICLE_DETAIL_LAUNCHER = "key_article_detail_launcher"
private const val KEY_COMMENT_LAUNCHER = "key_comment_launcher"
private const val KEY_QUESTION_DETAIL_LAUNCHER = "key_question_detail_launcher"
private const val KEY_FORUM_VIDEO_LAUNCHER = "KEY_forum_video_LAUNCHER"
}
private data class LauncherDestination(
val position: Int,
val historyEntity: PersonalHistoryEntity? = null,
val answerEntity: AnswerEntity? = null
)
private data class ArticleDetailResult(
val position: Int,
val articleDetailEntity: ArticleDetailEntity
)
private data class CommentResult(
val position: Int,
val commentEntity: CommentEntity
)
private data class QuestionsDetailResult(
val position: Int,
val questionsDetailEntity: QuestionsDetailEntity
)
private data class ForumVideoResult(
val position: Int,
val forumVideoEntity: ForumVideoEntity
)
interface OnResultListener {
fun onArticleDetailResult(position: Int, articleDetailEntity: ArticleDetailEntity)
fun onCommentResult(position: Int, commentEntity: CommentEntity)
fun onQuestionsDetailResult(position: Int, questionsDetailEntity: QuestionsDetailEntity)
fun onForumVideoResult(position: Int, forumVideoEntity: ForumVideoEntity)
}
}

View File

@ -0,0 +1,32 @@
package com.gh.gamecenter.forum.home.follow
import android.content.Context
import android.graphics.Canvas
import android.graphics.Path
import android.util.AttributeSet
import androidx.core.content.ContextCompat
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.dip2px
import com.gh.gamecenter.core.utils.DisplayUtils
class FollowClippedView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {
override fun onDraw(canvas: Canvas) {
val path = Path()
path.addRect(
0f,
0f,
width.toFloat(),
(DisplayUtils.getStatusBarHeight(resources) + 52f.dip2px() + 56f.dip2px()).toFloat(),
Path.Direction.CW
)
canvas.clipPath(path)
canvas.drawColor(ContextCompat.getColor(context, R.color.ui_background))
super.onDraw(canvas)
}
}

View File

@ -0,0 +1,69 @@
package com.gh.gamecenter.forum.home.follow
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.TextView
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.activity.BaseActivity
import com.gh.gamecenter.common.utils.DarkModeUtils.isDarkModeOn
import com.gh.gamecenter.common.utils.toArrayList
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.forum.home.follow.fragment.FollowDynamicFragment
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowDynamicViewModel
class FollowDynamicActivity : BaseActivity() {
private val viewModel by viewModels<FollowDynamicViewModel>()
override fun getLayoutId(): Int = R.layout.activity_amway
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DisplayUtils.transparentStatusAndNavigation(this)
DisplayUtils.setLightStatusBar(this, true)
val containerFragment = supportFragmentManager.findFragmentByTag(FollowDynamicFragment::class.java.name)
?: FollowDynamicFragment().with(intent.extras)
// 若 placeholder 外层为 RelativeLayout 的话,会出现莫名的偏移
supportFragmentManager.beginTransaction()
.replace(R.id.placeholder, containerFragment, FollowDynamicFragment::class.java.name)
.commitAllowingStateLoss()
}
override fun onDarkModeChanged() {
val tv = findViewById<TextView>(ID_NIGHT_INDICATOR)
if (tv != null) {
tv.text = if (isDarkModeOn(this)) "深色模式" else "浅色模式"
tv.alpha = if (isDarkModeOn(this)) 0.8f else 0.15f
}
if (isAutoResetViewBackgroundEnabled) {
updateStaticViewBackground(window.decorView)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.navigationBarColor = ContextCompat.getColor(this, R.color.ui_surface)
}
}
override fun handleBackPressed(): Boolean {
viewModel.finish()
return true
}
companion object {
const val KEY_SELECTED_POSITION = "key_selected_position"
const val KEY_USERS = "key_users"
fun start(context: Context, position: Int, followedList: List<FollowUserEntity>) {
val intent = Intent(context, FollowDynamicActivity::class.java)
intent.putParcelableArrayListExtra(KEY_USERS, followedList.toArrayList())
intent.putExtra(KEY_SELECTED_POSITION, position)
context.startActivity(intent)
}
}
}

View File

@ -0,0 +1,61 @@
package com.gh.gamecenter.forum.home.follow
import com.gh.gamecenter.entity.HistoryGameEntity
import com.gh.gamecenter.entity.PersonalHistoryEntity
import com.gh.gamecenter.feature.entity.AnswerEntity
abstract class FollowDynamicItem {
abstract val itemType: Int
fun areItemsTheSame(other: FollowDynamicItem) =
itemType == other.itemType && doAreItemsTheSame(other)
fun areContentsTheSame(other: FollowDynamicItem) =
itemType == other.itemType && doAreContentsTheSame(other)
protected open fun doAreItemsTheSame(other: FollowDynamicItem) = true
protected open fun doAreContentsTheSame(other: FollowDynamicItem) = this == other
companion object {
const val FOLLOW_DYNAMIC_ITEM_TYPE_FOOTER = -1
const val FOLLOW_DYNAMIC_ITEM_TYPE_PERSONAL = 0
const val FOLLOW_DYNAMIC_ITEM_TYPE_BBS = 1
}
}
data class FollowDynamicPersonalItem(
val data: PersonalHistoryEntity
) : FollowDynamicItem() {
override val itemType: Int
get() = FOLLOW_DYNAMIC_ITEM_TYPE_PERSONAL
override fun doAreItemsTheSame(other: FollowDynamicItem): Boolean {
return other is FollowDynamicPersonalItem
&& data.id == other.data.id
}
}
data class FollowDynamicBbsItem(
val data: AnswerEntity
) : FollowDynamicItem() {
override val itemType: Int
get() = FOLLOW_DYNAMIC_ITEM_TYPE_BBS
override fun doAreItemsTheSame(other: FollowDynamicItem): Boolean {
return other is FollowDynamicBbsItem
&& data.id == other.data.id
}
}
object FollowDynamicFooterItem : FollowDynamicItem() {
override val itemType: Int
get() = FOLLOW_DYNAMIC_ITEM_TYPE_FOOTER
}

View File

@ -0,0 +1,107 @@
package com.gh.gamecenter.forum.home.follow
import android.content.Context
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.MarginLayoutParams
import android.widget.CheckedTextView
import android.widget.PopupWindow
import androidx.core.view.children
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.utils.DisplayUtils
import com.gh.gamecenter.databinding.LayoutForumActivityCategoryItemBinding
import com.gh.gamecenter.databinding.PopFollowFilterBinding
import com.google.android.flexbox.FlexboxLayout
class FollowHomeFilterPopWindow(
private val binding: PopFollowFilterBinding
) : PopupWindow(binding.root, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) {
private val data = arrayListOf("全部", "关注的人", "关注的游戏")
init {
isOutsideTouchable = true
isFocusable = true
val layoutParams = binding.ivTopBackground.layoutParams as MarginLayoutParams
layoutParams.topMargin = -52F.dip2px() - DisplayUtils.getStatusBarHeight(binding.root.context.resources)
binding.ivTopBackground.layoutParams = layoutParams
val inflater = LayoutInflater.from(binding.root.context)
data.forEachIndexed { index, entity ->
LayoutForumActivityCategoryItemBinding.inflate(inflater).apply {
val params =
FlexboxLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
params.setMargins(0, 8F.dip2px(), 8F.dip2px(), 0)
params.height = 24F.dip2px()
root.layoutParams = params
name.text = entity
name.isChecked = index == 0
root.setOnClickListener {
updateCategory(index)
}
binding.fblFilter.addView(root)
}
}
binding.vBackground.setOnClickListener {
dismiss()
}
if (DarkModeUtils.isDarkModeOn(binding.root.context)) {
binding.clFilterContainer.setBackgroundColor(R.color.ui_background.toColor(binding.root.context))
binding.ivTopBackground.goneIf(true)
} else {
binding.clFilterContainer.background = null
binding.ivTopBackground.goneIf(false)
}
}
private fun updateCategory(index: Int) {
filteredListener?.filterFollowed(index)
binding.fblFilter.children.forEachIndexed { position, child ->
(child as? CheckedTextView)?.isChecked = (position == index)
}
dismiss()
}
private var filteredListener: OnFilteredChangedListener? = null
fun setFilterListener(filteredListener: OnFilteredChangedListener) {
this.filteredListener = filteredListener
}
fun onDarkModeChanged() {
if (DarkModeUtils.isDarkModeOn(binding.root.context)) {
binding.clFilterContainer.setBackgroundColor(R.color.ui_background.toColor(binding.root.context))
binding.ivTopBackground.goneIf(true)
} else {
binding.clFilterContainer.background = null
binding.ivTopBackground.goneIf(false)
}
val context = binding.root.context
binding.fblFilter.children.forEach {
if (it is CheckedTextView) {
val colorInt = R.color.forum_category_selector.toColor(context)
it.setTextColor(ColorStateList.valueOf(colorInt))
val drawable = R.drawable.selector_bg_forum_activity_category.toDrawable(context)
it.background = drawable
}
}
}
companion object {
fun create(context: Context): FollowHomeFilterPopWindow {
val inflater = LayoutInflater.from(context)
val binding = PopFollowFilterBinding.inflate(inflater, null, false)
return FollowHomeFilterPopWindow(binding)
}
}
interface OnFilteredChangedListener {
fun filterFollowed(position: Int)
}
}

View File

@ -0,0 +1,201 @@
package com.gh.gamecenter.forum.home.follow
import com.gh.gamecenter.entity.FollowCommonContentCollection
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.entity.FollowDynamicEntity
import com.gh.gamecenter.feature.entity.*
import com.gh.gamecenter.home.custom.model.CustomPageData
import com.gh.gamecenter.home.custom.model.CustomPageItem
import com.gh.gamecenter.home.custom.model.CustomPageItem.Companion.COMMON_CONTENT_COLLECTION_LAYOUT_BANNER
abstract class FollowItem {
abstract val itemType: Int
fun areItemsTheSame(other: FollowItem) =
itemType == other.itemType && doAreItemsTheSame(other)
fun areContentsTheSame(other: FollowItem) =
itemType == other.itemType && doAreContentsTheSame(other)
protected open fun doAreItemsTheSame(other: FollowItem) = true
protected open fun doAreContentsTheSame(other: FollowItem) = this == other
companion object {
const val FOLLOW_ITEM_TYPE_INVALID = -4
const val FOLLOW_ITEM_TYPE_EMPTY = -3
const val FOLLOW_ITEM_TYPE_FOOTER = -2
const val FOLLOW_ITEM_TYPE_HEADER = -1
const val FOLLOW_ITEM_TYPE_RECOMMEND_USER = 0
const val FOLLOW_ITEM_TYPE_COMMON_BANNER = 1
const val FOLLOW_ITEM_TYPE_COMMON_BANNER_WITH_CARDS = 2
const val FOLLOW_ITEM_TYPE_NAVIGATION = 3
const val FOLLOW_ITEM_TYPE_RECOMMEND = 4
const val FOLLOW_ITEM_TYPE_COMMON_HORIZONTAL_SLIDE = 5
const val FOLLOW_ITEM_TYPE_COMMON_VERTICAL_TWO_CARDS = 6
const val FOLLOW_ITEM_TYPE_POST_CARD = 7
const val FOLLOW_ITEM_TYPE_GIFT_PACK = 8
const val FOLLOW_ITEM_TYPE_ARTICLE = 9
// 通用内容合集 样式
val commonContentCollection: HashMap<Int, Int> = hashMapOf(
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_NAVIGATION to FOLLOW_ITEM_TYPE_NAVIGATION,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_KING_KONG to FOLLOW_ITEM_TYPE_RECOMMEND,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_HORIZONTAL_SLIDE_BANNER to FOLLOW_ITEM_TYPE_COMMON_HORIZONTAL_SLIDE,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_DOUBLE_ROW_BANNER to FOLLOW_ITEM_TYPE_COMMON_VERTICAL_TWO_CARDS,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_HORIZONTAL_VERTICAL_CARD to FOLLOW_ITEM_TYPE_COMMON_HORIZONTAL_SLIDE,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_DOUBLE_ROW_VERTICAL_CARD to FOLLOW_ITEM_TYPE_COMMON_VERTICAL_TWO_CARDS,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_VERTICAL_IMAGE_TEXT to FOLLOW_ITEM_TYPE_COMMON_VERTICAL_TWO_CARDS,
CustomPageItem.COMMON_CONTENT_COLLECTION_LAYOUT_HORIZONTAL_IMAGE_TEXT to FOLLOW_ITEM_TYPE_COMMON_HORIZONTAL_SLIDE
)
}
}
data class FollowRecommendUsersItem(
val data: List<UserEntity>
) : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_RECOMMEND_USER
}
class FollowCommonCollectionItem(
val data: FollowCommonContentCollection
) : FollowItem() {
override val itemType: Int
get() = if (data.linkCommonCollection.layout == COMMON_CONTENT_COLLECTION_LAYOUT_BANNER) {
// 轮播图右边卡片列表, 至少需要2个卡片才能显示
if (data.linkCommonCollection.slides.subSlide.size < 2) {
FOLLOW_ITEM_TYPE_COMMON_BANNER
} else {
FOLLOW_ITEM_TYPE_COMMON_BANNER_WITH_CARDS
}
} else {
commonContentCollection[data.linkCommonCollection.layout] ?: FOLLOW_ITEM_TYPE_INVALID
}
override fun doAreItemsTheSame(other: FollowItem): Boolean {
return other is FollowCommonCollectionItem
&& data.linkCommonCollection.id == other.data.linkCommonCollection.id
}
override fun doAreContentsTheSame(other: FollowItem): Boolean {
return other is FollowCommonCollectionItem
&& data.title == other.data.title
&& data.linkCommonCollection == other.data.linkCommonCollection
}
}
data class FollowSplitCommonContentCollectionItem(
val data: CustomPageData.CommonContentCollection,
val leftPosition: Int,
) : FollowItem() {
override val itemType
get() = commonContentCollection[data.layout] ?: FOLLOW_ITEM_TYPE_INVALID
override fun doAreItemsTheSame(other: FollowItem): Boolean {
return other is FollowSplitCommonContentCollectionItem
&& data.id == other.data.id
}
override fun doAreContentsTheSame(other: FollowItem): Boolean {
return other is FollowSplitCommonContentCollectionItem
&& data == other.data
&& data.data.getOrNull(leftPosition) == other.data.data.getOrNull(leftPosition)
&& data.data.getOrNull(leftPosition + 1) == other.data.data.getOrNull(leftPosition + 1)
}
}
data class FollowPostCardItem(
val data: ArticleEntity
) : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_POST_CARD
override fun doAreItemsTheSame(other: FollowItem): Boolean {
return other is FollowPostCardItem
&& data.id == other.data.id
}
}
data class FollowArticleItem(
val data: FollowDynamicEntity.Article
) : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_ARTICLE
override fun doAreItemsTheSame(other: FollowItem): Boolean {
return other is FollowArticleItem
&& data.id == other.data.id
}
}
data class FollowGiftPackItem(
val libao: LibaoEntity,
val time: Long
) : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_GIFT_PACK
override fun doAreItemsTheSame(other: FollowItem): Boolean {
return other is FollowGiftPackItem
&& libao.id == other.libao.id
}
override fun doAreContentsTheSame(other: FollowItem): Boolean {
return other is FollowGiftPackItem
&& time == other.time
&& libao.name == other.libao.name
&& libao.des == other.libao.des
&& libao.content == other.libao.content
&& libao.code == other.libao.code
&& libao.game?.mIcon == other.libao.game?.mIcon
&& libao.game?.iconFloat == other.libao.game?.iconFloat
&& libao.game?.iconSubscript == other.libao.game?.iconSubscript
&& libao.game?.name == other.libao.game?.name
}
}
data class FollowUserItem(
val isLogin: Boolean,
val data: List<FollowUserEntity>
) : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_HEADER
override fun doAreItemsTheSame(other: FollowItem): Boolean {
return true
}
override fun doAreContentsTheSame(other: FollowItem): Boolean {
return other is FollowUserItem
&& isLogin == other.isLogin
&& data == other.data
}
}
object FollowEmptyItem : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_EMPTY
}
object FollowFooterItem : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_FOOTER
}
object InvalidItem : FollowItem() {
override val itemType: Int
get() = FOLLOW_ITEM_TYPE_INVALID
}

View File

@ -0,0 +1,6 @@
package com.gh.gamecenter.forum.home.follow
class FollowPageConfiguration(
val entrance: String,
val path: String
)

View File

@ -0,0 +1,51 @@
package com.gh.gamecenter.forum.home.follow
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.gh.gamecenter.common.utils.DarkModeUtils
class ResetDataChangeHelper<T>(
private val context: Context,
private val adapter: RecyclerView.Adapter<*>
) {
private var darkMode = DarkModeUtils.isDarkModeOn(context)
private val _dataList = arrayListOf<T>()
val dataList: List<T> = _dataList
val dataCount: Int
get() = dataList.size
fun getItem(position: Int) = dataList[position]
private var _countAndKey: Pair<Int, String>? = null
fun submitList(data: List<T>?, getKey: (T) -> String) {
_dataList.clear()
data?.let(_dataList::addAll)
if (data.isNullOrEmpty()) {
_countAndKey = null
adapter.notifyDataSetChanged()
return
}
val countAndKey = _countAndKey ?: Pair(0, "")
val (oldSize, oldKeys) = countAndKey
val newSize = data.size
var newKeys = ""
data.forEach {
newKeys += getKey(it)
}
if (oldSize == newSize) {
if (oldKeys != newKeys || darkMode != DarkModeUtils.isDarkModeOn(context)) { // 数量不变,内容发生变化 || 切换浅色模式
adapter.notifyItemRangeChanged(0, adapter.itemCount, "")
}
} else {// 数量发生变化
adapter.notifyDataSetChanged()
}
darkMode = DarkModeUtils.isDarkModeOn(context)
_countAndKey = Pair(newSize, newKeys)
}
}

View File

@ -0,0 +1,209 @@
package com.gh.gamecenter.forum.home.follow.adapter
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.consume
import com.gh.gamecenter.common.utils.goneIf
import com.gh.gamecenter.common.utils.toBinding
import com.gh.gamecenter.databinding.RecyclerAllFollowMyFollowHeaderBinding
import com.gh.gamecenter.databinding.RecyclerAllFollowedBinding
import com.gh.gamecenter.databinding.RecyclerAllFollowedNoTopBinding
import com.gh.gamecenter.entity.FollowOperateTopRequest
import com.gh.gamecenter.forum.home.follow.AllFollowedItem
import com.gh.gamecenter.forum.home.follow.AllFollowedItem.Companion.ALL_FOLLOWED_ITEM_TYPE_MY_FOLLOW_HEADER
import com.gh.gamecenter.forum.home.follow.AllFollowedItem.Companion.ALL_FOLLOWED_ITEM_TYPE_NORMAL
import com.gh.gamecenter.forum.home.follow.AllFollowedItem.Companion.ALL_FOLLOWED_ITEM_TYPE_NO_TOP
import com.gh.gamecenter.forum.home.follow.AllFollowedMyFollowedHeaderItem
import com.gh.gamecenter.forum.home.follow.AllFollowedNoTopItem
import com.gh.gamecenter.forum.home.follow.AllFollowedNormalItem
import com.gh.gamecenter.forum.home.follow.viewholder.FollowInvalidViewHolder
import com.gh.gamecenter.forum.home.follow.viewmodel.AllFollowedViewModel
class AllFollowedAdapter(
private val viewModel: AllFollowedViewModel,
private val startDrag: (ViewHolder) -> Unit,
) : ListAdapter<AllFollowedItem, ViewHolder>(createDiffItemCallBack()) {
private var _isEdit = false
val isEdit: Boolean
get() = _isEdit
// 置顶需要移动到的位置
private var _topItemCount = 0
val topItemCount: Int
get() = _topItemCount
// 已关注用户的数量排除header footer等其它类型的item
private var _followedCount = 0
val followedCount: Int
get() = _followedCount
private val _dataList = arrayListOf<AllFollowedNormalItem>()
val dataList: List<AllFollowedNormalItem>
get() = _dataList
fun setData(data: List<AllFollowedItem>) {
_dataList.clear()
_dataList.addAll(data.filterIsInstance<AllFollowedNormalItem>())
_followedCount = dataList.size
_topItemCount = dataList.count(AllFollowedNormalItem::isTop)
val dataWithHeader = arrayListOf<AllFollowedItem>()
data.forEachIndexed { index, item ->
if (index == 0) {
dataWithHeader.add(AllFollowedMyFollowedHeaderItem(true))
}
if (topItemCount == 0 && index == 0) {
// 暂时没有置顶的关注,需要显示提示
dataWithHeader.add(AllFollowedNoTopItem())
dataWithHeader.add(AllFollowedMyFollowedHeaderItem(false))
} else if (topItemCount == index) {
dataWithHeader.add(AllFollowedMyFollowedHeaderItem(false))
}
dataWithHeader.add(item)
}
super.submitList(dataWithHeader)
}
public override fun getItem(position: Int): AllFollowedItem {
return super.getItem(position)
}
override fun getItemViewType(position: Int): Int = currentList[position].itemType
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
when (viewType) {
ALL_FOLLOWED_ITEM_TYPE_NO_TOP -> NoTopViewHolder(parent.toBinding())
ALL_FOLLOWED_ITEM_TYPE_NORMAL -> AllFollowedViewHolder(parent.toBinding())
ALL_FOLLOWED_ITEM_TYPE_MY_FOLLOW_HEADER -> MyFollowerHeaderViewHolder(parent.toBinding())
else -> FollowInvalidViewHolder(parent.toBinding())
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (holder is AllFollowedViewHolder) {
val itemBinding = holder.binding
val item = getItem(position) as AllFollowedNormalItem
val data = item.data
if (isEdit) {
itemBinding.ivUpOrDown.goneIf(false)
itemBinding.ivDrag.goneIf(!item.isTop)
itemBinding.vNewTips.goneIf(true)
} else {
itemBinding.ivUpOrDown.goneIf(true)
itemBinding.ivDrag.goneIf(true)
itemBinding.vNewTips.goneIf(!data.isShowTip)
}
val editResourceId = if (item.isTop) R.drawable.ic_all_followed_down else R.drawable.ic_all_followed_up
itemBinding.ivUpOrDown.setImageResource(editResourceId)
itemBinding.tvName.text = data.name
if (data.isUser) {
itemBinding.ivUserIcon.goneIf(false) {
itemBinding.ivUserIcon.displayGameIcon(data.icon, null)
}
itemBinding.ivGameIcon.goneIf(true)
} else {
itemBinding.ivUserIcon.goneIf(true)
itemBinding.ivGameIcon.goneIf(false) {
itemBinding.ivGameIcon.displayGameIcon(data.icon, null)
}
}
itemBinding.ivUpOrDown.setOnClickListener {
if (item.isTop) {
cancelToTop(data.id)
} else {
toTop(data.id)
}
}
itemBinding.ivDrag.setOnTouchListener { _, event ->
consume {
if (event.action == MotionEvent.ACTION_DOWN) {
startDrag(holder)
}
}
}
arrayOf(itemBinding.ivGameIcon, itemBinding.ivUserIcon, itemBinding.tvName)
.forEach {
it.setOnClickListener {
viewModel.navigateToDetailPage(item)
}
}
}
}
private fun toTop(id: String) {
if (topItemCount >= itemCount) {
return
}
val newList = arrayListOf<AllFollowedNormalItem>()
val targetItem = dataList.find { it.data.id == id }
dataList.forEach {
if (newList.size == topItemCount && targetItem != null) {
newList.add(targetItem.copy(isTop = true))
}
if (it.data.id != id) {
newList.add(AllFollowedNormalItem(it.data, it.isTop))
}
}
setData(newList)
}
private fun cancelToTop(id: String) {
if (topItemCount == 0) {
return
}
val newList = arrayListOf<AllFollowedNormalItem>()
val targetItem = dataList.find { it.data.id == id }
dataList.forEach {
if (topItemCount == newList.size && targetItem != null) {
newList.add(targetItem.copy(isTop = false))
}
if (it.data.id != id) {
newList.add(it.copy())
}
}
setData(newList)
}
fun changeToEditable() {
_isEdit = !isEdit
notifyItemRangeChanged(0, itemCount, "")
}
fun getTopItem() =
dataList.filter {
it.isTop
}.mapIndexed { index, item ->
FollowOperateTopRequest(item.data.id, index + 1)
}
companion object {
fun createDiffItemCallBack() = object : ItemCallback<AllFollowedItem>() {
override fun areItemsTheSame(oldItem: AllFollowedItem, newItem: AllFollowedItem): Boolean {
return oldItem.areItemsTheSame(newItem)
}
override fun areContentsTheSame(oldItem: AllFollowedItem, newItem: AllFollowedItem): Boolean {
return oldItem.areContentsTheSame(newItem)
}
}
}
class NoTopViewHolder(val binding: RecyclerAllFollowedNoTopBinding) : ViewHolder(binding.root)
class MyFollowerHeaderViewHolder(val binding: RecyclerAllFollowMyFollowHeaderBinding) : ViewHolder(binding.root)
class AllFollowedViewHolder(val binding: RecyclerAllFollowedBinding) : ViewHolder(binding.root)
}

View File

@ -0,0 +1,217 @@
package com.gh.gamecenter.forum.home.follow.adapter
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.gamecenter.R
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.utils.goneIf
import com.gh.gamecenter.common.utils.toBinding
import com.gh.gamecenter.databinding.CommunityAnswerItemBinding
import com.gh.gamecenter.entity.PersonalHistoryEntity
import com.gh.gamecenter.feature.entity.AnswerEntity
import com.gh.gamecenter.feature.entity.Questions
import com.gh.gamecenter.forum.home.ForumArticleAskItemViewHolder
import com.gh.gamecenter.forum.home.follow.FollowDynamicBbsItem
import com.gh.gamecenter.forum.home.follow.FollowDynamicFooterItem
import com.gh.gamecenter.forum.home.follow.FollowDynamicItem
import com.gh.gamecenter.forum.home.follow.FollowDynamicItem.Companion.FOLLOW_DYNAMIC_ITEM_TYPE_BBS
import com.gh.gamecenter.forum.home.follow.FollowDynamicItem.Companion.FOLLOW_DYNAMIC_ITEM_TYPE_FOOTER
import com.gh.gamecenter.forum.home.follow.FollowDynamicItem.Companion.FOLLOW_DYNAMIC_ITEM_TYPE_PERSONAL
import com.gh.gamecenter.forum.home.follow.FollowDynamicPersonalItem
import com.gh.gamecenter.forum.home.follow.viewholder.FollowFooterViewHolder
import com.gh.gamecenter.forum.home.follow.viewholder.FollowInvalidViewHolder
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowDynamicListViewModel
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowDynamicViewModel
import com.gh.gamecenter.forum.search.CommunitySearchEventListener
import com.gh.gamecenter.personalhome.PersonalItemViewHolder
class FollowDynamicAdapter(
private val context: Context,
private val viewModel: FollowDynamicListViewModel
) : ListAdapter<FollowDynamicItem, ViewHolder>(createDiffCallback()) {
private var _recyclerView: RecyclerView? = null
private var loadStatus: LoadStatus? = null
fun setLoadStatus(status: LoadStatus) {
loadStatus = status
notifyItemChanged(itemCount - 1)
}
override fun submitList(list: List<FollowDynamicItem>?) {
val dataWithFooter = if (list.isNullOrEmpty()) {
ArrayList(list)
} else {
list + FollowDynamicFooterItem
}
super.submitList(dataWithFooter)
}
override fun getItemViewType(position: Int): Int {
return currentList[position].itemType
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return when (viewType) {
FOLLOW_DYNAMIC_ITEM_TYPE_PERSONAL -> {
FollowDynamicPersonalViewHolder(context, parent.toBinding()) { history, position ->
viewModel.navigateToPersonDetailPage(history, position)
}
}
FOLLOW_DYNAMIC_ITEM_TYPE_BBS -> {
FollowDynamicBbSViewHolder(parent.toBinding(), viewModel.bbsId) { position, answer ->
viewModel.navigateToBbsDetailPage(position, answer)
}
}
FOLLOW_DYNAMIC_ITEM_TYPE_FOOTER ->
FollowFooterViewHolder(parent.toBinding())
else -> {
FollowInvalidViewHolder(parent.toBinding())
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is FollowDynamicPersonalViewHolder -> {
val item = getItem(position)
if (item is FollowDynamicPersonalItem) {
holder.onBind(item.data, "", position)
}
}
is FollowDynamicBbSViewHolder -> {
val item = getItem(position)
if (item is FollowDynamicBbsItem) {
holder.onBind(item.data, "", "", position)
}
}
is FollowFooterViewHolder -> {
holder.initFooterViewHolder(
loadStatus == LoadStatus.LIST_LOADING,
loadStatus == LoadStatus.LIST_FAILED,
loadStatus == LoadStatus.LIST_OVER,
R.string.load_over_with_click_hint
) {
if (loadStatus == LoadStatus.LIST_OVER) {
_recyclerView?.scrollToPosition(0)
} else if (loadStatus == LoadStatus.LIST_FAILED) {
viewModel.loadMore()
notifyItemChanged(itemCount - 1)
}
}
}
else -> Unit
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(onLoadMoreListener)
_recyclerView = recyclerView
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
recyclerView.removeOnScrollListener(onLoadMoreListener)
_recyclerView = null
}
private val onLoadMoreListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManger = recyclerView.layoutManager
if (layoutManger is LinearLayoutManager && itemCount != 0) {
val lastVisibleItemPosition = layoutManger.findLastVisibleItemPosition()
if (lastVisibleItemPosition == itemCount - 1 && loadStatus != LoadStatus.LIST_OVER && loadStatus != LoadStatus.LIST_LOADING) {
viewModel.loadMore()
}
}
}
}
}
companion object {
fun createDiffCallback() = object : ItemCallback<FollowDynamicItem>() {
override fun areItemsTheSame(oldItem: FollowDynamicItem, newItem: FollowDynamicItem): Boolean {
return oldItem.areItemsTheSame(newItem)
}
override fun areContentsTheSame(oldItem: FollowDynamicItem, newItem: FollowDynamicItem): Boolean {
return oldItem.areContentsTheSame(newItem)
}
}
}
class FollowDynamicPersonalViewHolder(
private val context: Context,
val binding: CommunityAnswerItemBinding,
private val itemClickCallback: (historyEntity: PersonalHistoryEntity, position: Int) -> Unit
) :
RecyclerView.ViewHolder(binding.root) {
// 复用原有 viewHolder
private val realViewHolder by lazy(LazyThreadSafetyMode.NONE) {
PersonalItemViewHolder(context, binding, itemClickCallback)
}
fun onBind(historyEntity: PersonalHistoryEntity, entrance: String, position: Int) {
realViewHolder.bindPersonalItem(historyEntity, entrance, position)
}
}
class FollowDynamicBbSViewHolder(
val binding: CommunityAnswerItemBinding,
private val bbsId: String,
private val onItemClick: (Int, AnswerEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
private val viewHolder by lazy(LazyThreadSafetyMode.NONE) {
ForumArticleAskItemViewHolder(binding)
}
fun onBind(answer: AnswerEntity, entrance: String, path: String, position: Int) {
val articlePosition = position - 1
if (answer.type != "answer") {
val questions = Questions()
questions.id = answer.id
questions.title = answer.title
questions.description = answer.description
questions.answerCount = answer.count.answer
answer.questions = questions
}
if (path == "精华" && answer.type != "answer" && answer.type != "video") answer.type = "community_article"
if (path == "问答") answer.type = "question"
if (path == "视频") answer.type = "video"
if (answer.bbs.id.isEmpty()) answer.bbs.id = bbsId
binding.forumNameLl.visibility = View.GONE
binding.topLine.goneIf(articlePosition == 0)
val params = binding.includeVoteAndComment.root.layoutParams as LinearLayout.LayoutParams
params.width = LinearLayout.LayoutParams.MATCH_PARENT
params.leftMargin = 0
params.rightMargin = 0
binding.includeVoteAndComment.root.layoutParams = params
viewHolder.bindForumAnswerItem(answer, entrance, path, position)
viewHolder.itemView.setOnClickListener {
onItemClick(position, answer)
}
}
}
}

View File

@ -0,0 +1,504 @@
package com.gh.gamecenter.forum.home.follow.adapter
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil.ItemCallback
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.gamecenter.R
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.entity.LinkEntity
import com.gh.gamecenter.common.syncpage.ISyncAdapterHandler
import com.gh.gamecenter.common.utils.SensorsBridge
import com.gh.gamecenter.common.utils.toBinding
import com.gh.gamecenter.common.utils.toColor
import com.gh.gamecenter.entity.FollowDynamicEntity
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.eventbus.EBUserFollow
import com.gh.gamecenter.feature.entity.ConcernEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.LibaoEntity
import com.gh.gamecenter.feature.exposure.ExposureEvent
import com.gh.gamecenter.forum.home.follow.*
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_ARTICLE
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_COMMON_BANNER
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_COMMON_BANNER_WITH_CARDS
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_COMMON_HORIZONTAL_SLIDE
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_COMMON_VERTICAL_TWO_CARDS
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_EMPTY
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_FOOTER
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_GIFT_PACK
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_HEADER
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_NAVIGATION
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_POST_CARD
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_RECOMMEND
import com.gh.gamecenter.forum.home.follow.FollowItem.Companion.FOLLOW_ITEM_TYPE_RECOMMEND_USER
import com.gh.gamecenter.forum.home.follow.viewholder.*
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowHomeViewModel
import com.gh.gamecenter.home.custom.OnViewHolderAttachListener
import com.gh.gamecenter.home.custom.model.CustomPageData
class FollowHomeAdapter(
private val lifecycleOwner: LifecycleOwner,
private val viewModel: FollowHomeViewModel,
private val pageConfiguration: FollowPageConfiguration
) : ListAdapter<FollowItem, ViewHolder>(createItemCallback()), ISyncAdapterHandler {
private var loadStatus: LoadStatus? = null
private var _recyclerView: RecyclerView? = null
override fun submitList(list: List<FollowItem>?) {
val dataWithFooter = if (list.isNullOrEmpty()) {
ArrayList(list)
} else {
list + FollowFooterItem
}
super.submitList(dataWithFooter)
}
fun setLoadStatus(status: LoadStatus) {
loadStatus = status
notifyItemChanged(itemCount - 1)
}
override fun getItemViewType(position: Int): Int {
return currentList[position].itemType
}
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
var change: EBUserFollow? = null
var readStatusUpdate: ReadStatusUpdate? = null
payloads.forEach {
if (it is EBUserFollow) {
change = it
}
if (it is ReadStatusUpdate) {
readStatusUpdate = it
}
}
when {
change != null -> { // 局部刷新,关注/取消关注
(holder as? FollowRecommendListViewHolder)?.updateFollowed(change!!)
}
readStatusUpdate != null -> { // 局部刷新 关注用户是否有新消息
(holder as? FollowHomeHeaderViewHolder)?.updateReadStatus(readStatusUpdate!!.ids)
}
else -> {
super.onBindViewHolder(holder, position, payloads)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
when (viewType) {
FOLLOW_ITEM_TYPE_HEADER -> {
FollowHomeHeaderViewHolder(parent.toBinding(), object : FollowHomeHeaderViewHolder.OnEventListener {
override fun onItemClick(position: Int, data: List<FollowUserEntity>) {
viewModel.viewItemFollowed(position, data)
}
override fun onAllClick() {
viewModel.viewAllFollowed()
}
override fun onLoginClick() {
viewModel.login()
}
})
}
FOLLOW_ITEM_TYPE_RECOMMEND_USER -> {
FollowRecommendListViewHolder(parent.toBinding(),
object : FollowRecommendListAdapter.OnChildEventListener {
override fun onItemClick(childPosition: Int, userId: String) {
SensorsBridge.trackFollowPageRecommendedUserCardClick(userId, childPosition + 1)
viewModel.navigateToUserHomePage(userId)
}
override fun onFollowClick(childPosition: Int, userId: String, isFollow: Boolean) {
SensorsBridge.trackRecommendedUserFollowButtonClick(userId, childPosition + 1)
viewModel.followUser(userId, isFollow)
}
})
}
FOLLOW_ITEM_TYPE_COMMON_BANNER -> {
FollowHomeSlideListViewHolder(
parent.toBinding(),
lifecycleOwner,
object : FollowHomeSlideListViewHolder.OnChildEventListener {
override fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
) {
viewModel.navigateToLinkPageInCommonContent(position, link, data, text, exposureEvent)
}
override fun navigateToGameDetailPage(position: Int, game: GameEntity, text: String) {
viewModel.navigateToGameDetailPage(position, game, text)
}
})
}
FOLLOW_ITEM_TYPE_COMMON_BANNER_WITH_CARDS -> {
FollowHomeSlideWithCardsViewHolder(
parent.toBinding(),
viewModel.commonContentCollectionUseCase,
lifecycleOwner,
object : FollowHomeSlideWithCardsViewHolder.OnChildEventListener {
override fun navigateToGameDetailPage(
childPosition: Int,
gameEntity: GameEntity,
text: String
) {
viewModel.navigateToGameDetailPage(childPosition, gameEntity, text)
}
override fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
) {
viewModel.navigateToLinkPageInCommonContent(position, link, data, text, exposureEvent)
}
})
}
FOLLOW_ITEM_TYPE_NAVIGATION -> {
FollowNavigationViewHolder(parent.toBinding(),
object : FollowNavigationViewHolder.OnChildEventListener {
override fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
) {
viewModel.navigateToLinkPageInCommonContent(position, link, data, text, exposureEvent)
}
})
}
FOLLOW_ITEM_TYPE_RECOMMEND -> {
FollowHomeRecommendItemViewHolder(
parent.toBinding(),
object : FollowHomeRecommendItemViewHolder.OnChildEventListener {
override fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
) {
viewModel.navigateToLinkPageInCommonContent(position, link, data, text, exposureEvent)
}
})
}
FOLLOW_ITEM_TYPE_COMMON_HORIZONTAL_SLIDE ->
FollowCommonCollectionViewHolder(
parent.toBinding(),
object : FollowCommonCollectionViewHolder.OnChildEventListener {
override fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
) {
viewModel.navigateToLinkPageInCommonContent(position, link, data, text, exposureEvent)
}
override fun navigateToCommonCollectionDetailPage(data: CustomPageData.CommonContentCollection) {
viewModel.navigateToCommonCollectionDetailPage(data)
}
})
FOLLOW_ITEM_TYPE_COMMON_VERTICAL_TWO_CARDS ->
FollowCommonCollection12ViewHolder(
parent.toBinding(),
object : FollowCommonCollection12ViewHolder.OnChildEventListener {
override fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
) {
viewModel.navigateToLinkPageInCommonContent(position, link, data, text, exposureEvent)
}
override fun navigateToCommonCollectionDetailPage(data: CustomPageData.CommonContentCollection) {
viewModel.navigateToCommonCollectionDetailPage(data)
}
})
FOLLOW_ITEM_TYPE_POST_CARD -> {
FollowPostCardViewHolder(
parent.toBinding(),
pageConfiguration.entrance,
pageConfiguration.path
)
}
FOLLOW_ITEM_TYPE_GIFT_PACK -> {
GiftPackViewHolder(parent.toBinding(), object : GiftPackViewHolder.OnChildEventListener {
override fun onGameClick(game: GameEntity) {
viewModel.navigateToGameDetailPage(0, game, "礼包")
}
override fun onGiftPackageClick(libaoEntity: LibaoEntity) {
SensorsBridge.trackFollowPageContentClick("游戏礼包", libaoEntity.name ?: "")
viewModel.navigateToLibaoDetailPage(libaoEntity)
}
override fun onCopy(code: String) {
viewModel.copyExchangeCode(code)
}
})
}
FOLLOW_ITEM_TYPE_ARTICLE -> {
GameArticleViewHolder(parent.toBinding(), object : GameArticleViewHolder.OnChildEventListener {
override fun onGameCLick(game: GameEntity) {
viewModel.navigateToGameDetailPage(0, game, "关注-文章")
}
override fun onArticleClick(article: FollowDynamicEntity.Article) {
SensorsBridge.trackFollowPageContentClick("文章", article.title)
viewModel.navigateToNewsDetailPage(article.id)
}
override fun onComment(article: FollowDynamicEntity.Article) {
SensorsBridge.trackFollowPageContentClick("文章", article.title)
viewModel.navigateToArticleCommentDetailPage(article.id)
}
override fun onShare(concernEntity: ConcernEntity) {
viewModel.onShareArticle(concernEntity)
}
})
}
FOLLOW_ITEM_TYPE_FOOTER -> FollowFooterViewHolder(parent.toBinding())
FOLLOW_ITEM_TYPE_EMPTY -> {
FollowHomeEmptyViewHolder(parent.toBinding())
}
else -> FollowInvalidViewHolder(parent.toBinding())
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is FollowHomeHeaderViewHolder -> {
getRealTypeItem<FollowUserItem>(position) {
holder.bind(it.isLogin, it.data)
}
}
is FollowRecommendListViewHolder -> {
getRealTypeItem<FollowRecommendUsersItem>(position) {
holder.bindView(it.data)
}
}
is FollowHomeSlideListViewHolder -> {
getRealTypeItem<FollowCommonCollectionItem>(position) {
holder.bindView(it.data)
}
}
is FollowHomeSlideWithCardsViewHolder -> {
getRealTypeItem<FollowCommonCollectionItem>(position) {
holder.bindView(it.data)
}
}
is FollowNavigationViewHolder -> {
getRealTypeItem<FollowCommonCollectionItem>(position) {
holder.bindView(it.data)
}
}
is FollowHomeRecommendItemViewHolder -> {
getRealTypeItem<FollowCommonCollectionItem>(position) {
holder.bindView(it.data)
}
}
is FollowCommonCollectionViewHolder -> {
getRealTypeItem<FollowCommonCollectionItem>(position) {
holder.bindView(it.data)
}
}
is FollowCommonCollection12ViewHolder -> {
getRealTypeItem<FollowSplitCommonContentCollectionItem>(position) {
holder.bind(it.data, it.leftPosition)
}
}
is FollowPostCardViewHolder -> {
getRealTypeItem<FollowPostCardItem>(position) {
setItemViewBackground(holder)
holder.bind(it.data, position)
}
}
is GameArticleViewHolder -> {
getRealTypeItem<FollowArticleItem>(position) {
setItemViewBackground(holder)
holder.bind(it.data)
}
}
is GiftPackViewHolder -> {
getRealTypeItem<FollowGiftPackItem>(position) {
setItemViewBackground(holder)
holder.bind(it.libao, it.time)
}
}
is FollowFooterViewHolder -> {
holder.initFooterViewHolder(
loadStatus == LoadStatus.LIST_LOADING,
loadStatus == LoadStatus.LIST_FAILED,
loadStatus == LoadStatus.LIST_OVER,
R.string.load_over_with_click_hint
) {
if (loadStatus == LoadStatus.LIST_OVER) {
_recyclerView?.scrollToPosition(0)
} else if (loadStatus == LoadStatus.LIST_FAILED) {
viewModel.loadMore()
notifyItemChanged(itemCount - 1)
}
}
}
else -> Unit
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
_recyclerView = recyclerView
recyclerView.addOnScrollListener(onLoadMoreListener)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
recyclerView.removeOnScrollListener(onLoadMoreListener)
_recyclerView = null
}
override fun onViewAttachedToWindow(holder: ViewHolder) {
if (holder is OnViewHolderAttachListener) {
holder.onViewAttach(_recyclerView)
}
}
override fun onViewDetachedFromWindow(holder: ViewHolder) {
if (holder is OnViewHolderAttachListener) {
holder.onViewDetach(_recyclerView)
}
}
override fun getSyncData(position: Int): Pair<String, Any>? {
if (position >= itemCount) return null
val item = getItem(position)
return when {
item is FollowPostCardItem -> {
Pair(item.data.id, item.data.count)
}
else -> null
}
}
private inline fun <T> getRealTypeItem(position: Int, block: (T) -> Unit) {
val item = getItem(position)
(item as? T)?.let(block)
}
fun notifyFollowUserChanged(change: EBUserFollow) {
val position = currentList.indexOfFirst { it is FollowRecommendUsersItem }
notifyItemChanged(position, change)
}
fun notifyFollowUserReadStatus(ids: List<String>) {
val position = currentList.indexOfFirst { it is FollowUserItem }
notifyItemChanged(position, ReadStatusUpdate(ids))
}
private val onLoadMoreListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManger = recyclerView.layoutManager
if (layoutManger is LinearLayoutManager) {
val lastVisibleItemPosition = layoutManger.findLastVisibleItemPosition()
if (lastVisibleItemPosition == itemCount - 1 && loadStatus != LoadStatus.LIST_OVER && loadStatus != LoadStatus.LIST_LOADING) {
viewModel.loadMore()
}
}
}
}
}
companion object {
fun createItemCallback() = object : ItemCallback<FollowItem>() {
override fun areItemsTheSame(oldItem: FollowItem, newItem: FollowItem): Boolean {
return oldItem.areItemsTheSame(newItem)
}
override fun areContentsTheSame(oldItem: FollowItem, newItem: FollowItem): Boolean {
return oldItem.areContentsTheSame(newItem)
}
}
fun setItemViewBackground(viewHolder: ViewHolder) {
if (viewHolder.bindingAdapterPosition == 1) {
viewHolder.itemView.setBackgroundResource(R.drawable.background_shape_white_radius_12_top_only)
} else {
viewHolder.itemView.setBackgroundColor(R.color.ui_surface.toColor(viewHolder.itemView.context))
}
}
}
data class ReadStatusUpdate(
val ids: List<String>
)
}

View File

@ -0,0 +1,101 @@
package com.gh.gamecenter.forum.home.follow.adapter
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.debounceActionWithInterval
import com.gh.gamecenter.common.utils.toBinding
import com.gh.gamecenter.common.utils.toDrawable
import com.gh.gamecenter.databinding.RecyclerFollowRecommendBinding
import com.gh.gamecenter.eventbus.EBUserFollow
import com.gh.gamecenter.feature.entity.UserEntity
import com.gh.gamecenter.forum.home.follow.ResetDataChangeHelper
class FollowRecommendListAdapter(
context: Context,
private val listener: OnChildEventListener
) : RecyclerView.Adapter<FollowRecommendListAdapter.RecommendViewHolder>() {
private val resetDataChangeHelper = ResetDataChangeHelper<UserEntity>(context, this)
fun submitList(data: List<UserEntity>) {
resetDataChangeHelper.submitList(data) {
it.id ?: ""
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecommendViewHolder {
return RecommendViewHolder(parent.toBinding())
}
override fun getItemCount(): Int =
resetDataChangeHelper.dataList.size
override fun onBindViewHolder(holder: RecommendViewHolder, position: Int) {
val item = resetDataChangeHelper.dataList[position]
val context = holder.itemView.context
holder.binding.ivIcon.displayGameIcon(item.icon, null)
holder.binding.tvFollowerCount.text = if (item.count.fans > NUMBER_MAX) {
context.getString(R.string.fans_with_number_max)
} else {
context.getString(R.string.fans_with_number, item.count.fans)
}
val createCount = item.count.question + item.count.communityArticle + item.count.video
holder.binding.tvArticleCount.text = if (createCount > NUMBER_MAX) {
context.getString(R.string.creations_with_number_max)
} else {
context.getString(R.string.creations_with_number, createCount)
}
holder.binding.tvSign.text = if (item.introduce.isBlank()) {
context.getString(R.string.user_default_introduce)
} else {
item.introduce
}
holder.binding.tvFollow.text = if (item.isFollowed) {
context.getString(R.string.concerned)
} else {
context.getString(R.string.menu_concern)
}
holder.itemView.background = R.drawable.bg_shape_recycler_follow_recommend.toDrawable(context)
holder.itemView.setOnClickListener {
listener.onItemClick(position, item.id ?: "")
}
holder.binding.tvFollow.setOnClickListener {
debounceActionWithInterval(it.id, 1000) {
listener.onFollowClick(position, item.id ?: "", !item.isFollowed)
}
}
}
fun updateFollowed(change: EBUserFollow) {
val position = resetDataChangeHelper.dataList.indexOfFirst { it.id == change.userId }
if (position != -1) {
val item = resetDataChangeHelper.dataList[position]
item.isFollowed = change.isFollow
notifyItemChanged(position)
}
}
companion object {
private const val NUMBER_MAX = 99
}
class RecommendViewHolder(val binding: RecyclerFollowRecommendBinding) : ViewHolder(binding.root)
interface OnChildEventListener {
fun onItemClick(childPosition: Int, userId: String)
fun onFollowClick(childPosition: Int, userId: String, isFollow: Boolean)
}
}

View File

@ -0,0 +1,214 @@
package com.gh.gamecenter.forum.home.follow.adapter
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.gamecenter.R
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.databinding.RecyclerFollowedListAllBinding
import com.gh.gamecenter.databinding.RecyclerFollowedListBbsBinding
import com.gh.gamecenter.databinding.RecyclerFollowedListPersonBinding
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.forum.home.follow.ResetDataChangeHelper
import com.gh.gamecenter.forum.home.follow.model.FollowRepository.Companion.FOLLOW_UPDATE_TYPE_USER
class FollowedHeaderAdapter(
private val context: Context,
private val listener: OnChildEventListener
) :
RecyclerView.Adapter<ViewHolder>() {
/**
* 1.当 _selectedPosition 为 -1 时
* 不显示 viewAll 按钮
* 显示选中框
*
* 2. 当_selectedPosition 为 -2 时
* 不显示 viewAll 按钮
* 暂无选中框
*/
private var _selectedPosition = -1
private val showAll: Boolean
get() = _selectedPosition == -1
private val resetDataChangeHelper = ResetDataChangeHelper<FollowUserEntity>(context, this)
val dataList: List<FollowUserEntity>
get() = resetDataChangeHelper.dataList
fun submitList(data: List<FollowUserEntity>, selectedPosition: Int = -1) {
_selectedPosition = selectedPosition
resetDataChangeHelper.submitList(data) {
"${it.id}-${it.type}-${it.isShowTip}"
}
}
override fun getItemCount(): Int =
if (showAll) resetDataChangeHelper.dataCount + 1 else resetDataChangeHelper.dataCount
override fun getItemViewType(position: Int): Int {
return if (showAll) {
if (position < resetDataChangeHelper.dataCount) {
if (resetDataChangeHelper.getItem(position).type == FOLLOW_UPDATE_TYPE_USER) {
VIEW_TYPE_BBS
} else {
VIEW_TYPE_GAME
}
} else {
VIEW_TYPE_VIEW_ALL
}
} else {
if (resetDataChangeHelper.getItem(position).type == FOLLOW_UPDATE_TYPE_USER) {
VIEW_TYPE_BBS
} else {
VIEW_TYPE_GAME
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
when (viewType) {
VIEW_TYPE_GAME -> {
FollowedGameViewHolder(parent.toBinding(), ::onSelectedChanged)
}
VIEW_TYPE_BBS -> {
FollowedPersonViewHolder(parent.toBinding(), ::onSelectedChanged)
}
else -> FollowedViewAllViewHolder(parent.toBinding(), listener::onViewAll)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is FollowedGameViewHolder -> {
holder.bindView(
resetDataChangeHelper.getItem(position),
position,
_selectedPosition == position,
showAll
)
}
is FollowedPersonViewHolder -> {
holder.bindView(
resetDataChangeHelper.getItem(position),
position,
_selectedPosition == position,
showAll
)
}
}
}
private fun onSelectedChanged(position: Int) {
selectPosition(position)
listener.onViewItem(position)
}
fun selectPosition(position: Int) {
if (_selectedPosition != -1 && _selectedPosition != position) {
val lastSelectedPosition = _selectedPosition
_selectedPosition = position
notifyItemChanged(lastSelectedPosition)
notifyItemChanged(_selectedPosition)
_selectedPosition = position
}
}
fun clearSelectedPosition() {
val lastSelectedPosition = _selectedPosition
_selectedPosition = -2
if (lastSelectedPosition >= 0) {
notifyItemChanged(lastSelectedPosition)
}
}
companion object {
private const val VIEW_TYPE_GAME = 0
private const val VIEW_TYPE_BBS = 1
private const val VIEW_TYPE_VIEW_ALL = 2
}
private class FollowedGameViewHolder(
val binding: RecyclerFollowedListBbsBinding,
private val itemClick: (Int) -> Unit,
) : ViewHolder(binding.root) {
fun bindView(item: FollowUserEntity, position: Int, isSelected: Boolean, showName: Boolean) {
if (showName) {
binding.tvName.goneIf(false)
binding.tvName.text = item.bbs.name
} else {
binding.tvName.goneIf(true)
}
binding.tvName.setTextColor(R.color.text_secondary.toColor(itemView.context))
binding.ivIcon.displayGameIcon(item.icon, null)
val resourceId = if (isSelected) {
R.drawable.background_shape_theme_radius_14
} else {
R.drawable.background_shape_white_radius_14
}
binding.gIndicator.goneIf(!item.isShowTip)
binding.vIconBackground.background = resourceId.toDrawable(itemView.context)
itemView.setOnClickListener {
itemClick(position)
}
}
}
private class FollowedPersonViewHolder(
val binding: RecyclerFollowedListPersonBinding,
private val itemClick: (Int) -> Unit,
) : ViewHolder(binding.root) {
fun bindView(item: FollowUserEntity, position: Int, isSelected: Boolean, showName: Boolean) {
if (showName) {
binding.tvName.goneIf(false)
binding.tvName.text = item.user.name
} else {
binding.tvName.goneIf(true)
}
binding.tvName.setTextColor(R.color.text_secondary.toColor(itemView.context))
binding.ivIcon.displayGameIcon(item.icon, null)
val resourceId = if (isSelected) {
R.drawable.background_shape_theme_radius_999
} else {
R.drawable.background_shape_white_radius_999
}
binding.gIndicator.goneIf(!item.isShowTip)
binding.vIconBackground.background = resourceId.toDrawable(itemView.context)
itemView.setOnClickListener {
itemClick(position)
}
}
}
private class FollowedViewAllViewHolder(
binding: RecyclerFollowedListAllBinding,
private val viewAll: () -> Unit,
) : ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
viewAll()
}
}
}
interface OnChildEventListener {
fun onViewItem(position: Int)
fun onViewAll()
}
}

View File

@ -0,0 +1,31 @@
package com.gh.gamecenter.forum.home.follow.adapter
import android.annotation.SuppressLint
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.forum.home.follow.fragment.FollowDynamicListFragment
class FollowedViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
private val dataList = arrayListOf<FollowUserEntity>()
@SuppressLint("NotifyDataSetChanged")
fun addData(newData: List<FollowUserEntity>) {
if (dataList.isEmpty()) {
dataList.addAll(newData)
notifyDataSetChanged()
} else {
val start = dataList.size
dataList.addAll(newData)
notifyItemRangeChanged(start, newData.size)
}
}
override fun getItemCount(): Int = dataList.size
override fun createFragment(position: Int): Fragment {
val item = dataList[position]
return FollowDynamicListFragment.newInstance(item.user.id ?: "", item.bbs.id,item.bbs.type)
}
}

View File

@ -0,0 +1,47 @@
package com.gh.gamecenter.forum.home.follow.eventlistener
import com.gh.gamecenter.common.entity.LinkEntity
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.feature.entity.ArticleEntity
import com.gh.gamecenter.feature.entity.ConcernEntity
import com.gh.gamecenter.feature.entity.GameEntity
import com.gh.gamecenter.feature.entity.LibaoEntity
import com.gh.gamecenter.feature.exposure.ExposureEvent
import com.gh.gamecenter.home.custom.model.CustomPageData
interface OnFollowHomeEventListener {
fun viewItemFollowed(position: Int, data: List<FollowUserEntity>)
fun viewAllFollowed()
fun login()
fun followUser(userId: String, isFollow: Boolean)
fun navigateToLinkPageInCommonContent(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
)
fun navigateToGameDetailPage(position: Int, game: GameEntity, text: String)
fun navigateToCommonCollectionDetailPage(data: CustomPageData.CommonContentCollection)
fun navigateToUserHomePage(userId: String)
fun onPostCardClick(position: Int, article: ArticleEntity)
fun navigateToLibaoDetailPage(libaoEntity: LibaoEntity)
fun navigateToNewsDetailPage(articleId: String)
fun navigateToArticleCommentDetailPage(articleId: String)
fun onShareArticle(concernEntity: ConcernEntity)
fun copyExchangeCode(code: String)
}

View File

@ -0,0 +1,283 @@
package com.gh.gamecenter.forum.home.follow.fragment
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
import androidx.core.view.children
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import com.gh.common.util.DirectUtils
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.fragment.LazyFragment
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.databinding.FragmentAllFollowedBinding
import com.gh.gamecenter.forum.detail.ForumDetailActivity
import com.gh.gamecenter.forum.home.follow.AllFollowedNoTopItem
import com.gh.gamecenter.forum.home.follow.AllFollowedNormalItem
import com.gh.gamecenter.forum.home.follow.adapter.AllFollowedAdapter
import com.gh.gamecenter.forum.home.follow.viewmodel.AllFollowedViewModel
import com.gh.gamecenter.livedata.EventObserver
import com.lightgame.listeners.OnBackPressedListener
import com.lightgame.utils.toast.ToastHelper
import splitties.resources.color
import java.util.*
import kotlin.math.abs
class AllFollowedFragment : LazyFragment(), OnBackPressedListener {
private lateinit var binding: FragmentAllFollowedBinding
private val viewModel by viewModels<AllFollowedViewModel>()
private val itemTouchHelper = ItemTouchHelper(MyItemTouchCallback())
private var originalTopList = listOf<AllFollowedNormalItem>()
private val paint by lazy {
Paint().apply {
color = R.color.text_primary.toColor(requireContext())
textSize = 14F.dip2px().toFloat()
}
}
private val overPaint by lazy {
Paint().apply {
color = R.color.ui_surface.toColor(requireContext())
}
}
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
AllFollowedAdapter(viewModel) {
startDrag(it)
}
}
override fun getRealLayoutId(): Int = R.layout.fragment_all_followed
override fun onRealLayoutInflated(inflatedView: View) {
binding = FragmentAllFollowedBinding.bind(inflatedView)
}
override fun onFragmentFirstVisible() {
super.onFragmentFirstVisible()
binding.rvFollowed.layoutManager = LinearLayoutManager(requireContext())
binding.rvFollowed.adapter = adapter
binding.rvFollowed.addItemDecoration(MyItemDecoration(requireContext(), paint, overPaint))
itemTouchHelper.attachToRecyclerView(binding.rvFollowed)
with(viewModel) {
dataList.observe(viewLifecycleOwner) {
originalTopList = it.filter(AllFollowedNormalItem::isTop)
adapter.setData(it)
}
operateTopSuccessfully.observe(viewLifecycleOwner, EventObserver {
// 提交成功
ToastHelper.showToast(requireContext(), getString(R.string.save_successfully))
finish(true)
})
detailDestination.observe(viewLifecycleOwner, EventObserver {
if (it.isUser) {
DirectUtils.directToHomeActivity(requireContext(), it.user.id)
} else {
startActivity(ForumDetailActivity.getIntent(requireContext(), it.bbs.id, ""))
}
})
loadFirst()
}
binding.tvRight.setOnClickListener {
if (adapter.isEdit && isDataChanged()) {
viewModel.operateTop(adapter.getTopItem())
} else {
adapter.changeToEditable()
binding.tvRight.text = if (adapter.isEdit) "保存" else "完成"
val textResId = if (adapter.isEdit) R.string.finish else R.string.manager
binding.tvRight.setText(textResId)
}
}
binding.ivBack.setOnClickListener {
if (adapter.isEdit && isDataChanged()) {
showSaveTipsDialog()
} else {
finish()
}
}
}
private fun showSaveTipsDialog() {
DialogHelper.showDialog(
requireContext(),
getString(R.string.archive_dialog_title),
getString(R.string.whether_to_save_changes),
cancelText = getString(R.string.dialog_nickname_cancel),
confirmText = getString(R.string.servers_subscribed_game_unsubscribe_dialog_confirm),
confirmClickCallback = {
viewModel.operateTop(adapter.getTopItem())
},
cancelClickCallback = {
finish()
},
extraConfig = DialogHelper.Config(showCloseIcon = false, centerTitle = true, centerContent = true)
)
}
private fun isDataChanged(): Boolean = originalTopList != adapter.dataList.filter(AllFollowedNormalItem::isTop)
private fun startDrag(viewHolder: RecyclerView.ViewHolder) {
itemTouchHelper.startDrag(viewHolder)
}
override fun onHandleBackPressed(): Boolean {
if (adapter.isEdit && isDataChanged()) {
showSaveTipsDialog()
return true
}
finish()
return true
}
private fun finish(isSorted: Boolean = false) {
if (viewModel.hasReadSetIds.isNotEmpty() || isSorted) {
val intent = Intent()
// 如果顺序发生变化,则直接通知外部重新加载列表,无需局部更新阅读状态
if (!isSorted) {
intent.putStringArrayListExtra(KEY_HAS_READ_ID, viewModel.hasReadSetIds.toList().toArrayList())
}
activity?.setResult(Activity.RESULT_OK, intent)
}
activity?.finish()
}
override fun onDarkModeChanged() {
super.onDarkModeChanged()
paint.color = R.color.text_primary.toColor(requireContext())
overPaint.color = R.color.ui_surface.toColor(requireContext())
binding.rvFollowed.tryToClearRecycler()
binding.rvFollowed.recycledViewPool.clear()
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}
companion object {
const val KEY_HAS_READ_ID = "key_has_read_id"
}
private class MyItemDecoration(
private val context: Context,
private val paint: Paint,
private val overPaint: Paint
) : ItemDecoration() {
private val headerHeight by lazy(LazyThreadSafetyMode.NONE) {
HEADER_HEIGHT.dip2px()
}
private val screenWidth by lazy(LazyThreadSafetyMode.NONE) {
context.resources.displayMetrics.widthPixels
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
// 是否需要绘制 置顶 悬浮头部
val adapter = parent.adapter
if (adapter is AllFollowedAdapter) {
// 寻找最后一个 置顶 的itemView
val lastTopView = parent.children.lastOrNull {
val position = parent.getChildAdapterPosition(it)
val item = if (position != -1) {
adapter.getItem(position)
} else {
null
}
(item is AllFollowedNormalItem && item.isTop) || item is AllFollowedNoTopItem
}
if (lastTopView != null) {
// 绘制 置顶 悬浮条
val bottom = if (lastTopView.bottom >= headerHeight) {
headerHeight
} else {
lastTopView.bottom
}
c.drawRect(
Rect(0, 0, screenWidth, bottom),
overPaint
)
val text = "置顶(${adapter.topItemCount}/${adapter.followedCount})"
drawFollowedHeader(text, c, bottom)
} else {
// 绘制 “我的关注” 悬浮条
c.drawRect(
Rect(0, 0, screenWidth, headerHeight),
overPaint
)
drawFollowedHeader("我的关注", c, headerHeight)
}
}
}
private fun drawFollowedHeader(text: String, c: Canvas, bottom: Int) {
val fontMetrics = paint.fontMetrics
val centerYDistanceToBaseline =
(abs(fontMetrics.descent) + abs(fontMetrics.ascent) + abs(fontMetrics.leading)) / 2 - abs(
fontMetrics.descent
)
val baselineY = bottom - 25F.dip2px().toFloat() + centerYDistanceToBaseline
c.drawText(text, 0, text.length, 16F.dip2px().toFloat(), baselineY, paint)
}
companion object {
private const val HEADER_HEIGHT = 50F
}
}
private class MyItemTouchCallback : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val adapter = recyclerView.adapter as? AllFollowedAdapter
if (adapter != null) {
val position = viewHolder.bindingAdapterPosition
val item = adapter.getItem(position)
if (item is AllFollowedNormalItem && item.isTop) {
val targetPosition = target.bindingAdapterPosition
val targetItem = adapter.getItem(targetPosition)
if (targetItem is AllFollowedNormalItem && targetItem.isTop) {
val newList = ArrayList(adapter.dataList)
val dataPosition = newList.indexOfFirst { it.data.id == item.data.id }
val targetDataPosition = newList.indexOfFirst { it.data.id == targetItem.data.id }
Collections.swap(newList, dataPosition, targetDataPosition)
adapter.setData(newList)
return true
}
}
}
return false
}
override fun isLongPressDragEnabled(): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
}
}

View File

@ -0,0 +1,271 @@
package com.gh.gamecenter.forum.home.follow.fragment
import android.animation.ValueAnimator
import android.graphics.PointF
import android.os.Bundle
import android.view.View
import android.view.animation.Interpolator
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.fragment.LazyFragment
import com.gh.gamecenter.common.utils.dip2px
import com.gh.gamecenter.common.utils.visibleIf
import com.gh.gamecenter.databinding.FragmentFollowDynamicBinding
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.forum.home.follow.FollowDynamicActivity.Companion.KEY_SELECTED_POSITION
import com.gh.gamecenter.forum.home.follow.FollowDynamicActivity.Companion.KEY_USERS
import com.gh.gamecenter.forum.home.follow.adapter.FollowedHeaderAdapter
import com.gh.gamecenter.forum.home.follow.adapter.FollowedViewPagerAdapter
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowDynamicViewModel
import com.gh.gamecenter.livedata.EventObserver
import kotlin.math.pow
class FollowDynamicFragment : LazyFragment() {
private lateinit var binding: FragmentFollowDynamicBinding
private val viewModel by activityViewModels<FollowDynamicViewModel>()
private var selectedPosition = 0
private val screenCenterX by lazy(LazyThreadSafetyMode.NONE) {
resources.displayMetrics.widthPixels / 2
}
private val offsetX by lazy(LazyThreadSafetyMode.NONE) {
screenCenterX - 32F.dip2px()
}
private val layoutManager by lazy(LazyThreadSafetyMode.NONE) {
LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
}
private val helper by lazy {
OrientationHelper.createHorizontalHelper(layoutManager)
}
private val followedAdapter by lazy {
FollowedHeaderAdapter(requireContext(), object : FollowedHeaderAdapter.OnChildEventListener {
override fun onViewItem(position: Int) {
selectedPosition = position
binding.viewPager.setCurrentItem(position, false)
alignIndicator()
}
override fun onViewAll() = Unit
})
}
private val dynamicListVpAdapter by lazy {
FollowedViewPagerAdapter(this)
}
override fun getRealLayoutId(): Int = R.layout.fragment_follow_dynamic
override fun onRealLayoutInflated(inflatedView: View) {
binding = FragmentFollowDynamicBinding.bind(inflatedView)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val list = arguments?.getParcelableArrayList<FollowUserEntity>(KEY_USERS)
if (list != null) {
viewModel.addFirstPageUsers(list)
}
selectedPosition = arguments?.getInt(KEY_SELECTED_POSITION, 0) ?: 0
}
override fun onFragmentFirstVisible() {
super.onFragmentFirstVisible()
with(viewModel) {
followedUsers.observe(viewLifecycleOwner, Observer { (isFirst, data) ->
followedAdapter.submitList(data, selectedPosition)
if (isFirst) {
scrollSelectedPositionToCenter(false)
startAnimation(true)
}
dynamicListVpAdapter.addData(data)
})
}
binding.rvFollowedHeader.layoutManager = layoutManager
binding.rvFollowedHeader.adapter = followedAdapter
binding.viewPager.adapter = dynamicListVpAdapter
binding.viewPager.offscreenPageLimit = 1
binding.viewPager.setCurrentItem(selectedPosition, false)
binding.viewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
override fun onPageScrollStateChanged(state: Int) {
super.onPageScrollStateChanged(state)
if (state != 0) {
binding.ivIndicator.visibleIf(false)
followedAdapter.clearSelectedPosition()
} else {
binding.ivIndicator.visibleIf(true)
followedAdapter.selectPosition(selectedPosition)
}
}
override fun onPageSelected(position: Int) {
if (selectedPosition != position) {
selectedPosition = position
followedAdapter.selectPosition(position)
scrollSelectedPositionToCenter(true)
}
}
})
with(viewModel) {
finishAction.observe(viewLifecycleOwner, EventObserver {
startAnimation(false)
requireActivity().finish()
requireActivity().overridePendingTransition(0, R.anim.no_anim)
})
}
}
private fun scrollSelectedPositionToCenter(isSmoothScroll: Boolean) {
binding.rvFollowedHeader.clearOnScrollListeners()
if (isSmoothScroll) {
// 1. 如果当前 itemView 在屏幕内,则只需要直接将其滚动到中间即可
if (!checkTargetViewIfExist()) {
// 2. 如果当前itemView不在屏幕内则需要先将 itemView 滚动到屏幕内,在将其滚到屏幕中间
binding.rvFollowedHeader.addOnScrollListener(object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
binding.rvFollowedHeader.clearOnScrollListeners()
checkTargetViewIfExist()
}
}
})
val vector = layoutManager.computeScrollVectorForPosition(selectedPosition) ?: PointF()
val linearSmoothScroller = object : LinearSmoothScroller(requireContext()) {
override fun getHorizontalSnapPreference(): Int {
super.getHorizontalSnapPreference()
return if (vector.x > 0) SNAP_TO_END else SNAP_TO_START
}
}
linearSmoothScroller.targetPosition = selectedPosition
layoutManager.startSmoothScroll(linearSmoothScroller)
}
} else {
layoutManager.scrollToPositionWithOffset(selectedPosition, offsetX)
}
binding.rvFollowedHeader.addOnScrollListener(object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
alignIndicator()
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
alignIndicator()
}
})
}
private fun alignIndicator() {
val targetView = layoutManager.findViewByPosition(selectedPosition)
if (targetView != null) {
binding.ivIndicator.translationX = distanceToStart(targetView).toFloat()
}
}
private fun checkTargetViewIfExist(): Boolean {
val targetView = layoutManager.findViewByPosition(selectedPosition)
if (targetView != null) {
val distanceToCenter = distanceToCenter(targetView)
if (distanceToCenter != 0) {
binding.rvFollowedHeader.smoothScrollBy(distanceToCenter, 0)
}
}
return targetView != null
}
private val easeOutInterpolator = Interpolator { input -> 1 - (1 - input).pow(2) }
private fun startAnimation(isStart: Boolean) {
val values = if (isStart) {
floatArrayOf(ANIMATION_START_VALUE, ANIMATION_END_VALUE)
} else {
floatArrayOf(ANIMATION_END_VALUE, ANIMATION_START_VALUE)
}
val valueAnimator = ValueAnimator.ofFloat(*values)
val followHeaderTranslationY =
requireContext().resources.getDimensionPixelSize(R.dimen.follow_detail_header_translation_y)
val dynamicListTranslationY =
requireContext().resources.getDimensionPixelSize(R.dimen.follow_detail_content_translation_y)
with(valueAnimator) {
duration = ANIMATION_DURATION
interpolator = easeOutInterpolator
addUpdateListener {
val value = it.animatedValue as Float
val alphaEnd = 5F / 8F
val alpha = if (value >= 0 && value <= alphaEnd) {
value / alphaEnd
} else {
1F
}
binding.ivTopBackground.alpha = alpha
binding.rvFollowedHeader.alpha = alpha
binding.viewPager.alpha = alpha
if (value >= 0.5F) {
binding.ivIndicator.alpha = (value - 0.5F) * 2
} else {
binding.ivIndicator.alpha = 0F
}
binding.ivTopBackground.translationY = followHeaderTranslationY * (1 - value)
binding.rvFollowedHeader.translationY = followHeaderTranslationY * (1 - value)
binding.viewPager.translationY = dynamicListTranslationY * (1 - value)
if (isStart && value == 1F) {
viewModel.startLoad()
}
}
start()
}
}
private fun distanceToCenter(targetView: View): Int {
val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2
return childCenter - containerCenter
}
private fun distanceToStart(targetView: View): Int = distanceToCenter(targetView) + screenCenterX
override fun onDarkModeChanged() {
super.onDarkModeChanged()
followedAdapter.notifyItemRangeChanged(0, followedAdapter.itemCount, "")
}
companion object {
private const val ANIMATION_DURATION = 400L
private const val ANIMATION_START_VALUE = 0F
private const val ANIMATION_END_VALUE = 1F
}
}

View File

@ -0,0 +1,318 @@
package com.gh.gamecenter.forum.home.follow.fragment
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.gh.common.util.DirectUtils
import com.gh.gamecenter.R
import com.gh.gamecenter.common.base.fragment.BaseFragment
import com.gh.gamecenter.common.base.fragment.ToolbarFragment
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.databinding.FragmentFollowDynamicListBinding
import com.gh.gamecenter.feature.entity.CommentEntity
import com.gh.gamecenter.feature.entity.ForumVideoEntity
import com.gh.gamecenter.forum.detail.ForumDetailActivity
import com.gh.gamecenter.forum.home.follow.FollowActivityResultLauncher
import com.gh.gamecenter.forum.home.follow.FollowDynamicBbsItem
import com.gh.gamecenter.forum.home.follow.FollowDynamicPersonalItem
import com.gh.gamecenter.forum.home.follow.adapter.FollowDynamicAdapter
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowDynamicListViewModel
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowDynamicViewModel
import com.gh.gamecenter.livedata.EventObserver
import com.gh.gamecenter.qa.BbsType
import com.gh.gamecenter.qa.entity.ArticleDetailEntity
import com.gh.gamecenter.qa.entity.QuestionsDetailEntity
class FollowDynamicListFragment : BaseFragment<Unit>() {
private var userId: String = ""
private var bbsId: String = ""
private var bbsType: String = ""
private val paint by lazy {
Paint().apply {
color = R.color.ui_divider.toColor(requireContext())
strokeWidth = 0.5F.dip2px().toFloat()
}
}
private val viewModel by viewModels<FollowDynamicListViewModel>()
private val followDynamicViewModel by activityViewModels<FollowDynamicViewModel>()
private val binding by lazy {
FragmentFollowDynamicListBinding.inflate(layoutInflater, null, false)
}
private val adapter by lazy {
FollowDynamicAdapter(requireContext(), viewModel)
}
private lateinit var launcher: FollowActivityResultLauncher
override fun getInflatedLayout(): View {
return binding.root
}
override fun getLayoutId(): Int = R.layout.fragment_follow_dynamic_list
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userId = arguments?.getString(KEY_USER_ID) ?: ""
bbsId = arguments?.getString(KEY_BBS_ID) ?: ""
bbsType = arguments?.getString(KEY_BBS_TYPE) ?: ""
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
with(viewModel) {
loadStatus.observe(viewLifecycleOwner) { (loadStatus, isPullRefresh) ->
if (isPullRefresh) {
if (loadStatus != LoadStatus.INIT_LOADING) {
binding.srlRefresh.isRefreshing = false
}
} else {
binding.reuseLoading.root.goneIf(loadStatus != LoadStatus.INIT_LOADING)
binding.reuseNoData.root.goneIf(loadStatus != LoadStatus.INIT_EMPTY)
binding.reuseNoConnection.root.goneIf(loadStatus != LoadStatus.LIST_FAILED)
binding.srlRefresh.isEnabled =
!(loadStatus == LoadStatus.INIT_FAILED || loadStatus == LoadStatus.INIT_LOADING)
}
adapter.setLoadStatus(loadStatus)
}
dataList.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
personDetailDestination.observe(viewLifecycleOwner, EventObserver { (personalEntity, position) ->
launcher.onPersonItemClick(position, personalEntity)
})
bbsDetailDestination.observe(viewLifecycleOwner, EventObserver { (position, answer) ->
launcher.onBbsItemClick(position, answer)
})
}
followDynamicViewModel.isStartLoadAction.observe(viewLifecycleOwner, Observer {
if (it) {
viewModel.loadFirst(userId, bbsId, false)
}
})
binding.tvTitle.text = if (userId.isNotBlank()) {
getString(R.string.follow_detail_person_updates)
} else {
if (bbsType == BbsType.OFFICIAL_BBS.value) {
getString(R.string.follow_detail_bbs_updates)
} else {
getString(R.string.follow_detail_game_updates)
}
}
binding.tvRight.text =
if (userId.isNotBlank()) {
getString(R.string.user_home_page)
} else {
getString(R.string.go_to_forum)
}
binding.tvRight.setOnClickListener {
if (userId.isNotBlank()) {
DirectUtils.directToHomeActivity(requireContext(), userId)
} else {
startActivity(ForumDetailActivity.getIntent(requireContext(), bbsId, ""))
}
}
}
private fun initView() {
launcher = FollowActivityResultLauncher(requireActivity().activityResultRegistry,
object : FollowActivityResultLauncher.OnResultListener {
override fun onArticleDetailResult(position: Int, articleDetailEntity: ArticleDetailEntity) {
val item = adapter.currentList[position]
if (item is FollowDynamicPersonalItem) {
item.data.apply {
val newCount = count
newCount.vote = articleDetailEntity.count.vote
newCount.comment = articleDetailEntity.count.comment
count = newCount
title = articleDetailEntity.title
brief = articleDetailEntity.content.removeVideoContent().removeInsertedContent()
.clearHtmlFormatCompletely()
me.isCommunityArticleVote = articleDetailEntity.me.isCommunityArticleVote
}
}
if (item is FollowDynamicBbsItem) {
item.data.apply {
val newCount = count
newCount.vote = articleDetailEntity.count.vote
newCount.comment = articleDetailEntity.count.comment
count = newCount
title = articleDetailEntity.title
brief = articleDetailEntity.content.removeVideoContent().removeInsertedContent()
.clearHtmlFormatCompletely()
me.isCommunityArticleVote = articleDetailEntity.me.isCommunityArticleVote
}
}
adapter.notifyItemChanged(position)
}
override fun onCommentResult(position: Int, commentEntity: CommentEntity) {
val item = adapter.currentList[position]
if (item is FollowDynamicPersonalItem) {
item.data.apply {
val newCount = count
newCount.vote = commentEntity.vote
newCount.comment = commentEntity.reply
count = newCount
brief = commentEntity.content ?: ""
me.isAnswerVoted = commentEntity.me?.isCommentVoted ?: false
}
}
if (item is FollowDynamicBbsItem) {
item.data.apply {
val newCount = count
newCount.vote = commentEntity.vote
newCount.comment = commentEntity.reply
count = newCount
brief = commentEntity.content ?: ""
me.isAnswerVoted = commentEntity.me?.isCommentVoted ?: false
}
}
adapter.notifyItemChanged(position)
}
override fun onQuestionsDetailResult(position: Int, questionsDetailEntity: QuestionsDetailEntity) {
val item = adapter.currentList[position]
if (item is FollowDynamicPersonalItem) {
item.data.apply {
val newCount = count
newCount.answer = questionsDetailEntity.count.answer - count.reply
count = newCount
title = questionsDetailEntity.title ?: ""
}
}
if (item is FollowDynamicBbsItem) {
item.data.apply {
val newCount = count
newCount.answer = questionsDetailEntity.count.answer - count.reply
count = newCount
title = questionsDetailEntity.title ?: ""
}
}
adapter.notifyItemChanged(position)
}
override fun onForumVideoResult(position: Int, forumVideoEntity: ForumVideoEntity) {
val item = adapter.currentList[position]
if (item is FollowDynamicPersonalItem) {
item.data.apply {
val newCount = count
newCount.vote = forumVideoEntity.count.vote
newCount.comment = forumVideoEntity.count.comment
count = newCount
des = forumVideoEntity.des
title = forumVideoEntity.title
me.isVoted = forumVideoEntity.me.isVoted
}
}
if (item is FollowDynamicBbsItem) {
item.data.apply {
val newCount = count
newCount.vote = forumVideoEntity.count.vote
newCount.comment = forumVideoEntity.count.comment
count = newCount
des = forumVideoEntity.des
title = forumVideoEntity.title
me.isVoted = forumVideoEntity.me.isVoted
}
}
adapter.notifyItemChanged(position)
}
})
viewLifecycleOwner.lifecycle.addObserver(launcher)
binding.rvDynamic.layoutManager = LinearLayoutManager(requireContext())
binding.rvDynamic.itemAnimator = null
binding.rvDynamic.addItemDecoration(FollowItemDecoration(paint))
binding.rvDynamic.adapter = adapter
binding.srlRefresh.setOnRefreshListener {
viewModel.loadFirst(userId, bbsId, true)
}
binding.ivClose.setOnClickListener {
followDynamicViewModel.finish()
}
}
override fun onDarkModeChanged() {
super.onDarkModeChanged()
context?.let {
binding.vHeaderBackground.background = R.drawable.background_shape_white_radius_8_top_only.toDrawable(it)
}
binding.rvDynamic.tryToClearRecycler()
binding.rvDynamic.recycledViewPool.clear()
adapter.notifyItemRangeChanged(0, adapter.itemCount)
paint.color = R.color.ui_divider.toColor(requireContext())
}
private class FollowItemDecoration(
private val paint: Paint
) : RecyclerView.ItemDecoration() {
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
parent.children.forEach { child ->
val bottom = child.bottom
c.drawLine(
16F.dip2px().toFloat(),
bottom.toFloat(),
(child.width - 16F.dip2px()).toFloat(),
bottom.toFloat(),
paint
)
}
}
}
companion object {
private const val KEY_USER_ID = "key_user_id"
private const val KEY_BBS_ID = "key_bbs_id"
private const val KEY_BBS_TYPE = "key_bbs_type"
fun newInstance(userId: String, bbsId: String, bbsType: String): FollowDynamicListFragment {
val fragment = FollowDynamicListFragment()
val bundle = Bundle().apply {
putString(KEY_USER_ID, userId)
putString(KEY_BBS_ID, bbsId)
putString(KEY_BBS_TYPE, bbsType)
}
fragment.arguments = bundle
return fragment
}
}
}

View File

@ -0,0 +1,550 @@
package com.gh.gamecenter.forum.home.follow.fragment
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import com.alibaba.android.arouter.launcher.ARouter
import com.gh.common.util.CheckLoginUtils
import com.gh.common.util.DirectUtils
import com.gh.gamecenter.GameDetailActivity
import com.gh.gamecenter.R
import com.gh.gamecenter.ShareCardActivity
import com.gh.gamecenter.ShareCardPicActivity
import com.gh.gamecenter.common.base.fragment.LazyFragment
import com.gh.gamecenter.common.baselist.LoadStatus
import com.gh.gamecenter.common.constant.EntranceConsts
import com.gh.gamecenter.common.constant.RouteConsts
import com.gh.gamecenter.common.utils.*
import com.gh.gamecenter.core.AppExecutor
import com.gh.gamecenter.core.iinterface.IScrollable
import com.gh.gamecenter.core.provider.IVisitManagerProvider
import com.gh.gamecenter.core.utils.MD5Utils
import com.gh.gamecenter.databinding.FragmentFollowHomeBinding
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.eventbus.EBUserFollow
import com.gh.gamecenter.feature.provider.IMessageDetailProvider
import com.gh.gamecenter.forum.home.CommunityHomeFragment
import com.gh.gamecenter.forum.home.CommunityHomeViewModel
import com.gh.gamecenter.forum.home.ForumScrollCalculatorHelper
import com.gh.gamecenter.forum.home.follow.*
import com.gh.gamecenter.forum.home.follow.adapter.FollowHomeAdapter
import com.gh.gamecenter.forum.home.follow.fragment.AllFollowedFragment.Companion.KEY_HAS_READ_ID
import com.gh.gamecenter.forum.home.follow.model.FollowRepository.Companion.FOLLOW_UPDATE_TYPE_GAME
import com.gh.gamecenter.forum.home.follow.model.FollowRepository.Companion.FOLLOW_UPDATE_TYPE_USER
import com.gh.gamecenter.forum.home.follow.viewholder.FollowHomeHeaderViewHolder
import com.gh.gamecenter.forum.home.follow.viewholder.FollowRecommendListViewHolder
import com.gh.gamecenter.forum.home.follow.viewmodel.FollowHomeViewModel
import com.gh.gamecenter.game.commoncollection.detail.CustomCommonCollectionDetailActivity
import com.gh.gamecenter.libao.LibaoDetailActivity
import com.gh.gamecenter.livedata.EventObserver
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.login.user.UserViewModel
import com.gh.gamecenter.message.view.concern.ConcernFragment
import com.gh.gamecenter.newsdetail.NewsShareDialog
import com.shuyu.gsyvideoplayer.video.base.GSYVideoView
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class FollowHomeFragment : LazyFragment(), IScrollable {
override fun addSyncPageObserver(): Boolean {
return true
}
private val viewModel by viewModels<FollowHomeViewModel>()
private val communityHomeViewModel by viewModels<CommunityHomeViewModel>(
ownerProducer = { requireParentFragment() }
)
private lateinit var userViewModel: UserViewModel
private var userId: String? = null
private var dynamicType: String? = null
private var binding: FragmentFollowHomeBinding? = null
private var hasLoaded = false
private lateinit var pageConfiguration: FollowPageConfiguration
private var isFirstTimeEnter = true
private val handler = Handler(Looper.getMainLooper())
private val dividerPaint by lazy {
Paint().apply {
color = R.color.ui_divider.toColor(requireContext())
strokeWidth = 0.5F.dip2px().toFloat()
}
}
private val rvBackgroundPaint by lazy {
Paint().apply {
color = R.color.ui_surface.toColor(requireContext())
}
}
private val layoutManager by lazy {
LinearLayoutManager(context)
}
private val adapter: FollowHomeAdapter by lazy {
FollowHomeAdapter(viewLifecycleOwner, viewModel, pageConfiguration)
}
// 3s 以后才开始计算
private var enterStartTime = 0L
private lateinit var allFollowLauncher: ActivityResultLauncher<Unit>
private var mScrollCalculatorHelper: ForumScrollCalculatorHelper? = null
private val headerViewHolder by lazy(LazyThreadSafetyMode.NONE) {
binding?.let {
FollowHomeHeaderViewHolder(it.layoutHeader, object : FollowHomeHeaderViewHolder.OnEventListener {
override fun onItemClick(position: Int, data: List<FollowUserEntity>) {
viewModel.viewItemFollowed(position, data)
}
override fun onAllClick() {
viewModel.viewAllFollowed()
}
override fun onLoginClick() {
viewModel.login()
}
})
}
}
override fun provideSyncAdapter(): RecyclerView.Adapter<*> {
return adapter
}
override fun getRealLayoutId(): Int = R.layout.fragment_follow_home
override fun onRealLayoutInflated(inflatedView: View) {
binding = FragmentFollowHomeBinding.bind(inflatedView)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageConfiguration = FollowPageConfiguration("社区-关注", "")
allFollowLauncher = requireActivity().activityResultRegistry.register(
KEY_LAUNCH_ALL_FOLLOW,
this,
object : ActivityResultContract<Unit, List<String>?>() {
override fun createIntent(context: Context, input: Unit?): Intent {
return AllFollowedActivity.getIntent(context)
}
override fun parseResult(resultCode: Int, intent: Intent?): List<String>? {
if (resultCode == Activity.RESULT_OK) {
val hasReadIds = intent?.getStringArrayListExtra(KEY_HAS_READ_ID)
return hasReadIds ?: listOf()
}
return null
}
}
) { value ->
value?.let {
if (it.isEmpty()) {
viewModel.loadFollowedUsers()
} else {
adapter.notifyFollowUserReadStatus(it)
}
}
}
viewModel.hasFollowedUser.observe(this, Observer {
communityHomeViewModel.updateHasFollowUser(it)
})
}
override fun initRealView() {
super.initRealView()
mScrollCalculatorHelper = ForumScrollCalculatorHelper(R.id.horizontalVideoView, R.id.verticalVideoView, 0)
binding?.recyclerView?.itemAnimator = null
userViewModel = viewModelProvider(UserViewModel.Factory(requireActivity().application))
userViewModel.loginObsUserinfo.observe(viewLifecycleOwner) {
/* 由于其它页面每次初始化 userViewModel时loginObsUserInfo都会被赋值一次为避免重复加载这里需要过滤重复的用户信息 */
if (hasLoaded && userId == it?.data?.userId) {
// 已经加载过数据并且userId不变不需要重复加载
return@observe
}
hasLoaded = true
if (it?.data == null) {
// 未登录,加载推荐关注列表
userId = null
viewModel.loadFirst(userId != null, isPullToRefresh = false)
} else {
// 已登录,加载已关注列表
userId = it.data.userId
if (userId != null) {
viewModel.loadFirst(userId != null, isPullToRefresh = false)
}
}
}
communityHomeViewModel.filterFollowedAction.observe(viewLifecycleOwner, EventObserver {
val newDynamicType = when (it) {
0 -> ""
1 -> FOLLOW_UPDATE_TYPE_USER
else -> FOLLOW_UPDATE_TYPE_GAME
}
if (dynamicType != newDynamicType) {
dynamicType = newDynamicType
viewModel.loadFirst(userId != null, dynamicType ?: "", false)
}
})
with(viewModel) {
loadStatus.observe(viewLifecycleOwner) { (loadStatus, isPullRefresh) ->
if (isPullRefresh) {
if (loadStatus != LoadStatus.INIT_LOADING) {
binding?.srlRefresh?.isRefreshing = false
}
} else {
binding?.layoutLoading?.root?.goneIf(loadStatus != LoadStatus.INIT_LOADING)
}
adapter.setLoadStatus(loadStatus)
if (loadStatus == LoadStatus.INIT_LOADED) {
AppExecutor.uiExecutor.executeWithDelay(Runnable {
tryCatchInRelease {
scroll()
mScrollCalculatorHelper?.onScrollStateChanged(
binding?.recyclerView!!,
RecyclerView.SCROLL_STATE_IDLE
)
}
}, 100)
}
}
dataList.observe(viewLifecycleOwner) {
if (it.size == 1) {
// 由于第一个是头部如果list长度为1则说明后面没有数据
binding?.llEmptyContainer?.goneIf(false)
binding?.recyclerView?.goneIf(true)
val headerItem = it.firstOrNull() as? FollowUserItem
if (headerItem != null) {
headerViewHolder?.bind(headerItem.isLogin, headerItem.data)
setEmptyLayout(headerItem)
}
} else {
binding?.llEmptyContainer?.goneIf(true)
binding?.recyclerView?.goneIf(false)
setDataList(it)
}
}
followedDetailDestination.observe(viewLifecycleOwner, EventObserver { (position, data) ->
FollowDynamicActivity.start(requireContext(), position, data)
activity?.overridePendingTransition(0, 0) // 取消进入和退出动画
})
myFollowedListDestination.observe(viewLifecycleOwner, EventObserver {
allFollowLauncher.launch(Unit)
})
loginDestination.observe(viewLifecycleOwner, EventObserver {
CheckLoginUtils.checkLogin(context, "关注", null)
})
gameDetailDestination.observe(viewLifecycleOwner, EventObserver {
GameDetailActivity.startGameDetailActivity(
requireContext(),
it.id,
"",
-1,
traceEvent = it.exposureEvent
)
})
linkDestination.observe(viewLifecycleOwner, EventObserver { (link, exposureEvent) ->
DirectUtils.directToLinkPage(requireContext(), link, "关注页面", "", exposureEvent)
})
commonCollectionDestination.observe(viewLifecycleOwner, EventObserver {
val intent = CustomCommonCollectionDetailActivity.getIntent(
requireContext(),
it.id,
it.layout,
"自定义页面",
)
startActivity(intent)
})
userHomeDestination.observe(viewLifecycleOwner, EventObserver {
DirectUtils.directToHomeActivity(requireContext(), it, 1, "", "")
})
libaoDetailDestination.observe(viewLifecycleOwner, EventObserver {
val intent = LibaoDetailActivity.getIntent(requireContext(), it, false, "$mEntrance+关注列表")
startActivity(intent)
})
newDetailDestination.observe(viewLifecycleOwner, EventObserver {
ARouter.getInstance().build(RouteConsts.activity.newsDetailActivity)
.withString(EntranceConsts.KEY_NEWSID, it)
.withString(EntranceConsts.KEY_ENTRANCE, "")
.navigation(requireActivity(), ConcernFragment.NEWS_MESSAGE_ARTICLE_REQUEST)
})
articleCommentDetailDestination.observe(viewLifecycleOwner, EventObserver {
val messageDetailProvider = ARouter.getInstance().build(RouteConsts.provider.messageDetail)
.navigation() as? IMessageDetailProvider
if (messageDetailProvider != null) {
val intent = messageDetailProvider.getIntentById(requireContext(), it, -1, false, "")
startActivityForResult(intent, ConcernFragment.NEWS_MESSAGE_ARTICLE_REQUEST)
}
})
shareArticleDestination.observe(viewLifecycleOwner, EventObserver { concernEntity ->
NewsShareDialog.show(
requireActivity() as AppCompatActivity,
concernEntity.shortId,
concernEntity.id,
concernEntity.gameIcon,
concernEntity.title
)
})
copyExchangeCodeAction.observe(viewLifecycleOwner, EventObserver {
it.copyTextAndToast()
})
updateOkhttpCacheAction.observe(viewLifecycleOwner, EventObserver {
// 更新okhttp缓存数据
val visitManagerProvider =
ARouter.getInstance()
.build(RouteConsts.provider.visitManager)
.navigation() as? IVisitManagerProvider
visitManagerProvider?.updateOkhttpCache(requireContext(), it)
})
}
binding?.srlRefresh?.setOnRefreshListener {
mScrollCalculatorHelper?.currentPlayer?.release()
mScrollCalculatorHelper?.reset()
val isLogin = userId != null
viewModel.loadFirst(isLogin, dynamicType ?: "", true)
}
binding?.recyclerView?.addOnScrollListener(object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
binding?.recyclerView?.let {
mScrollCalculatorHelper?.onScrollStateChanged(it, newState)
}
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (dy != 0) scroll()
}
})
}
private fun setEmptyLayout(headerItem: FollowUserItem) {
binding?.layoutEmpty?.run {
root.goneIf(false)
if (headerItem.isLogin) {
reuseNoneDataDescTv.goneIf(true)
} else {
reuseNoneDataDescTv.goneIf(false)
reuseNoneDataDescTv.setText(R.string.follow_home_no_login_tips)
}
}
}
private fun scroll() {
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
mScrollCalculatorHelper?.onScroll(viewModel.videoList, firstVisibleItem, lastVisibleItem)
}
override fun onFragmentResume() {
resumeVideo()
super.onFragmentResume()
handler.postDelayed({
enterStartTime = System.currentTimeMillis()
SensorsBridge.trackFollowTabBrowse(isFirstTimeEnter)
}, TRACK_ENTER_TIME)
}
override fun onFragmentPause() {
super.onFragmentPause()
pauseVideo()
isFirstTimeEnter = false
handler.removeCallbacksAndMessages(null)
if (enterStartTime != 0L) {
val endTime = System.currentTimeMillis()
val stayLength = "${(endTime - enterStartTime) / MS_UNIT}"
SensorsBridge.trackFollowTabBrowseDuration(stayLength)
}
enterStartTime = 0L
}
private fun resumeVideo() {
mScrollCalculatorHelper?.run {
if (currentPlayer != null
&& currentPosition >= 0
&& currentPosition < viewModel.videoList.size
) {
val video = viewModel.videoList.safelyGetInRelease(currentPosition)
if (video != null) {
val position = ForumScrollCalculatorHelper.getPlaySchedule(MD5Utils.getContentMD5(video.url))
//这里必须要延迟操作,否则会白屏
mBaseHandler.postDelayed({
tryCatchInRelease {
if (position != 0L) {
if (currentPlayer?.currentState == GSYVideoView.CURRENT_STATE_PAUSE) {
currentPlayer?.startPlayLogic(true)
}
} else {
currentPlayer?.release()
}
}
}, 50)
}
}
}
}
private fun pauseVideo() {
mScrollCalculatorHelper?.run {
if (currentPlayer != null
&& currentPosition >= 0
&& currentPosition < viewModel.videoList.size
) {
currentPlayer?.onVideoPause()
val position = currentPlayer?.getCurrentPosition() ?: 0L
val video = viewModel.videoList.safelyGetInRelease(currentPosition)
if (video != null) {
ForumScrollCalculatorHelper.savePlaySchedule(MD5Utils.getContentMD5(video.url), position)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
mScrollCalculatorHelper?.currentPlayer?.release()
binding = null
}
private fun setDataList(dataList: List<FollowItem>) {
binding?.run {
if (recyclerView.adapter == null) {
recyclerView.layoutManager = layoutManager
recyclerView.addItemDecoration(FollowItemDecoration(dividerPaint, rvBackgroundPaint))
recyclerView.adapter = adapter
}
adapter.submitList(dataList)
}
}
override fun scrollToTop() {
binding?.recyclerView?.scrollToPosition(0)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserFollow(change: EBUserFollow) {
adapter.notifyFollowUserChanged(change)
}
override fun onDarkModeChanged() {
super.onDarkModeChanged()
val rvFollows = binding?.recyclerView
if (rvFollows != null) {
if (parentFragment is CommunityHomeFragment) {
(parentFragment as CommunityHomeFragment).translateTopBg(rvFollows.computeVerticalScrollOffset() - 8F.dip2px())
}
rvFollows.tryToClearRecycler()
rvFollows.recycledViewPool.clear()
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}
dividerPaint.color = R.color.ui_divider.toColor(requireContext())
rvBackgroundPaint.color = R.color.ui_surface.toColor(requireContext())
}
private class FollowItemDecoration(
private val dividerPaint: Paint,
private val backgroundPaint: Paint
) : RecyclerView.ItemDecoration() {
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
parent.children.forEach { child ->
val position = parent.getChildAdapterPosition(child)
// 1.第一个itemView为头部关注人列表不需要分割线
// 2.默认推荐数据不需要分割线
val isLoggedIn = UserManager.getInstance().isLoggedIn
if (isLoggedIn && position > 0) {
val bottom = child.bottom
c.drawLine(
16F.dip2px().toFloat(),
bottom.toFloat(),
(child.width - 16F.dip2px()).toFloat(),
bottom.toFloat(),
dividerPaint
)
}
}
}
/**
* 由于 关注用户/论坛 列表需要和顶部 tab连在一起组成渐变色所以RecyclerView不能直接设置背景颜色
* 在这里绘制除了 关注用户/论坛 列表其它位置的背景颜色
* + 12 是为了预留出圆角的高度
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val firstChild = parent.children.firstOrNull()
if (firstChild != null) {
c.drawRect(Rect(0, firstChild.bottom + 12F.dip2px(), parent.width, parent.bottom), backgroundPaint)
}
}
}
companion object {
private const val KEY_LAUNCH_ALL_FOLLOW = "key_launch_all_follow"
const val LOCATION_FOLLOW_PAGE = "关注页面"
private const val TRACK_ENTER_TIME = 3000L
private const val MS_UNIT = 1000.0
}
}

View File

@ -0,0 +1,42 @@
package com.gh.gamecenter.forum.home.follow.model
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.common.utils.toRequestBody
import com.gh.gamecenter.entity.FollowOperateTopRequest
import com.gh.gamecenter.forum.home.follow.AllFollowedNormalItem
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.retrofit.service.ApiService
import com.halo.assistant.HaloApp
import io.reactivex.Single
import okhttp3.ResponseBody
class AllFollowRepository(private val apiService: ApiService) {
fun loadMyFollowedUser(pageNo: Int) =
apiService.getMyFollowedUsers(
UserManager.getInstance().userId,
PackageUtils.getGhVersionName(),
HaloApp.getInstance().channel,
pageNo,
true,
mapOf()
)
.map { list ->
list.map {
AllFollowedNormalItem(it, it.isTop)
}
}
fun operateTop(tops: List<FollowOperateTopRequest>): Single<ResponseBody> {
val body = mapOf("top" to tops).toRequestBody()
return apiService.operateTop(
UserManager.getInstance().userId,
PackageUtils.getGhVersionName(),
HaloApp.getInstance().channel,
body
)
}
fun postRead(type: String, typeId: String) =
apiService.postRead(UserManager.getInstance().userId, type, typeId)
}

View File

@ -0,0 +1,47 @@
package com.gh.gamecenter.forum.home.follow.model
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.forum.home.follow.FollowDynamicBbsItem
import com.gh.gamecenter.forum.home.follow.FollowDynamicPersonalItem
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.gamecenter.retrofit.service.ApiService
import com.halo.assistant.HaloApp
class FollowDynamicRepository(
private val apiService: ApiService
) {
fun loadData(userId: String, bbsId: String, pageNo: Int) =
if (userId.isNotBlank()) {
loadPersonDynamic(userId, pageNo)
} else {
loadBbsDynamic(bbsId, pageNo)
}
private fun loadPersonDynamic(userId: String, pageNo: Int) =
apiService.getPersonalHistory(
userId,
pageNo,
HaloApp.getInstance().channel,
PERSON_DYNAMIC_FILTER
).map {
it.map(::FollowDynamicPersonalItem)
}
private fun loadBbsDynamic(bbsId: String, pageNo: Int) =
apiService.getAllForumList(bbsId, BBS_DYNAMIC_SORT, pageNo, mapOf())
.map {
it.map(::FollowDynamicBbsItem)
}
companion object {
private const val PERSON_DYNAMIC_FILTER = "scene:follow_personal,type:all"
private const val BBS_DYNAMIC_SORT = "time.reply:-1&type=community_article|question|video"
fun newInstance(): FollowDynamicRepository = FollowDynamicRepository(
RetrofitManager.getInstance().api
)
}
}

View File

@ -0,0 +1,204 @@
package com.gh.gamecenter.forum.home.follow.model
import com.gh.common.util.LibaoUtils
import com.gh.common.util.PackageUtils
import com.gh.gamecenter.core.utils.GsonUtils
import com.gh.gamecenter.core.utils.UrlFilterUtils
import com.gh.gamecenter.entity.FollowCommonContentCollection
import com.gh.gamecenter.entity.FollowDynamicEntity
import com.gh.gamecenter.entity.FollowDynamicEntity.Companion.FOLLOW_UPDATE_TYPE_ARTICLE
import com.gh.gamecenter.entity.FollowDynamicEntity.Companion.FOLLOW_UPDATE_TYPE_LIBAO
import com.gh.gamecenter.entity.FollowDynamicEntity.Companion.FOLLOW_UPDATE_TYPE_LIBAO_EXCHANGE
import com.gh.gamecenter.entity.FollowDynamicEntity.Companion.FOLLOW_UPDATE_TYPE_USER_POST
import com.gh.gamecenter.entity.FollowUserEntity
import com.gh.gamecenter.feature.entity.LibaoEntity
import com.gh.gamecenter.forum.home.follow.*
import com.gh.gamecenter.home.custom.model.CustomPageData
import com.gh.gamecenter.home.custom.model.CustomPageItem.Companion.COMMON_CONTENT_COLLECTION_LAYOUT_DOUBLE_ROW_BANNER
import com.gh.gamecenter.home.custom.model.CustomPageItem.Companion.COMMON_CONTENT_COLLECTION_LAYOUT_DOUBLE_ROW_VERTICAL_CARD
import com.gh.gamecenter.home.custom.model.CustomPageItem.Companion.COMMON_CONTENT_COLLECTION_LAYOUT_VERTICAL_IMAGE_TEXT
import com.gh.gamecenter.login.user.UserManager
import com.gh.gamecenter.retrofit.RetrofitManager
import com.gh.gamecenter.retrofit.service.ApiService
import com.halo.assistant.HaloApp
import io.reactivex.Single
class FollowRepository(
private val apiService: ApiService,
private val newApiService: ApiService
) {
private var hasFollowUser = false
private val version: String
get() = PackageUtils.getGhVersionName()
private val channel: String
get() = HaloApp.getInstance().channel
fun loadHomeData(isLogin: Boolean, type: String, pageNo: Int) =
if (isLogin) {
loadFollowedUsers()
.flatMap { users ->
if (users.isEmpty()) {
hasFollowUser = false
// 默认推荐
loadFollowRecommend(pageNo)
.map {
listOf(FollowUserItem(true, listOf())) + it
}
} else {
// 关注动态
hasFollowUser = true
loadFollowUpdates(type, pageNo)
.map {
listOf(FollowUserItem(true, users)) + it
}
}
}
} else {
loadFollowRecommend(pageNo)
.map {
listOf(FollowUserItem(false, listOf())) + it
}
}
fun loadMore(type: String, pageNo: Int) =
if (hasFollowUser) {
loadFollowUpdates(type, pageNo)
} else {
loadFollowRecommend(pageNo)
}
fun loadFollowedUsers(): Single<List<FollowUserEntity>> {
return newApiService.getMyFollowedUsers(
UserManager.getInstance().userId,
PackageUtils.getGhVersionName(),
HaloApp.getInstance().channel,
1,
true,
mapOf("page_size" to 20)
).onErrorReturnItem(listOf())
}
private fun loadFollowUpdates(type: String, pageNo: Int): Single<List<FollowItem>> =
newApiService.getFollowUpdates(
UserManager.getInstance().userId,
type,
pageNo,
PackageUtils.getGhVersionName(),
HaloApp.getInstance().channel
)
.onErrorReturnItem(listOf())
.map(::transformFollowUpdatesToItemData)
fun loadFollowRecommend(pageNo: Int): Single<List<FollowItem>> =
if (pageNo == 1) {
loadRecommendUsers().zipWith(
loadFollowCommonCollection(pageNo)
) { user, commonCollections ->
val data = arrayListOf<FollowItem>()
if (user.data.isNotEmpty()) {
data.add(user)
}
data.addAll(commonCollections)
data
}
} else {
Single.just(listOf())
}
private fun loadRecommendUsers(): Single<FollowRecommendUsersItem> =
newApiService.getRecommendUser(version, channel)
.onErrorReturnItem(listOf())
.map(::FollowRecommendUsersItem)
private fun loadFollowCommonCollection(pageNo: Int): Single<List<FollowItem>> =
newApiService.getFollowCommonCollection(version, channel, pageNo)
.onErrorReturnItem(listOf())
.map { components ->
val data = arrayListOf<FollowItem>()
components.forEach {
val layout = it.linkCommonCollection?.layout
if (layout == COMMON_CONTENT_COLLECTION_LAYOUT_DOUBLE_ROW_BANNER
|| layout == COMMON_CONTENT_COLLECTION_LAYOUT_DOUBLE_ROW_VERTICAL_CARD
|| layout == COMMON_CONTENT_COLLECTION_LAYOUT_VERTICAL_IMAGE_TEXT
) {
data.addAll(convertSplitCommonContentCollection(it))
} else {
// data.title 为后台配置的板块title
it.linkCommonCollection.name = it.title
data.add(
FollowCommonCollectionItem(it)
)
}
}
data
}
private fun convertSplitCommonContentCollection(item: FollowCommonContentCollection): List<FollowSplitCommonContentCollectionItem> {
val linkCommonCollection = item.linkCommonCollection
// data.title 为后台配置的板块title
linkCommonCollection.name = item.title
val data = linkCommonCollection.data
val items = arrayListOf<FollowSplitCommonContentCollectionItem>()
(data.indices step 2).forEach { left ->
items.add(
FollowSplitCommonContentCollectionItem(
linkCommonCollection,
left
)
)
}
return items
}
fun followUser(userId: String, isFollow: Boolean) =
if (isFollow) {
apiService.postFollowing(userId)
} else {
apiService.deleteFollowing(userId)
}
fun postArticleVisit(articleId: String) =
apiService.postArticleVisit(articleId)
companion object {
const val FOLLOW_UPDATE_TYPE_USER = "user"
const val FOLLOW_UPDATE_TYPE_GAME = "game"
fun transformFollowUpdatesToItemData(updates: List<FollowDynamicEntity>): List<FollowItem> {
val data = arrayListOf<FollowItem>()
updates.forEach {
when (it.type) {
FOLLOW_UPDATE_TYPE_LIBAO -> {
if (it.libao != null) {
FollowGiftPackItem(it.libao, it.time)
} else {
null
}
}
FOLLOW_UPDATE_TYPE_LIBAO_EXCHANGE -> {
if (it.libaoExchange != null) {
FollowGiftPackItem(it.libaoExchange, it.time)
} else {
null
}
}
FOLLOW_UPDATE_TYPE_ARTICLE -> it.article?.let(::FollowArticleItem)
FOLLOW_UPDATE_TYPE_USER_POST -> it.userPost?.let(::FollowPostCardItem)
else -> null
}?.let(data::add)
}
return data
}
fun newInstance() = FollowRepository(RetrofitManager.getInstance().api, RetrofitManager.getInstance().newApi)
}
}

View File

@ -0,0 +1,85 @@
package com.gh.gamecenter.forum.home.follow.viewholder
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.common.util.NewLogUtils
import com.gh.gamecenter.R
import com.gh.gamecenter.common.entity.LinkEntity
import com.gh.gamecenter.common.utils.toColor
import com.gh.gamecenter.databinding.CommonCollection12ItemCustomBinding
import com.gh.gamecenter.entity.CommonCollectionContentEntity
import com.gh.gamecenter.feature.exposure.ExposureEvent
import com.gh.gamecenter.home.custom.model.CustomPageData
import com.gh.gamecenter.home.custom.ui.CommonContentCollection12Ui
/**
* 关注-双列banner 双列竖式卡片 竖排图文列表
* 通用链接合集(1-2样式)
*/
class FollowCommonCollection12ViewHolder(
val binding: CommonCollection12ItemCustomBinding,
private val listener: OnChildEventListener
) :
ViewHolder(binding.root) {
private lateinit var _data: CustomPageData.CommonContentCollection
private val collection12Ui by lazy {
CommonContentCollection12Ui(binding, object : CommonContentCollection12Ui.OnCollection12Listener {
override fun onChildItemClick(childPosition: Int, contentEntity: CommonCollectionContentEntity) {
val linkEntity = contentEntity.linkEntity
NewLogUtils.logCommonCollectionClick(
_data.id,
_data.name,
"",
"",
"关注列表",
"关注首页",
linkEntity.title ?: "",
contentEntity.addedContent1 ?: "",
contentEntity.addedContent2 ?: "",
linkEntity.type ?: "",
linkEntity.text ?: "",
childPosition + 1
)
NewLogUtils.logCommonCategoryHomeContentClick(
contentEntity.title,
linkEntity.type ?: "",
linkEntity.link ?: "",
linkEntity.text ?: "",
_data.name,
_data.id,
"关注",
""
)
listener.navigateToLinkPage(bindingAdapterPosition, linkEntity, _data, "内容卡片", null)
}
override fun navigateToCommonCollectionDetailPage(collection: CustomPageData.CommonContentCollection) {
listener.navigateToCommonCollectionDetailPage(collection)
}
})
}
fun bind(data: CustomPageData.CommonContentCollection, leftPosition: Int) {
_data = data
collection12Ui.bind(data, leftPosition)
itemView.setBackgroundColor(R.color.ui_surface.toColor(itemView.context))
}
interface OnChildEventListener {
fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
)
fun navigateToCommonCollectionDetailPage(data: CustomPageData.CommonContentCollection)
}
}

View File

@ -0,0 +1,105 @@
package com.gh.gamecenter.forum.home.follow.viewholder
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.gh.common.exposure.ExposureManager
import com.gh.common.exposure.ExposureTraceUtils
import com.gh.common.util.NewLogUtils
import com.gh.gamecenter.R
import com.gh.gamecenter.common.entity.LinkEntity
import com.gh.gamecenter.common.utils.toColor
import com.gh.gamecenter.databinding.CommonCollectionListCustomBinding
import com.gh.gamecenter.entity.CommonCollectionContentEntity
import com.gh.gamecenter.entity.ExposureLinkEntity
import com.gh.gamecenter.entity.FollowCommonContentCollection
import com.gh.gamecenter.feature.exposure.ExposureEvent
import com.gh.gamecenter.feature.exposure.ExposureType
import com.gh.gamecenter.home.custom.model.CustomPageData
import com.gh.gamecenter.home.custom.ui.CommonContentHorizontalSlideListUi
/**
* 关注-通用内容合集-横排滑动Banner
* 关注-通用内容合集-横排竖式卡片
* 关注-通用内容合集-横排图文列表
*/
class FollowCommonCollectionViewHolder(
val binding: CommonCollectionListCustomBinding,
private val listener: OnChildEventListener
) : ViewHolder(binding.root) {
private lateinit var _data: FollowCommonContentCollection
private val horizontalSlideUi by lazy {
CommonContentHorizontalSlideListUi(binding,
object : CommonContentHorizontalSlideListUi.OnHorizontalSlideListListener {
override fun addExposureEvent(childPosition: Int, link: ExposureLinkEntity) = Unit
override fun onChildItemClick(childPosition: Int, entity: CommonCollectionContentEntity) {
val linkEntity = entity.linkEntity
NewLogUtils.logCommonCollectionClick(
_data.linkCommonCollection.id,
_data.linkCommonCollection.name,
"",
"",
"关注推荐列表",
"关注页",
linkEntity.title ?: "",
entity.addedContent1 ?: "",
entity.addedContent2 ?: "",
linkEntity.type ?: "",
linkEntity.text ?: "",
childPosition + 1
)
NewLogUtils.logCommonCategoryHomeContentClick(
entity.title,
linkEntity.type ?: "",
linkEntity.link ?: "",
linkEntity.text ?: "",
_data.linkCommonCollection.name,
_data.linkCommonCollection.id,
"",
""
)
linkEntity.exposureEvent?.let {
val clickEvent = ExposureEvent(
payload = it.payload,
source = it.source,
eTrace = ExposureTraceUtils.appendTrace(it),
event = ExposureType.CLICK
)
if (linkEntity.type != "game") {
ExposureManager.log(clickEvent)
}
}
listener.navigateToLinkPage(bindingAdapterPosition, linkEntity, _data.linkCommonCollection, "内容卡片", null)
}
override fun navigateToCommonCollectionDetailPage(data: CustomPageData.CommonContentCollection) {
listener.navigateToCommonCollectionDetailPage(data)
}
})
}
fun bindView(data: FollowCommonContentCollection) {
_data = data
horizontalSlideUi.bind(_data.linkCommonCollection)
itemView.setBackgroundColor(R.color.ui_surface.toColor(itemView.context))
}
interface OnChildEventListener {
fun navigateToLinkPage(
position: Int,
link: LinkEntity,
data: CustomPageData.CommonContentCollection,
text: String,
exposureEvent: ExposureEvent?
)
fun navigateToCommonCollectionDetailPage(data: CustomPageData.CommonContentCollection)
}
}

Some files were not shown because too many files have changed in this diff Show More