Compare commits

..

1943 Commits

Author SHA1 Message Date
1d4333ffe4 Merge branch 'hotfix-v5.5.4-454-fix_launch_type' into 'release'
修复启动类型上报异常问题

See merge request halo/android/assistant-android!188
2022-01-18 15:12:10 +08:00
337bbe7ec3 修复启动类型上报异常问题 2022-01-18 15:11:16 +08:00
fd482f32f0 Merge branch 'hotfix-v5.5.4-454-argument_error' into 'release'
修改我的收藏、历史记录页面参数传递错误

See merge request halo/android/assistant-android!187
2022-01-18 14:05:45 +08:00
755e4f24ff 修改我的收藏、历史记录页面参数传递错误 2022-01-18 12:11:16 +08:00
2f1137b97b Merge branch 'hotfix-v5.5.4-454-generic_crash' into 'release'
Hotfix v5.5.4 454 generic crash

See merge request halo/android/assistant-android!185
2022-01-18 10:21:36 +08:00
78541a56f8 调整包被篡改后的闪退提示弹窗 2022-01-18 10:18:29 +08:00
7d9a0d3308 尝试修复 Fresco 未能被正常初始化的问题 2022-01-18 10:18:29 +08:00
5bef19fac2 Merge branch 'hotfix-v5.5.4-454-generic_crash' into 'release'
修复 5.5.4 的一些闪退

See merge request halo/android/assistant-android!184
2022-01-18 09:25:08 +08:00
beb1c95a2b 修复 5.5.4 的一些闪退
1. 捕抓选择图片上传时的异常
2. 捕抓骨架图在老旧 oppo 设备上的夜间模式闪退
2022-01-18 09:25:08 +08:00
4bd2d0aff7 Merge branch 'hotfix-v5.5.4-454-generic_crash' into 'release'
修复一些闪退

See merge request halo/android/assistant-android!183
2022-01-17 17:58:17 +08:00
ca73e267d8 Merge branch 'hotfix-v5.5.4-454-crashes' into 'release'
修改sentry上一些闪退问题

See merge request halo/android/assistant-android!182
2022-01-17 17:57:28 +08:00
628b91eed8 修复一些闪退
1. 修复未成年弹窗偶发的闪退
2. 捕抓富文本点击闪退
3. 修复首页轮播图偶发的数组越界闪退
2022-01-17 17:56:36 +08:00
9c56b67dcc 1.修改帖子详情闪退问题
2.修改视频贴评论页还未显示出来点击评论闪退问题
3.修改视频帖信息展开功能闪退问题
2022-01-17 16:36:07 +08:00
f4e7c9acc5 Merge branch 'hotfix-v5.5.4-454-my_game_activity_crash' into 'release'
修复我的游戏页显示右上角引导图导致的闪退问题

See merge request halo/android/assistant-android!181
2022-01-17 16:26:52 +08:00
lyr
0a94a27331 修复我的游戏页显示右上角引导图导致的闪退问题 2022-01-17 16:24:21 +08:00
62d6ea002d 版本更新至 5.5.5 2022-01-17 10:02:06 +08:00
e0aa84c877 Merge branch 'hotfix-v5.5.4-454-fix_wrong_so_loaded' into 'release'
尝试修复 VIVO 和 OPPO 设备加载错误 SO 的问题

See merge request halo/android/assistant-android!180
2022-01-17 10:00:57 +08:00
25f4c027bf 尝试修复 VIVO 和 OPPO 设备加载错误 SO 的问题 2022-01-17 09:59:08 +08:00
56f817bb7a Merge branch 'dev' into 'release'
修复下载管理sentry问题

See merge request halo/android/assistant-android!179
2022-01-13 14:43:54 +08:00
1b976106e4 尝试修复下载管理游戏更新列表数组越界闪退 2022-01-13 11:02:16 +08:00
0e3ab3c3a7 Merge branch 'hotfix-v5.5.3-453-crash' into 'release'
修改sentry上闪退问题

See merge request halo/android/assistant-android!178
2022-01-13 09:10:53 +08:00
948588b874 1.修改外部浏览器跳转app闪退问题 2.修改帖子详情空指针问题 2022-01-12 19:55:10 +08:00
095139079e 修复下载管理已安装列表空指针闪退 尝试修复数组越界闪退 2022-01-12 17:40:16 +08:00
228ab0ca40 Merge branch 'hotfix-v5.5.3-453-room_crash' into 'release'
尝试处理数据库的CursorWindowAllocationException异常

See merge request halo/android/assistant-android!176
2022-01-12 14:11:06 +08:00
fdcb7cbbf7 Merge branch 'hotfix-v5.5.3-453-optimizate' into 'release'
修复游戏单、搜索论坛问题

See merge request halo/android/assistant-android!175
2022-01-12 11:21:38 +08:00
933f46e250 1.修改游戏单选择游戏列表复用问题
2.修改论坛搜索页热门论坛超过20个闪退问题
2022-01-12 11:08:44 +08:00
2282787bd7 Merge branch 'hotfix-v5.5.3-453-wrong_so_crash' into 'release'
再次启用 64 位 SO, 尝试修复部分 5.0 & 5.1 设备依旧读取错误图片 SO 的问题

See merge request halo/android/assistant-android!174
2022-01-12 10:52:08 +08:00
c4d3698a9b 再次启用 64 位 SO, 尝试修复部分 5.0 & 5.1 设备依旧读取错误图片 SO 的问题 2022-01-12 10:50:29 +08:00
80cc86a791 尝试处理数据库的CursorWindowAllocationException异常 2022-01-12 10:30:38 +08:00
3646672849 版本更新至 5.5.4 2022-01-11 17:26:15 +08:00
9a4c8f1f06 Merge branch 'dev' into 'release'
合并 5.5.0 游戏单活动

See merge request halo/android/assistant-android!173
2022-01-11 17:16:39 +08:00
ce303b37b7 修复下载管理已安装列表在安装/卸载游戏后的显示异常 2022-01-10 17:50:39 +08:00
0bbffb20cd Merge branch 'release' into 'dev'
Merge hotfix from release

See merge request halo/android/assistant-android!172
2022-01-05 11:49:50 +08:00
415626ef58 版本更新至 5.5.3 2022-01-04 16:25:03 +08:00
e9ad25dc36 Merge branch 'hotfix-v5.5.2-452-realname_compat_layout' into 'release'
处理实名页面在小屏设备上的显示问题

See merge request halo/android/assistant-android!171
2022-01-04 15:44:48 +08:00
92215163bf 处理实名页面在小屏设备上的显示问题 2022-01-04 15:44:06 +08:00
b3ffe61376 Merge branch 'hotfix-v5.5.2-452-select_id_image_crash' into 'release'
修复部分设备在实名认证人工审核选择图片时的闪退问题

See merge request halo/android/assistant-android!170
2022-01-04 11:28:51 +08:00
8e6048883a 修复部分设备在实名认证人工审核选择图片时的闪退问题 2022-01-04 11:27:50 +08:00
532e40efc3 Merge branch 'hotfix-v5.5.2-452-meizu_skeleton_crash' into 'release'
捕抓骨架图在魅族设备上的闪退

See merge request halo/android/assistant-android!169
2022-01-04 10:52:12 +08:00
b775083437 捕抓骨架图在魅族设备上的闪退 2022-01-04 10:51:09 +08:00
lyr
fc5de50f61 【光环助手V5.5.0】游戏单活动:解锁好游种草新姿势(1230测试 7)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1638#note_130160 2021-12-31 16:43:59 +08:00
lyr
b896af2a15 【光环助手V5.5.0】游戏单活动:解锁好游种草新姿势(完善"url参数增加是否可以由web端处理返回键")https://git.shanqu.cc/pm/halo-app-issues/-/issues/1638#note_129888 2021-12-30 19:10:10 +08:00
lyr
93ba447192 【光环助手V5.5.0】游戏单活动:解锁好游种草新姿势(url参数增加是否可以由web端处理返回键)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1638 2021-12-30 16:56:15 +08:00
064b041d28 版本更新至 5.5.2 2021-12-30 09:28:33 +08:00
a131aca047 Merge branch 'hotfix-v5.5.1-451-installed_crash' into 'release'
修复下载管理已安装列表的空列表闪退问题

See merge request halo/android/assistant-android!168
2021-12-30 09:27:32 +08:00
ad9b39a94a 修复下载管理已安装列表的空列表闪退问题 2021-12-30 09:25:22 +08:00
lyr
c3cf93eaee 【光环助手V5.5.0】游戏单活动:解锁好游种草新姿势(更新递增任务进度接口)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1638#note_129888 2021-12-29 18:32:32 +08:00
b83f98e90c Merge branch 'hotfix-v5.5.1-451-community_home_crash' into 'release'
修复首页-社区Tab重建后页面空白问题以及按返回键闪退问题

See merge request halo/android/assistant-android!167
2021-12-29 17:48:39 +08:00
lyr
5e4c451ccd 修复首页-社区Tab重建后页面空白问题以及按返回键闪退问题 2021-12-29 17:47:12 +08:00
ab5e0136a7 Merge branch 'hotfix-v5.5.1-451-forum_crash' into 'release'
修复论坛详情页面重建后闪退问题

See merge request halo/android/assistant-android!166
2021-12-29 16:40:04 +08:00
89ca11bb81 修复论坛详情页面重建后闪退问题 2021-12-29 16:40:03 +08:00
f220b2fd43 Merge branch 'hotfix-v5.5.1-451-recyclerview_crash' into 'release'
尝试修复下载管理更新页面列表闪退

See merge request halo/android/assistant-android!165
2021-12-29 15:02:19 +08:00
28d99cbe38 尝试修复下载管理更新页面列表闪退 2021-12-29 14:59:08 +08:00
9c2130562b Merge branch 'hotfix-v5.5.1-451-log_anr' into 'release'
处理部分因为日志上报造成的无响应问题

See merge request halo/android/assistant-android!164
2021-12-29 10:58:39 +08:00
d242aca942 处理部分因为日志上报造成的无响应问题 2021-12-29 10:56:39 +08:00
a341dcf0b7 Merge branch 'hotfix-v5.5.1-451-comment_detail' into 'release'
修改游戏/新闻评论详情评论输入框显示异常

See merge request halo/android/assistant-android!163
2021-12-28 18:05:03 +08:00
8a1c7ed5a0 修改游戏/新闻评论详情评论输入框显示异常 2021-12-28 17:49:17 +08:00
d76d430672 Merge branch 'hotfix-v5.5.1-451-message_center' into 'release'
修改点赞游戏单评论回复消息跳转错误

See merge request halo/android/assistant-android!162
2021-12-28 16:50:03 +08:00
32034473ac 修改点赞游戏单评论回复消息跳转错误 2021-12-28 16:16:31 +08:00
cf1dc4e4f1 版本更新至 5.5.1 2021-12-27 18:31:09 +08:00
1d9a877f59 Merge branch 'hotfix-v5.5.0-450-installed_game' into 'release'
修复已安装游戏列表空指针

See merge request halo/android/assistant-android!161
2021-12-27 18:29:27 +08:00
772226fb1a Merge branch 'hotfix-v5.5.0-450-generic_crash' into 'release'
处理闪退

See merge request halo/android/assistant-android!160
2021-12-27 18:26:57 +08:00
fe62dee207 Merge branch 'hotfix-v5.5.0-450-multiple_download_complete_event' into 'release'
临时处理因为优化性能而引起的重复触发下载完成事件的问题

See merge request halo/android/assistant-android!159
2021-12-27 18:25:52 +08:00
37309918dd 临时处理因为优化性能而引起的重复触发下载完成事件的问题 2021-12-27 18:25:11 +08:00
7f8e853657 Merge branch 'hotfix-v5.5.0-450-image_so_crash' into 'release'
去除 arm64-v8a

See merge request halo/android/assistant-android!158
2021-12-27 15:19:08 +08:00
006a8a523e 去除 arm64-v8a 2021-12-27 15:18:20 +08:00
8e497122f2 Merge branch 'hotfix-v5.5.0-450-build_script' into 'release'
处理 sentry 插件编译问题

See merge request halo/android/assistant-android!157
2021-12-27 11:10:36 +08:00
33d5353f75 处理 sentry 插件编译问题 2021-12-27 11:08:41 +08:00
6da3d46d71 Merge branch 'feature-issues1638' into 'dev'
【光环助手V5.5.0】游戏单活动:解锁好游种草新姿势(客户端部分)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1638

See merge request halo/android/assistant-android!156
2021-12-27 11:07:31 +08:00
a26e61ae4b 【光环助手V5.5.0】游戏单活动:解锁好游种草新姿势(客户端部分)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1638 2021-12-27 11:07:31 +08:00
c3d347bffe - 捕抓已收录游戏列表类型异常
- 修复无内容查看大图时的闪退问题
2021-12-27 11:02:00 +08:00
39cc7e98a8 Merge branch 'hotfix-v5.5.0-450-rank_list_crash' into 'release'
捕抓首页排行榜列表数组越界异常

See merge request halo/android/assistant-android!155
2021-12-27 10:34:59 +08:00
2a4e500893 捕抓首页排行榜列表数组越界异常 2021-12-27 10:32:25 +08:00
d6e0cfd644 修复已安装游戏列表空指针 2021-12-27 10:29:56 +08:00
dbbcd29303 MOD限制规则(新)(修复重复安装不算更新的问题) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1646 2021-12-24 16:28:02 +08:00
f3a5f28236 MOD限制规则(新)(修复请求头异常的问题) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1646 2021-12-24 14:32:26 +08:00
bfae7143b1 MOD限制规则(新)(补充请求头) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1646 2021-12-24 11:55:58 +08:00
d6f75d6dd0 完成MOD限制规则(新) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1646 2021-12-23 18:14:02 +08:00
fd9abc7619 【光环助手5.5.0】新社区运营验收问题(5,7) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1662 2021-12-23 09:43:19 +08:00
lyr
e3476f9956 【光环助手V5.5.0】同步正式环境后测试问题汇总(12(2))https://git.shanqu.cc/pm/halo-app-issues/-/issues/1656 2021-12-21 18:08:24 +08:00
aedb08ce5d 修改编辑游戏单获取详情数据错误问题 2021-12-21 17:33:40 +08:00
9515676876 恢复 arm64 SO,5.0 & 5.1 设备禁用 fresco native 解码 2021-12-21 16:13:36 +08:00
4d4d2c91e2 完成国家实名认证系统接入 https://git.shanqu.cc/pm/halo-app-issues/-/issues/1645 2021-12-21 15:16:18 +08:00
1283ec94de 【光环助手V5.5.0】同步正式环境后测试问题汇总(12(1)) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1656 2021-12-21 10:09:07 +08:00
lyr
4565819dd8 【光环助手V5.5.0】同步正式环境后测试问题汇总(游戏icon为动图的情况下不会显示白色边框)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1656#note_128656 2021-12-20 14:27:58 +08:00
fa5207855e 修改提交游戏单后我的游戏单页面没有刷新数据 2021-12-17 17:53:23 +08:00
7f586a042b 修改编辑游戏单游戏不足8个且未勾选仅自己可见提示错误问题 2021-12-17 17:12:46 +08:00
1adad849e0 【光环助手V5.5.0】同步正式环境后测试问题汇总(11) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1656 2021-12-17 16:26:50 +08:00
lyr
e898b8a7a4 【光环助手V5.5.0】新社区运营测试问题汇总(2)(第10点) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1636 2021-12-15 16:25:28 +08:00
34b46a6143 正式环境接口切换到 5.5.0 2021-12-15 15:06:34 +08:00
422cc5d396 处理代码合并异常 2021-12-15 15:06:09 +08:00
a048e00489 Merge remote-tracking branch 'origin/feature-issues1625' into dev
# Conflicts:
#	app/src/main/java/com/gh/common/util/UploadImageUtils.kt
#	app/src/main/java/com/halo/assistant/HaloApp.java
2021-12-15 11:29:33 +08:00
903276e5ed 修复没有后门的实名下载任务也会被有后门的任务触发的问题 2021-12-14 16:22:33 +08:00
e222f504f6 更新 flutter 打包脚本 2021-12-14 10:59:11 +08:00
86707ccc7d 更新 flutter 打包脚本 2021-12-14 10:47:25 +08:00
ed8c1a6b6c 更新 flutter 打包脚本 2021-12-14 10:43:55 +08:00
545bb6b47d Merge branch 'dev-5.5.0' into 'dev'
5.5.0 功能变更

See merge request halo/android/assistant-android!154
2021-12-14 09:22:35 +08:00
lyr
0b40894cda 【光环助手V5.5.0】前端优化汇总第一周(1213测试 1-3)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1617#note_127947 2021-12-13 20:35:44 +08:00
e6246314fd 修复热门游戏单组件内无法上下滑动的问题 2021-12-13 20:12:20 +08:00
2c23f03cc4 修改游戏单广场布局 2021-12-13 19:26:10 +08:00
91b9502d0b 修复游戏单广场列表下滑时偏移留白的问题 2021-12-13 17:24:36 +08:00
lyr
5bbe039e9f 【光环助手V5.5.0】游戏单功能测试汇总(1213测试 8)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1651 2021-12-13 16:45:02 +08:00
8be8fbee19 去除游戏单广场头图的视差的效果 2021-12-13 15:56:30 +08:00
2e5361f844 【光环助手V5.5.0】更换引导图 https://git.shanqu.cc/pm/halo-app-issues/-/issues/1643 2021-12-13 14:49:36 +08:00
f4ecf28808 调整实名认证按钮样式 2021-12-13 11:13:53 +08:00
5dd88e30f8 发布视频贴添加防抖处理 https://git.shanqu.cc/pm/halo-app-issues/-/issues/1641 2021-12-13 10:41:02 +08:00
0095c82edb 修复测试包的登录问题 2021-12-13 10:20:42 +08:00
b8260cada6 文字字号从固定值改成 dimens.xml中引用的值 2021-12-13 10:04:50 +08:00
39ec77faae 调整测试环境和正式环境切换的逻辑 2021-12-10 17:57:25 +08:00
lyr
5d4434eebd 修改游戏单详情顶部遮罩 2021-12-10 16:13:25 +08:00
553ab70d78 Merge remote-tracking branch 'origin/dev' into dev-5.5.0
# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/gh/common/DefaultUrlHandler.kt
#	app/src/main/java/com/gh/common/util/DirectUtils.kt
#	app/src/main/java/com/gh/common/util/EntranceUtils.java
#	build.gradle
2021-12-10 15:19:25 +08:00
23bffe7c5e 处理榜单卡片游戏文字的显示问题 2021-12-10 15:05:33 +08:00
babc41a0cb 【光环助手V5.5.0】游戏单功能测试汇总(1)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1651 2021-12-10 14:26:19 +08:00
a272033834 统一颜色取值 2021-12-10 11:55:45 +08:00
82ebdd3908 【光环助手V5.5.0】游戏单数据埋点(1209测试2-3)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1549 2021-12-10 10:18:24 +08:00
lyr
30cac9f86e 【光环助手V5.5.0】游戏单数据埋点(1209测试 1) 2021-12-09 19:00:34 +08:00
lyr
6f19a2b668 【光环助手V5.5.0】游戏单详情-玩家评论(1209测试 2)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1602#note_127599 2021-12-09 17:52:39 +08:00
lyr
9235c19475 【光环助手V5.5.0】游戏单-个人主页(1209测试 3)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1591#note_127565 2021-12-09 17:22:27 +08:00
lyr
79d9d05775 Merge remote-tracking branch 'origin/dev-5.5.0' into dev-5.5.0 2021-12-09 16:36:08 +08:00
lyr
2b816a5914 修改游戏单详情顶部遮罩 2021-12-09 16:36:03 +08:00
7d163f454e 【光环助手V5.5.0】游戏单广场(1209补充2)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1598
修改游戏单广场发布控件动效
修改游戏单卡片黑色渐变遮罩
删除选择标签刷新动画
2021-12-09 16:31:37 +08:00
b44b676a59 【光环助手V5.5.0】游戏单-社区相关(1209测试:1) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1595 2021-12-09 15:33:39 +08:00
c71429750c 修改添加游戏搜索时提示信息的显示问题 2021-12-09 15:13:24 +08:00
a8c273f68a 【光环助手V5.5.0】前端优化汇总第六周(1)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1650 2021-12-09 10:24:52 +08:00
2588999270 Merge remote-tracking branch 'origin/dev-5.5.0' into dev-5.5.0 2021-12-09 10:20:49 +08:00
99f7bd9192 【光环助手V5.5.0】游戏单 UI 修复(创建游戏单(2)选择标签)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-09 10:20:37 +08:00
70419e412e 修改我的游戏单标签显示错误问题 2021-12-09 09:54:03 +08:00
df3df01327 修改热门游戏单跳转游戏单详情参数 2021-12-08 18:32:36 +08:00
lyr
4bdc051d34 【光环助手V5.5.0】游戏单详情-顶部区域/游戏列表(1208测试 1)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1601#note_127384 2021-12-08 16:29:12 +08:00
575dab06f6 完成实名认证第四期优化(20211207UI测试反馈) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1625 2021-12-08 16:12:05 +08:00
lyr
78cc7b116a 【【光环助手V5.5.0】游戏单-个人主页(1206测试 2)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1591#note_126908 2021-12-08 15:15:05 +08:00
6ebc4f1f43 【光环助手V5.5.0】首页顶部tab-游戏单广场(1208测试1)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1599 2021-12-08 15:09:52 +08:00
lyr
82bb0e26ec 【光环助手V5.5.0】游戏单 UI 修复(社区 - 发帖子 - 引用游戏单)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-08 10:54:37 +08:00
lyr
6c4030c965 【光环助手V5.5.0】游戏单 UI 修复(游戏单介绍页)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-07 17:41:17 +08:00
lyr
ba6d3746ab 【光环助手V5.5.0】游戏单 UI 修复(游戏单详情页)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-07 17:26:49 +08:00
03faa42bab 删除游戏单广场多余代码 2021-12-07 16:32:52 +08:00
151a4fd2bc DataBinding改为使用ViewBinding
优化游戏单曝光统计代码
2021-12-07 16:11:02 +08:00
lyr
3d8c4038a6 【光环助手V5.5.0】游戏单详情-玩家评论(1202测试: 15)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1602#note_126460 2021-12-07 15:20:05 +08:00
lyr
1e0265f6ff 【光环助手V5.5.0】游戏单详情-玩家评论(1202测试)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1602#note_126460 2021-12-07 15:03:10 +08:00
19871d2bed 【光环助手V5.5.0】首页顶部tab-游戏单广场(1206测试1)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1599
【光环助手V5.5.0】游戏单广场(1206测试1、3)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1598
2021-12-07 11:38:13 +08:00
3da57acf12 Merge branch 'release' into 'dev'
Merge hotfix from release

See merge request halo/android/assistant-android!153
2021-12-07 11:32:53 +08:00
f6b5e5ecc7 修复进入类型为热门回答的游戏专区会闪退的问题 2021-12-07 11:31:23 +08:00
8f471b76eb Merge branch 'feature-optimise_home_page_performance' into 'dev-5.5.0'
优化首页嵌套列表滑动性能

See merge request halo/android/assistant-android!152
2021-12-06 18:30:24 +08:00
8218aa004b 【光环助手V5.5.0】首页/版块内容列表-游戏单广场(1201测试3)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1600 2021-12-06 18:29:58 +08:00
lyr
741a4c1640 【光环助手V5.5.0】游戏单详情-顶部区域/游戏列表(1202测试)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1601#note_126457 2021-12-06 18:28:08 +08:00
63e9f02cda 优化首页嵌套列表滑动性能 2021-12-06 18:27:44 +08:00
8ed3f759b4 【光环助手V5.5.0】创建/编辑游戏单(1206测试:3) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1604 2021-12-06 16:54:39 +08:00
616abbe04c 【光环助手V5.5.0】我的光环-我的游戏单(1206测试:1) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1603 2021-12-06 15:56:58 +08:00
8ea3511451 【光环助手V5.5.0】游戏单 UI 修复(1、2、4、8(2))https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-06 15:21:50 +08:00
52475e83e9 Merge branch 'feature-flutter-game-comments' into 'dev'
Feature flutter game comments

See merge request halo/android/assistant-android!151
2021-12-06 09:40:20 +08:00
c573fc3b8f Fixed Sentry插件冲突问题
Updated 更新Sentry DSN地址
Added 游戏动态大图长按保存功能
2021-12-06 09:40:20 +08:00
bc23240003 【光环助手V5.5.0】游戏单 UI 修复(7,8) https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-04 14:21:47 +08:00
f92490406e 【光环助手V5.5.0】游戏单 UI 修复(1-4、6)https://git.shanqu.cc/pm/halo-app-issues/-/issues/1647 2021-12-03 18:30:05 +08:00
9ab5b67ae7 更新 Sentry 版本和 DSN 地址 2021-12-03 18:07:48 +08:00
adaffbac77 修改首页tab游戏单广场接口 2021-12-03 11:08:56 +08:00
3c9150adbc 完善游戏单广场列表分页加载 2021-12-03 10:37:16 +08:00
884e5b3095 【光环助手V5.5.0】创建/编辑游戏单(1201测试1-4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1604 2021-12-02 17:21:16 +08:00
lyr
5bafb565a2 【光环助手V5.5.0】游戏单-个人主页(1201测试:5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1591#note_126169 2021-12-02 17:12:42 +08:00
c750e3a50c 【光环助手V5.5.0】游戏单广场(1202测试2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-12-02 17:01:09 +08:00
lyr
917ed615f9 【光环助手V5.5.0】游戏单-浏览记录(1201测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1593#note_126175 2021-12-02 15:18:43 +08:00
19399824c2 Merge branch 'feature-flutter-game-comments' into 'dev'
Feature flutter game comments

See merge request halo/android/assistant-android!150
2021-12-02 15:00:37 +08:00
998cfb9233 Feature flutter game comments 2021-12-02 15:00:37 +08:00
ea86b09444 【光环助手V5.5.0】游戏单-社区相关(1201测试:4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1595 2021-12-02 11:45:35 +08:00
lyr
b399e6284c Merge remote-tracking branch 'origin/dev-5.5.0' into dev-5.5.0 2021-12-02 11:15:40 +08:00
lyr
adcc6fa5ef 【光环助手V5.5.0】游戏单-社区相关(1201测试 8)https://git.ghzs.com/pm/halo-app-issues/-/issues/1595#note_126269 2021-12-02 11:15:34 +08:00
da3fe16ac0 【光环助手V5.5.0】首页/版块内容列表-游戏单广场(1201测试2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1600
修复首页tab游戏单广场列表接口加载逻辑
2021-12-02 11:10:50 +08:00
5f2695f4fb 修复游戏单卡片UI问题 2021-12-02 10:17:33 +08:00
lyr
8408091b33 Merge remote-tracking branch 'origin/dev-5.5.0' into dev-5.5.0 2021-12-02 10:16:03 +08:00
lyr
8de2ba1c14 【光环助手V5.5.0】前端优化汇总第一周(1、2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1617 2021-12-02 10:15:57 +08:00
41680a3440 【光环助手V5.5.0】游戏单-社区相关(1201测试:1,3,9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1595 2021-12-02 10:15:23 +08:00
320c44815e 【光环助手V5.5.0】游戏单广场(1201测试2、3、6)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-12-01 18:35:30 +08:00
f4fb26ffa9 Merge branch 'test_flutter_script' into 'dev'
修改混 flutter 编译脚本

See merge request halo/android/assistant-android!149
2021-12-01 18:23:32 +08:00
39ba62e5bd 修改混 flutter 编译脚本 2021-12-01 18:22:49 +08:00
a2a28d801c 【光环助手V5.5.0】我的光环-我的游戏单(1201测试:2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1603 2021-12-01 17:32:32 +08:00
491c94e536 移除部分 bintray 仓库地址 2021-12-01 16:15:14 +08:00
lyr
78a805a6a2 游戏单评论列表与评论详情页同步点赞数据 2021-12-01 15:04:21 +08:00
86f6c66f97 修复游戏单广场标签筛选文字过长时显示问题
调整热门游戏单组件间距
2021-12-01 10:52:05 +08:00
lyr
674ebbfe65 通用链接模块支持跳转游戏单详情 2021-11-30 18:08:30 +08:00
48d964b957 完成1119测试反馈(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1625 2021-11-30 18:02:10 +08:00
e08ba51951 添加 jenkins flutter 编译脚本 2021-11-30 16:35:47 +08:00
1c1c271195 【光环助手V5.5.0】新社区运营测试问题汇总(2)8 https://git.ghzs.com/pm/halo-app-issues/-/issues/1636 2021-11-30 15:50:34 +08:00
9a0d49741d Merge branch 'feature-flutter-game-comments' into 'dev'
Flutter模块合并

See merge request halo/android/assistant-android!148
2021-11-30 15:16:22 +08:00
lyr
91c07dad2c 游戏单列表与游戏单详情同步数据
(cherry picked from commit bd8fd8cec33667d9460a75d28416175b0bd2995f)
2021-11-30 14:57:26 +08:00
lyr
bfb2130517 1.修复游戏单数据库迁移报错问题;
2.游戏单详情顶部Toolbar适配小米系统


(cherry picked from commit 6cca83a7a1bd8512886a8c26db78e1fd254271c4)
2021-11-30 14:56:43 +08:00
1942cf8ce9 Added 添加Tracker初始化
Updated Lancet注解依赖声明迁移回app的build.gradle
2021-11-30 14:45:52 +08:00
3f5b5ac2ed Merge branch 'dev' into feature-flutter-game-comments 2021-11-30 11:01:53 +08:00
7ab424f107 Updated 移除无用代码 2021-11-30 09:35:32 +08:00
8403db9c0e Updated Pull away configurations of Flutter project and add them into init.flutter.gradle 2021-11-30 09:17:22 +08:00
2139c19b40 Merge branch 'feature-games_collection' into dev-5.5.0 2021-11-29 18:17:10 +08:00
lyr
34d332129b 【光环助手V5.5.0】游戏单详情-玩家评论(增加分享统计) 2021-11-29 17:54:48 +08:00
19617c7e26 【光环助手V5.5.0】首页/版块内容列表-游戏单广场 https://git.ghzs.com/pm/halo-app-issues/-/issues/1600
【光环助手V5.5.0】游戏单数据埋点(首页/版块游戏单) https://git.ghzs.com/pm/halo-app-issues/-/issues/1549
2021-11-29 17:49:27 +08:00
lyr
1ca9472eee 【光环助手V5.5.0】游戏单-个人主页(细节完善)https://git.ghzs.com/pm/halo-app-issues/-/issues/1591 2021-11-29 16:11:34 +08:00
lyr
798503cf26 【光环助手V5.5.0】游戏单详情web页(提供跳转)https://git.ghzs.com/pm/halo-app-issues/-/issues/1546 2021-11-29 10:46:06 +08:00
lyr
fb251f95ce 1.优化点击评论按钮点击自动定位到[玩家评论]区域;
2.游戏单详情添加浏览记录增加状态判断
2021-11-28 16:45:54 +08:00
44b69e3d06 修复热门游戏单组件内无法上下滑动的问题 2021-11-26 15:48:44 +08:00
lyr
b44caf693b 【光环助手V5.5.0】游戏单数据埋点(游戏单详情部分(遗漏部分)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-26 15:17:30 +08:00
lyr
21c456e1d0 【光环助手V5.5.0】游戏单数据埋点(游戏单详情部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-26 15:11:24 +08:00
aeb41616d9 更换 gid 依赖源 2021-11-26 10:22:56 +08:00
92b4658123 Merge remote-tracking branch 'origin/feature-games_collection' into feature-games_collection 2021-11-25 15:27:24 +08:00
95c9afa550 修复游戏单广场列表分页 2021-11-25 15:26:39 +08:00
lyr
0d28fc15e9 Merge remote-tracking branch 'origin/feature-games_collection' into feature-games_collection 2021-11-25 15:02:45 +08:00
lyr
f5ff23ff3a 【光环助手V5.5.0】游戏单详情-玩家评论(完善字段) https://git.ghzs.com/pm/halo-app-issues/-/issues/1602 2021-11-25 15:02:40 +08:00
1b838b473c 1.修改我的游戏单页面刷新问题
2.关闭游戏单发布页面时清空选择的游戏
2021-11-25 15:00:30 +08:00
lyr
f0c6e437a9 【光环助手V5.5.0】游戏单详情-玩家评论(修复评论详情闪退问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1602 2021-11-25 14:33:30 +08:00
lyr
95a955c51e 【光环助手V5.5.0】游戏单详情-玩家评论(重新优化) https://git.ghzs.com/pm/halo-app-issues/-/issues/1602 2021-11-25 13:07:15 +08:00
f3fe3990c8 【光环助手V5.5.0】游戏单数据埋点(首页游戏单)https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-24 17:13:47 +08:00
8690559646 【光环助手V5.5.0】新社区运营测试问题汇总(26) https://git.ghzs.com/pm/halo-app-issues/-/issues/1582 2021-11-24 09:43:51 +08:00
3e71115443 修正拼写错误 2021-11-23 11:45:30 +08:00
5534e6f1a7 【光环助手V5.5.0】游戏单数据埋点(游戏单广场)https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-23 11:09:28 +08:00
ffe98bfac0 【光环助手V5.5.0】游戏单-消息中心 https://git.ghzs.com/pm/halo-app-issues/-/issues/1596 2021-11-23 10:45:37 +08:00
lyr
946503d469 Merge remote-tracking branch 'origin/feature-games_collection' into feature-games_collection 2021-11-22 18:06:51 +08:00
lyr
381ce29ae9 【光环助手V5.5.0】游戏单详情-玩家评论 https://git.ghzs.com/pm/halo-app-issues/-/issues/1602
【光环助手V5.5.0】游戏单详情-顶部区域/游戏列表(对接视频数据) https://git.ghzs.com/pm/halo-app-issues/-/issues/1601
2021-11-22 18:06:43 +08:00
67bfcc82fa 【光环助手V5.5.0】游戏单数据埋点(选择标签)https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-22 18:02:01 +08:00
65e07959a5 【光环助手V5.5.0】游戏单数据埋点(选择标签)https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-22 17:59:30 +08:00
a91195d829 修复请求头的 gid 更新不及时的问题 2021-11-22 15:30:42 +08:00
3d75d8507c 优化通用导航栏标题居中的实现
优化资讯详情页面导航栏标题居中
2021-11-22 10:11:35 +08:00
2ec43d4ff2 修复ToolbarActivity导致的内存泄漏问题 2021-11-19 18:33:32 +08:00
7c55f71bb5 修复 6.0 以下设备读取 DSA 签名会闪退的问题 2021-11-19 17:47:53 +08:00
99ac44de9e 游戏单禁言弹窗判断是否永久禁言 2021-11-19 17:10:04 +08:00
9df38387f2 修复 DSA 签名游戏无法正常更新的问题 2021-11-19 16:26:02 +08:00
e870dc9fd7 修复ToolbarActivity导致的内存泄漏问题 2021-11-19 14:53:45 +08:00
d7faf04a2e Fixed 修复礼包中心搜索框清空关键字后页面状态不对的问题 2021-11-19 11:31:19 +08:00
f0e325cd2e 首页顶部tab新增游戏单类型
跳转链接新增游戏单广场类型
2021-11-19 11:19:43 +08:00
lyr
c64e0da8c7 【光环助手V5.5.0】游戏单详情web页(完成分享面板)https://git.ghzs.com/pm/halo-app-issues/-/issues/1546 2021-11-19 09:44:40 +08:00
e2140f501f 【光环助手V5.5.0】首页顶部tab-游戏单广场 https://git.ghzs.com/pm/halo-app-issues/-/issues/1599 2021-11-18 18:26:40 +08:00
020c8d4456 1.处理编辑游戏单闪退问题
2.处理选择游戏页面内存泄露问题
2021-11-18 17:47:29 +08:00
401c0bfdfe Added 添加Flutter上报事件日志 2021-11-18 16:59:01 +08:00
lyr
33c411befa 【光环助手V5.5.0】游戏单-浏览记录 https://git.ghzs.com/pm/halo-app-issues/-/issues/1593 2021-11-18 16:48:57 +08:00
041619ed0d Merge branch 'feature-cache_download_status' into 'dev-5.5.0'
尝试缓存下载队列状态,避免下载按钮直接访问数据库

See merge request halo/android/assistant-android!147
2021-11-18 15:30:45 +08:00
ad6114a44b 尝试缓存下载队列状态,避免下载按钮直接访问数据库 2021-11-18 15:30:03 +08:00
d61008349a Merge branch 'dev' into feature-flutter-game-comments 2021-11-18 15:08:06 +08:00
39cfa01f5a Fixed 游戏动态我的关注入口显示和隐藏
Fixed 游戏动态评论详情评论列表缺失问题
Fixed 我的关注无数据ui展示和取消关注列表闪烁问题
2021-11-18 14:06:43 +08:00
924f83dacf 【光环助手V5.5.0】游戏单数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1549 2021-11-18 11:21:28 +08:00
lyr
68db40c4c8 【光环助手V5.5.0】游戏单-我的收藏(对接接口) https://git.ghzs.com/pm/halo-app-issues/-/issues/1592
【光环助手V5.5.0】游戏单详情-顶部区域/游戏列表(二、视频样式-顶部区域(视频数据未对接))https://git.ghzs.com/pm/halo-app-issues/-/issues/1601
2021-11-18 10:49:14 +08:00
b53a793e1c 完成【光环助手V5.5.0】实名认证第四期优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1625 2021-11-18 09:55:14 +08:00
1097300211 修改搜索游戏意见反馈内容格式 2021-11-17 16:16:58 +08:00
1052663db0 【光环助手V5.5.0】游戏单广场https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-11-17 15:34:16 +08:00
e3f0311cc7 【光环助手V5.5.0】游戏单-禁言相关 https://git.ghzs.com/pm/halo-app-issues/-/issues/1597 2021-11-17 14:55:04 +08:00
f0af5787c3 【光环助手V5.5.0】游戏单-禁言相关 https://git.ghzs.com/pm/halo-app-issues/-/issues/1597 2021-11-17 14:45:31 +08:00
4ed1da1777 完成创建游戏单页面选择标签UI 2021-11-17 11:17:37 +08:00
6cca81c4bb Added 游戏动态添加loading图 2021-11-17 11:13:09 +08:00
047c45db9a 完成【光环助手V5.5.0】前端优化汇总第三周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1627 2021-11-17 10:30:13 +08:00
lyr
689fec168f Merge remote-tracking branch 'origin/feature-games_collection' into feature-games_collection
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java
2021-11-16 18:31:52 +08:00
lyr
cff7f4a9f5 【光环助手V5.5.0】游戏单详情-顶部区域/游戏列表(一、大图样式-顶部区域)https://git.ghzs.com/pm/halo-app-issues/-/issues/1601 2021-11-16 18:30:28 +08:00
fcb8a60722 【光环助手V5.5.0】游戏单广场(选择标签)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-11-16 18:21:18 +08:00
f2ad9b2235 合并冲突 2021-11-16 15:11:58 +08:00
49067706b7 【光环助手V5.5.0】游戏单-社区相关 https://git.ghzs.com/pm/halo-app-issues/-/issues/1595 2021-11-16 15:08:49 +08:00
098f02f0aa Added 礼包中心添加请先登录界面,登录成功后刷新关注和领号箱列表 2021-11-15 18:29:45 +08:00
772aa7ca03 【光环助手V5.5.0】游戏单广场(选择标签UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-11-15 17:38:13 +08:00
4e78162d7f 【光环助手V5.5.0】游戏单广场(UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598
【光环助手V5.5.0】首页顶部tab-游戏单广场(UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1599
【光环助手V5.5.0】首页/版块内容列表-游戏单广场(UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1600
2021-11-15 17:27:41 +08:00
017600f90e 完成【光环助手V5.5.0】新社区运营测试问题汇总(24) https://git.ghzs.com/pm/halo-app-issues/-/issues/1582#note_123972 2021-11-15 15:52:11 +08:00
86d1749989 【光环助手V5.5.0】新社区运营测试问题汇总(11,12,18,22) https://git.ghzs.com/pm/halo-app-issues/-/issues/1582 2021-11-15 14:54:34 +08:00
32bc1a4a6f 优化创建游戏单 2021-11-15 09:36:26 +08:00
lyr
58b37f8922 【光环助手V5.5.0】游戏单-个人主页(大致完成) https://git.ghzs.com/pm/halo-app-issues/-/issues/1591
【光环助手V5.5.0】游戏单-我的收藏(大致完成)https://git.ghzs.com/pm/halo-app-issues/-/issues/1592
2021-11-14 11:47:15 +08:00
135b55b204 Fixed 礼包中心列表无数据UI不居中问题
Fixed 存号箱已过期列表下拉加载无数据时没有显示“没有更多了”
2021-11-12 18:38:18 +08:00
1f025f3556 【光环助手V5.5.0】我的光环-我的游戏单 https://git.ghzs.com/pm/halo-app-issues/-/issues/1603 2021-11-12 12:19:36 +08:00
54bf1af524 修复礼包详情领取礼包时会出现重复数据的问题 2021-11-12 11:42:16 +08:00
11cae2c56f Fixed 游戏动态-礼包详情领取礼包后页面刷新问题 2021-11-11 11:00:15 +08:00
7075f3bcc1 更新 gradle 版本 2021-11-10 18:13:07 +08:00
5df2a1c2f5 更新 sentry mapping plugin 2021-11-10 17:08:33 +08:00
831ca71a58 【光环助手V5.5.0】创建/编辑游戏单 https://git.ghzs.com/pm/halo-app-issues/-/issues/1604 2021-11-10 16:52:01 +08:00
bb854161f5 Updated 实现原生页面点击关注,领取礼包后,Flutter页面的更新 2021-11-09 18:46:41 +08:00
a98d6125da Merge remote-tracking branch 'origin/dev-5.5.0' into dev-5.5.0 2021-11-09 18:22:15 +08:00
b178961b9b 尝试修复 WebView 不可用时的闪退 https://git.ghzs.com/halo/android/assistant-android/-/issues/47 2021-11-09 18:20:58 +08:00
9c6664aed2 Merge remote-tracking branch 'origin/dev' into dev-5.5.0 2021-11-09 17:59:59 +08:00
1d254248d9 Merge remote-tracking branch 'origin/release' into dev
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/personal/PersonalFunctionAdapter.kt
#	dependencies.gradle
2021-11-09 17:52:15 +08:00
844c028b7b 版本升级至 5.3.4 2021-11-09 14:26:54 +08:00
cf320fa01b Merge branch 'hotfix-v5.3.3-413-crash' into 'release'
修复一部分闪退

See merge request halo/android/assistant-android!145
2021-11-09 14:21:24 +08:00
739b729654 Merge branch 'hotfix-v5.3.3-413-forced_update' into 'release'
修复用户快速跳转页面可以跳过强更的问题

See merge request halo/android/assistant-android!144
2021-11-09 14:18:16 +08:00
48729b298f Merge branch 'hotfix-v5.3.3-413-optimize_real_name_input' into 'release'
去除实名认证姓名的输入限制

See merge request halo/android/assistant-android!143
2021-11-09 11:53:58 +08:00
df2d5a93a0 去除实名认证的输入限制 2021-11-09 11:49:33 +08:00
328d200a78 修复多游戏可更新时点击全部更新按钮出现的闪退问题 2021-11-09 11:36:53 +08:00
a5f5bd8828 尝试修复通用列表数据变更偶发的越界闪退 2021-11-09 11:21:40 +08:00
760aa6f8f6 停用因未正确初始化已失效的 WorkManager 2021-11-09 10:58:47 +08:00
d6a341337a 修复部分设备跳转通知设置页面的闪退问题 2021-11-09 10:46:17 +08:00
82d79028de 捕抓部分设备网页不支持关闭安全浏览触发的闪退 2021-11-09 10:40:57 +08:00
92eece6289 修复排行榜列表点击数组越界闪退 2021-11-09 10:38:16 +08:00
edd8b86144 修复下载更新页面偶发的数组越界闪退 2021-11-09 10:37:16 +08:00
5d5c8dfddb 修复首页轮播图刷新获取新渐变颜色时的越界闪退问题 2021-11-09 10:17:01 +08:00
1dc2e661d1 修复进入游戏详情页时,非活跃 tab 也会被同时初始化的问题 2021-11-09 10:13:17 +08:00
feb46a5887 测试环境api版本升级为v5d5d0 2021-11-08 18:12:53 +08:00
4086fcdb30 修复用户快速跳转页面可以跳过强更的问题 2021-11-08 11:56:58 +08:00
8dd8d21e85 Fixed Flutter页面第一次加载不显示的问题 2021-11-08 11:30:57 +08:00
42a4210290 【光环助手V5.5.0】游戏单广场(选择标签UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-11-08 09:40:57 +08:00
9a1266d9d9 【光环助手V5.5.0】游戏单广场(选择标签UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-11-08 09:24:26 +08:00
2915042719 Fixed 解决礼包中心模块原生页面数据更新后,Flutter页面数据的更新 2021-11-08 09:09:56 +08:00
b4730589bf Merge branch 'feature-games_collection' of git.ghzs.com:halo/android/assistant-android into feature-games_collection
# Conflicts:
#	app/src/main/res/layout/layout_menu_save.xml
#	app/src/main/res/menu/menu_save.xml
2021-11-06 15:57:04 +08:00
001cbfcdb6 【光环助手V5.5.0】创建/编辑游戏单(选择游戏/添加游戏UI) https://git.ghzs.com/pm/halo-app-issues/-/issues/1604 2021-11-06 15:55:06 +08:00
44dab8246f 【光环助手V5.5.0】游戏单广场(选择标签UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1598 2021-11-05 18:08:15 +08:00
lyr
be5cbf0ca0 【光环助手V5.5.0】游戏单-我的游戏 https://git.ghzs.com/pm/halo-app-issues/-/issues/1594 2021-11-05 14:27:15 +08:00
fa2fb66f44 取消实名认证身份证类型输入限制 2021-11-05 11:12:16 +08:00
9982ebea9e 版本更新到 5.3.3 2021-11-05 11:07:34 +08:00
2b30cc1f41 Merge branch 'hotfix-v5.3.2-412-crash' into 'release'
修复部分闪退

See merge request halo/android/assistant-android!142
2021-11-05 11:06:54 +08:00
96a19eb5ee 修复部分闪退
1. 部分5.0设备启动读取错误abi造成闪退的问题
2. 部分华为鸿蒙设备读取系统类型闪退的问题
2021-11-05 11:05:26 +08:00
85276d462f Merge branch 'hotfix-v5.3.2-412-fix_realname_input' into 'release'
修复实名身份证号无法输入 X 的问题

See merge request halo/android/assistant-android!141
2021-11-05 09:59:50 +08:00
44aebc39ea 修复实名身份证号无法输入 X 的问题 2021-11-05 09:58:42 +08:00
52294ebb3a Merge branch 'dev' into feature-flutter-game-comments
# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/gh/gamecenter/gamedetail/desc/GameLibaoAdapter.kt
#	app/src/main/java/com/gh/gamecenter/libao/LibaoNewAdapter.kt
#	app/src/main/java/com/gh/gamecenter/libao/LibaoSearchAdapter.kt
#	gradle.properties
2021-11-04 16:54:56 +08:00
623156bdb5 【光环助手V5.5.0】创建/编辑游戏单(创建游戏单UI) https://git.ghzs.com/pm/halo-app-issues/-/issues/1604 2021-11-04 10:19:34 +08:00
lyr
6bac5d79a9 【光环助手V5.5.0】前端优化汇总第一周(5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1617 2021-11-03 17:44:37 +08:00
293fc635e1 Updated 整合环境配置
Added AssistantNavigator整合路由跳转代码,自动根据环境使用路由跳转类
2021-11-03 16:42:20 +08:00
0f0dc6dd9b Updated 更新文档 2021-11-02 17:36:19 +08:00
46de8f5f9e Added 添加我的关注跳转实现 2021-11-02 17:34:11 +08:00
ed6b2960cc Updated 更新文档 2021-11-02 16:55:54 +08:00
f3ef7930f3 Updated 更新Flutter路由跳转 2021-11-02 16:51:04 +08:00
lyr
5ecb3d5378 【光环助手V5.4.0】骨架屏 UI 优化(1102补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1575#note_122935 2021-11-02 16:22:33 +08:00
1d4fe89900 版本更新至 5.5.0 2021-11-02 10:18:18 +08:00
3f3b19f2cf 更新依赖库 2021-11-02 10:17:33 +08:00
af1ac4e888 Merge remote-tracking branch 'origin/dev' into dev-5.4.0 2021-11-02 10:14:47 +08:00
1478b59479 相关版本合并及处理要求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1605 2021-11-01 16:14:04 +08:00
lyr
335cde94fe 【光环助手V5.4.0】客户端内部需求(新分类2.0游戏Item-UI问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1567#note_122549 2021-11-01 15:27:00 +08:00
dd1f399425 Merge branch 'feature-issues1572' into 'release'
完成实名认证优化第三期 https://git.ghzs.com/pm/halo-app-issues/-/issues/1572

See merge request halo/android/assistant-android!140
2021-11-01 15:03:26 +08:00
3e8923f34a 完成实名认证优化第三期 https://git.ghzs.com/pm/halo-app-issues/-/issues/1572 2021-11-01 15:00:35 +08:00
612fc8f444 【光环助手V5.2.0】光环助手UI组件库(第2期)(1101UI测试问题2-3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1327 2021-11-01 14:58:32 +08:00
0cbc104092 正式环境切换到 5.4.0 的接口 2021-11-01 07:31:32 +01:00
41f7e3a05a Merge branch 'hotfix-v5.3.1-411-crash' into 'release'
修复一些闪退

See merge request halo/android/assistant-android!139
2021-11-01 11:26:26 +08:00
25ce3ccdc9 修复一些闪退
1. 修复游戏详情页回到顶部的闪退
2. 尝试修复获取游戏更新时偶发的闪退
2021-11-01 11:24:43 +08:00
82a60413e1 【光环助手V5.4.0】客户端内部需求(Toolbar样式补充3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1567 2021-11-01 09:32:15 +08:00
2b4126fa71 【光环助手V5.4.0】客户端内部需求(修复视频数据出现2个返回按钮)https://git.ghzs.com/pm/halo-app-issues/-/issues/1567 2021-10-29 18:08:56 +08:00
lyr
52a7ab7b3c Merge remote-tracking branch 'origin/dev-5.4.0' into dev-5.4.0 2021-10-29 17:30:28 +08:00
lyr
9b8ce918d8 【光环助手V5.4.0】骨架屏 UI 优化(优化部分页面骨架屏显示)https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-29 17:30:23 +08:00
57081f4003 修复Toolbar按钮点击动画卡顿的问题 2021-10-29 17:28:01 +08:00
5df0d85004 Merge branch 'hotfix-v5.3.1-411-comment' into 'release'
修改未登录评论时一键登录弹窗弹起键盘没有收起问题

See merge request halo/android/assistant-android!138
2021-10-29 16:00:47 +08:00
63b88383a4 修改未登录评论时一键登录弹窗弹起键盘没有收起问题 2021-10-29 14:59:55 +08:00
7564a0a771 尝试修改发视频贴快速点击发送按钮会生成多条数据 2021-10-29 11:58:16 +08:00
36bc35a7e3 版本更新至 5.3.1 2021-10-29 11:13:46 +08:00
1c55ab8f86 Merge branch 'hotfix-v5.3.0-410-generic_crash' into 'release'
修复一系列闪退

See merge request halo/android/assistant-android!137
2021-10-29 11:02:35 +08:00
9a1f3b0fe3 修复一系列闪退
1. 跳转用户详情参数有误造成的闪退
2. 标记更新已读红点时的闪退
3. 5.0 以下设备点击更新按钮的闪退
4. 横向列表专题点击时偶发的闪退
5. 首页滑动顶部 BANNER 偶发的闪退
2021-10-29 11:01:16 +08:00
438b55c08b Updated 完成礼包中心-领取礼包功能
Updated 完成礼包中心-领号箱复制功能
2021-10-28 21:59:30 +08:00
lyr
12fcb98412 Merge remote-tracking branch 'origin/dev-5.4.0' into dev-5.4.0 2021-10-28 21:58:08 +08:00
lyr
33030cf550 优化部分页面骨架屏显示 2021-10-28 21:58:03 +08:00
9a7acef9a3 修改评论点赞图片闪烁问题 2021-10-28 21:48:09 +08:00
61f92e544b Merge remote-tracking branch 'origin/dev-5.4.0' into dev-5.4.0 2021-10-28 21:44:30 +08:00
57d6cc7d0e 【光环助手V5.4.0】儿童/青少年模式(修改密码输入框输入时的背景)https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-28 21:44:22 +08:00
919ace95b9 修改评论楼层显示错位 2021-10-28 21:16:21 +08:00
ec1af285af 【光环助手V5.4.0】儿童/青少年模式(修改密码输入框输入时的背景)https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-28 21:02:22 +08:00
6b4c02e965 【光环助手V5.4.0】儿童/青少年模式(测试反馈2、UI测试1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-28 20:06:21 +08:00
913bcdeeb2 捕抓卸载应用时偶发的闪退异常 2021-10-28 17:52:43 +08:00
e40857d24b Merge branch 'hotfix-v5.3.0-410-image_viewer_crash' into 'release'
修复查看大图传入数组为空时的闪退异常

See merge request halo/android/assistant-android!136
2021-10-28 17:40:56 +08:00
d3fab1aa73 修复查看大图传入数组为空时的闪退异常 2021-10-28 17:40:18 +08:00
d32698ca91 Merge branch 'hotfix-v5.3.0-410-store_old_version_info' into 'release'
为后续版本提供当前版本的版本和渠道

See merge request halo/android/assistant-android!135
2021-10-28 17:26:27 +08:00
7832276560 为后续版本提供当前版本的版本和渠道 2021-10-28 17:25:37 +08:00
2fca73dfe8 Merge branch 'hotfix-v5.3.0-410-crash' into 'release'
修复游戏搜索页面列表数组越界问题

See merge request halo/android/assistant-android!134
2021-10-28 17:11:06 +08:00
lyr
0f194a80bb 修复游戏搜索页面列表数组越界问题 2021-10-28 17:09:08 +08:00
lyr
e0cdf42d26 【光环助手V5.4.0】儿童/青少年模式(修复青少年模式下部分页面右上角依然显示下载按钮的问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-28 16:48:14 +08:00
c501248408 【光环助手V5.4.0】骨架屏 UI 优化(1027UI测试问题1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-28 16:40:13 +08:00
a559df99ec 添加青少年模式使用须知、找回密码url 2021-10-28 15:24:54 +08:00
1cdd7ed6ef 修改评论上传图片失败没有删除违规图片问题 2021-10-28 15:07:20 +08:00
7e3bd14db4 【光环助手V5.4.0】新社区5期-内容详情页优化(1027UI测试问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1533 2021-10-28 14:41:47 +08:00
ab08a725ca 【光环助手V5.4.0】新社区5期-发布页优化(1028 产品验收1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1531 2021-10-28 10:19:00 +08:00
1977c80038 【光环助手V5.4.0】骨架屏 UI 优化 (中心色宽度调整为0.1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-28 10:13:06 +08:00
d9d8eaa611 【光环助手V5.4.0】骨架屏 UI 优化(1027UI测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-28 10:05:13 +08:00
e0dcb70fcd 【光环助手V5.4.0】新社区5期-内容详情页优化(1027UI测试问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1533 2021-10-27 22:06:02 +08:00
bb60f7f22a 修改模拟器游戏下载按钮显示错误问题
(cherry picked from commit d3e21251d9)
2021-10-27 17:52:03 +08:00
70821c73c1 【光环助手V5.4.0】新社区5期-内容详情页优化(1027UI测试问题1,2,3,7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1533 2021-10-27 17:49:07 +08:00
lyr
1560234105 【光环助手V5.4.0】新社区5期-内容详情页优化(1027UI测试问题 6)https://git.ghzs.com/pm/halo-app-issues/-/issues/1533#note_122064 2021-10-27 17:38:12 +08:00
lyr
59f74ebb9e 重构通用游戏 item 布局(修改部分页面LayoutParams不适配问题)https://git.ghzs.com/halo/android/assistant-android/-/issues/46 2021-10-27 17:07:20 +08:00
d3e21251d9 修改模拟器游戏下载按钮显示错误问题 2021-10-27 15:44:00 +08:00
aedd996c3f 【光环助手V5.4.0】骨架屏 UI 优化 (1027UI测试问题 2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-27 15:03:30 +08:00
lyr
a589b77de5 修改社区-推荐页最近浏览无数据也会显示标题的问题 2021-10-27 14:49:29 +08:00
86330c74b5 Merge remote-tracking branch 'origin/dev' into dev-5.4.0 2021-10-27 11:00:54 +08:00
lyr
54a77555cc 【光环助手V5.4.0】骨架屏 UI 优化(1026测试问题 1、2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1575#note_121834 2021-10-26 17:34:36 +08:00
30ae815717 尝试修复游戏搜索详情可能发生的进度同步问题 2021-10-26 17:18:35 +08:00
1eb1b6a956 处理合并异常 2021-10-26 16:35:52 +08:00
f46b303a67 Merge branch 'feature-issues1526' into dev
# Conflicts:
#	app/src/main/java/com/gh/download/DownloadManager.java
#	app/src/main/java/com/halo/assistant/fragment/WebFragment.java
#	dependencies.gradle
2021-10-26 16:28:25 +08:00
a1ecb784e2 完成实名认证第二期优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1526 2021-10-26 16:16:08 +08:00
lyr
142e04fa60 重构通用游戏 item 布局(适配新分类2.0游戏item)https://git.ghzs.com/halo/android/assistant-android/-/issues/46 2021-10-26 15:55:35 +08:00
540d6cd1e4 完成【光环助手V5.4.0】骨架屏 UI 优化 (1026测试问题 3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-26 15:13:24 +08:00
b078f38e62 【光环助手V5.4.0】骨架屏 UI 优化(1026测试问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-26 14:43:11 +08:00
a2e5415b5c 【光环助手V5.4.0】新社区5期-内容详情页优化(1026产品测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1533 2021-10-26 11:44:57 +08:00
9aae1c5476 Keep Constraints.Group 用到的 R.id 2021-10-26 10:42:02 +08:00
e73b431378 Merge branch 'revert-6957ca54' into 'dev-5.4.0'
Revert "Merge branch 'revert-65a82369' into 'dev-5.4.0'"

See merge request halo/android/assistant-android!131
2021-10-26 10:24:58 +08:00
0fe60f52e2 Revert "Merge branch 'revert-65a82369' into 'dev-5.4.0'"
This reverts merge request !130
2021-10-26 10:22:32 +08:00
6957ca54f0 Merge branch 'revert-65a82369' into 'dev-5.4.0'
Revert "Merge branch 'feature-fast_build' into 'dev-5.4.0'"

See merge request halo/android/assistant-android!130
2021-10-25 18:18:23 +08:00
31de4089ad Revert "Merge branch 'feature-fast_build' into 'dev-5.4.0'"
This reverts merge request !128
2021-10-25 18:17:15 +08:00
d73a4704c8 Merge remote-tracking branch 'origin/dev' into dev-5.4.0
# Conflicts:
#	app/src/main/res/layout/rank_collection_item.xml
2021-10-25 09:47:18 +08:00
2f5f52471e Merge branch 'cherry-pick-37460cea' into 'dev-5.4.0'
优化首页滑动到专题合集-排行榜样式刷新加载

See merge request halo/android/assistant-android!129
2021-10-25 09:37:52 +08:00
lyr
97f82d70f5 优化首页滑动到专题合集-排行榜样式刷新加载
(cherry picked from commit 37460ceac2)
2021-10-25 09:37:39 +08:00
lyr
c355bc2b79 【光环助手V5.4.0】骨架屏 UI 优化(首页&社区)https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-24 17:32:06 +08:00
a011b5a947 修改微博分享图片异常问题 2021-10-22 22:03:02 +08:00
efdf02158c 【光环助手V5.4.0】新社区5期-内容详情页优化(一(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1533 2021-10-22 18:13:45 +08:00
4ca302f5fc 微调社区评论列表UI 2021-10-22 15:55:06 +08:00
lyr
e669df00b4 Merge remote-tracking branch 'origin/dev-5.4.0' into dev-5.4.0 2021-10-22 14:41:48 +08:00
lyr
19e6f1e7b7 重构通用游戏 item 布局(优化游戏名字布局) https://git.ghzs.com/halo/android/assistant-android/-/issues/46 2021-10-22 14:41:36 +08:00
025d237be0 修改社区评论选择图片且文本为空不能提交问题 2021-10-22 14:39:50 +08:00
lyr
2eb5fb7526 【光环助手V5.4.0】儿童/青少年模式(对接设置页面)https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-22 12:28:46 +08:00
3818e17976 修复专题列表游戏大小不受后台配置控制的问题 2021-10-22 10:36:45 +08:00
1f43b8b220 修复专题列表游戏大小不受后台配置控制的问题 2021-10-22 10:35:41 +08:00
lyr
10003fe49d 重构通用游戏 item 布局 https://git.ghzs.com/halo/android/assistant-android/-/issues/46 2021-10-22 10:22:11 +08:00
80a8cadaae 【光环助手V5.4.0】前端优化汇总第四周(6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1574 2021-10-22 09:51:19 +08:00
88098a8255 适配接口返回 DSA 形式的游戏签名 2021-10-21 18:07:02 +08:00
66a543146a 【光环助手V5.4.0】新社区5期-内容详情页优化(一(1)、二) https://git.ghzs.com/pm/halo-app-issues/-/issues/1533 2021-10-21 17:22:42 +08:00
3aafc3fcbe 【光环助手V5.4.0】儿童/青少年模式https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-21 11:57:22 +08:00
050da95a8f 测试环境api版本升级为v5d4d0 2021-10-21 10:59:52 +08:00
lyr
40b7b09de4 【光环助手V5.4.0】前端优化汇总第二周(4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1574 2021-10-21 10:27:49 +08:00
lyr
16091f759c 【光环助手V5.4.0】儿童/青少年模式(客户端展示,未对接设置页面)https://git.ghzs.com/pm/halo-app-issues/-/issues/1571 2021-10-20 17:55:41 +08:00
f65664b885 【光环助手V5.4.0】骨架屏 UI 优化(详情页&合集) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-20 17:43:01 +08:00
lyr
3eef9c465a 【光环助手V5.4.0】前端优化汇总第二周(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1574 2021-10-20 10:33:43 +08:00
a1b4c726dd Updated 更新游戏动态文档,新增filter字段描述
Added 礼包中心UI
2021-10-20 10:32:56 +08:00
5e2752569b 完成【光环助手V5.4.0】骨架屏 UI 优化 (游戏库&版块) https://git.ghzs.com/pm/halo-app-issues/-/issues/1575 2021-10-19 11:39:35 +08:00
65a823698a Merge branch 'feature-fast_build' into 'dev-5.4.0'
提高项目的编译性能 https://git.ghzs.com/halo/android/assistant-android/-/issues/42

See merge request halo/android/assistant-android!128
2021-10-19 10:32:57 +08:00
fdfad0edf1 【光环助手V5.4.0】前端优化汇总第二周(1,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1574 2021-10-19 10:22:06 +08:00
dcadb529c7 修复Fragment中的Toobar返回按钮无效的问题 2021-10-19 09:54:03 +08:00
lyr
37460ceac2 优化首页滑动到专题合集-排行榜样式刷新加载 2021-10-18 18:09:21 +08:00
lyr
994397ec35 页面刷新优化(我的主页优化)https://git.ghzs.com/halo/android/assistant-android/-/issues/43 2021-10-18 17:57:06 +08:00
9354ff9af8 版本升级到 5.4.0 (430) 2021-10-18 17:46:17 +08:00
2811e21911 Updated 将AOP注入代码迁移到Flutter插件中
Fixed 项目热更新失效问题
Updated 更新文档
Updated 更新版本
2021-10-18 16:33:49 +08:00
746f81e45b Merge branch 'cherry-pick-bfa236f8' into 'dev-5.4.0'
【光环助手V5.4.0】新社区5期-活动主页(3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1535

See merge request halo/android/assistant-android!127
2021-10-18 14:42:39 +08:00
lyr
69f1887926 【光环助手V5.4.0】新社区5期-活动主页(3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1535
(cherry picked from commit bfa236f802)
2021-10-18 14:42:12 +08:00
lyr
6dfded50d6 Merge remote-tracking branch 'origin/dev' into dev 2021-10-18 11:37:12 +08:00
lyr
5e01ecf0a2 修改视频发布页-内容来源-转载网址弹窗无法触发回调问题 2021-10-18 11:37:06 +08:00
a1be42a135 【光环助手V5.4.0】后台优化汇总第三周(10(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1564
(cherry picked from commit 5bdd655715)
2021-10-18 11:34:45 +08:00
5bdd655715 【光环助手V5.4.0】后台优化汇总第三周(10(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1564 2021-10-16 09:40:35 +08:00
c32e9bd0fc 【光环助手V5.2.0】光环助手UI组件库(第2期)(二级标题导航栏)https://git.ghzs.com/pm/halo-app-issues/-/issues/1327 2021-10-15 18:28:28 +08:00
lyr
2e2985cb90 页面刷新优化 https://git.ghzs.com/halo/android/assistant-android/-/issues/43 2021-10-15 18:21:34 +08:00
lyr
f4a5dd47fc 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总(13)https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-15 18:15:16 +08:00
81873bd898 Updated 更新文档 2021-10-15 15:01:26 +08:00
d6604ff2c9 Updated 更新文档 2021-10-15 14:59:31 +08:00
298b7af692 Updated 更新文档 2021-10-15 14:56:38 +08:00
433df3bebd Updated 更新文档 2021-10-15 14:49:31 +08:00
84dba2036c Updated 更新文档
Added 新增路由声明
2021-10-15 14:36:38 +08:00
924ef2a727 Added 添加游戏动态、我的关注路由
Updated 开发文档新增资源描述
2021-10-15 14:21:29 +08:00
5a2b997693 Updated 更新文档 2021-10-14 09:50:05 +08:00
ebb87962ac Updated 游戏动态模块文档新增原生支持,资源列表和路由配置描述 2021-10-14 09:44:06 +08:00
075eb1a21b 【光环助手V5.4.0】新社区5期-发布页优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1531 2021-10-13 16:57:45 +08:00
lyr
b9554d11a8 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总(12)https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-13 11:36:52 +08:00
b910075c4c Merge remote-tracking branch 'origin/dev-5.3.0' into dev 2021-10-13 09:40:23 +08:00
1966e844bd 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总 (8,9,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-12 17:46:12 +08:00
e760a63181 重构下载管理-已安装页面https://git.ghzs.com/halo/android/assistant-android/-/issues/44 2021-10-12 16:33:23 +08:00
lyr
bfa236f802 【光环助手V5.4.0】新社区5期-活动主页(3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1535 2021-10-12 16:24:28 +08:00
lyr
7f64b51582 适配评论详情Footer显示 2021-10-12 15:31:32 +08:00
4324126660 Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0 2021-10-12 15:22:37 +08:00
a8c54de47d 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总(6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-12 15:22:28 +08:00
562b3e9d3b 【光环助手V5.2.0&V5.3.0】同步正式环境后测试问题汇总 (1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1565 2021-10-12 10:59:29 +08:00
679c6da972 修复礼包中心、工具箱清理搜索文字功能实效的问题 2021-10-11 18:22:51 +08:00
lyr
793adb9a03 解决首页滑动到专题合集-排行榜样式卡顿问题 2021-10-11 18:10:21 +08:00
23c62f1092 缺省渠道改为 GH_TEST3 2021-10-11 17:25:07 +08:00
70acea8e92 Updated 优化调整Flutter项目架构,实现Flutter项目模块化 2021-10-11 10:56:33 +08:00
516d8d8044 正式环境切换到 5.3.0 的接口 2021-10-11 10:35:08 +08:00
ed2b41bc55 【光环助手V5.3.0】Q&A管理-事件埋点(1009测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1393 2021-10-11 09:10:29 +08:00
5351ffd6b6 【光环助手V5.3.0】Q&A管理-事件埋点(替换埋点字段qa_title为help_id) https://git.ghzs.com/pm/halo-app-issues/-/issues/1393 2021-10-09 11:56:53 +08:00
aaffbb376c Added 添加反馈页、QA页和QA集合页搜索内容为空的toast提示 2021-10-08 13:12:11 +08:00
ac852c0484 Deleted 删除多余代码 2021-10-08 10:33:13 +08:00
4bd93ad958 Fixed 反馈页点击清空按钮会强制刷新界面的问题 2021-10-08 09:46:51 +08:00
5f131848e7 Updated UI调整 2021-09-30 17:40:48 +08:00
4915f99412 Deleted 移除测试代码
Added 接入日志上报
2021-09-30 15:03:40 +08:00
92841346cd Merge branch 'dev-5.3.0' into feature-flutter-feedback 2021-09-30 11:58:38 +08:00
b0e8db6e0e Added 添加日志上报 2021-09-30 11:57:57 +08:00
d533e0ceab 提高项目的编译性能 https://git.ghzs.com/halo/android/assistant-android/-/issues/42 2021-09-30 10:15:27 +08:00
672b697209 Added 添加日志上报方法 2021-09-29 17:51:51 +08:00
c9bef1fb1a 修复 MUMU 模拟器无法显示部分图片的问题 2021-09-29 14:19:56 +08:00
454e8bb934 修复 MUMU 模拟器无法显示部分图片的问题 2021-09-29 12:00:41 +08:00
e587d0daaa 【光环助手V5.3.0】通用链接内容合集-客户端需求(0928测试:5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-28 20:24:33 +08:00
fcc23ddfc2 修改部分弹窗为居中样式 2021-09-28 18:51:10 +08:00
c541805bed 【光环助手V5.3.0】通用链接内容合集-客户端需求(0928补充:1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-28 17:45:35 +08:00
lyr
2c38c8fd91 优化专题合集-排行榜样式标签加载 2021-09-28 17:10:36 +08:00
15385cf78c 统一视频贴加精/取消加精接口错误处理 2021-09-28 16:33:52 +08:00
6df3a8ec51 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化(0922 产品验收问题6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-28 16:13:10 +08:00
bca9c6d96f Merge branch 'feature-optimise_home_page_performance' into 'dev-5.3.0'
优化首页列表性能

See merge request halo/android/assistant-android!126
2021-09-28 16:04:19 +08:00
26feccc28b 优化首页列表性能 2021-09-28 16:04:18 +08:00
a1fc7d2e48 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-28 15:42:29 +08:00
0b42b10e41 【光环助手V5.3.0】游戏详情-开服相关优化(20210928UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-28 15:42:11 +08:00
lyr
0a0912dbab 微调游戏搜索-专题样式UI 2021-09-28 15:32:55 +08:00
5e3ff2de31 Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0 2021-09-28 15:31:48 +08:00
55db52f8f0 【光环助手V5.3.0】新社区4期-提问帖详情页评论区权限优化(0923 产品验收问题2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1502 2021-09-28 15:31:43 +08:00
2888ab56cd 【光环助手V5.3.0】客户端内部需求(整理简单弹窗样式测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1527 2021-09-28 15:30:45 +08:00
1edc20f665 更改通用链接合集数据结构 2021-09-28 14:34:25 +08:00
72c818b17b 【光环助手V5.3.0】游戏详情-开服相关优化(20210927UI测试补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-28 11:49:25 +08:00
7393dc83f5 修改选择视频时删除本地视频闪退问题 2021-09-28 10:27:23 +08:00
6043553a39 【光环助手V5.3.0】开发者中心相关配套需求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1518 2021-09-28 10:25:19 +08:00
lyr
87461b52ce 【光环助手V5.3.0】专题合集-排行榜功能优化(20210927UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471#note_119397 2021-09-28 10:07:25 +08:00
lyr
df807d746f 修改搜索结果页面加载下一页时,页面会跳动即滑动不流畅问题 2021-09-27 19:07:57 +08:00
lyr
9e7a519806 【光环助手V5.3.0】游戏搜索-客户端功能优化(第8期)(0926测试 第1点、0927测试 第1点、20210927UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1402 2021-09-27 18:35:03 +08:00
6ee4af82ad 【光环助手V5.3.0】光环前端优化汇总第二周(完成0927测试:1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-27 18:07:28 +08:00
75c8a4597c Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0 2021-09-27 17:21:54 +08:00
de274f0954 修改通用链接详情刷新过程中滑动列表闪退问题 2021-09-27 17:21:35 +08:00
82195950b3 论坛详情精华 tab 支持视频贴 https://git.ghzs.com/pm/halo-app-issues/-/issues/1461#note_119153 2021-09-27 17:21:22 +08:00
806b3ac77e 【光环助手V5.3.0】游戏详情-开服相关优化(20210927UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-27 17:17:44 +08:00
lyr
ae95bd1db8 【光环助手V5.3.0】游戏搜索-数据埋点(0927测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1403#note_119302 2021-09-27 16:48:33 +08:00
62ffefb92f 论坛发帖添加使用移动网络上传视频的弹窗 2021-09-27 16:10:58 +08:00
ca190c6d57 Updated 更新文档 2021-09-27 16:01:39 +08:00
1c739a6269 【光环助手V5.3.0】客户端内部需求(整理简单弹窗样式测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1527 2021-09-27 15:21:54 +08:00
lyr
f5386cd3c1 优化专题合集-排行榜样式显示更新逻辑 2021-09-27 15:16:06 +08:00
65aab99639 Updated 将Flutter镜像地址配置迁移到dependencies.gradle 2021-09-27 12:01:00 +08:00
lyr
7540659396 【光环助手V5.3.0】专题合集-排行榜功能优化(20210926UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471#note_119218 2021-09-27 11:56:21 +08:00
bbb7904477 Updated 更新Flutter模块 2021-09-27 11:48:02 +08:00
056985531b 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化(0922 产品验收问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-27 11:07:36 +08:00
730739077b 【光环助手V5.3.0】游戏详情-开服相关优化(0926测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-27 09:41:15 +08:00
6c82184d1d 【光环助手V5.3.0】通用链接内容合集-客户端需求(20210926UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-26 20:15:05 +08:00
d3b6f6a1e4 修改通用链接列表滑动闪退问题 2021-09-26 17:29:42 +08:00
lyr
d35550900b 【光环助手V5.3.0】专题合集-排行榜功能优化(0926测试 2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471#note_119210 2021-09-26 17:11:30 +08:00
dfc16e38eb 修改游戏详情标签被切割问题 2021-09-26 15:28:01 +08:00
08bd5f3081 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化(0922 产品验收问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-26 11:08:02 +08:00
7d98fde53a Merge branch 'dev-5.3.0' of git.ghzs.com:halo/android/assistant-android into dev-5.3.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/entity/LinkEntity.kt
2021-09-26 10:38:50 +08:00
486e9ae0c5 【光环助手V5.3.0】通用链接内容合集-数据埋点(测试补充:3(1)(3),4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-26 10:37:32 +08:00
d51587aa66 【光环助手V5.3.0】通用链接内容合集-数据埋点 (0924测试 2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-26 10:34:13 +08:00
1fabcc756d 视频帖详情页的埋点新增字段 2021-09-26 09:44:45 +08:00
ee3fc058e1 【光环助手V5.3.0】通用链接内容合集-客户端需求(0924测试3,4,5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-24 17:59:02 +08:00
lyr
e66e9e1ba8 【光环助手V5.3.0】游戏搜索-客户端功能优化(第8期)(20210917测试问题 2、9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1402#note_118197 2021-09-24 17:28:44 +08:00
1cb4bea73c 修改QA详情关闭页面由web端控制 2021-09-24 16:37:59 +08:00
lyr
b3a491e723 【光环助手V5.3.0】专题合集-排行榜功能优化(2、3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1471 2021-09-24 15:28:37 +08:00
b583aafa25 Fixed 使用帮助无搜索结果情况下,点击反馈按钮无反应的问题 2021-09-23 18:46:51 +08:00
9709c6cbf0 【光环助手V5.3.0】光环前端优化汇总第二周 (0922测试 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-23 18:32:19 +08:00
ca81eb2ecb 【光环助手V5.3.0】游戏管理-镜像设置优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1400 2021-09-23 17:58:32 +08:00
910d886f65 【光环助手V5.3.0】通用链接内容合集-数据埋点 (二) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-23 17:18:44 +08:00
376411c0cf 调整申请版主按钮点击逻辑 2021-09-23 17:12:44 +08:00
ad019eff69 修改申请版主弹窗REQUEST_CODE 2021-09-23 16:43:42 +08:00
3712e67594 修改通用链接详情列表滑动闪退问题 2021-09-23 16:40:52 +08:00
c98084ec39 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-23 16:25:46 +08:00
5a851ea99d 调整申请版主按钮点击逻辑 2021-09-23 16:25:39 +08:00
849cdb234c 调整通用链接合集图片圆角 2021-09-23 15:43:54 +08:00
9f9313b5e7 调整申请版主按钮点击逻辑 2021-09-23 15:40:12 +08:00
1888d8cfc7 修改通用链接合集列表复用问题 2021-09-23 12:09:06 +08:00
393b747f0e Updated 优化项目架构 2021-09-23 11:58:58 +08:00
d8f9a7ed7f 【光环助手V5.3.0】[安装方式管理]新增[白名单]功能https://git.ghzs.com/pm/halo-app-issues/-/issues/1264 2021-09-23 11:09:52 +08:00
7be4541f89 调整帖子/提问详情间距 2021-09-23 10:29:24 +08:00
97e87a9f32 Fixed 从原生页面回退到Flutter页面,输入框粘贴按钮点击失效 2021-09-22 19:34:56 +08:00
2d84309f04 【光环助手V5.3.0】新社区4期-详情页布局优化(0916UI测试问题6,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1500 2021-09-22 17:05:09 +08:00
f2ed7f8b72 【光环助手V5.3.0】通用链接内容合集-数据埋点(一) https://git.ghzs.com/pm/halo-app-issues/-/issues/1474 2021-09-22 16:47:31 +08:00
c839d535d8 【光环助手V5.3.0】通用链接内容合集-客户端需求(20210918测试问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-22 11:24:37 +08:00
623e155c75 【光环助手V5.3.0】光环前端优化汇总第三周 (4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-18 17:16:36 +08:00
b20a74de00 整理下载管理类方法描述 2021-09-18 15:52:11 +08:00
090b5fe04f Added 添加页面注释 2021-09-18 15:49:57 +08:00
b7dca9b652 Fixed 搜索框长按复制粘贴文字为英文的问题
Fixed 搜索结果无数据时点击反馈时无反应的问题(应该跳转到反馈页)
Fixed 搜索关键字相同时点击搜索的逻辑问题(相同关键字时不进行搜索)
2021-09-18 15:44:50 +08:00
c9402dec4e 整理下载管理类方法描述 2021-09-18 15:39:51 +08:00
6dc097f60f Fixed 状态栏颜色问题
Fixed UI调整
2021-09-18 12:01:55 +08:00
636a826583 【光环助手V5.3.0】后台优化汇总第三周(10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1525 2021-09-18 10:41:25 +08:00
5c313ab7ac Fixed 解决ToolBar后退按钮适配问题
Updated 移除Flutter Debug标记
2021-09-17 18:52:58 +08:00
4aacc425a9 简单弹窗样式整理https://git.ghzs.com/halo/android/assistant-android/-/issues/39 2021-09-17 16:28:24 +08:00
lyr
c7db544684 【光环助手V5.3.0】光环前端优化汇总第三周(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-17 16:09:21 +08:00
c7833bae11 处理游戏实体数组越界问题 2021-09-17 15:53:00 +08:00
lyr
3d777781b8 【光环助手V5.3.0】新社区4期-游戏详情页埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1468 2021-09-17 11:57:21 +08:00
f2501eeb3b Fixed 意见反馈Q群点击无反应
Fixed 全版本点击跳转黑屏
Fixed 意见反馈Q群被写死
Fixed 搜索无结果缺省图丢失
Updated 使用Aop的形式注入Flutter Module代码代替面向接口编程形式
2021-09-16 17:41:29 +08:00
b72f9819d7 【光环助手V5.3.0】光环前端优化汇总第三周(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-16 16:59:22 +08:00
5f134fad33 【光环助手V5.3.0】新社区4期-详情页布局优化(0916UI测试问题 2,6,7,8,9,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1500 2021-09-16 16:09:36 +08:00
21cec51fad 优化首页滑动流畅度 2021-09-16 15:19:54 +08:00
048d3aa361 【光环助手V5.3.0】光环前端优化汇总第三周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1529 2021-09-16 14:24:44 +08:00
lyr
8871d12fa8 微信分享图片改用FileProvider方式分享 2021-09-16 11:43:54 +08:00
lyr
67f931f9c8 修复游戏动态-分享图片过程中保存路径与分享路径不一致的问题 2021-09-16 11:15:57 +08:00
lyr
2fe9d6c3e1 修复游戏动态-分享图片过程中保存路径与分享路径不一致的问题 2021-09-16 11:05:51 +08:00
a662d5b90c Merge branch 'feature-issue1473' into dev-5.3.0 2021-09-16 10:52:56 +08:00
c30ec4bd87 【光环助手V5.3.0】通用链接内容合集-客户端需求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1473 2021-09-16 10:49:48 +08:00
ee0a3a9d05 Updated 将Flutter调用Native方法的实现逻辑优化
Fixed 反馈页背景色不正确问题
2021-09-15 19:58:56 +08:00
094a8abfe2 Merge branch 'feature-v2signature_verifier' into 'dev-5.3.0'
提高 V2 签名公钥获取效率

See merge request halo/android/assistant-android!124
2021-09-15 11:36:57 +08:00
e1c39e90a9 提高 V2 签名公钥获取效率 2021-09-15 11:36:57 +08:00
aca3f2d2fc 【光环助手V5.3.0】曝光事件数据埋点优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-15 10:02:02 +08:00
05d95259e2 Deleted 移除多余的build.gradle声明 2021-09-14 18:54:55 +08:00
c278359909 Fixed 解决Flutter模块加载慢的问题(库下载导致的问题)
Fixed 添加缺失的Flutter资源混淆声明
2021-09-14 18:40:15 +08:00
lyr
b5d8d40462 【光环助手V5.3.0】游戏搜索-数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1403 2021-09-14 18:25:35 +08:00
c1b7c380da 【光环助手V5.3.0】曝光事件数据埋点优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-14 16:21:54 +08:00
e4f0ba0495 Merge branch 'cherry-pick-92941bcc' into 'dev'
修复新分类-精选-专题合集图片无法正常显示问题

See merge request halo/android/assistant-android!123
2021-09-14 15:53:12 +08:00
lyr
ccd4a479a9 修复新分类-精选-专题合集图片无法正常显示问题
(cherry picked from commit 92941bcc38)
2021-09-14 15:52:20 +08:00
lyr
92941bcc38 修复新分类-精选-专题合集图片无法正常显示问题 2021-09-14 15:49:50 +08:00
6b55875547 调整开服快速填写弹窗逻辑 2021-09-14 11:06:00 +08:00
a0947a84d9 Merge remote-tracking branch 'origin/dev' into dev-5.3.0 2021-09-14 10:33:47 +08:00
fca0e03180 【光环助手V5.3.0】曝光事件数据埋点优化 (4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-14 10:31:28 +08:00
3dad7d8850 【光环助手V5.3.0】曝光事件数据埋点优化 20210913测试问题(2) 2021-09-13 18:46:31 +08:00
bc18301dc1 Fixed 修复正式环境下帮助与反馈接口报错问题
Added 添加阿里云镜像,加速镜像加载
Fixed Flutter方法注入的方法名错误问题
Added 将Flutter使用的资源加入混淆白名单
Added 添加Flutter模块相关的混淆规则
2021-09-13 18:37:08 +08:00
1ebe8eba4e 【光环助手V5.3.0】曝光事件数据埋点优化 (20210913测试问题 1~3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-13 18:27:09 +08:00
lyr
3ec3388b08 【光环助手V5.3.0】游戏搜索-客户端功能优化(第8期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1402 2021-09-13 18:22:27 +08:00
71c2e8eb6b 移除WebFragment多余代码 2021-09-13 17:49:55 +08:00
009aa9f73e ROM 信息获取支持鸿蒙 2021-09-13 14:49:19 +08:00
14e38bc738 调整开服快速填写弹窗UI 2021-09-13 11:20:27 +08:00
74ffd80b10 处理评论列表卡片不存在机型信息时版本信息显示错位的问题 2021-09-12 18:08:22 +08:00
cb7a04ff09 修复长按环境标签不能保存部分页面快捷跳转的问题 2021-09-12 17:24:49 +08:00
96640ad91e 修复长按环境标签不能保存部分页面快捷跳转的问题 2021-09-12 16:44:34 +08:00
3791dfd558 Updated 使用Fresco代替Glide加载Flutter图片 2021-09-10 18:20:34 +08:00
91017caac4 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-10 18:13:48 +08:00
5d8720687a 【光环助手V5.3.0】曝光事件数据埋点优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1322 2021-09-10 18:11:34 +08:00
75b1234376 Merge branch 'feature-download_btn' into dev-5.3.0 2021-09-10 16:44:28 +08:00
1e866a9eab 【光环助手V5.3.0】新社区4期-详情页布局优化(调整页面边距) https://git.ghzs.com/pm/halo-app-issues/-/issues/1500 2021-09-10 16:31:14 +08:00
80e58080c4 整理 WebFragment/WebActivity 的代码https://git.ghzs.com/halo/android/assistant-android/-/issues/36 2021-09-10 16:29:00 +08:00
394036a0d0 处理首页内存泄露问题 2021-09-10 15:53:11 +08:00
2f2355f36e 首页视频封面图改用fresco加载 2021-09-10 11:37:27 +08:00
e711e30c21 Update README.md 2021-09-09 11:45:12 +08:00
e3d32057ee 统一处理列表下载按钮文案和样式(优化) https://git.ghzs.com/halo/android/assistant-android/-/issues/10 2021-09-09 11:42:18 +08:00
e42c4e595e 【光环助手V5.3.0】埋点数据问题汇总[3(2)] https://git.ghzs.com/pm/halo-app-issues/-/issues/1369 2021-09-09 09:38:17 +08:00
73d71e5dac qa详情链接增加qa_title参数 2021-09-09 09:31:01 +08:00
d6cad14e7b Merge branch 'dev' into dev-5.3.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/fragment/HomeSearchToolWrapperFragment.kt
2021-09-08 20:18:15 +08:00
lyr
9beaf93595 调整 DialogFragment 的实例化代码(优化)https://git.ghzs.com/halo/android/assistant-android/-/issues/38#note_117062 2021-09-08 18:46:17 +08:00
074610a9f0 统一处理列表下载按钮文案和样式 https://git.ghzs.com/halo/android/assistant-android/-/issues/10 2021-09-08 18:04:10 +08:00
7881edaf21 Merge remote-tracking branch 'origin/dev-5.3.0' into dev-5.3.0 2021-09-08 17:05:30 +08:00
f2643308f9 【光环助手V5.3.0】游戏详情-开服相关优化(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-08 17:05:17 +08:00
1168be9a92 更新 leakCanary 和 lottie 的依赖版本 2021-09-08 16:12:03 +08:00
c34d6141bc 【光环助手V5.3.0】游戏详情-开服相关优化(1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-08 15:31:12 +08:00
e1f62e3a3a 去掉不必要的 armeabi 2021-09-08 14:55:20 +08:00
1d067dbaa2 【光环助手V5.3.0】Q&A管理-事件埋点(1(1)-(4)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1393 2021-09-08 10:44:17 +08:00
f1b6e742d9 Merge branch 'dev' into feature-flutter-feedback 2021-09-08 09:58:24 +08:00
149448c04c Deleted 移除QaActivity的startActivity方法(已弃用) 2021-09-08 09:57:13 +08:00
e388a60d43 Updated 修改跳转问题页的Hook为DirectUtils.directToQa和DirectUtils.directToQaCollection
Added 将DirectUtils加入app模块的proguard-rules.txt中
2021-09-08 09:56:16 +08:00
ea49ef2691 Updated 更新Flutter Module 2021-09-07 22:39:07 +08:00
741b2c23d2 Updated 更新Flutter Module 2021-09-07 18:40:16 +08:00
dca3542cec 停用游戏替换数量统计 2021-09-07 18:12:49 +08:00
lyr
4a37af4ce4 调整 DialogFragment 的实例化代码 https://git.ghzs.com/halo/android/assistant-android/-/issues/38 2021-09-07 17:31:49 +08:00
a2e907faeb 调整新游戏更新页面的隐藏逻辑 2021-09-07 15:28:07 +08:00
7e33243fbb 【光环助手V5.3.0】游戏详情-开服相关优化(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1469 2021-09-07 15:04:00 +08:00
2b0abb1740 Added 添加可选优化编译选项 2021-09-07 12:49:17 +08:00
60e4f2a378 Fixed 解决接入Flutter模块后编译速度慢的问题,大大提升编译速度
Added 接口Host改为从app中读取
2021-09-07 12:28:52 +08:00
a4603213b9 修复新游戏更新页面其它游戏版本忽略更新无效的问题 2021-09-07 11:56:02 +08:00
da9ea61fbf 显示版本更新为 5.3.0 2021-09-07 11:08:33 +08:00
4ceef741ea 【光环助手V5.3.0】光环前端优化汇总第二周 (3, 4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-07 11:06:21 +08:00
d323fc07f9 【光环助手V5.3.0】光环前端优化汇总第二周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-06 18:19:38 +08:00
2356e175f9 【光环助手V5.3.0】光环前端优化汇总第二周(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1517 2021-09-06 18:00:52 +08:00
7a9c0ed8f2 修复光环插件更新没有在新游戏更新页面显示的问题 2021-09-06 17:42:51 +08:00
d406cf3fcb 【光环助手V5.3.0】新社区4期-详情页分享面板权限优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1501 2021-09-06 15:16:13 +08:00
9741cac1cf Updated 提升编译速度 2021-09-06 10:55:09 +08:00
456519e06c 优化首页滑动改变背景颜色性能问题 2021-09-03 17:01:53 +08:00
bad6b495b1 Updated 提升编译速度 2021-09-03 16:53:50 +08:00
638e0c988f 【光环助手V5.3.0】新社区4期-提问帖详情页评论区权限优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1502 2021-09-03 16:02:18 +08:00
40aaabad7d 测试环境api版本改为v5d3d0 2021-09-03 15:49:11 +08:00
46cf8cfbdc 网页保存图片到图库添加存储权限检测 2021-09-03 15:32:58 +08:00
2226bcf515 调整游戏更新页面我的版本的筛选逻辑 2021-09-03 15:22:56 +08:00
27ceb7b042 Updated 提升编译速度 2021-09-03 11:24:23 +08:00
c2226f8452 Updated 提升编译速度 2021-09-03 11:21:57 +08:00
38dcdaf8e3 修复社区 tab 选中异常问题 2021-09-03 11:15:38 +08:00
2a440e91fa 修改首页点击tab切换背景异常问题 2021-09-03 10:16:02 +08:00
2cecb9a14c 修复随机生成数组的越界问题 2021-09-02 19:05:25 +08:00
241f976866 修复返回活动页面显示异常的问题 2021-09-02 16:32:02 +08:00
560c270723 添加初始化微博 SDK 异常日志 2021-09-02 12:36:47 +08:00
5159957722 【光环助手V5.3.0】Q&A管理-前端优化需求(第三期)(1(3),2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1394 2021-09-02 11:14:32 +08:00
cfb851c23e Merge branch 'dev' into feature-flutter-feedback 2021-09-02 10:17:54 +08:00
e45218de17 微调游戏详情详情信息间距 2021-09-02 09:45:04 +08:00
0ff34bd3f7 【光环助手V5.3.0】Q&A管理-前端优化需求(第三期)(1(1)(2),3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1394 2021-09-01 17:59:11 +08:00
6bc7a9f504 Merge branch 'dev' into feature-flutter-feedback 2021-09-01 17:45:25 +08:00
7ea53f305d merge 2021-09-01 17:45:15 +08:00
lyr
4a04f6bb0f 修复开服表游戏分类筛选结果为空而导致的闪退问题 2021-09-01 12:35:46 +08:00
633b7df021 升级 QQ/微信 SDK https://git.ghzs.com/halo/android/assistant-android/-/issues/37 2021-09-01 11:47:57 +08:00
4ddaccd3f4 Updated Flutter AndroidManifest声明迁移到hook模块 2021-09-01 10:50:49 +08:00
389c20b6bf 更改游戏安装引导显示逻辑 2021-09-01 10:10:58 +08:00
20149b6041 更改游戏安装引导显示逻辑 2021-08-31 20:57:16 +08:00
2f6af6c18e Updated 优化代码 2021-08-31 18:54:41 +08:00
b764a43abe Merge remote-tracking branch 'origin/dev' into dev 2021-08-31 18:08:20 +08:00
c3fb8b9c89 修复帖子详情页底部的阴影显示问题 2021-08-31 18:08:09 +08:00
d574b09dfa 完善安装引导显示逻辑 2021-08-31 17:11:13 +08:00
7fd7197909 版本升级至 5.1.2 2021-08-31 15:12:20 +08:00
58178d2871 Merge branch 'hotfix-v5.1.1-371-crash' into 'release'
处理一系列闪退

See merge request halo/android/assistant-android!122
2021-08-31 15:09:32 +08:00
880838c263 处理一系列闪退
1. 修复文本自动填充偶发的闪退
2. 修复软键盘弹起时切换页面触发的闪退
3. 修复在后台执行下载任务时偶发的闪退
4. 修复用户发表旧视频内容在新版本个人主页浏览时的闪退
5. 修复游戏详情专区返回时偶发的闪退
2021-08-31 15:06:33 +08:00
c196d5cdf5 【光环助手V5.2.0】小米等设备第三方安装引导功能-客户端(测试1、2、4,数据埋点0830测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1410 2021-08-31 14:23:37 +08:00
c370edaa72 Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0 2021-08-30 21:38:14 +08:00
0640aee554 更新游戏安装引导接口 2021-08-30 21:37:06 +08:00
8a5f11e6bd 移除测试用旧游戏更新 tab 2021-08-30 21:31:43 +08:00
92b6ebcb08 更新游戏安装引导接口 2021-08-30 21:28:49 +08:00
758c331af5 【光环助手V5.2.0】新社区3期-论坛展示-社区搜索页(20200830UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1405 2021-08-30 19:12:49 +08:00
17e5c0535d Merge branch 'hotfix-v5.1.1-indicator' into 'release'
修改首页 indicator 不显示问题

See merge request halo/android/assistant-android!121
2021-08-30 18:06:01 +08:00
66fee84b49 Merge branch 'hotfix-v5.1.1-setting' into 'release'
修复"游戏下载设置页面的自动安装游戏选项和自动关注游戏选项默认值错误"问题

See merge request halo/android/assistant-android!120
2021-08-30 18:00:20 +08:00
lyr
bbe0350f2c 修复"游戏下载设置页面的自动安装游戏选项和自动关注游戏选项默认值错误"问题 2021-08-30 17:57:19 +08:00
b31fe4b94a Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-30 17:46:45 +08:00
11b414fd29 修改首页indicator不显示问题 2021-08-30 17:45:48 +08:00
1a35b5ded0 修改首页indicator不显示问题 2021-08-30 17:35:50 +08:00
4245c1511e Merge remote-tracking branch 'origin/release' into dev-5.2.0
# Conflicts:
#	dependencies.gradle
2021-08-30 17:22:45 +08:00
989c3706dd 【光环助手V5.2.0】专题推荐图标功能优化 (20210826测试问题 3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1390 2021-08-30 17:09:17 +08:00
49500768db 修复正式环境微博分享内容不显示的问题 2021-08-30 16:28:17 +08:00
65becdac85 【光环助手V5.2.0】下载管理相关优化 (0830UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-30 15:49:42 +08:00
125c78fccd 【光环助手V5.2.0】光环前端优化汇总第四周 (0827测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1454 2021-08-30 15:44:14 +08:00
lyr
58c576b950 【光环助手V5.2.0】开服表相关优化(0827测试 2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115509 2021-08-30 14:25:53 +08:00
b08aa46992 【光环助手V5.2.0】下载管理相关优化 (0830测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-30 14:20:25 +08:00
c77f0cf817 完成下载管理相关优化(20210827UI测试问题 & 0827测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-30 11:44:42 +08:00
bb0378c216 【光环助手V5.2.0】游戏礼包优化(第二期)(0830测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1325 2021-08-30 11:26:35 +08:00
f2e7a64e99 【光环助手V5.2.0】适龄等级提示(0830测试反馈2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1397 2021-08-30 10:04:59 +08:00
759cacf933 更新游戏安装引导实体类 2021-08-27 17:32:34 +08:00
lyr
0b213253ce 【光环助手V5.2.0】开服表相关优化(0827测试 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115509 2021-08-27 16:11:56 +08:00
f0ab333892 【光环助手V5.2.0】首页轮播图显示背景色功能(20210826UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-27 16:06:26 +08:00
lyr
7516db3a60 【光环助手V5.2.0】开服表相关优化(0827测试 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115509 2021-08-27 15:54:34 +08:00
lyr
29cd798ca8 【光环助手V5.2.0】开服表相关优化(0826测试 第3、4、5、6点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_115199 2021-08-27 12:00:57 +08:00
40cb902baf Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-27 11:51:07 +08:00
15bbcf3eae 完成下载管理相关优化(0826测试 2~5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-27 11:49:06 +08:00
f508e103c1 【光环助手V5.2.0】适龄等级提示(20210827UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1397 2021-08-27 11:38:38 +08:00
94fa36125d Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-27 10:41:59 +08:00
1c754a6dd0 【光环助手V5.2.0】首页轮播图显示背景色功能(20210827测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-27 10:41:42 +08:00
609e4a49b8 完成光环前端优化汇总第三周(0827测试 1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1449 2021-08-27 09:45:18 +08:00
98710c9542 Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0 2021-08-26 18:34:33 +08:00
0e2f5fdf53 【光环助手V5.2.0】浏览器安装功能优化(0826测试、0826UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1323 2021-08-26 18:34:01 +08:00
e9ed5530d5 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-26 18:28:36 +08:00
113f1d3ed8 【光环助手V5.2.0】新社区3期-论坛展示-头像信息(20210826UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-26 18:28:26 +08:00
3fc60a57b6 完成光环前端优化汇总第四周(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1454 2021-08-26 18:18:52 +08:00
d14e09187e 完成光环前端优化汇总第三周(1) 2021-08-26 18:10:26 +08:00
bf90131841 完成开服表相关优化(0826测试第7点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1220 2021-08-26 17:22:39 +08:00
40b7a16b21 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-26 16:26:43 +08:00
882d7273a1 【光环助手V5.2.0】首页轮播图显示背景色功能(20210826测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-26 16:26:38 +08:00
6cfeddf3d4 处理首页 debug 状态闪退 2021-08-26 11:43:38 +08:00
lyr
a06e6c581d 优化开测表加载逻辑 2021-08-26 11:04:52 +08:00
bd8bda40ec 【光环助手V5.2.0】新社区3期-论坛展示-头像信息(0825 产品验收问题2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-26 10:42:08 +08:00
efd81a4e0c 版本升级到 5.1.1 2021-08-26 10:22:00 +08:00
493e08ce2c Merge branch 'hotfix-v5.1.0-370-crash' into 'release'
修复 5.1.0-370 的一些闪退

See merge request halo/android/assistant-android!119
2021-08-26 10:21:23 +08:00
1906451060 修复一系列闪退问题
1. 修复因为特殊处理光遇更新判断而触发的其它下载闪退问题
2. 修复帖子视频详情页点击点赞/关注等按钮触发一键登录时的闪退问题
3. 修复下载完成点击通知栏下载完成通知偶发的闪退问题
4. 修复从首页安利墙发表新安利后点击到达评论可能触发的闪退问题
5. 修复论坛详情在页面被内存回收重建时下拉刷新的闪退问题
6. 修复游戏详情页浏览专区在页面被内存回收重建时点返回按钮偶发的闪退问题
7. 修复开测表列表不存在推荐标签时的闪退问题
2021-08-26 10:17:08 +08:00
f865e95ae1 Revert "临时简单处理光遇游戏包点击更新时会卡住的问题"
This reverts commit 86edc8b9
2021-08-25 18:23:30 +08:00
480e18b79a 完成开服表相关优化(0825测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1220#note_114996 2021-08-25 18:02:17 +08:00
6436bc6191 修复下载管理优化 0824测试问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1244 2021-08-25 18:00:24 +08:00
7ec7eb29e4 【光环助手V5.2.0】新社区3期-论坛展示-社区搜索页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1405 2021-08-25 17:47:04 +08:00
5deb744ba3 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-25 14:56:06 +08:00
e908939560 【光环助手V5.2.0】新社区3期-论坛展示-头像信息(0825 产品验收问题1,2,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-25 14:55:55 +08:00
e02708bb0e 【光环助手V5.2.0】浏览器安装功能优化(0825测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1323 2021-08-25 14:52:41 +08:00
4826c35a70 【光环助手V5.2.0】新社区3期-论坛展示-内容举报弹窗(0825 产品验收问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1421 2021-08-25 11:52:59 +08:00
c51f185438 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-24 18:07:40 +08:00
65bd15b573 处理首页轮播图Tab切换颜色问题 2021-08-24 18:07:30 +08:00
5283254c1a Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0 2021-08-24 17:41:53 +08:00
15458df982 添加UI通用组件 2021-08-24 17:41:22 +08:00
eb2228fb17 Added HaloApp添加Native方法注册入口--initFlutterDyncExtension 2021-08-24 15:15:49 +08:00
0e27e8ee3b 处理礼包详情使用说明不显示问题 2021-08-24 14:29:05 +08:00
17455efe82 应用版本更新至 5.2.0 2021-08-24 11:45:35 +08:00
e2c80cb416 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-24 11:33:49 +08:00
a48b4db550 【光环助手V5.2.0】游戏礼包优化(第二期)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1325 2021-08-24 11:33:40 +08:00
1d66637b11 补充数据埋点注释 2021-08-24 10:20:07 +08:00
lyr
3e7e98d555 添加在新页面打开全屏webview的JS调用方法 2021-08-23 17:21:44 +08:00
60e542923c Updated 将hook模块集成到Flutter Module中 2021-08-23 17:13:23 +08:00
e737d16f0f 【光环助手V5.2.0】小米等设备第三方安装引导功能-数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1412 2021-08-23 16:16:22 +08:00
a04219518b Merge remote-tracking branch 'origin/dev-5.2.0' into dev-5.2.0
# Conflicts:
#	app/src/main/res/values/colors.xml
2021-08-23 15:39:20 +08:00
ce2c6bed0a 【光环助手V5.2.0】小米等设备第三方安装引导功能-客户端 https://git.ghzs.com/pm/halo-app-issues/-/issues/1410 2021-08-23 15:37:35 +08:00
84a08493e3 优化首页轮播图切换时的背景显示效果 2021-08-23 14:42:32 +08:00
3d411fa49c 优化首页轮播图显示背景色性能问题 2021-08-23 11:55:07 +08:00
c0a0c90a49 Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-23 10:58:02 +08:00
a67d41296e 【光环助手V5.2.0】适龄等级提示 https://git.ghzs.com/pm/halo-app-issues/-/issues/1397 2021-08-23 10:57:50 +08:00
25a95ddba0 Added Flutter Hook 2021-08-22 10:48:03 +08:00
fac255a27c 完成游戏更新相关 https://git.ghzs.com/pm/halo-app-issues/-/issues/1392 2021-08-21 15:27:42 +08:00
0811c8dc15 完成专题推荐图标功能优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1390 2021-08-21 15:12:04 +08:00
7537963a8e 支持url跳转视频详情 2021-08-21 14:50:33 +08:00
36f8b1de0b Merge branch 'feature-issues1244' into 'dev-5.2.0'
下载管理相关优化

See merge request halo/android/assistant-android!118
2021-08-21 10:57:45 +08:00
1d9e1bc9d8 下载管理相关优化 2021-08-21 10:57:45 +08:00
bb9fd24068 【光环助手V5.1.0】新社区2期-埋点文档(0820 产品验收问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-08-20 16:13:21 +08:00
9c044ae98b Merge branch 'dev-5.2.0' of git.ghzs.com:halo/android/assistant-android into dev-5.2.0 2021-08-20 15:25:37 +08:00
9807883d65 【光环助手V5.2.0】游戏礼包优化(第二期)(1-3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1325 2021-08-20 15:25:26 +08:00
lyr
04e410615d 开服表向上加载末尾增加开服时间 2021-08-20 11:25:42 +08:00
eda1eea2de 完成社区探索之旅(测试汇总8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-08-20 10:54:52 +08:00
lyr
c1f6ef35f2 优化开服表页面处理数据逻辑 2021-08-20 10:09:41 +08:00
lyr
b61ce1e42a 【光环助手V5.2.0】开服表相关优化(前端部分)https://git.ghzs.com/pm/halo-app-issues/-/issues/1220 2021-08-19 18:20:10 +08:00
62e60f4309 修改删除社区帖子toast文案 2021-08-19 15:50:17 +08:00
c6f544136f 测试环境api版本改为v5d2d0 2021-08-19 11:13:42 +08:00
d588315759 【光环助手V5.2.0】新社区3期-论坛展示-头像信息 https://git.ghzs.com/pm/halo-app-issues/-/issues/1406 2021-08-19 11:04:19 +08:00
b1fef73c54 【光环助手V5.1.0】新社区运营验收问题(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-18 18:54:56 +08:00
c4fc31c963 【光环助手V5.1.0】新社区运营验收问题(17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-18 18:49:15 +08:00
85b0d0eef4 【光环助手V5.2.0】新社区3期-论坛展示-内容举报弹窗 https://git.ghzs.com/pm/halo-app-issues/-/issues/1421 2021-08-18 17:36:20 +08:00
812eb842e2 【光环助手V5.1.0】新社区运营验收问题(12) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-18 14:43:51 +08:00
783cb95f24 对接版主删除视频评论接口 2021-08-18 11:04:14 +08:00
63b7f294c4 【光环助手V5.2.0】浏览器安装功能优化(1-3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1323 2021-08-18 10:10:48 +08:00
80c95b0f11 【光环助手V5.2.0】首页轮播图显示背景色功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1321 2021-08-17 17:23:46 +08:00
5969fa2ca5 网页跳转上传视频的论坛类型改为可选 2021-08-17 15:47:45 +08:00
0187608918 【光环助手V5.1.0】新社区运营验收问题(27) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-17 10:57:55 +08:00
f7b155d3b4 Fixed 修复一些BUG 2021-08-17 10:36:14 +08:00
82656a7187 Deleted 移除无用的文件 2021-08-17 09:57:20 +08:00
9993824d93 Updated 将Flutter模块设为子module 2021-08-16 19:11:03 +08:00
e2fa4990bd 重构我的光环页面https://git.ghzs.com/halo/android/assistant-android/-/issues/31 2021-08-16 18:34:38 +08:00
6753d04817 Merge branch 'dev' into dev-5.2.0
# Conflicts:
#	app/build.gradle
2021-08-16 18:26:15 +08:00
9679c75f0a Added 添加自定义SearchBar 2021-08-16 16:53:28 +08:00
c203fa5b9d Updated 完成通用tabbar开发
Updated 优化项目架构
2021-08-16 16:24:40 +08:00
98531376d9 Merge remote-tracking branch 'origin/dev' into dev 2021-08-16 15:30:27 +08:00
86edc8b919 临时简单处理光遇游戏包点击更新时会卡住的问题 2021-08-16 15:30:15 +08:00
7903751d85 【光环助手V5.1.0】新社区运营验收问题(23) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-16 14:28:35 +08:00
2620a29a2b Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-16 11:25:57 +08:00
5cc40c09dc 【光环助手V5.1.0】新社区运营验收问题(17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-16 11:25:46 +08:00
5c02d37852 启动事件添加应用安装来源信息 2021-08-16 11:19:04 +08:00
2e5d445d65 完成暑期好游安利大赏(0813测试汇总反馈 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1284 2021-08-16 10:19:28 +08:00
456822f56f Added 添加通用Tabbar 2021-08-13 18:23:50 +08:00
c8bae7d89b 【光环助手V5.1.0】新社区运营验收问题(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1442 2021-08-13 16:20:20 +08:00
a5aceb3d1f 更新微博 SDK https://git.ghzs.com/halo/android/assistant-android/-/issues/30 2021-08-13 16:13:01 +08:00
578ed5d1f1 Added 添加反馈页
Added 添加通用AppBar
Added 设置基础路由
2021-08-12 18:11:17 +08:00
3ef7343309 Merge branch 'weibo' into dev-5.2.0 2021-08-12 16:41:24 +08:00
2c98c38721 修改视频贴详情原创标签被切割 2021-08-11 23:05:33 +08:00
lyr
4f28c54591 论坛活动url增加分类id参数 2021-08-11 22:47:07 +08:00
06b5b885e9 修改视频帖详情展开按钮是否显示 2021-08-11 21:05:42 +08:00
612cc2ca9b Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-11 18:26:56 +08:00
18f7b695e5 修改发布视频帖内容来源改为必选 2021-08-11 18:26:45 +08:00
f29e2fe1de 添加简单的扩大点击区域方法 2021-08-11 18:14:56 +08:00
799c22093f 处理编译警告 2021-08-11 16:38:06 +08:00
2496c1d96e 修复全屏网页网络异常加载失败时无法返回的问题 2021-08-11 16:28:31 +08:00
8ee76af30f 优化内存占用 2021-08-11 15:53:40 +08:00
lyr
2f35442558 【光环助手V5.1.0】 0809产品测试问题(第14点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-11 15:38:01 +08:00
lyr
81fa6a6233 【光环助手V5.1.0】 0809产品测试问题(第11点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-11 15:10:51 +08:00
lyr
e5778d5b5f 【光环助手V5.1.0】 0809产品测试问题(第6点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-10 17:39:35 +08:00
f69d607ecb Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-10 16:48:30 +08:00
79fc2e1f93 【光环助手V5.1.0】 0809产品测试问题(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-10 16:48:20 +08:00
6dfdbb9ee8 【光环助手V5.1.0】 0809产品测试问题(5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1439 2021-08-10 15:55:35 +08:00
5d8f7b3f8d 模拟器游戏启动时更新金手指文件 https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-08-10 12:02:17 +08:00
cd4601a9c9 添加下载更新简单文件的方法 2021-08-10 12:00:04 +08:00
462a011401 提交LGLibrary 2021-08-10 09:09:59 +08:00
31d55dae14 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-09 20:03:56 +08:00
8dc9731299 【光环助手V5.1.0】新社区2期-论坛展示-提问帖详情页(0806UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1347 2021-08-09 20:03:45 +08:00
66c19c644e 【光环助手V5.1.0】 0809运营测试问题(1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1438 2021-08-09 17:24:31 +08:00
2b617e2697 完成单机模拟器-金手指功能后台(0809测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-08-09 16:35:24 +08:00
3a60a497e1 更换版主修改问题提示弹窗文案 2021-08-09 16:15:18 +08:00
a9337aee63 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-09 15:52:49 +08:00
c80cd47ccc 【光环助手V5.1.0】 0803产品运营测试问题(2)(8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-09 15:52:38 +08:00
lyr
d9c2371488 移除冗余的资源文件 https://git.ghzs.com/halo/android/assistant-android/-/issues/33 2021-08-09 15:42:15 +08:00
096d19751a 【光环助手V5.1.0】0805产品测试问题(9)https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-09 15:15:41 +08:00
ef70119090 尝试修复 Loghub 日志上报 logstore 错乱的问题 https://github.com/aliyun/aliyun-log-android-sdk/issues/68 2021-08-09 11:56:16 +08:00
95dba71bb9 添加异步登录 JS Api 2021-08-09 10:17:16 +08:00
ec37c7a6f8 【光环助手V5.1.0】 0806产品运营测试问题(7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1436 2021-08-09 09:28:17 +08:00
dcc9352301 调整 Kotlin 版本 2021-08-06 14:34:43 +08:00
c509c6bb38 添加使用快捷方式启动模拟器游戏失败的日志记录 2021-08-06 12:01:23 +08:00
4784d689f8 处理内嵌网页的一些空指针异常 2021-08-06 12:00:31 +08:00
8903a075be Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-06 11:46:37 +08:00
48d6e91b0e 【光环助手V5.1.0】0805运营测试问题(4,6,8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1435 2021-08-06 11:46:27 +08:00
30865239d2 【光环助手V5.1.0】 0806产品运营测试问题(3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1436 2021-08-06 11:31:09 +08:00
8b7cd92ae8 微调 chucker 编译脚本 2021-08-06 11:21:46 +08:00
69f336553e 调整 Kotlin 版本,修复 XAPK 解压在 4.4 以下设备的闪退问题 2021-08-06 11:07:10 +08:00
ed82f96ae3 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-06 10:17:22 +08:00
8f02016a76 【光环助手V5.1.0】新社区2期-论坛展示-提问帖详情页(0806UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1347 2021-08-06 10:17:12 +08:00
c06f397d12 【光环助手V5.1.0】0805产品测试问题(9,10,12)https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-06 10:08:34 +08:00
lyr
912b7280cd 【光环助手V5.1.0】0805产品测试问题(第5点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-05 17:15:48 +08:00
50f7dd2c63 【光环助手V5.1.0】0805产品测试问题(3,4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1434 2021-08-05 15:49:12 +08:00
23469543c7 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-05 14:48:14 +08:00
2d3f70dd12 【光环助手V5.1.0】 0804产品运营测试问题(1)(12) https://git.ghzs.com/pm/halo-app-issues/-/issues/1430 2021-08-05 14:48:09 +08:00
lyr
f57d19e797 【光环助手V5.1.0】 0804产品运营测试问题(1)(第10点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1430#note_112113 2021-08-05 10:48:52 +08:00
lyr
8c529f0724 【光环助手V5.1.0】新社区2期-论坛展示-信息流-提问帖评论(0804UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1344#note_111919 2021-08-05 10:19:24 +08:00
lyr
1583f1957a 完成积分体系-光能中心、光能屋UI更改 https://git.ghzs.com/pm/halo-app-issues/-/issues/1300 2021-08-04 18:44:40 +08:00
lyr
676ccb133f 【光环助手V5.1.0】 0803产品运营测试问题(2)(第3点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-04 15:45:31 +08:00
489b8143a1 【光环助手V5.1.0】 0804产品运营测试问题(1) (1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1430 2021-08-04 15:06:44 +08:00
c4cf3efa21 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-04 14:26:57 +08:00
871cfd638d 【光环助手V5.1.0】新社区2期-论坛展示-帖子详情页(0804UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1346 2021-08-04 14:26:47 +08:00
lyr
1f3ae1c687 【光环助手V5.1.0】 0803产品运营测试问题(1)(第23、24点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-04 14:21:55 +08:00
59eb101a48 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-04 12:14:09 +08:00
b3b8b6ba29 【光环助手V5.1.0】 0803产品运营测试问题(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-04 12:14:02 +08:00
lyr
42baaa8950 【光环助手V5.1.0】 0803产品运营测试问题(2)(第3点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1428 2021-08-04 11:07:51 +08:00
lyr
61ad70d3b8 【光环助手V5.1.0】新社区2期-论坛展示-信息流-提问帖评论(0804UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1344#note_111919 2021-08-04 10:59:35 +08:00
lyr
04252e3b91 【光环助手V5.1.0】 0803产品运营测试问题(22)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 18:20:41 +08:00
lyr
b40543d7b5 【光环助手V5.1.0】 0803产品运营测试问题(16)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 17:30:26 +08:00
lyr
903fa49b71 【光环助手V5.1.0】 0803产品运营测试问题(13)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 16:28:08 +08:00
lyr
03ae2699c6 【光环助手V5.1.0】 0803产品运营测试问题(11)https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 16:09:05 +08:00
8ecc4078bb 【光环助手V5.1.0】 0803产品运营测试问题(1,20,27,31) https://git.ghzs.com/pm/halo-app-issues/-/issues/1426 2021-08-03 15:59:02 +08:00
ebaf4f02f0 【光环助手V5.1.0】新社区2期-论坛展示-论坛详情-版主成员(0802UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1345 2021-08-03 11:21:03 +08:00
e85437379a 调整金手指功能数据字段 https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-08-02 17:20:44 +08:00
2e9642019b 微博SDK更新到11.6 2021-08-02 17:08:58 +08:00
5a77522a04 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-02 16:18:48 +08:00
e5512604fa 修改上传图片的 type 参数 2021-08-02 16:18:39 +08:00
lyr
146a46f8be 【光环助手V5.1.0】0802产品测试问题(1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1423 2021-08-02 16:04:00 +08:00
91f33f06d3 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-02 15:59:00 +08:00
59da7b6ba6 【光环助手V5.1.0】新社区2期-视频帖发布页(0802UI测试1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1355 2021-08-02 15:58:45 +08:00
lyr
0a1ad238f8 【光环助手V5.1.0】新社区2期-论坛详情页引导(0802UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1352#note_111430 2021-08-02 15:43:24 +08:00
302e6c145c 【光环助手V5.1.0】新社区2期-论坛展示-帖子详情页(0802UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1346 2021-08-02 15:37:40 +08:00
a6bbf0dfc7 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-08-02 15:17:06 +08:00
9c3d22964c 【光环助手V5.1.0】0730产品测试问题(10,22,23,24) https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 15:16:56 +08:00
ab60ed8473 【光环助手V5.1.0】新社区2期-论坛展示-论坛详情-版主成员(0802UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1345 2021-08-02 14:31:07 +08:00
lyr
9a00e70cc4 【光环助手V5.1.0】0730产品测试问题(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 11:16:46 +08:00
lyr
35954f52c5 【光环助手V5.1.0】0730产品测试问题(1-3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 10:58:25 +08:00
e13d80d063 【光环助手V5.1.0】0730产品测试问题(11)https://git.ghzs.com/pm/halo-app-issues/-/issues/1422 2021-08-02 10:13:56 +08:00
73ed15689d 基本完成金手指功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1362 2021-07-30 18:07:23 +08:00
lyr
65d949bade 【光环助手V5.1.0】新社区2期-推荐页引导(0729UI测试)https://git.ghzs.com/pm/halo-app-issues/-/issues/1350#note_110977 2021-07-29 18:08:01 +08:00
33d0d01051 【光环助手V5.1.0】新社区2期-帖子发布页(0729UI测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1354 2021-07-29 16:34:29 +08:00
9998ed0a14 【光环助手V5.1.0】新社区2期-发布页引导(帖子、提问)(0729UI测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1351 2021-07-29 16:15:41 +08:00
85a8a17acb Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-29 15:44:34 +08:00
4032cb8abe 【光环助手V5.1.0】新社区2期-视频帖发布页(0729UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1355 2021-07-29 15:44:18 +08:00
fc2e87063e Merge remote-tracking branch 'origin/dev' into dev 2021-07-29 14:34:37 +08:00
a63b809642 清理测试内容 2021-07-29 14:34:22 +08:00
c30048c268 正式环境接口切换到 5.0 2021-07-29 11:01:41 +08:00
956d5a39be 修复优化汇总(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1342#note_110932 2021-07-29 09:59:51 +08:00
76fd1ab7e3 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-28 20:47:40 +08:00
2c4c954c64 【光环助手V5.1.0】新社区2期-论坛展示-信息流-审核中/审核不通过(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1343 2021-07-28 20:47:23 +08:00
aef4da961d 正式包丢失渠道后的默认渠道改为 GH_LOST 2021-07-28 17:24:17 +08:00
lyr
31e00bb681 【光环助手V5.1.0】新社区2期-埋点文档(版规说明遗漏部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-28 16:50:30 +08:00
369fdd24ef 调整内容及评论投诉弹窗交互 https://git.ghzs.com/pm/halo-app-issues/-/issues/1373 2021-07-28 15:28:25 +08:00
2a57994c7f Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-28 15:21:58 +08:00
6f84b14277 处理升级gsyplayer版本报错问题 2021-07-28 15:21:52 +08:00
10ed05c8ef Merge branch 'feature-upgrade_gsyplayer' into dev
# Conflicts:
#	app/build.gradle
2021-07-28 15:05:18 +08:00
lyr
799bd1042f 【光环助手V5.1.0】新社区2期-埋点文档 https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-28 14:07:27 +08:00
6f9acab2d2 修改版主申请审核中的字段 2021-07-28 11:29:45 +08:00
lyr
387d8eb5af 优化社区-论坛页代码 2021-07-27 18:31:10 +08:00
c813c5d5ef 暂时放弃 arm64 的 SO 2021-07-27 17:35:58 +08:00
1c6df6741f 完成光环前端优化汇总第五周(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1388 2021-07-27 16:45:11 +08:00
b54ab5f824 支持网页将图片保存至图库中 https://git.ghzs.com/pm/halo-app-issues/-/issues/1286 2021-07-27 16:31:05 +08:00
lyr
37fdf6c18e 【光环助手V5.1.0】光环前端优化汇总第五周(4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1388 2021-07-27 16:00:00 +08:00
2685de9b3e Merge remote-tracking branch 'origin/dev' into dev 2021-07-27 15:17:59 +08:00
c049100f5f webView 支持选择本地图片 2021-07-27 15:16:13 +08:00
711d1f9d65 【光环助手V5.1.0】新社区2期-论坛展示-论坛详情-版主成员https://git.ghzs.com/pm/halo-app-issues/-/issues/1345 2021-07-27 15:14:11 +08:00
5d7453afec 完成浏览论坛页面时间任务功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-07-27 11:00:21 +08:00
e4acf466ba 版主加精问题评论更换接口 2021-07-26 14:49:45 +08:00
2d66cbd29c 【光环助手V5.1.0】新社区2期-埋点文档(分享面板) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-26 10:07:10 +08:00
8786f7d500 【光环助手V5.1.0】新社区2期-论坛展示-提问帖详情页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1347 2021-07-23 18:07:13 +08:00
3d40d7a819 新增默认选中论坛首页tab的 url scheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-07-22 17:50:22 +08:00
6f7862a0f2 新增默认选中论坛首页tab的 url scheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1285 2021-07-22 17:37:43 +08:00
58c6211d54 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-22 17:34:02 +08:00
d10b187a1f 视频帖详情版主修改活动标签 2021-07-22 17:33:52 +08:00
lyr
1ebf34c83a 【光环助手V5.1.0】新社区2期-埋点文档(版规说明页以及处理漏传)https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-22 16:53:01 +08:00
01e2a3c708 1.版主修改活动标签 2.审核中、审核不通过点击图片toast提示 2021-07-22 15:08:20 +08:00
lyr
c9b97552d4 【光环助手V5.1.0】新社区2期-埋点文档(论坛详情页)https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-21 17:59:30 +08:00
428f39aa86 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-21 11:13:48 +08:00
5fecb723bc 【光环助手V5.1.0】新社区2期-论坛展示-帖子详情页(1(1)(3)(4),2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1346 2021-07-21 11:13:38 +08:00
0168b0d93a 【光环助手V5.1.0】新社区2期-帖子发布页(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1354 2021-07-21 11:09:39 +08:00
895fa510fe 【光环助手V5.1.0】新社区2期-论坛展示-视频帖详情页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1348 2021-07-21 11:04:00 +08:00
0aae59a1ba 恢复网页跳转旧视频流详情页的能力 2021-07-21 10:39:11 +08:00
574a2b4dc7 完成内容举报弹窗更改 https://git.ghzs.com/pm/halo-app-issues/-/issues/1373 2021-07-21 09:25:30 +08:00
acae608966 添加跳转至个人主页定位到视频分类的 url scheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1284#note_107842 2021-07-20 18:29:37 +08:00
lyr
6ca6c34f92 【光环助手V5.1.0】新社区2期-埋点文档(部分)https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-20 17:55:13 +08:00
16724a2d2e 完成部分内容举报弹窗更改(帖子、提问帖、视频帖) https://git.ghzs.com/pm/halo-app-issues/-/issues/1373 2021-07-20 16:15:54 +08:00
8b50cc561a 补充判断用户类型的逻辑 https://git.ghzs.com/pm/halo-app-issues/-/issues/1293 2021-07-20 09:23:01 +08:00
1ec1d482c4 【光环助手V5.1.0】新社区2期-埋点文档(跳出事件埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-19 19:49:47 +08:00
869b9d507b Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-19 16:21:14 +08:00
514fe66347 【光环助手V5.1.0】新社区2期-埋点文档 https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-19 16:21:04 +08:00
lyr
657c91a5ef 【光环助手V5.1.0】新社区2期-论坛展示-信息流-提问帖评论 https://git.ghzs.com/pm/halo-app-issues/-/issues/1344 2021-07-19 11:29:36 +08:00
82354a1156 添加从网页跳转至发视频页面的跳转 urlScheme https://git.ghzs.com/pm/halo-app-issues/-/issues/1284 2021-07-16 18:05:40 +08:00
b7619f0c93 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-16 16:38:25 +08:00
598a3ad0b6 【光环助手V5.1.0】新社区2期-埋点文档(部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1303 2021-07-16 16:38:18 +08:00
bf0be93dc9 完成"返回活动"的小浮窗功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1284 2021-07-16 15:28:54 +08:00
lyr
90a676f778 【光环助手V5.1.0】新社区2期-论坛展示-信息流-审核中/审核不通过 https://git.ghzs.com/pm/halo-app-issues/-/issues/1343 2021-07-16 10:29:50 +08:00
lyr
a675e4bbe3 【光环助手V5.1.0】光环前端优化汇总第二周(6)https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-16 10:08:01 +08:00
5eee8c6785 更改积分"返回活动"浮窗的实现方式 2021-07-15 18:11:12 +08:00
3c54e4313c Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-15 17:08:09 +08:00
67c4b76de9 【光环助手V5.1.0】新社区2期-帖子发布页(UI部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1354 2021-07-15 17:07:59 +08:00
34fd08d06a 【光环助手V5.1.0】新社区2期-视频帖发布页(UI部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1355 2021-07-15 17:07:21 +08:00
lyr
d08b95021d 暂时隐藏社区-论坛-其他福利-万能加速器(因为跳转目的地不唯一) 2021-07-15 16:12:16 +08:00
8517a2aef1 应用启动日志上报添加包名/签名等其它信息 2021-07-14 14:57:43 +08:00
lyr
cbe90141f1 修改社区-推荐引导图错位问题 2021-07-14 14:50:10 +08:00
lyr
fd43bc9426 【光环助手V5.1.0】新社区2期-论坛详情页引导 https://git.ghzs.com/pm/halo-app-issues/-/issues/1352 2021-07-14 14:48:52 +08:00
lyr
34a143b632 【光环助手V5.1.0】新社区2期-推荐页引导 https://git.ghzs.com/pm/halo-app-issues/-/issues/1350 2021-07-14 09:39:56 +08:00
d96e02eca0 完成光环前端优化汇总第二周(1~3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-13 17:52:36 +08:00
lyr
398b1ae58b 【光环助手V5.1.0】新社区2期-引导页 https://git.ghzs.com/pm/halo-app-issues/-/issues/1349 2021-07-13 14:30:59 +08:00
0749205bef 更新离线已收录游戏列表 2021-07-13 14:13:56 +08:00
54ec115fee 修复判断新老用户接口问题 2021-07-13 12:37:08 +08:00
lyr
ebe7b84dc8 【光环助手V5.1.0】个性化推荐合规相关(2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1308 2021-07-13 09:38:49 +08:00
lyr
332abe66f5 【光环助手V5.1.0】光环前端优化汇总第二周(5 优化)https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-12 14:33:31 +08:00
lyr
9dccbfbd51 【光环助手V5.1.0】光环前端优化汇总第二周(5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1342 2021-07-12 14:13:22 +08:00
23a8c9e6aa 临时处理 JCenter 依赖问题 2021-07-12 11:32:41 +08:00
1e4408ac6e 【光环助手V5.1.0】新社区2期-消息中心相关优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1309 2021-07-09 16:06:33 +08:00
lyr
05ece9c999 光环助手v5.0.0-新社区后台问题汇总(20210708)(8 优化) https://git.ghzs.com/pm/halo-app-issues/-/issues/1364 2021-07-09 15:18:23 +08:00
lyr
216905e455 光环助手v5.0.0-新社区APP端问题汇总(20210708)(8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1364 2021-07-09 14:27:37 +08:00
a1bd88dad8 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-09 12:41:14 +08:00
16c66c707f 【光环助手V5.1.0】新社区2期-发布页引导(帖子、提问)https://git.ghzs.com/pm/halo-app-issues/-/issues/1351 2021-07-09 12:40:56 +08:00
lyr
50cdea2b03 光环助手v5.0.0-新社区APP端问题汇总(20210707)(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-09 11:12:49 +08:00
95b0fda15a 版本号更新到 5.1.0 2021-07-09 10:29:32 +08:00
9c99273f81 光环助手v5.0.0-新社区APP端问题汇总(20210707)(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-08 21:47:46 +08:00
8d9d36a5df 移除部分无用代码 2021-07-08 18:25:18 +08:00
lyr
b22d168e97 光环助手v5.0.0-新社区APP端问题汇总(20210706)(10 优化) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-08 15:53:49 +08:00
lyr
1fee166e3b 光环助手v5.0.0-新社区APP端问题汇总(20210707)(13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-08 14:54:25 +08:00
lyr
7c098c29b2 光环助手v5.0.0-新社区APP端问题汇总(20210706)(10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-08 14:42:19 +08:00
ffc9dc46e2 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-08 11:19:41 +08:00
c491a4084c 修改选择活动标签抖动问题 2021-07-08 11:19:24 +08:00
lyr
0f076a03c5 光环助手v5.0.0-新社区APP端问题汇总(20210707)(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-08 10:44:33 +08:00
3345ab2044 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-07 20:41:58 +08:00
5874bdd311 发布帖子/问题视频未上传完不能发布 2021-07-07 20:41:34 +08:00
lyr
4669211196 微调我的光环UI 2021-07-07 18:47:29 +08:00
4879e7f0aa Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-07 18:27:53 +08:00
f5283d386b 1.视频详情点赞按钮防重复点击实现限制为2s 2.修改插入视频时间显示错误问题 2021-07-07 18:27:44 +08:00
lyr
602333f923 光环助手v5.0.0-新社区APP端问题汇总(20210707)第6点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1360 2021-07-07 16:39:23 +08:00
e9810c129e 光环助手v5.0.0-新社区后台问题汇总(20210707)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1361 2021-07-07 15:22:56 +08:00
c503f15df9 光环助手v5.0.0-新社区APP端问题汇总(20210706)(13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-07 14:40:02 +08:00
cba1012bbd Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-07 11:47:39 +08:00
646493ae61 光环助手v5.0.0-新社区APP端问题汇总(20210706)(11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-07 11:47:30 +08:00
b649089ab0 更换发帖子/问题插入视频接口 2021-07-07 11:46:49 +08:00
lyr
e97fd9023e 光环助手v5.0.0-新社区APP端问题汇总(20210706)第2点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-07 10:42:39 +08:00
478dd658ab 光环助手v5.0.0-新社区APP端问题汇总(20210706)(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1358 2021-07-06 18:04:47 +08:00
b313feac01 Merge branch 'dev' of git.ghzs.com:halo/android/assistant-android into dev 2021-07-06 17:24:36 +08:00
c04b82549e 视频帖详情页全屏播放顶部增加标题 2021-07-06 17:24:23 +08:00
lyr
842e62f909 微调个人主页和新分类2.0的UI 2021-07-06 16:28:12 +08:00
lyr
51c6de0506 微调社区-论坛UI 2021-07-06 15:20:19 +08:00
lyr
d350f65c1f 修改活动详情测试环境地址 2021-07-05 18:36:49 +08:00
lyr
aaf8e28abc 光环助手v5.0.0-新社区APP端问题汇总(4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1329 2021-07-05 18:28:07 +08:00
7cb921e969 选择活动标签接口增加location参数 2021-07-05 16:35:57 +08:00
lyr
3a4b41058d 光环助手V5.0.0-新社区展示功能(20200702UI测试问题、UI测试问题补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106840 2021-07-05 15:03:28 +08:00
lyr
26308d1852 光环助手V5.0.0-新社区其他相关改动功能(0702 产品测试问题 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1254#note_106775 2021-07-05 14:24:34 +08:00
af54932309 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-05 11:51:03 +08:00
83799048ed 光环助手v5.0.0-新社区APP端问题汇总(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1329 2021-07-05 11:50:54 +08:00
ce410ad2a0 更改网页 url 的地址添加来源 query 的实现 2021-07-05 09:30:55 +08:00
933c40458f 社区插入视频length字段的统一单位为秒 2021-07-05 09:24:10 +08:00
6ae7578281 光环助手V5.0.0-新社区展示功能(0702 产品测试问题1,3,4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-07-03 15:25:57 +08:00
lyr
72b804e26e 处理论坛信息流GIF图边框显示不对的问题 2021-07-02 18:03:48 +08:00
lyr
7e58cdc59e 修改社区-论坛Tab每次进入页面都会被切割的问题 2021-07-02 17:20:20 +08:00
lyr
d782ea90b6 光环助手V5.0.0-新社区展示功能(0701 产品测试问题 3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106529 2021-07-02 15:18:49 +08:00
lyr
efffd752f5 提交漏传资源文件 2021-07-02 14:25:20 +08:00
47f3f45fe6 修复推荐优先专题游戏没有推荐信息时简介不显示大小的问题 2021-07-02 10:30:00 +08:00
b4e12e2ec3 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-01 21:08:15 +08:00
2d331f6294 光环助手V5.0.0-新社区展示功能(0701 产品测试问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-07-01 21:08:09 +08:00
lyr
feb4137f84 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题汇总 7 gif没有边框问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105161 2021-07-01 18:57:45 +08:00
6a2c4a2967 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-01 18:16:32 +08:00
68fc5532ff 1.用户回答了问题消息通知跳转新回答详情 2.删除无用导包 2021-07-01 18:16:26 +08:00
lyr
43ae479271 光环助手V5.0.0-新社区展示功能(0630 产品测试问题 5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106346 2021-07-01 18:12:52 +08:00
7a3ea4d939 处理版本升级时的数据库更新问题 2021-07-01 18:04:43 +08:00
lyr
1bcb343355 光环助手V5.0.0-新社区展示功能(0630 运营测试问题 4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106346 2021-07-01 17:54:36 +08:00
1b7a601cee 处理游戏详情点击启动状态的下载按钮的滚动问题 2021-07-01 16:26:53 +08:00
lyr
8bc9544dc2 光环助手V5.0.0-新社区展示功能(0630 运营测试问题 2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106346 2021-07-01 16:03:13 +08:00
e586e65e44 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-07-01 15:47:38 +08:00
8096da5f50 光环助手V5.0.0-新社区展示功能(0701 产品测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-07-01 15:47:34 +08:00
91868fda15 微调专题游戏推荐语布局 2021-07-01 14:48:23 +08:00
lyr
a71e8824ce 光环助手V5.0.0-新社区展示功能(0629 产品测试问题 15-17)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106052 2021-07-01 11:04:28 +08:00
6cbc354921 完成前端优化汇总(16) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-30 18:14:50 +08:00
lyr
79e2077a3d 光环助手V5.0.0-新社区展示功能(0629 产品测试问题 14)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106052 2021-06-30 18:04:55 +08:00
lyr
6ea3ed067d 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题汇总 7)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105805 2021-06-30 17:06:22 +08:00
lyr
84fe33becc 光环助手V5.0.0-新社区其他相关改动功能 (0629UI问题 4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1254#note_106173 2021-06-30 16:38:25 +08:00
276f53fead Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-30 16:15:06 +08:00
0789cc6fdf 光环助手V5.0.0-新社区其他相关改动功能(0625 运营测试问题2(1)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1254 2021-06-30 16:15:01 +08:00
lyr
3a563b7bd0 光环助手V5.0.0-新社区其他相关改动功能 (0628 产品测试问题 2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1254#note_105919 2021-06-30 15:55:30 +08:00
lyr
9b8ba0c3e6 光环助手V5.0.0-新社区其他相关改动功能(20210622UI测试问题、0623 产品测试问题、0625 运营测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1254 2021-06-30 15:38:38 +08:00
258f054161 光环助手V5.0.0-新社区展示功能(0629 运营测试问题22) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-30 15:15:19 +08:00
258c8db0f9 光环助手V5.0.0-新社区展示功能(0629 运营测试问题23,24) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-30 11:44:16 +08:00
741d0b11af Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-29 18:47:13 +08:00
9267896b50 光环助手V5.0.0-新社区展示功能(0629 运营测试问题13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-29 18:47:03 +08:00
85c0bd69c1 调整 VolumeObserver 入参,避免内存泄漏 2021-06-29 18:45:58 +08:00
2436507b7b Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-29 17:58:06 +08:00
1b68b07e53 光环助手V5.0.0-新社区展示功能(0629 产品测试问题8,10) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-29 17:57:55 +08:00
lyr
c4ba28de1a 光环助手V5.0.0-新社区展示功能(20200629UI测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_106168 2021-06-29 17:26:37 +08:00
lyr
8daa55c16a 光环助手V5.0.0-新社区展示功能 (0629 产品测试问题 第4、5点) 2021-06-29 17:09:47 +08:00
lyr
5e6c6aca13 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题补充 第2点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105806 2021-06-29 16:31:06 +08:00
lyr
e08ab790ee 优化新分类2.0引导UI 2021-06-29 15:59:26 +08:00
lyr
a394901cb4 光环前端优化汇总(2021年5月)(20210629UI问题补充) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260#note_106103 2021-06-29 15:34:12 +08:00
lyr
2aaa7f2795 优化社区-论坛Tab的UI 2021-06-29 15:07:10 +08:00
7b0d2a25c8 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-29 09:40:35 +08:00
e3f553a96f 光环助手V5.0.0-新社区展示功能(0628 产品测试问题4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-29 09:40:25 +08:00
lyr
809a25e3e2 光环助手V5.0.0-新社区展示功能(0628测试问题-热门论坛应该不展示已关注的论坛)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105293 2021-06-28 18:58:20 +08:00
lyr
88041d9b5f 优化社区-推荐Tab搜索框UI 2021-06-28 16:57:40 +08:00
lyr
a11d741f6f 修复社区推荐Tab和论坛详情在当前论坛列表为空时,发布新内容而未显示的问题 2021-06-28 16:44:06 +08:00
1283def6da 修复首页搜索栏嵌套滚动不灵敏的问题 2021-06-28 16:33:21 +08:00
9c621c31a2 光环助手V5.0.0-新社区展示功能(0621 产品测试问题8(11)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-28 15:54:50 +08:00
9e3f40723f 光环助手V5.0.0-新社区发布功能(0628 产品测试问题1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-28 15:13:46 +08:00
682bb9a3e7 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-28 14:53:55 +08:00
9f2d164c70 光环助手V5.0.0-新社区展示功能(0628 产品测试问题1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-28 14:53:49 +08:00
lyr
bad5130047 修复社区-推荐Tab本人新发布的视频贴时间显示不对问题 2021-06-28 14:23:23 +08:00
e3f831e0ca 修复应用内网页因为地址带特殊字符而出现的加载问题 2021-06-28 11:45:04 +08:00
a1432452f5 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-28 11:31:30 +08:00
b3cc68b7c6 修改跳转至新的问题详情和回答详情 2021-06-28 11:31:25 +08:00
lyr
cc1f5a269a 修改个人主页发布Tab-切换子tab视频还在播放的问题 2021-06-28 11:27:25 +08:00
a2892a713a Merge branch 'release' into dev-5.0.0
# Conflicts:
#	dependencies.gradle
2021-06-28 11:01:59 +08:00
lyr
c342e22ce4 Merge remote-tracking branch 'origin/dev-5.0.0' into dev-5.0.0 2021-06-28 10:02:51 +08:00
lyr
57dcbab096 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题补充)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105806 2021-06-28 10:02:39 +08:00
5aa4c5bcbc 光环助手V5.0.0-新社区展示功能(0625 产品测试问题3,4(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-28 09:58:16 +08:00
lyr
039e1fc957 光环助手V5.0.0-新社区展示功能(0625 产品测试问题 第2点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105792 2021-06-25 18:55:30 +08:00
f7b2d10543 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-25 17:24:13 +08:00
38972eca76 光环助手V5.0.0-新社区发布功能(UI测试问题汇总0621AM 9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-25 17:24:05 +08:00
lyr
c2d853d709 【光环助手V5.0.0】提问帖相关需求(V5.0.0-新社区提问帖相关需求-UI测试问题汇总 第1点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1251#note_105291 2021-06-25 16:47:04 +08:00
lyr
239209dbf2 光环助手V5.0.0-新社区展示功能(0623 产品测试问题 第1、2、4点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105473 2021-06-25 16:05:43 +08:00
lyr
ba1f3e20d3 优化新分类2.0引导图逻辑 2021-06-25 15:02:57 +08:00
lyr
92ac62d949 光环助手V5.0.0-新社区展示功能(20210622UI测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105369 2021-06-25 15:00:29 +08:00
6499e3b718 修改视频贴详情用户关注按钮样式 2021-06-25 11:12:23 +08:00
5f0d2604ec Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-25 10:09:49 +08:00
5f7bb6cb49 光环助手V5.0.0-新社区发布功能(0622 产品测试问题2(2)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-25 10:09:40 +08:00
lyr
7360eef244 光环助手V5.0.0-新社区展示功能(V5.0.0-新社区展示功能-UI测试问题汇总 第1-10点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105161 2021-06-25 09:27:03 +08:00
52e6feab59 修改视频帖详情游戏标签样式 2021-06-24 20:56:09 +08:00
5f44252410 视频贴详情调整视频区域高度 2021-06-24 18:35:13 +08:00
7ae3b1cafb Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 18:13:49 +08:00
0fb77e79fd 光环助手V5.0.0-新社区发布功能(UI测试问题补充0624PM 1,2,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-24 18:13:39 +08:00
lyr
d4ae878947 光环前端优化汇总(2021年5月)(第9-11点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-24 17:58:04 +08:00
37a2e3cc0e 光环助手V5.0.0-新社区发布功能(UI测试问题汇总4,9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-24 17:04:21 +08:00
30595741cc Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 16:46:02 +08:00
11f177d572 完成视频贴详情仿bilibili交互效果 2021-06-24 16:45:51 +08:00
9e13675ca5 完成前端优化汇总(0623测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260#note_105557 2021-06-24 15:14:34 +08:00
144a0641b6 完成游戏专题功能优化(20210617测试问题6) 和 20210622UI测试 2021-06-24 15:13:35 +08:00
95266f6f68 【光环助手V5.0.0】提问帖相关需求(0623测试反馈2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1251 2021-06-24 11:27:56 +08:00
9ded647c0d 光环助手V5.0.0-新社区发布功能(0623 产品测试问题1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-24 10:57:49 +08:00
f566e4d5c3 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 10:40:31 +08:00
5df3efd087 光环助手V5.0.0-新社区展示功能(0623 产品测试问题5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-24 10:40:19 +08:00
98f4361bf0 Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-24 10:09:21 +08:00
000d1e020a 完成关于MOD游戏下载用户的相关限制(APP 接口修改) https://git.ghzs.com/pm/halo-app-issues/-/issues/1293 2021-06-24 10:07:58 +08:00
lyr
93431a7e37 光环助手V5.0.0-新社区展示功能(0622 产品测试问题 2、4、5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105293 2021-06-23 17:59:34 +08:00
lyr
d54523cdb6 光环助手V5.0.0-新社区展示功能(0621 产品测试问题 0、1、2、5)https://git.ghzs.com/pm/halo-app-issues/-/issues/1253#note_105232 2021-06-23 15:59:03 +08:00
ed468b7c73 【光环助手V5.0.0】提问帖相关需求(0623测试反馈1,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1251 2021-06-23 15:55:11 +08:00
bad6211bf5 光环助手V5.0.0-新社区发布功能 0622 产品测试问题 1,2(1)(3)(4)(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-23 14:33:27 +08:00
2743d4d0de Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-22 15:43:58 +08:00
a0f5387917 光环助手V5.0.0-新社区展示功能(0621 产品测试问题4,6,7,8(1,3,4,5,7,8,9,11)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-22 15:43:49 +08:00
1abfcfdb51 abiFilters 添加 arm64 2021-06-22 14:28:42 +08:00
ecc0a75f52 光环助手V5.0.0-新社区展示功能(UI测试问题汇总10-14) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-21 16:53:21 +08:00
c98bf26337 光环助手V5.0.0-新社区发布功能(UI测试问题汇总1,3-9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1252 2021-06-21 15:44:25 +08:00
187d07d02d 完成光环前端优化汇总(5, 14, 16, 17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-18 17:51:59 +08:00
c47c23ad21 修复因懒加载优化而造成的下载进度更新异常问题 2021-06-18 09:46:30 +08:00
8d011ae7ef Merge branch 'hotfix-v4.9.6-337-generic_bugs' into 'release'
修复下载模拟器时会暂停其它下载任务的问题

See merge request halo/android/assistant-android!117
2021-06-17 18:38:04 +08:00
36b58383f7 版本更新至 4.9.6-338 2021-06-17 18:37:32 +08:00
e6ea1738e5 光环前端优化汇总(2021年5月)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-17 16:32:55 +08:00
70b7689ac5 修复下载模拟器时会暂停其它下载任务的问题 2021-06-17 15:18:41 +08:00
40983ecebc 光环前端优化汇总(2021年5月)(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-17 14:41:35 +08:00
47b0a8d1ac 版本更新至 4.9.6-337 2021-06-17 10:43:33 +08:00
1ae63d3010 Merge branch 'hotfix-v4.9.6-336-recreate' into 'release'
修复首页和游戏详情页页面重建时的 tab 显示问题

See merge request halo/android/assistant-android!116
2021-06-17 10:39:51 +08:00
fbe73ce4da Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-16 18:43:48 +08:00
d46617237f 光环助手V5.0.0-新社区展示功能(社区搜索页的搜索结果增加论坛tab) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-16 18:43:43 +08:00
lyr
0894957d73 修改帖子再次编辑修改成功后的闪退问题 2021-06-16 17:58:15 +08:00
9b9777ae19 修复首页和游戏详情页页面重建时的 tab 显示问题 2021-06-16 16:18:55 +08:00
lyr
7f2145d0ef 论坛信息流视频在切换tab时暂停播放 2021-06-16 15:19:21 +08:00
lyr
acefbba6bf Merge remote-tracking branch 'origin/dev-5.0.0' into dev-5.0.0 2021-06-16 14:12:31 +08:00
lyr
660a091b7b 【光环助手5.0.0】一键登录相关优化(第1点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1294 2021-06-16 14:12:25 +08:00
103078d63e 修改图片预览页面点击查看帖子异常 2021-06-16 11:00:28 +08:00
lyr
178395f531 优化论坛信息流审核中视频的显示 2021-06-16 09:52:52 +08:00
lyr
8db213d040 【光环助手V5.0.0】提问帖相关需求(0615测试反馈 3)https://git.ghzs.com/pm/halo-app-issues/-/issues/1251#note_104578 2021-06-16 09:51:13 +08:00
lyr
5904d3c5c7 光环助手V5.0.0-新社区展示功能(前置说明 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1253 2021-06-15 17:53:01 +08:00
1a511ebbad 【光环助手V5.0.0】提问帖相关需求 (0615测试反馈 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1251 2021-06-15 17:09:04 +08:00
ef97e8cc6d 新社区插入视频后封面上传到oss 2021-06-15 16:07:21 +08:00
814cef6218 Merge branch 'hotfix-v4.9.6-336-crash' into 'release'
1.修复Xapk解压过程中获取解压文件大小时的闪退问题

See merge request halo/android/assistant-android!115
2021-06-15 11:55:02 +08:00
lyr
fd6c4836d4 1.修复Xapk解压过程中获取解压文件大小时的闪退问题
2.修复新分类2.0游戏列表时的空指针问题
3.修复新分类2.0已选分类列表的数组越界问题
2021-06-15 11:51:22 +08:00
9df138ed7c Merge branch 'hotfix-v4.9.6-336-install' into 'release'
特殊处理山寨机在后台开启使用浏览器安装时的安装 intent

See merge request halo/android/assistant-android!114
2021-06-15 11:02:37 +08:00
7b11b0e99a Merge branch 'dev-5.0.0' of git.ghzs.com:halo/android/assistant-android into dev-5.0.0 2021-06-11 17:47:24 +08:00
01034f093a 处理合并冲突 2021-06-11 17:45:52 +08:00
8e28766449 Merge branch 'feature-bbs' into dev-5.0.0
# Conflicts:
#	app/src/main/AndroidManifest.xml
#	app/src/main/java/com/gh/common/util/NewLogUtils.kt
#	app/src/main/java/com/gh/gamecenter/personalhome/UserHomeFragment.kt
#	app/src/main/java/com/gh/gamecenter/qa/dialog/MoreFunctionPanelDialog.kt
#	app/src/main/res/layout/fragment_home.xml
#	app/src/main/res/values/colors.xml
2021-06-11 17:34:03 +08:00
lyr
d06d0bd15c 一键登录设置每步超时时间为1.5秒 2021-06-11 17:04:31 +08:00
4fe4a33b80 光环前端优化汇总(2021年5月)(13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-06-11 14:55:10 +08:00
4a8e91bbff 对接视频帖加精 2021-06-11 10:09:13 +08:00
lyr
b1f6c7d55c 1.优化游戏详情顶部视频和论坛信息流视频的静音设置
2.修改个人主页发布记录的类型、点赞、评论、回答数据字段转化
2021-06-10 18:06:55 +08:00
lyr
727eeac52e 修改游戏详情顶部静音设置 2021-06-10 16:26:35 +08:00
fb26ec0c13 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-10 15:45:55 +08:00
3e25619bf8 论坛修改点赞、评论、回答、收藏数据字段 2021-06-10 15:45:50 +08:00
lyr
e81fa4ff1a 论坛根据类型展示图标 2021-06-10 11:41:02 +08:00
d7d2bf2667 更换问题收藏/取消收藏接口 2021-06-10 11:16:39 +08:00
c763a0a9e6 完成选择论坛-搜索论坛功能 2021-06-10 10:28:06 +08:00
1f34f95e7c 特殊处理山寨机在后台开启使用浏览器安装时的安装 intent 2021-06-10 09:53:54 +08:00
2afb99b603 修改编辑视频帖发布按钮一直是置灰状态 2021-06-09 18:38:06 +08:00
lyr
2e09ac033b 优化视频贴类型转化 2021-06-09 18:25:27 +08:00
lyr
54747143d1 1.修改论坛详情-视频Tab无法下拉刷新问题
2.修改论坛详情-列表视频贴未显示内容问题
3.修改论坛详情-全部Tab的问答贴回答数量不对问题
2021-06-09 18:10:10 +08:00
076c98977d 1.修改问题详情评论不显示作者标签 2.修改问题详情点击顶部返回按钮无法关闭页面 2021-06-09 16:47:19 +08:00
lyr
b96104fcd9 对接社区推荐Tab的接口 2021-06-09 16:26:04 +08:00
lyr
daece9337c 修改浏览记录-帖子Item左下角对应论坛UI未显示问题 2021-06-09 14:35:13 +08:00
lyr
3547330e2a 论坛详情页-内容搜索结果增加视频帖的展示 2021-06-08 18:19:20 +08:00
4d76fb0159 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-08 17:47:04 +08:00
f34a95b36f 1.更换提问、草稿、问题详情接口 2.对接视频帖评论接口 2021-06-08 17:46:56 +08:00
lyr
6e3cb7a196 1.论坛列表增加视频自动播放功能
2.论坛首页搜索适配视频贴展示
2021-06-08 17:39:06 +08:00
03f87c46ba 游戏更新相关接口添加版本和渠道 https://git.ghzs.com/pm/halo-app-issues/-/issues/1267 2021-06-08 17:08:02 +08:00
b052ba1635 移除无用的 activity 声明 2021-06-08 16:34:27 +08:00
d1183199cd 全局更改跳转视频详情的页面 https://git.ghzs.com/pm/halo-app-issues/-/issues/1254 2021-06-08 16:31:30 +08:00
31fed948f0 更换社区-推荐的接口 https://git.ghzs.com/pm/halo-app-issues/-/issues/1246 2021-06-08 15:54:44 +08:00
f1c5b80dc2 完成社区内容浏览日志上报 https://git.ghzs.com/pm/halo-app-issues/-/issues/1246 2021-06-08 15:51:10 +08:00
86f2aec9ca 修复社区内容分享日志上报丢失的问题 2021-06-08 15:28:26 +08:00
f826dc07c6 版本更新至 4.9.6 2021-06-08 10:23:08 +08:00
34670d08c0 Merge branch 'hotfix-v4.9.5-335-crash' into 'release'
Hotfix v4.9.5 335 crash

See merge request halo/android/assistant-android!113
2021-06-08 10:22:04 +08:00
5e5646468e 1. 捕抓分类2.0页面延迟操作造成的闪退异常
2. 修复首页滑动 tab 时偶发的闪退
3. 捕抓模拟器游戏延迟操作造成的闪退异常
4. 捕抓礼包详情页延迟操作造成的闪退异常
2021-06-08 10:22:04 +08:00
a218f13ccb Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-08 10:15:23 +08:00
1fc37571df 修改ImageUtils不能加载视频第一帧的图片 2021-06-08 10:15:12 +08:00
lyr
576f06f250 1.修改论坛详情页列表内容显示不对问题
2.修改论坛信息流左下角对应论坛图标显示不出来问题
2021-06-08 10:03:36 +08:00
3805cb52f6 优化论坛插入视频 2021-06-07 21:38:07 +08:00
lyr
f8d7377236 完成新社区-活动Tab 2021-06-07 18:45:43 +08:00
354fa13a35 优化论坛发布功能 2021-06-07 16:02:29 +08:00
3c533b896b 对接我的论坛帖子、我的收藏帖子 2021-06-05 19:18:57 +08:00
11c6618c0f Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-05 10:27:55 +08:00
898bf3e432 粗略完成新问题详情页面 2021-06-05 10:27:49 +08:00
lyr
0faf408762 修复新社区相关数据库迁移报错问题 2021-06-04 17:44:49 +08:00
lyr
1dbabe3cda 提交漏传文件 2021-06-04 16:19:07 +08:00
lyr
a107a19d95 完成社区-论坛Tab-官方论坛对接 2021-06-04 15:43:53 +08:00
lyr
58ea0bb51d 完善论坛信息流视频播放逻辑 2021-06-04 14:25:17 +08:00
557f22b23b rxPermission 更新至 androidX 版本 2021-06-03 16:59:40 +08:00
f40a192e82 调整带 chucker 打包脚本 2021-06-03 16:54:55 +08:00
84f3556275 Merge remote-tracking branch 'origin/release' into dev-5.0.0
# Conflicts:
#	dependencies.gradle
2021-06-03 16:47:45 +08:00
cddc2bff2b 添加简单的 Log 工具类 2021-06-03 16:38:58 +08:00
a0b8caa60a 完成5.0.0游戏专题功能优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1247 2021-06-03 16:38:12 +08:00
lyr
22cadc77f0 1.修改"社区-推荐Tab最近浏览列表首个Item未读红点已读但未消失"问题
2.修改首页底部导航栏动画不对应问题
3.修改论坛详情页关注按钮显示不全问题
2021-06-03 11:28:09 +08:00
28fb45f0b8 添加简单的 Log 工具类 2021-06-03 11:27:44 +08:00
3d7f8b3f41 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-06-03 10:30:04 +08:00
bba326cfd9 1.收藏/取消收藏问题 2.修改我的论坛 2021-06-03 10:29:57 +08:00
lyr
f76bb7070a 优化社区-推荐Tab列表显示 2021-06-03 10:28:36 +08:00
lyr
a6a3d769bd 完成论坛详情页的视频Tab和个人主页的发布Tab的视频分类 2021-06-02 18:16:56 +08:00
ea5627b3da 区分正式包和测试包的默认渠道,避免正式包因渠道丢失而回落到测试渠道 2021-06-02 16:58:41 +08:00
d9807227ec 处理列表分割线显示异常问题 2021-06-01 09:26:26 +08:00
039203408a 版本更新至 4.9.5 2021-05-31 10:32:07 +08:00
60b325812e Merge branch 'hotfix-v4.9.4-334-crash' into 'release'
Hotfix v4.9.4 334 crash

See merge request halo/android/assistant-android!112
2021-05-31 10:29:00 +08:00
06a43f617b 处理闪退
1. 处理首页 tab 触摸时某些设备可能会闪退的问题
2. 修复游戏库刷新按钮被遮盖的问题
3. 尝试修复游戏库横向列表因恢复滚动状态而出现的闪退
4. 修复偶发的因为获取不到渠道号而触发的闪退
5. 尝试修复启动时偶发的初始化图片加载库触发的闪退
6. 修复 Android 11 首次安装 XAPK 获取权限回到页面时的闪退问题
2021-05-31 10:29:00 +08:00
5cc1cfa1a6 完成视频详情推荐列表 2021-05-29 17:17:09 +08:00
35cdd140cc Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-29 16:07:48 +08:00
87597dbd1f 完成帖子、问题发布功能 2021-05-29 16:07:33 +08:00
lyr
398f7b6642 1.完成首页和游戏详情的视频播放设置
2.完成个人主页的发布Tab(除视频分类)
2021-05-28 18:52:10 +08:00
85d3412fd8 版本更新到 4.9.4 2021-05-28 10:28:06 +08:00
71b8cbbef3 Merge branch 'hotfix-v4.9.3-333-crash' into 'release'
Hotfix v4.9.3 333 crash

See merge request halo/android/assistant-android!111
2021-05-28 10:26:50 +08:00
dfc0183a14 1. 处理包名检测弹窗上报日志的空指针闪退问题
2. 捕抓评论列表一键登录可能触发的闪退问题
3. 修复礼包详情页重建时的闪退问题
4. 修复视频流评论页页面重建时的闪退问题
2021-05-28 10:26:50 +08:00
lyr
f0ca0a2ab1 推荐页的浏览记录和论坛页的关注论坛增加未读状态 2021-05-27 15:44:01 +08:00
c9ffd4e5bd Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-26 20:45:06 +08:00
d354b07fe2 1.抽离公共评论详情页面 2.粗略完成问题详情页面UI(未对接接口) 2021-05-26 20:44:46 +08:00
lyr
71a61fdef0 粗略完成社区的推荐Tab和论坛Tab 2021-05-26 19:00:06 +08:00
156e52f619 版本升级到 4.9.3 2021-05-26 14:26:49 +08:00
252cb3825b Merge branch 'hotfix-v4.9.2-332-crash' into 'release'
Hotfix v4.9.2 332 crash

See merge request halo/android/assistant-android!110
2021-05-26 14:26:14 +08:00
2eded37321 首页社区 TAB 和首页视频 TAB 互换位置 2021-05-26 10:20:06 +08:00
075a7e4e77 处理专题详情滚到顶部空指针异常 2021-05-26 09:55:30 +08:00
0c95f911d1 捕抓插件化数据库读写异常 2021-05-26 09:43:40 +08:00
f35baf0e5b 完成[安装方式管理]新增[白名单]功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1264 2021-05-25 17:54:02 +08:00
49b74c9a37 修复包名检测弹窗的重建闪退问题 2021-05-25 14:37:31 +08:00
c4240440d1 版本更新到 4.9.2 2021-05-25 10:56:09 +08:00
e0205ec060 Merge branch 'hotfix-v4.9.1-331-crash' into 'release'
修复闪退

See merge request halo/android/assistant-android!109
2021-05-25 10:55:35 +08:00
895024aa09 1. 修复启动时初始化图片加载库可能的闪退问题
2. 修复游戏详情最新开服可能的闪退问题
3. 修复在越南/法语设备上启动会闪退的问题
2021-05-25 10:53:47 +08:00
4d64281c78 修改视频贴保存草稿功能 2021-05-24 21:20:04 +08:00
95f1692434 Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-24 15:51:12 +08:00
f1302dd1bb 完成视频详情顶部区域 2021-05-24 15:51:00 +08:00
fd946703a9 版本更新至 4.9.1 2021-05-24 15:01:38 +08:00
806935a81e Merge branch 'hotfix-v4.9.0-330-privacy' into 'release'
4.9.0 隐私变更更改

See merge request halo/android/assistant-android!108
2021-05-24 14:59:14 +08:00
feb30ff3a4 1. 去掉 SYSTEM_ALERT_WINDOW 权限声明
2. 完成 https://git.ghzs.com/pm/halo-app-issues/-/issues/1262 的第32点
3. 取消 WorkManager 自动初始化,关闭 Sentry 读取系统事件能力
2021-05-24 14:59:14 +08:00
lyr
fec34f1efa 提交遗漏资源文件 2021-05-24 10:45:18 +08:00
lyr
def3b55e51 完成光环助手V5.0.0-新社区其他相关改动功能(第七点UI) 2021-05-24 09:57:08 +08:00
4549f78ede 粗略完成发视频贴功能 2021-05-23 21:04:00 +08:00
3054b56f4a Merge branch 'feature-bbs' of git.ghzs.com:halo/android/assistant-android into feature-bbs 2021-05-20 14:13:28 +08:00
4cf5e0f2f4 论坛富文本支持插入视频功能 2021-05-20 14:13:22 +08:00
lyr
1a18c82f06 个人主页-根据个人信息内容设置背景的高度 2021-05-19 18:51:49 +08:00
lyr
b8fbb429f1 个人主页-根据个人信息内容设置背景的高度 2021-05-19 18:40:25 +08:00
lyr
005229fe8a 提交个人主页游戏tab改动遗漏部分 2021-05-19 18:31:25 +08:00
lyr
3a0bbbf3d0 完成个人主页游戏tab改动 2021-05-19 18:09:34 +08:00
4b851732c9 正式包暂不启用 AndResGuard (旧版本在新AGP上默认启用 V3 签名,新版本在蓝叠模拟器上无法运行) 2021-05-19 16:53:51 +08:00
lyr
3abf29fa75 微调登录页UI 2021-05-19 15:57:21 +08:00
lyr
4bb7d2a043 光环助手V4.7.0-积分体系(第2期)--光能中心(0519 运营反馈 去掉签到提醒)https://git.ghzs.com/pm/halo-app-issues/-/issues/1110#note_101733 2021-05-19 15:34:23 +08:00
lyr
e3d830235e 微调登录页UI 2021-05-19 14:59:00 +08:00
lyr
6425f3d50c 光环助手V4.9.0-优化登录功能(第3期)(0518 运营反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_101568 2021-05-19 11:11:47 +08:00
lyr
868e6d31b8 提交论坛详情页顶部UI遗漏部分 2021-05-18 15:31:29 +08:00
lyr
a687f8d877 完成论坛详情页顶部UI调整 2021-05-18 15:15:38 +08:00
e9d2c6573f 完成同步正式环境后测试问题汇总(22) https://git.ghzs.com/pm/halo-app-issues/-/issues/1262 2021-05-18 15:02:26 +08:00
05e36f11cf 处理视频流闪退问题 2021-05-18 14:57:36 +08:00
lyr
bc76e6eddb 光环助手V4.9.0-同步正式环境后测试问题汇总(14、16) https://git.ghzs.com/pm/halo-app-issues/-/issues/1262#note_101438 2021-05-18 11:59:50 +08:00
lyr
6de543261b 修改"邀请好友"正式环境地址 2021-05-18 09:15:19 +08:00
lyr
d9a8fa0a2d 光环助手V4.9.0-个人主页优化(第2期)(0517 运营测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203#note_101240 2021-05-17 18:08:40 +08:00
lyr
51c88e8586 光环助手V4.9.0-优化登录功能(第3期)(0517 运营测试问题 1) 2021-05-17 17:10:12 +08:00
9e930f1b35 去掉游戏详情顶部区域自动隐藏时的动画 2021-05-17 15:34:15 +08:00
eb8fb191bb 修改视频流全屏后的显示错误问题 2021-05-17 14:27:37 +08:00
6653467259 完成首页顶部 tab 栏功能(20210517运营测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-05-17 11:39:27 +08:00
9ec29ad367 修复因优化首页 tab 性能造成开服表页面的开测 tab 不显示的问题 2021-05-17 10:13:15 +08:00
406ac98616 激活数据上报里补充 oaid (若存在) 2021-05-15 15:12:52 +08:00
5832c75909 修复同步正式环境后测试问题汇总(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1262 2021-05-14 11:36:59 +08:00
9d8681c50f 光环助手V4.8.0-视频优化(第1期)(0514 运营测试问题3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1107 2021-05-14 10:47:47 +08:00
lyr
9c28bf162e 新分类2.0页-侧边栏关闭手势滑动 2021-05-14 09:37:54 +08:00
3e86a40215 发提问改为富文本 2021-05-13 20:34:09 +08:00
061d5c19ed 微调首页轮播图上边距 2021-05-13 17:38:50 +08:00
1241207dd2 处理首页 tab 无数据时的回落问题 2021-05-13 15:59:30 +08:00
6a68839d0f 1完成选择活动弹窗UI 2.发布帖子、提问删除添加标签入口 2021-05-13 15:31:18 +08:00
2140c9eb40 Update README.md 2021-05-13 15:13:41 +08:00
lyr
702da08bc4 修复问题:在验证码登录页点击一键登录,然后授权电话权限后,无法弹起一键登录页 2021-05-12 18:58:18 +08:00
942af39b2d 大致完成选择论坛、选择本地视频、预览视频、发视频UI 2021-05-12 16:55:18 +08:00
lyr
a0331f0437 光环前端优化汇总(2021年5月)第6点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1260 2021-05-12 14:46:31 +08:00
8c885e38a1 完成4月光环前端优化汇总(6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1236 2021-05-12 11:50:34 +08:00
acc6935608 正式环境 API 切换到 4.9.0 2021-05-12 11:47:56 +08:00
lyr
63798c94cf Merge branch 'dev' into dev-5.0.0
# Conflicts:
#	app/src/main/java/com/gh/common/util/QuickLoginHelper.kt
2021-05-10 17:06:12 +08:00
lyr
62a87d1c71 完善是否能进行一键登录的判断 2021-05-10 09:34:15 +08:00
lyr
2ba479d3ca 光环助手V4.9.0-优化登录功能(第3期)(0507 补充运营意见--增加一键登录说明弹窗 遗漏部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_100061 2021-05-08 17:29:56 +08:00
lyr
6c2ff6a94f 光环助手V4.9.0-优化登录功能(第3期)(0507 补充运营意见--增加一键登录说明弹窗) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_100061
(cherry picked from commit 3f5f3bf57c)
2021-05-08 16:15:54 +08:00
lyr
3f5f3bf57c 光环助手V4.9.0-优化登录功能(第3期)(0507 补充运营意见--增加一键登录说明弹窗) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_100061 2021-05-08 15:55:42 +08:00
2211f562c7 抽离发布帖子、回答公共代码 2021-05-08 15:52:41 +08:00
4d10ca0c6c Merge branch 'dev-4.9.0' into dev 2021-05-08 15:41:28 +08:00
c816fd87f4 Merge branch 'dev-4.9.0' into dev
# Conflicts:
#	app/src/main/res/layout/fragment_login.xml
2021-05-08 10:41:39 +08:00
lyr
547bfd98ac 个人主页-根据个人信息内容设置背景的高度 2021-05-07 11:02:37 +08:00
34ba85f099 修改网页链接全局添加from=ghzs参数 2021-05-06 15:17:00 +08:00
lyr
2d251ef453 调整"我的光环"和"个人主页"徽章标签的红点提示位置 2021-04-29 14:57:29 +08:00
7a0165375c 去掉视频预加载打印日志 2021-04-29 14:46:47 +08:00
lyr
d7c7fe8740 优化个人主页背景UI 2021-04-29 01:10:53 +08:00
295e783d4b Merge remote-tracking branch 'origin/dev-4.9.0' into dev-4.9.0 2021-04-28 23:56:57 +08:00
58dfad4123 调整实名认证解析 2021-04-28 23:56:47 +08:00
f31d8b51fa 修改礼包中心-关注tab游戏icon显示异常 2021-04-28 23:52:41 +08:00
lyr
01ea8b9834 光环助手V4.9.0-个人主页优化(第2期)(0427 产品测试问题 个人主页没有显示圆角问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203 2021-04-28 22:51:31 +08:00
5152060515 调整实名认证结束时间判断 2021-04-28 22:32:42 +08:00
0a0d805e9d 完成防沉迷下载流程 https://git.ghzs.com/pm/halo-app-issues/-/issues/1218 2021-04-28 22:24:08 +08:00
51511c8be8 更换礼包是否被隐藏字段 2021-04-28 21:53:22 +08:00
4bbfa3e16e 修改推荐弹窗按钮点击范围 2021-04-28 21:50:30 +08:00
lyr
81c0c6ee09 Merge remote-tracking branch 'origin/dev-4.9.0' into dev-4.9.0 2021-04-28 20:48:38 +08:00
lyr
50b449ca64 光环助手V4.9.0-优化登录功能(第3期)(0428 产品测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_99487 2021-04-28 20:48:28 +08:00
8764412492 优化首页 tab 滑动性能 2021-04-28 20:30:31 +08:00
c1e011cae8 优化首页 tab 滑动性能 2021-04-28 20:29:36 +08:00
018f814a6b 更新渠道JAR 2021-04-28 20:01:54 +08:00
lyr
ef9cb24b61 个人主页的背景裁剪比例调整为1:1 2021-04-28 17:27:59 +08:00
245a4273de 完成光环前端优化汇总4月(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1236 2021-04-28 16:58:23 +08:00
lyr
94c1651510 一键登录增加错误码log 2021-04-28 16:38:07 +08:00
lyr
07f55acb3a 光环助手V4.9.0-积分体系(第4期)(0428 产品测试问题 2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1204#note_99369 2021-04-28 16:34:17 +08:00
lyr
43ee3b3488 一键登录未弹出授权页增加等待弹窗,以及防止多次点击 2021-04-28 16:33:00 +08:00
32625f59e0 Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-28 16:04:05 +08:00
e1e3aa5598 光环助手V4.9.0-游戏礼包功能优化汇总(0428测试1,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1209 2021-04-28 16:03:58 +08:00
c5ff2cb5bd Merge remote-tracking branch 'origin/dev-4.9.0' into dev-4.9.0 2021-04-28 16:02:23 +08:00
acf02c99e7 完成首页顶部Tab栏功能(20210428测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-04-28 16:02:14 +08:00
9a00999d9e 回退test_build.sh脚本 2021-04-28 15:41:42 +08:00
lyr
4c5b602a3b 光环助手V4.9.0-个人主页优化(第2期)(0427 产品测试问题) (0428 产品测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203 2021-04-28 15:39:24 +08:00
cbde36f3bd 完成0420测试反馈 https://git.ghzs.com/pm/halo-app-issues/-/issues/1231 2021-04-28 15:01:39 +08:00
f4b6193a6b 处理潜在的 toast 内存泄漏问题 2021-04-28 14:32:04 +08:00
85a596766d Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-28 12:03:54 +08:00
785cfecf81 光环助手V4.9.0-游戏相关优化汇总(0428测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-28 12:03:41 +08:00
0c8b98084a Merge remote-tracking branch 'origin/dev-4.9.0' into dev-4.9.0 2021-04-28 12:00:05 +08:00
87a6a3a539 游戏库接口添加版本号 2021-04-28 11:59:56 +08:00
lyr
6c50082de5 光环助手V4.9.0-个人主页优化(第2期)(20210427UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203#note_99322 2021-04-28 11:57:43 +08:00
41195ec9d4 游戏库和搜索接口添加版本号 2021-04-28 11:49:28 +08:00
e280ea008a 升级gsyVideoPlayer版本并修改预加载 2021-04-28 11:08:04 +08:00
223646cf3d 更改专题的筛选字段为 tags 2021-04-28 11:06:47 +08:00
ab4bb79a32 首页接口补充版本信息 2021-04-28 10:16:28 +08:00
lyr
50b51e2f3c 光环助手V4.9.0-个人主页优化(第2期)(20210427UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203#note_99322 2021-04-28 09:23:03 +08:00
lyr
1fef7d51d6 修复光能中心-兑换区-分类面板展开后缺失关闭图标问题 2021-04-28 09:15:44 +08:00
lyr
6546961f81 更新一键登录错误码提示 2021-04-27 19:10:36 +08:00
lyr
75e4f32840 光环助手V4.9.0-积分体系(第4期)(分类面板问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1204#note_99323 2021-04-27 19:01:33 +08:00
lyr
a3f3c95e19 优化:一键登录页跳转验证码登录页后,再登录成功后,增加销毁一键登录页的逻辑 2021-04-27 17:43:34 +08:00
lyr
508231d547 光环助手V4.9.0-注销功能优化(第2期)(0427 产品测试问题 1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1207#note_99285 2021-04-27 17:40:44 +08:00
8649b98032 处理分类2.0在首页的显示问题 2021-04-27 17:25:57 +08:00
d35211262f 回退 AndResGuard 2021-04-27 16:37:10 +08:00
lyr
96a5838155 光环助手V4.9.0-积分体系(第4期)(ui反馈 图1)(0427 产品测试问题 1-4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1204 2021-04-27 16:35:57 +08:00
fc21799288 完成首页顶部Tab栏功能(20210426测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-04-27 16:17:25 +08:00
lyr
3e04d77d50 优化一键登录关闭授权页的逻辑 2021-04-27 15:35:15 +08:00
lyr
35f47e363a 光环助手V4.9.0-个人主页优化(第2期)(0426 产品测试问题) (0427 产品测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203 2021-04-27 15:27:30 +08:00
lyr
019724e9c6 光环助手V4.9.0-优化登录功能(第3期)(0426 产品测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_99181 2021-04-27 14:56:57 +08:00
70f059b4de 微调礼包中心按钮UI 2021-04-27 11:59:53 +08:00
34cd63d36d Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-27 10:43:47 +08:00
033cf6b566 光环助手V4.9.0-游戏礼包功能优化汇总(0427补充1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1209 2021-04-27 10:43:38 +08:00
513fc3be1c V4.9.0推荐弹窗功能:后台通知频率设置累计次数无效 https://jira.ghzs.com/browse/HALO-68 2021-04-27 10:43:00 +08:00
fd1273a092 更新 AndResGuard 修复在高版本 AGP 默认启用 V3 签名的问题 2021-04-27 10:25:55 +08:00
lyr
27a6f14d18 修复Jira问题63( V4.9.0 光能中心后台优化:APP端,抽奖乐园和兑换商品点击进入的页面搞反了) 2021-04-26 18:01:09 +08:00
000c7978fd 完成推荐入口优化及数据埋点(20210426测试问题2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1214 2021-04-26 17:41:35 +08:00
486c8f691c 修复自定义 tab 指示器在 tab 内容变小时会变形的问题 2021-04-26 17:39:30 +08:00
16450a7f8d 完成光环前端优化汇总(4,8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1236 2021-04-26 17:30:50 +08:00
438e660449 Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-26 17:15:16 +08:00
b9bf4311cf 礼包中心-最新tab接口判断分页完毕条件改为10 2021-04-26 17:14:56 +08:00
lyr
75a324747c 光环助手V4.9.0-优化登录功能(第3期)(20210425UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1206#note_99017 2021-04-26 17:10:33 +08:00
04b579cda7 完成首页顶部Tab栏功能(20210426UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-04-26 17:04:03 +08:00
5962a8cf08 光环助手V4.9.0-游戏礼包功能优化汇总(0425测试2,3,4,5,6,10,11,12,19) https://git.ghzs.com/pm/halo-app-issues/-/issues/1209 2021-04-26 16:53:52 +08:00
ea388a8e1b Merge remote-tracking branch 'origin/dev-4.9.0' into dev-4.9.0 2021-04-26 16:20:37 +08:00
8a98d6323a 完成合规问题前端优化汇总(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1219 2021-04-26 16:20:28 +08:00
lyr
1b67ff5d12 光环助手V4.9.0-个人主页优化(第2期)(20210426UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1203#note_99085 2021-04-26 16:13:54 +08:00
9ee0f65e02 完成推荐入口优化和数据埋点(20210426测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1214 2021-04-26 15:56:30 +08:00
557b6fee21 完成合规问题前端优化汇总(1~4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1219 2021-04-26 15:52:34 +08:00
c90d6ec0dc 完成首页顶部Tab栏功能遗漏的曝光位置命名 https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-04-26 11:56:54 +08:00
lyr
8109aa58c9 光环助手V4.9.0-积分体系(第4期)UI反馈 https://git.ghzs.com/pm/halo-app-issues/-/issues/1204#note_98963 2021-04-26 09:50:38 +08:00
213e079e9d 修复首页排行榜切换 tab 时标题的闪烁问题 2021-04-26 09:20:49 +08:00
40e38e50b9 修复自滚动栏目的图标变形问题 2021-04-26 09:20:38 +08:00
b70e18af96 Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-25 19:13:11 +08:00
4c91f06d94 光环助手V4.9.0-游戏礼包功能优化汇总(20210425UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1209 2021-04-25 19:13:00 +08:00
199067cf8a 完成游戏相关优化汇总(0423测试 1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-25 18:07:41 +08:00
fa01260f0b Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-25 17:40:57 +08:00
f8dac32bd6 光环助手V4.9.0-游戏相关优化汇总(20210425UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-25 17:40:40 +08:00
d5641d437c 微调首页 tab 切换样式 2021-04-25 15:09:14 +08:00
lyr
c27f62e726 优化一键登录页逻辑 2021-04-25 14:33:04 +08:00
b98d4e6ec6 完成首页顶部Tab栏功能(20200423UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-04-25 14:14:37 +08:00
a4e67cda91 光环助手V4.9.0-游戏相关优化汇总(0423测试5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-25 10:21:21 +08:00
ab3616f237 Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-23 17:34:59 +08:00
bd9032c659 光环助手V4.9.0-新增[推荐弹窗]功能(0423测试,回退代码) https://git.ghzs.com/pm/halo-app-issues/-/issues/1212 2021-04-23 17:34:50 +08:00
lyr
4ded8cbfc8 微调个人主页UI 2021-04-23 16:47:00 +08:00
5a14afa71e Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-23 16:02:42 +08:00
7736ef37e5 光环助手V4.9.0-新增[推荐弹窗]功能(0423测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1212 2021-04-23 16:02:35 +08:00
lyr
40adf1938e 提交漏传文件 2021-04-23 16:00:02 +08:00
lyr
7e1f821d7a 光环助手V4.9.0-个人主页优化(第2期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1203 2021-04-23 15:42:31 +08:00
4f81996857 首页放弃 fragment 自动重建 2021-04-23 14:49:27 +08:00
328a559ef0 1.修改推荐弹窗显示规则 2.提前加载推荐弹窗数据 2021-04-23 11:42:51 +08:00
lyr
ee01512f84 光环助手V4.9.0-积分体系(第4期)(我的光环-二、收货信息) https://git.ghzs.com/pm/halo-app-issues/-/issues/1204 2021-04-22 18:41:16 +08:00
a366a87324 修复首页页面重建时选中 TAB 与重建前不一致的问题 2021-04-22 18:34:41 +08:00
3c7a1e2ae3 修复专题详情页面进入显示空白的问题 2021-04-22 18:23:08 +08:00
87c71882f5 完成光环前端优化汇总(3,5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1236 2021-04-22 18:22:44 +08:00
7fa912bc0c 首页页面重建时复用 fm 重建的底部五个 tab,避免内存泄漏 2021-04-22 17:49:50 +08:00
lyr
27dc3f73ae 一键登录页适配底部导航栏 2021-04-22 17:44:48 +08:00
be630e39cd 光环助手V4.9.0-新增[推荐弹窗]功能(UI反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1212 2021-04-22 17:06:21 +08:00
cded31298c Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-22 16:43:02 +08:00
54030fd1df 光环助手V4.9.0-包名检测弹窗优化汇总(ui反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1210 2021-04-22 16:42:51 +08:00
lyr
98c1565dfb 首页和游戏库的"猜你喜欢"专题拆分成两个接口分别获取 2021-04-22 16:42:13 +08:00
d89c2d4a70 Merge branch 'feature-issues1209' into dev-4.9.0 2021-04-22 15:28:03 +08:00
0f11f6344f 光环助手V4.9.0-游戏礼包功能优化汇总(数据埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1209 2021-04-22 15:27:11 +08:00
a25f265bd3 光环助手V4.9.0-游戏礼包功能优化汇总 https://git.ghzs.com/pm/halo-app-issues/-/issues/1209 2021-04-22 14:15:12 +08:00
68adc0ff23 首页页面重建时复用旧 fragment 2021-04-22 11:58:35 +08:00
ab5aa01ec5 避免首页第一个 tab 被默认选中 2021-04-22 11:58:07 +08:00
lyr
71e5f798d9 1.恢复首页的猜你喜欢专题
2.板块展示猜你喜欢专题增加是否为游戏库判断
2021-04-22 10:29:06 +08:00
2572c53305 修复首页 tab 快速滚动文字可能出现大小不一的问题 2021-04-21 18:29:19 +08:00
lyr
64b5ca8efa 首页去掉智能推荐专题(只在游戏库页展示) 2021-04-21 18:09:50 +08:00
b5ef73517f 完成游戏相关优化(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-21 17:53:24 +08:00
lyr
feb2f68778 完成 "推荐算法优化——猜你喜欢专题 第4(1)点" https://git.ghzs.com/pm/halo-app-issues/-/issues/1224 2021-04-21 16:45:57 +08:00
lyr
f0a6e79418 调整光能中心和光能屋UI 2021-04-21 16:43:36 +08:00
lyr
78699cc0e8 修复Jira问题48(V4.9.0-优化登录功能(第3期):无法打开登录界面) 2021-04-21 16:39:59 +08:00
96805537f4 基本完成首页顶部Tab栏功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1215 2021-04-21 10:19:50 +08:00
13c4be2ab0 处理 proguard 语法问题 2021-04-21 10:09:27 +08:00
lyr
c2e5e8adbc 光环助手V4.9.0-积分体系(第4期)(光能中心和光能屋UI改动) 2021-04-20 18:56:05 +08:00
c5c9a3f4d2 放弃所有 activity 重建的 config 2021-04-20 17:34:28 +08:00
8bd2fa9b3a 避免 MIUI 夜间模式切换时重建页面 2021-04-20 17:07:53 +08:00
lyr
4bd8fc4e51 1.优化光能任务悬浮图标显示逻辑
2.修复未登录状态下在光能中心登录后,刷新任务列表后列表可以无限加载问题
2021-04-19 19:02:23 +08:00
lyr
5b26165aaa 微调默认Toast样式 2021-04-19 18:55:45 +08:00
lyr
1bb4c1cc2c 光环助手V4.9.0-优化登录功能(第3期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1206 2021-04-19 18:53:21 +08:00
d38b347eb8 Merge branch 'feature-issues1215' into 'dev-4.9.0'
Feature issues1215

See merge request halo/android/assistant-android!107
2021-04-19 18:15:13 +08:00
feaf2d0bfc 完成粗糙的首页顶部Tab栏功能(细节带完善) https://git.ghzs.com/pm/halo-app-issues/-/issues/1225 2021-04-19 18:15:13 +08:00
210dcdc698 设置app支持x86架构 2021-04-19 15:31:00 +08:00
fec291156a 【光环助手】论坛相关bug汇总(0413bug反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1223 2021-04-19 11:43:03 +08:00
62bb23abc9 光环后台需求汇总(2021年4月第1周)(25) https://git.ghzs.com/pm/halo-app-issues/-/issues/1193 2021-04-19 11:41:22 +08:00
19be9e1842 支持获取上一个页面的名称 2021-04-16 17:00:31 +08:00
lyr
a5cfd147b4 微调登录页和光能中心UI 2021-04-16 16:53:37 +08:00
lyr
f15d572785 微调登录页和光能中心UI 2021-04-16 16:53:13 +08:00
5e99e8b032 光环助手V4.9.0-新增[推荐弹窗]功能(数据埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1212 2021-04-16 15:26:53 +08:00
af39b82da1 Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-16 11:22:19 +08:00
ba3a4ede11 光环后台需求汇总(2021年4月第1周)(17) https://git.ghzs.com/pm/halo-app-issues/-/issues/1193 2021-04-16 11:20:40 +08:00
db591d0249 完成推荐入口优化及数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1214 2021-04-16 10:25:01 +08:00
lyr
1370261b87 Merge remote-tracking branch 'origin/dev-4.9.0' into dev-4.9.0 2021-04-15 15:34:36 +08:00
lyr
3824de4719 微调登录页UI 2021-04-15 15:34:09 +08:00
lyr
6ee26fb401 微调登录页UI 2021-04-15 15:33:45 +08:00
865fe2729f Merge branch 'feature-issues1212' into dev-4.9.0 2021-04-15 15:28:20 +08:00
85c556c593 光环助手V4.9.0-新增[推荐弹窗]功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1212 2021-04-15 15:27:02 +08:00
86681b8ab1 修改包名检测弹窗进度条兼容性问题 2021-04-14 20:38:41 +08:00
5f2ce22506 完成游戏相关优化汇总(1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-14 16:41:53 +08:00
dafa85a791 光环助手V4.9.0-包名检测弹窗优化汇总 https://git.ghzs.com/pm/halo-app-issues/-/issues/1210 2021-04-14 11:18:21 +08:00
lyr
9cc3a29b5a 处理登录页面底部适配问题(有些手机在有导航栏情况下位置不够) 2021-04-13 18:35:34 +08:00
lyr
d2e8a67971 处理登录页面底部适配问题(有些手机在有导航栏情况下位置不够) 2021-04-13 18:30:56 +08:00
1bd3168add 1.修改关注视频流内容重叠问题
2.修改关注视频流刷新时上传缓存视频id
2021-04-13 15:58:43 +08:00
81d81882e0 Merge branch 'dev-4.9.0' of git.ghzs.com:halo/android/assistant-android into dev-4.9.0 2021-04-13 12:01:14 +08:00
3f4e4f1a52 光环助手V4.9.0-包名检测弹窗优化汇总(自定义进度条) https://git.ghzs.com/pm/halo-app-issues/-/issues/1210 2021-04-13 12:00:50 +08:00
3845e1f3ee 完成预约事件曝光位置上报优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1221 2021-04-12 18:09:06 +08:00
6d2ae4dca5 版本号更新至 4.9.0 2021-04-12 17:31:08 +08:00
e2efa26ac0 Merge branch 'dev' into dev-4.9.0 2021-04-12 17:24:57 +08:00
70e12ebd08 光环助手V4.9.0-游戏相关优化汇总(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1211 2021-04-12 11:13:33 +08:00
91fc511fe6 光环助手V4.9.0-新增[推荐弹窗]功能(UI部分) https://git.ghzs.com/pm/halo-app-issues/-/issues/1212 2021-04-12 09:11:36 +08:00
lyr
c5530ba307 完成组件库(第1期)测试问题(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162#note_96797 2021-04-09 10:38:03 +08:00
67cbd9b641 完成组件库(第1期)测试问题(3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-04-09 09:45:31 +08:00
6735047e67 完成组件库(第1期)测试问题(1,4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-04-08 17:50:18 +08:00
9993bca582 处理UI组件库遗漏的细节部分(2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-04-08 15:06:16 +08:00
07258b08af 临时处理 mapping_id 写入包文件失败的问题 https://github.com/getsentry/sentry-android-gradle-plugin/issues/37 2021-04-08 10:14:34 +08:00
lyr
f8bd7787f6 更新 Loghub SDK (https://git.ghzs.com/halo/assistant-android/-/issues/28) 2021-04-07 18:05:55 +08:00
b6a3ef5faa [ 光环助手 V4.9 - 游戏顶部视频 ] 光环助手APP,游戏详情页顶部视频全屏播放优化(https://git.ghzs.com/pm/halo-app-issues/-/issues/1213) 2021-04-07 15:28:08 +08:00
lyr
730151f6ba 补全新分类2.0列表曝光遗漏步骤 2021-04-06 17:51:56 +08:00
532b98e002 修复进入游戏详情页时快速点击 tab 栏可能触发闪退的问题 2021-04-06 10:51:37 +08:00
lyr
202a26b09c 光环助手V4.8.0-光环助手UI组件库(第1期)(9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-04-01 14:17:28 +08:00
lyr
fe1821884a 光环助手V4.8.0-光环助手UI组件库(第1期)(6,8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-04-01 11:52:00 +08:00
eb1605cc0a 调整搜索热门标签/历史搜索区域的高度,避免在 mumu 模拟器上被裁切 2021-03-31 17:34:11 +08:00
08ad5c3455 模拟器支持 FBN 类型 2021-03-31 17:13:21 +08:00
0774525de6 修复游戏详情预览图过于模糊的问题 2021-03-31 16:01:03 +08:00
b69ebc8122 完成游戏搜索功能优化(20210331测试问题 2,3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1173 2021-03-31 15:20:42 +08:00
lyr
49f31da303 光环助手V4.8.0-新分类功能2.0(前端)(20210330测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1171#note_95153 2021-03-31 14:50:55 +08:00
66568b09d5 完成开测表功能0331补充 https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-31 14:13:01 +08:00
57a944f954 取消限制历史搜索/热门标签文字长度限制 https://git.ghzs.com/pm/halo-app-issues/-/issues/1173 2021-03-31 12:00:31 +08:00
1fb8a60b2b 正式环境切换到 4.8.0 2021-03-31 09:34:46 +08:00
489a57a379 完成游戏功能搜索优化(20210330需求补充) https://git.ghzs.com/pm/halo-app-issues/-/issues/1173 2021-03-31 09:17:16 +08:00
0aa02c186c 恢复因为压缩影响质量的新版本提示图标 2021-03-30 14:59:25 +08:00
lyr
fd463ec1e3 光环助手V4.8.0-新分类功能2.0(前端)(20210330修改) https://git.ghzs.com/pm/halo-app-issues/-/issues/1171#note_95001 2021-03-30 14:54:43 +08:00
lyr
0c16ae5f3f 解决邀请好友过程中无法保存海报的问题 2021-03-30 14:40:37 +08:00
76cca46015 修复开测表测试问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-29 22:55:39 +08:00
748cacce3f Merge branch 'dev' of git.ghzs.com:halo/assistant-android into dev 2021-03-29 22:47:23 +08:00
b47985d4ff 修改游戏搜索热门搜索图标 2021-03-29 22:47:20 +08:00
lyr
9e41bd2846 光环助手V4.8.0-新分类功能2.0(前端)(20210329测试问题 5) 2021-03-29 22:32:11 +08:00
0e4b18fc5d 修复开测表手动选择日期时与自动滚动时的冲突问题 2021-03-29 22:05:50 +08:00
lyr
5594a03cee 1.修复Jira HALO-46 问题;
2.光环助手V4.8.0-新分类功能2.0(前端)(20210329测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1171#note_94892
2021-03-29 21:59:01 +08:00
lyr
59ece3be49 光环助手V4.7.0-积分体系(第2期)--个人主页(头像挂件页面相关优化-UI测试内容2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1112#note_94854 2021-03-29 21:22:16 +08:00
a7f77076a5 Merge remote-tracking branch 'origin/dev' into dev 2021-03-29 21:04:14 +08:00
6526d5b3ce 完成开测表功能(0329测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-29 21:04:07 +08:00
3a2f877d0c Merge branch 'dev' of git.ghzs.com:halo/assistant-android into dev 2021-03-29 21:00:51 +08:00
0d344e66a1 光环助手V4.8.0-游戏弹窗功能优化(0329测试) https://jira.ghzs.com/browse/HALO-1 2021-03-29 21:00:47 +08:00
d3de323c06 完成开测表功能(0329测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-29 20:54:34 +08:00
721d5acc1d 完成开测表功能0329测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-29 18:33:15 +08:00
7069744191 完成隐私权限相关问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1192 2021-03-29 17:59:55 +08:00
lyr
b1f56d5dcd 提交遗漏资源文件 2021-03-29 17:42:07 +08:00
lyr
5aaa5bd076 光环助手V4.8.0-新分类功能2.0(前端)(20210329测试问题 1)、 (20210329UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1171#note_94764 2021-03-29 17:41:41 +08:00
lyr
7e74dee0eb 1.光环助手V4.7.0-积分体系(第2期)--光能中心(0329 产品验收问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1110#note_94783
2.光环助手V4.7.0-积分体系(第2期)--邀请好友(0329 产品验收回归)https://git.ghzs.com/pm/halo-app-issues/-/issues/1114#note_94794
2021-03-29 15:59:05 +08:00
45b8f7f82f 修复使用浏览器安装判断不准确的问题 2021-03-29 15:55:42 +08:00
7c9b8732e2 光环助手V4.8.0-游戏搜索功能优化(第7期)(0329UI补充调整) https://git.ghzs.com/pm/halo-app-issues/-/issues/1173 2021-03-29 15:24:08 +08:00
86ae96253b 去掉视频流引导动画 2021-03-29 15:11:39 +08:00
9dcc0b5587 Merge branch 'dev' of git.ghzs.com:halo/assistant-android into dev 2021-03-29 15:05:16 +08:00
9ceba793e8 修改搜索框背景 2021-03-29 15:05:13 +08:00
175cc55b75 修复开测表的备注信息上报问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-29 15:03:37 +08:00
5486d1803f Merge branch 'dev' of git.ghzs.com:halo/assistant-android into dev 2021-03-29 14:51:32 +08:00
cb885f8b66 光环助手V4.8.0-游戏弹窗功能优化(0323测试2,3,4) https://jira.ghzs.com/browse/HALO-1 2021-03-29 14:51:27 +08:00
ab9f5cf7d8 完成光环助手UI组件库的2 https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-03-29 14:40:22 +08:00
7f4fafc637 Merge remote-tracking branch 'origin/dev' into dev 2021-03-29 12:01:17 +08:00
acccd2330d 修复游戏预览图过于模糊的问题 2021-03-29 12:01:00 +08:00
lyr
2745147e73 光环助手V4.7.0-积分体系(第2期)--邀请好友(0329 产品验收问题 1)https://git.ghzs.com/pm/halo-app-issues/-/issues/1114#note_94755 2021-03-29 12:00:56 +08:00
8fb0e3feaf 曝光数据添加区分浏览器安装字段,修复开测日志上报异常 2021-03-29 12:00:38 +08:00
lyr
335fa8ba39 1.调整新分类2.0UI;
2.修复新分类2.0若干显示bug
2021-03-29 11:47:52 +08:00
c739074057 修复通用列表分页刷新时偶发的闪退问题,捕抓首页游戏视频偶发的数组越界异常 2021-03-29 11:05:24 +08:00
a2569cf876 处理部分 4.7.4 遗留的空指针异常和数组越界异常 2021-03-29 10:39:11 +08:00
a81b0a1603 修复开测表游戏下载任务被删除缺没有及时更新的问题 2021-03-29 09:33:37 +08:00
lyr
76c20e4b82 修复新分类页面对象未初始化问题 2021-03-26 18:40:18 +08:00
lyr
4144cc2c4f 光环助手V4.7.0-积分体系(第2期)--光能中心(调整UI间距)https://git.ghzs.com/pm/halo-app-issues/-/issues/1110#note_94623 2021-03-26 18:24:23 +08:00
5465ff7731 Merge remote-tracking branch 'origin/dev' into dev 2021-03-26 18:05:32 +08:00
fc6dedb320 修复开测表游戏显示游戏标签时无法进行筛选的问题 2021-03-26 18:05:26 +08:00
lyr
a76b98f20c 1.光环助手V4.8.0-积分体系(第3期)--光能中心(0326 产品测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1146#note_94597
2.光环助手V4.7.0-积分体系(第2期)--邀请好友(0326 产品验收问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1114#note_94513
2021-03-26 17:59:07 +08:00
lyr
f05c0324fa 光环助手V4.8.0-新分类功能2.0(前端)(20210325UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1171#note_94296 2021-03-26 17:44:06 +08:00
606eb30d9f 修改图片上传超时时间20s,重试次数2次 2021-03-26 16:59:30 +08:00
aa41da022d 光环助手V4.8.0-多版本下载面板优化(第二期)(UI测试问题-5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-26 15:33:21 +08:00
b62d2c1b9e Merge branch 'dev' of git.ghzs.com:halo/assistant-android into dev 2021-03-26 15:24:26 +08:00
ba21543800 修改qa搜索icon 2021-03-26 15:24:21 +08:00
6f5b832a68 调整 R8 配置 2021-03-26 15:20:30 +08:00
eb4bbb9efa Merge remote-tracking branch 'origin/dev' into dev 2021-03-26 15:16:40 +08:00
233f91d2b9 修改热门搜索标签图片 2021-03-26 15:14:26 +08:00
1c51b2fe41 Merge branch 'release' into dev
# Conflicts:
#	dependencies.gradle
2021-03-26 15:09:26 +08:00
5123d7ff11 Merge branch 'dev-4.8.0' into dev
# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/gh/common/util/PackageUtils.java
#	app/src/main/java/com/gh/gamecenter/catalog/CatalogFragment.kt
2021-03-26 15:01:57 +08:00
c20e06f2eb Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-26 14:55:51 +08:00
df55caa1f8 光环助手V4.8.0-游戏搜索功能优化(第7期)(20210326UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1173 2021-03-26 14:55:48 +08:00
lyr
61bdac3aa2 光环前端优化汇总(2021年2月)(UI测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1154#note_94518 2021-03-26 14:47:15 +08:00
d56dc53b06 完成开测表功能(0326AM UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-26 14:38:44 +08:00
18c02ad9c3 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-26 14:25:17 +08:00
8a262532e1 光环助手V4.8.0-多版本下载面板优化(第二期)(0326AM) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-26 14:25:13 +08:00
lyr
876d920251 光环助手V4.7.0-积分体系(第2期)--个人主页(头像挂件页面相关优化-UI内容 4)https://git.ghzs.com/pm/halo-app-issues/-/issues/1112#note_94511 2021-03-26 11:42:38 +08:00
1bad70a83f Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-26 10:44:46 +08:00
c04a1a5802 光环助手V4.8.0-Q&A管理优化需求(第二期)(0322测试3,6) https://jira.ghzs.com/browse/HALO-2 2021-03-26 10:44:41 +08:00
lyr
ea8c47a6d6 光环助手V4.8.0-积分体系(第3期)--光能中心(0325 产品测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1146#note_94377 2021-03-26 09:16:33 +08:00
53f3f48c4d 修复开测表(0325测试 4~7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-25 20:56:10 +08:00
lyr
706df0c22f 光环助手V4.8.0-新分类功能2.0(前端)20210325测试问题 第2点 2021-03-25 18:41:44 +08:00
lyr
74cee421f9 光环助手V4.8.0-新分类功能2.0(前端)20210325测试问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1171#note_94292 2021-03-25 18:05:48 +08:00
lyr
4b08fa56da 光环助手V4.7.0-积分体系(第2期)--消息文案梳理(新增 win_order_detail 中奖订单详情 (中奖订单详情)) https://git.ghzs.com/pm/halo-app-issues/-/issues/1113#note_94198 2021-03-25 15:50:53 +08:00
522f664f30 光环前端优化汇总(2021年2月)(0325补充1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-25 15:18:00 +08:00
87a92055a7 光环助手V4.8.0-模拟器游戏优化汇总(0325测试2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1159 2021-03-25 14:29:38 +08:00
f23aa496ae 关闭 testOnly,避免部分 vivo 设备装不了 debug 包 2021-03-25 11:33:07 +08:00
65d7322546 修复部分 vivo 设备上无法显示游戏图标圆角的问题 2021-03-25 11:32:24 +08:00
d9178f8538 调整浏览器安装提示文案 https://git.ghzs.com/pm/halo-app-issues/-/issues/1132 2021-03-25 11:18:31 +08:00
lyr
5de471d89a Merge remote-tracking branch 'origin/dev-4.8.0' into dev-4.8.0 2021-03-25 11:14:28 +08:00
lyr
63cc104e9f 光环助手V4.7.0-积分体系(第2期)--个人主页(头像挂件页面相关优化-UI内容)https://git.ghzs.com/pm/halo-app-issues/-/issues/1112#note_92945 2021-03-25 11:14:14 +08:00
f06ba9bf45 修复 XAPK 原文件后缀为 .apk.apk 时无法使用浏览器下载的问题 2021-03-25 11:11:31 +08:00
lyr
682d5a2cd4 增加判断网络状态的JS调用方法 2021-03-25 11:06:14 +08:00
76ce4e2f04 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-25 10:15:25 +08:00
54221cc976 编辑帖子插入图片成功后取消滚动到底部位置 2021-03-25 10:15:22 +08:00
79ae36c51f 修复浏览器安装端口在部分 vivo 手机被占用的问题 2021-03-24 18:33:45 +08:00
lyr
335d6787a7 意见反馈页的反馈提交都需要先登录 2021-03-24 17:21:12 +08:00
lyr
2bf9c1e4d7 提交闪退类型反馈不需要登录 2021-03-24 16:21:33 +08:00
lyr
133635c495 光环助手V4.7.0-积分体系(第2期)--光能中心(0324 ui反馈)https://git.ghzs.com/pm/halo-app-issues/-/issues/1110#note_94065 2021-03-24 15:40:00 +08:00
2aaf6d8a9c 修复开测表测试问题(0324测试 3,4,5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-24 15:26:20 +08:00
f119f6751a Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-24 15:05:11 +08:00
18f964c167 光环助手V4.8.0-视频优化(第1期)(0324 产品验收问题1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1107 2021-03-24 15:05:07 +08:00
lyr
369275225e 光环助手V4.8.0-新分类功能2.0(前端)https://git.ghzs.com/pm/halo-app-issues/-/issues/1171 2021-03-24 14:23:22 +08:00
d80daf104f Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-24 10:08:16 +08:00
71c697d596 1.修改专区热门回答图片被裁剪的问题
2.修改游戏搜索求功能标签显示问题
2021-03-24 10:08:07 +08:00
lyr
169814d6a9 光环助手V4.7.0-积分体系(第2期)--邀请好友(V4.7.0-积分体系(第2期)--邀请好友-UI测试问题-4、0319 产品验收问题 第1点) 2021-03-23 18:36:20 +08:00
lyr
52cffde739 正式发布环境隐藏个人等级入口 2021-03-23 18:11:23 +08:00
lyr
11f84b69ac 正式发布环境隐藏光能签到入口、邀请码输入框、上报光能任务 2021-03-23 18:10:36 +08:00
2c3e03bd0f 完成2月光环前端优化(12) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-23 16:57:31 +08:00
264e103395 修复预约列表曝光下载数据没有上报的问题 2021-03-23 16:48:54 +08:00
7f4d3ffe3e 完成开测表的数据埋点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-23 16:36:19 +08:00
79b7a15f7d 增加获取 activity 栈中顶部倒数第二个元素的方法 2021-03-23 16:27:00 +08:00
638dd4ad1d 对接上传图片新错误码 https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-23 14:48:51 +08:00
lyr
04f58e036b 光环助手V4.7.0-积分体系(第2期)--光能中心(0322 产品验收问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1110 2021-03-23 14:31:07 +08:00
6624868bfe Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-23 11:43:30 +08:00
2ff83b8cc5 光环助手V4.8.0-多版本下载面板优化(第二期)(UI测试问题-4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-23 11:43:26 +08:00
lyr
cab412ab70 优化插入日程和删除日程流程 2021-03-23 11:32:11 +08:00
1a83e64f3a 完成插件跳转意见反馈的相关需求 https://git.ghzs.com/pm/halo-app-issues/-/issues/1160 2021-03-23 10:40:47 +08:00
lyr
be76fb0526 消息推送新增"兑换商品"链接类型 2021-03-23 10:28:21 +08:00
lyr
5cc25482a9 1.消息推送新增链接类型;
2.积分体系埋点补充邀请好友结果埋点
2021-03-23 10:21:53 +08:00
c683e6786c Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-22 17:01:04 +08:00
5f182eeddb 光环前端优化汇总(2021年2月)(0322补充13) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-22 17:01:01 +08:00
3e0979fabb 完成2月光环前端优化(3,8) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-22 16:20:51 +08:00
lyr
13685ec605 光能助手V4.8.0-积分体系(第3期)--光能屋(0317 产品验收测试 第1点)https://git.ghzs.com/pm/halo-app-issues/-/issues/1147 2021-03-22 16:04:20 +08:00
4eac5ff61c Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-22 15:36:28 +08:00
98f0cf5fc0 光环助手V4.8.0-Q&A管理优化需求(第二期)(20210319UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1156 2021-03-22 15:36:24 +08:00
lyr
cb345f49eb 光环前端优化汇总(2021年2月)第5、11点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-22 15:26:20 +08:00
ef95be60f1 修改多版本下载错位问题 2021-03-22 14:46:33 +08:00
a85cfa47e2 光环前端优化汇总(2021年2月)(0320补充1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-22 14:42:47 +08:00
66123b0ae8 为 Application 异步初始化 Fresco 超时兜底 2021-03-22 10:37:07 +08:00
cdb660b884 Gradle Transform 切换到 Kotlin 实现 2021-03-19 16:27:24 +08:00
026e4f71de Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-19 16:20:56 +08:00
01ecde0425 光环助手V4.8.0-模拟器游戏优化汇总(0319测试1-5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1159 2021-03-19 16:20:52 +08:00
lyr
f748fcbd48 光环助手V4.7.0-积分体系(第2期)--邀请好友 (0318 产品验收问题 第3点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1114 2021-03-19 15:11:25 +08:00
65464eed3d 修复使用浏览器安装询问弹窗总是弹起的问题 2021-03-19 11:42:07 +08:00
6a5d8e915c 完成数据统计需求(曝光位置补充) https://git.ghzs.com/pm/halo-app-issues/-/issues/1174 2021-03-19 09:33:22 +08:00
5fde455bcd Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-19 09:18:25 +08:00
e1e257b1d8 光环助手V4.8.0-模拟器游戏优化汇总(ui测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1159 2021-03-19 09:18:21 +08:00
7a87f8dc14 Merge branch 'feature-issues1134' into 'dev-4.8.0'
Feature issues1134

See merge request halo/assistant-android!106
2021-03-18 18:15:25 +08:00
3fe2b8ffcd 基本完成开测表功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1134 2021-03-18 18:15:25 +08:00
565b7b2c38 修改编辑帖子删除图片后无法点击发布按钮 2021-03-18 14:50:32 +08:00
b3bf9d862f 光环助手V4.8.0-多版本下载面板优化(第二期)(UI测试问题-3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-18 11:18:52 +08:00
c6f5ef64d5 光环助手V4.8.0-视频优化(第1期)(UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1107 2021-03-18 10:13:24 +08:00
95b21b5bb5 光环助手V4.8.0-多版本下载面板优化(第二期)(UI测试问题-2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-17 18:21:27 +08:00
0da331d109 Merge branches 'dev-4.8.0' and 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-17 16:08:18 +08:00
8c1f8833b1 光环前端优化汇总(2021年2月)(9) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-17 16:08:11 +08:00
e53cd6dede 处理 R8 混淆造成的微信分享问题 2021-03-17 15:27:42 +08:00
fa27e172e4 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-17 15:22:33 +08:00
544f5102ea 光环助手V4.8.0-游戏搜索功能优化(第7期)(4,5,6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1173 2021-03-17 15:22:28 +08:00
lyr
5d6134f0dd 光环助手V4.8.0-积分体系(第3期https://git.ghzs.com/pm/halo-app-issues/-/issues/1146)--光能中心(0315 产品测试问题) 2021-03-17 14:12:09 +08:00
e0b951e043 Merge remote-tracking branch 'origin/dev-4.8.0' into dev-4.8.0 2021-03-17 11:18:59 +08:00
f2476c84c4 完成浏览器安装(0317补充) https://git.ghzs.com/pm/halo-app-issues/-/issues/1132 2021-03-17 11:18:51 +08:00
c186c32d47 包名检测弹窗进度条降低更新频率,防止卡顿 2021-03-17 09:45:46 +08:00
lyr
194f30650e 修改兑换商品地址错误导致web无法正常返回问题 2021-03-16 16:49:06 +08:00
a475b176b4 光环助手V4.8.0-模拟器游戏优化汇总(ui测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1159 2021-03-16 15:27:26 +08:00
73cac4d39d 修改视频关注tab下拉刷新一直加载问题 2021-03-16 14:42:44 +08:00
93d0f30352 光环助手V4.8.0-多版本下载面板优化(第二期)(UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-16 10:41:25 +08:00
98d30d1183 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0
# Conflicts:
#	app/src/main/java/com/gh/common/util/EntranceUtils.java
2021-03-16 09:12:42 +08:00
b1d178f8df 光环前端优化汇总(2021年2月)(4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-16 09:11:45 +08:00
lyr
0550df7039 光环助手V4.8.0-积分体系(第3期)--我的光环(0315 产品测试问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1145 2021-03-15 16:37:57 +08:00
lyr
13c7b8ff1d 1.补充积分体系web入口地址;
2.增加客服跳转类型
2021-03-12 18:29:59 +08:00
d7b7633ffe 调整游戏详情提示文案 2021-03-12 12:00:19 +08:00
5e536db94e 修复使用浏览器下载功能在山寨机上的适配问题 2021-03-12 11:50:33 +08:00
55d8b6a866 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-11 17:54:57 +08:00
a2a8610083 光环助手V4.8.0-光环助手UI组件库(第1期)(7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-03-11 17:54:53 +08:00
lyr
8d9f0adebe 修改web跳转头像挂件页方法,使用分类id来定位tab 2021-03-11 15:02:50 +08:00
lyr
e386994e4d 完成积分体系第3期:
1.光环助手V4.8.0-积分体系(第3期)--光能中心 pm/halo-app-issues#1146
2.光环助手V4.8.0-积分体系(第3期)--我的光环 一、光能明细 1、文案修改 pm/halo-app-issues#1145
3.光能助手V4.8.0-积分体系(第3期)--光能屋 四、光能明细,以及增加兑换商品、抽奖乐园入口
4.光能助手V4.8.0-积分体系埋点(第1期)- 安卓端埋点 pm/halo-app-issues#1149
5.增加web上报埋点、跳转头像挂件页的JS调用方法
2021-03-11 10:45:45 +08:00
fc5f1e9830 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-11 10:25:45 +08:00
fd17eaea00 光环助手V4.8.0-光环助手UI组件库(第1期)(4,5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-03-11 10:25:11 +08:00
8a65d6dd85 关闭测试 mapping 上传 2021-03-11 09:09:41 +08:00
8d3c609ad0 getIndentifier 调用的资源添加到 AndResGuard 白名单 2021-03-11 09:08:54 +08:00
2e71e11a09 删除无用依赖 2021-03-11 09:07:18 +08:00
lyr
b9c17bcdb8 正式发布环境隐藏个人等级入口 2021-03-10 18:53:59 +08:00
55bb1a1873 Merge branch 'feature-issues1156' into dev-4.8.0 2021-03-10 14:47:21 +08:00
54601bc083 光环助手V4.8.0-Q&A管理优化需求(第二期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1156 2021-03-10 14:45:36 +08:00
df2bb9c020 完成浏览器安装(数据埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1132 2021-03-09 17:54:06 +08:00
0bf297d094 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-09 15:03:27 +08:00
db8a2a06aa 光环助手V4.8.0-多版本下载面板优化(第二期)(UI相关补充0309AM) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-09 15:03:23 +08:00
44a9db7294 使用滴滴的SP优化和系统异常捕抓等插件,将插件整体搬迁至 gradle init 文件中提高开发编译速度 2021-03-09 14:45:25 +08:00
7db48b6923 Merge branch 'feature-issues1158' into dev-4.8.0 2021-03-09 11:08:12 +08:00
0a8f2d51db 光环助手V4.8.0-游戏弹窗功能优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1158 2021-03-09 11:06:20 +08:00
b06d2a479a 版本更新至 4.7.4 2021-03-09 10:56:25 +08:00
d56bd0078e Merge branch 'hotfix-4.7.3-293_crash' into 'release'
修复闪退

See merge request halo/assistant-android!105
2021-03-09 10:55:22 +08:00
7b87aadd86 修复首页列表对比差异时偶发的数组越界闪退 2021-03-08 17:26:02 +08:00
8f2da187f2 Revert "主 SP 迁移到 MMKV",原因是存储空间不足时有各种奇怪的问题 2021-03-08 15:50:44 +08:00
8291caa5e3 主 SP 迁移到 MMKV 2021-03-08 15:15:04 +08:00
e30ec540bd Merge branch 'feature-issues1157' into dev-4.8.0 2021-03-08 10:02:51 +08:00
3c4da21bf6 光环助手V4.8.0-多版本下载面板优化(第二期)(埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-08 10:02:00 +08:00
lyr
800260b6a4 光环前端优化汇总(2021年2月)第1、2、6、7点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1154 2021-03-05 18:23:51 +08:00
lyr
ddea202f85 Merge remote-tracking branch 'origin/dev-4.8.0' into dev-4.8.0 2021-03-05 18:17:40 +08:00
lyr
b3a786ee22 光环助手V4.8.0-xapk解压过程助手卡顿问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1155 2021-03-05 18:17:21 +08:00
332e24b874 jenkins 打包测试 mapping 上传 2021-03-05 18:16:12 +08:00
5413ebfba4 更改活跃统计的上报的 logstore https://git.ghzs.com/pm/halo-app-issues/-/issues/1115 2021-03-05 18:08:44 +08:00
7ead1c89f3 Merge branch 'dev-4.8.0' of git.ghzs.com:halo/assistant-android into dev-4.8.0 2021-03-05 17:34:13 +08:00
03300a41f7 缺省渠道改为 GH_TEST2 2021-03-05 17:33:35 +08:00
2d8698e070 缺省渠道改为 GH_TEST2 2021-03-05 17:32:48 +08:00
078b1a18a0 Merge branch 'feature-issues1159' into dev-4.8.0
# Conflicts:
#	app/src/main/java/com/gh/common/constant/Constants.java
#	app/src/main/java/com/gh/gamecenter/gamedetail/GameDetailFragment.kt
2021-03-05 16:57:42 +08:00
de1e3ba7a7 Merge branch 'feature-issues1107' into dev-4.8.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/MainActivity.java
2021-03-05 16:54:39 +08:00
a9275c83d2 光环助手V4.8.0-视频优化(第1期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1107 2021-03-05 16:52:21 +08:00
02894879e3 Merge branch 'hotfix-v4.7.2-292-crash' into 'release'
修复闪退

See merge request halo/assistant-android!104
2021-03-05 15:01:14 +08:00
56a202ae5c 版本更新至 4.7.3 2021-03-05 14:48:30 +08:00
013969a73a 修复闪退
1. 捕抓首页刷新列表时偶发的数组越界闪退
2. 提高线程池并发数量,避免安装了大量游戏的设备检查更新不及时
3. 捕抓插件化时因获取不到包名而出现的闪退
2021-03-05 14:45:46 +08:00
1a5a4ba149 完成UI组件化-常规弹窗 https://git.ghzs.com/pm/halo-app-issues/-/issues/1162 2021-03-04 14:56:23 +08:00
9bdae33655 更新大图查看库 2021-03-04 09:59:34 +08:00
be1184583b Merge branch 'feature-browser_install' into dev-4.8.0
# Conflicts:
#	app/src/main/java/com/gh/common/constant/Constants.java
2021-03-03 18:32:05 +08:00
68ee926897 基本完成使用浏览器下载功能,埋点细节待补充 https://git.ghzs.com/pm/halo-app-issues/-/issues/1132 2021-03-03 18:30:14 +08:00
4d0db8cbcc Merge branch 'dev-4.8.0' into feature-issues1159 2021-03-03 14:42:57 +08:00
59e4b6e063 光环助手V4.8.0-多版本下载面板优化(第二期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-03 14:35:36 +08:00
e74cf92033 版本更新至 4.7.2 2021-03-03 11:31:29 +08:00
811b1bbfce 修复下载完成时可能出现多个完成事件的问题 2021-03-03 10:41:02 +08:00
00c25d0d47 缺省渠道改为 test2 2021-03-03 10:39:52 +08:00
66b4b4b9f0 缺省渠道改为 test2 2021-03-03 10:38:39 +08:00
4bab47d2a6 Merge branch 'hotfix-v4.7.1-291-generic_fixes' into 'release'
Hotfix v4.7.1 291 generic fixes

See merge request halo/assistant-android!102
2021-03-03 10:06:02 +08:00
2c9567aa61 修复文章详情点击图片会产生位移的问题 2021-03-03 09:59:45 +08:00
97598dc7ec 修复下载完成时可能出现多个完成事件的问题 2021-03-03 09:58:37 +08:00
4771c79fa9 版本调整为 4.7.1 2021-03-02 15:23:29 +08:00
3458a5d7f3 Merge branch 'hotfix-v4.7.0-290_crash_and_mapping' into 'release'
Hotfix v4.7.0 290 crash and mapping

See merge request halo/assistant-android!101
2021-03-02 15:22:43 +08:00
5bc7c333d8 捕抓 vivo 6.0 以下设备获取不到部分 apk 信息时的异常 2021-03-02 15:17:57 +08:00
c7a24555a2 "关闭 R8",因未知情况 sentry 没收到包里的 mapping id 2021-03-02 15:07:33 +08:00
e19a678699 处理 fresco 版本冲突 2021-03-02 15:04:53 +08:00
795af0528f 调整 jenkins 打包脚本 2021-03-02 10:56:20 +08:00
d0b4ea8ec2 修复 base64 图片保存被覆盖问题 https://git.ghzs.com/pm/web-issues/-/issues/123 2021-03-02 10:02:08 +08:00
45861dff61 光环助手V4.8.0-模拟器游戏优化汇总 https://git.ghzs.com/pm/halo-app-issues/-/issues/1159 2021-03-02 09:21:33 +08:00
5f41463fc3 启用插件内联 access 2021-03-01 16:04:23 +08:00
89cca6dbfd 更新编译脚本 2021-03-01 15:35:06 +08:00
e43c0efdcb 版本号更改为 4.8.0 2021-03-01 15:34:40 +08:00
2dbd635b8e Merge branch 'feature-shrink_size' into 'dev-4.8.0'
引入微信的资源优化和头条的 dex 优化插件

See merge request halo/assistant-android!100
2021-03-01 15:20:07 +08:00
728d4fd8e1 引入微信的资源优化和头条的 dex 优化插件 2021-03-01 15:19:17 +08:00
48b5c7efc1 光环助手V4.8.0-多版本下载面板优化(第二期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1157 2021-03-01 10:05:35 +08:00
278dcc33be Merge remote-tracking branch 'origin/release' into dev
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/catalog/CatalogFragment.kt
#	app/src/main/java/com/gh/gamecenter/catalog/SubCatalogFragment.kt
2021-02-27 10:58:57 +08:00
87be3bed61 Merge branch 'hotfix-v4.6.1-271-generic_crash' into 'release'
Hotfix v4.6.1 271 generic crash

See merge request halo/assistant-android!98
2021-02-27 10:42:34 +08:00
078b102152 Update .gitmodules 2021-02-27 10:32:08 +08:00
4f6969a70a 完成粗糙的使用浏览器下载功能,接口与UI细节待补充 https://git.ghzs.com/pm/halo-app-issues/-/issues/1132 2021-02-26 16:48:16 +08:00
lyr
29140b6c0b 添加日程前增加是否已经添加判断 2021-02-26 16:34:53 +08:00
lyr
11bd9bed95 任务管理 - 【前端】任务列表,新增常驻任务类型,UI沿用日常任务 https://git.ghzs.com/pm/halo-app-issues/-/issues/1121#note_87359 2021-02-26 11:45:01 +08:00
997b5676dc 修改数据库升级错误 2021-02-26 09:46:22 +08:00
lyr
9b8755e035 光环助手V4.7.0-积分体系(第2期)--个人主页 - 0224产品测试问题-2(1) 2021-02-26 09:22:24 +08:00
85e97bc64d 修改包名检测弹窗按钮热区大小 2021-02-25 15:39:59 +08:00
0ea7c81ce9 光环助手V4.8.0-模拟器游戏优化汇总 https://git.ghzs.com/pm/halo-app-issues/-/issues/1159 2021-02-25 15:27:09 +08:00
951ac06948 Merge branch 'dev' of git.ghzs.com:halo/assistant-android into dev 2021-02-25 15:20:30 +08:00
92cdb6fdbb 修复低于 vivo 低于 5.1.1 版本系统的设备获取大文件APK包名会 ANR 的问题 2021-02-25 15:20:20 +08:00
lyr
cb0dd1d082 正式发布环境隐藏光能签到入口、邀请码输入框、上报光能任务 2021-02-25 15:18:06 +08:00
276655a8ec 更新打包脚本,仅正式包上传 mapping 2021-02-24 18:15:43 +08:00
df8b612188 恢复 R8 2021-02-24 18:01:30 +08:00
a550305947 Merge branch 'feature-tracker' into 'dev-4.8.0'
Merge feature tracker

See merge request halo/assistant-android!97
2021-02-24 11:49:45 +08:00
3af4dd5494 完成页面/应用可见上报功能 https://git.ghzs.com/pm/halo-app-issues/-/issues/1115 2021-02-24 11:49:45 +08:00
4d413eb1f2 关闭 R8 (Sentry 服务端暂未支持 mapping 自动上传) 2021-02-24 09:34:20 +08:00
d496ab0283 Merge branch 'release' into 'dev'
Merge hotfix changes

See merge request halo/assistant-android!96
2021-02-23 16:08:58 +08:00
b519ff6aac Merge branch 'dev-4.7.0' into 'dev'
Merge 4.7.0 changes

See merge request halo/assistant-android!95
2021-02-23 16:05:09 +08:00
8c05d2be8a 修复 5.1.1 系统的 vivo 设备安装大文件的 ANR 问题 2021-02-23 10:41:55 +08:00
23dd89198b 修复 5.1.1 系统的 vivo 设备安装大文件的 ANR 问题 2021-02-22 18:41:41 +08:00
6569ab2d33 修改模拟器不上传记录问题 2021-02-22 16:42:32 +08:00
1bead6a756 Merge branch 'hotfix-v4.6.1-271-crash' into 'release'
修改Sentry上一些闪退问题

See merge request halo/assistant-android!94
2021-02-08 16:14:13 +08:00
c0df3af298 尝试替换ConnectionPool类中的线程池,避免OutOfMemoryError: pthread_create failed 2021-02-08 09:52:08 +08:00
lyr
267363f46d 1.修复oppo手机无法启动服务问题;
2.修复新分类页面对象未初始化问题;
3.修复新分类页下载通知包名为空问题;
4.修复新分类中子分类列表为空问题;
5.修复问题反馈页游标越界问题;
6.修复个人信息编辑页获取菜单为空问题
2021-02-07 14:40:37 +08:00
44cf54aee6 1.修改WebView闪退问题
2.修改已安装列表更新数据时闪退问题
3.修改包名检测弹窗弹出闪退问题
2021-02-07 10:17:20 +08:00
lyr
322d7117f8 完成积分体系第2期 2021-02-05 17:54:31 +08:00
c8684f837a 修复闪退问题
1. 修复首页游戏库数据库更新闪退问题
2. 修复部分设备获取下载列表时的闪退问题
3. 修复重复发送设备应用列表的问题
2021-02-05 17:40:56 +08:00
9e5ecf1520 尝试解决TransactionTooLargeException异常 2021-02-05 17:03:14 +08:00
2df94dddc5 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-05 10:49:37 +08:00
1731980e88 升级fresco版本至2.4.0 2021-02-05 10:49:17 +08:00
f789f0cb73 正式环境接口改为 4.7.0 2021-02-04 20:57:44 +08:00
d043a89f3e 修复在部分设备上启动广告会闪烁的问题 2021-02-04 20:41:46 +08:00
4ab0f066df 修复在部分设备上首页广告消失过早的问题 2021-02-04 20:31:57 +08:00
096679029b Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-04 20:20:35 +08:00
2634228d21 光环助手V4.7.0-首页游戏功能强化(视频自动播放)(20210204测试问题3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1122 2021-02-04 20:20:14 +08:00
a33784221d 完成光环前端优化汇总(0203测试:2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1138 2021-02-04 20:05:34 +08:00
2ef7216bda 光环助手V4.7.0-首页游戏功能强化(视频自动播放)(20210204测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1122 2021-02-04 19:22:22 +08:00
2554fab389 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-04 18:25:49 +08:00
a4e3121489 【光环助手V_4.7.0】论坛交互优化(0204测试补充3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-04 18:25:27 +08:00
8dfde8ddd0 修復同包名遊戲安裝完成時可能誤刪錯誤遊戲的問題 2021-02-04 18:17:25 +08:00
63403c7c29 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-04 17:18:17 +08:00
aa9269aecc 【光环助手V_4.7.0】论坛交互优化(0202PM UI补充3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-04 17:17:57 +08:00
489e7105bb 添加 mapping 上传配置 (测试暂未通过) 2021-02-04 16:18:16 +08:00
8dbc407503 修复启动弹窗先于首页出现的问题 2021-02-04 16:08:56 +08:00
3e032fa540 修复 gh_version 异常造成的闪退 2021-02-04 16:07:51 +08:00
3df381b255 处理帖子是纯文字不会滚动到评论位置 2021-02-04 15:20:29 +08:00
7c59181bc6 提交LGLibrary 2021-02-04 11:59:27 +08:00
9244a65371 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-04 11:52:49 +08:00
a7b8b678f7 【光环助手V_4.7.0】论坛交互优化(0204AM补充2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-04 11:52:31 +08:00
9877d4d51e 优化包名检测弹窗弹出速度 2021-02-04 11:44:15 +08:00
32b4570738 修复下载游戏历史版本 游戏 ID 异常的问题 2021-02-04 11:04:45 +08:00
lyr
af3fdb3cfb 光环助手V4.7.0-新分类精选页功能优化_20210203测试问题_第4、5点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1123#note_89121 2021-02-04 10:47:28 +08:00
5ce992ee63 【光环助手V_4.7.0】论坛交互优化(0204AM补充1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-04 10:22:04 +08:00
ba17019428 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-04 10:00:45 +08:00
f77087907f 修改帖子详情定位到评论位置错位问题 2021-02-04 10:00:16 +08:00
3080f59ef6 Merge remote-tracking branch 'origin/dev-4.7.0' into dev-4.7.0 2021-02-04 09:52:03 +08:00
74b1f388e2 修复大图不能放大的问题 2021-02-04 09:51:53 +08:00
dcbc2fe352 还原自滚动专题优化 2021-02-04 09:37:56 +08:00
b892c58c52 【光环助手V_4.7.0】论坛交互优化(0203PM补充) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-04 09:36:21 +08:00
0a455c7b09 修改跳转游戏详情评论tab位置错误 2021-02-03 18:17:36 +08:00
7a254c4609 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-03 17:39:01 +08:00
4f08a4b9d2 光环助手V4.7.0-首页游戏功能强化(视频自动播放)(曝光) https://git.ghzs.com/pm/halo-app-issues/-/issues/1122 2021-02-03 17:29:35 +08:00
75670b7801 完成启动图功能强化(0203测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1131 2021-02-03 17:05:19 +08:00
af386f6bdc 解决禅道bug 226,229 2021-02-03 15:19:41 +08:00
ff813d8784 【光环助手V_4.7.0】论坛交互优化(0202测试补充3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-03 15:05:32 +08:00
dba87fe09c 【光环助手V_4.7.0】论坛交互优化(0202测试补充3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-03 15:02:33 +08:00
cf13003c2a 【光环助手V_4.7.0】论坛交互优化(0202PMUI补充:1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-03 14:26:27 +08:00
7fe337c557 【光环助手V_4.7.0】论坛交互优化(0202测试补充1,2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-03 11:32:53 +08:00
1902ed29c1 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-02 18:28:09 +08:00
a67927e9e7 修改包名检测弹窗显示逻辑 2021-02-02 18:27:51 +08:00
33b6835ae6 完成启动图功能强化(0202测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1131 2021-02-02 18:22:42 +08:00
1ca6a2c795 修复游戏下载红点更新问题 2021-02-02 17:36:35 +08:00
372230092a 去掉自滚动栏目的焦点 2021-02-02 17:11:54 +08:00
2ee1db1074 修复镜像游戏在已安装显示成 null 的问题 2021-02-02 17:11:16 +08:00
caabf416bb Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-02 17:01:50 +08:00
d33d0e98c9 光环助手V4.7.0-首页游戏功能强化(视频自动播放)(20210202测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1122 2021-02-02 17:01:30 +08:00
a4a029f31e 完成启动图功能强化(数据埋点) https://git.ghzs.com/pm/halo-app-issues/-/issues/1131 2021-02-02 16:49:48 +08:00
lyr
3b8d34f8c3 解决禅道问题221(【UI优化】在我的光环,界面上下滑动昵称重叠的问题) 2021-02-02 14:43:23 +08:00
2368e268bf 【光环助手V_4.7.0】论坛交互优化(0201测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-02 14:15:22 +08:00
6390198899 修改点击安装包解析错误弹窗的确定按钮没有删除安装包 2021-02-02 09:25:32 +08:00
4a6aef4caa javassit动态修改DiskLruCache#trimToSize 2021-02-01 23:04:43 +08:00
91529f51a3 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-01 18:54:18 +08:00
57096b2b08 首页滑动视频不跳转进度 2021-02-01 18:53:54 +08:00
40f1f57844 完成光环前端优化汇总(8,9,10,11) https://git.ghzs.com/pm/halo-app-issues/-/issues/1138 2021-02-01 18:42:36 +08:00
b9da7fbcb8 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-01 16:14:26 +08:00
7138177df2 光环前端优化汇总(2021年1月)(5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1138 2021-02-01 16:13:55 +08:00
aa4cf291dc Merge remote-tracking branch 'origin/dev-4.7.0' into dev-4.7.0 2021-02-01 16:03:34 +08:00
8f4c1651a3 Merge branch 'release' into dev-4.7.0
# Conflicts:
#	app/src/main/java/com/gh/common/exposure/ExposureManager.kt
#	app/src/main/java/com/gh/common/loghub/LoghubUtils.kt
#	app/src/main/java/com/gh/common/videolog/VideoRecordUtils.kt
#	app/src/main/java/com/gh/download/DownloadManager.java
#	app/src/main/java/com/gh/gamecenter/SplashScreenActivity.java
#	app/src/main/java/com/gh/gamecenter/gamedetail/dialog/GameDetailMoreDialog.kt
#	app/src/main/java/com/halo/assistant/HaloApp.java
#	dependencies.gradle
#	libraries/LGLibrary
2021-02-01 15:50:53 +08:00
30a6cfd7d9 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-01 15:45:42 +08:00
c1abb977bf 【光环助手V_4.7.0】论坛交互优化(UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-01 15:45:23 +08:00
fef633e9d8 缺省渠道改为 TEST 2021-02-01 14:53:38 +08:00
5207b5675c 光环助手V4.7.0-首页游戏功能强化(视频自动播放)(20200129UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1122 2021-02-01 09:44:48 +08:00
b7059abcee Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-02-01 09:21:50 +08:00
45a84fc45c 【光环助手V_4.7.0】论坛交互优化(0128测试反馈) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-02-01 09:21:22 +08:00
cf96b00036 无网络不使用启动广告 2021-02-01 09:20:37 +08:00
49dacc472d 更新 okhttp 修复 bio==null 闪退 2021-02-01 09:19:34 +08:00
72d069ad7e 提高首页图集的滚动流畅度 2021-01-31 18:19:22 +08:00
2f49e05891 完成专题合集首页图片优化(20200129UI测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1124 2021-01-31 14:16:07 +08:00
fd2d277470 启用 R8 2021-01-29 18:51:11 +08:00
fc52694fdc webview 支持视频带声音自动播放 2021-01-29 15:51:35 +08:00
7123b3d59a 【光环助手V_4.7.0】论坛交互优化(UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-01-29 15:43:42 +08:00
7684c247bf Merge branch 'hotfix-v4.6.0-270-install_observer' into 'release'
修复下载完成状态通知问题

See merge request halo/assistant-android!92
2021-01-29 12:35:59 +08:00
f2b9d26e8a 修复下载完成状态通知问题 2021-01-29 12:29:22 +08:00
6d5d407aa8 【光环助手V_4.7.0】论坛交互优化(UI测试问题1-5) https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-01-29 10:06:05 +08:00
338de605ec 版本增加至 4.6.1 2021-01-28 18:24:54 +08:00
53930b0257 修改帖子详情底部布局 2021-01-28 18:02:15 +08:00
34d5e2cc29 光环助手V4.7.0-外部应用跳转光环助手功能优化(UI测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1130 2021-01-28 16:47:38 +08:00
591a694bdf Merge branch 'hotfix-v4.6.0-270-generic_crash' into 'release'
Hotfix v4.6.0 270 generic crash

See merge request halo/assistant-android!91
2021-01-28 16:25:02 +08:00
9fd9a4ebcc 捕抓日志上报数据库因为磁盘空间不足引起的闪退 2021-01-28 16:23:58 +08:00
f2712c3db7 将接收到安装完成事件后的操作从 DownloadService 移至 DownloadManger 2021-01-28 16:22:01 +08:00
61c2c7b218 Merge branch 'hotfix-v4.6.0-270-generic_crash' into 'release'
修复 4.6.0 的闪退

See merge request halo/assistant-android!90
2021-01-28 15:44:26 +08:00
8a54c47d9e 修复闪退问题
1. 捕抓个别 vivo 设备初始化下载证书时拿不到文件的闪退
2. 捕抓 ViewPager 和 ScrollView 在部分设备上多指操作的闪退
3. 捕抓接收商店应用跳转时链接格式不支持造成的闪退
4. 修复光环应用内更新弹窗关闭弹窗后偶发的闪退
5. 修复游戏评分列表数据更新时偶发的闪退
6. 修复下载管理已安装列表更新排序时偶发的闪退
7. 捕抓搜索历史数据库/日志上报数据库因为磁盘空间不足引起的闪退
8. 捕抓内存回收对象时因为系统缺陷导致的超时闪退
2021-01-28 15:44:26 +08:00
7f60a575a4 草稿箱恢复显示回答草稿 2021-01-28 15:16:13 +08:00
eb2dd7c6b8 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-01-28 14:45:39 +08:00
ae8433d8bd 修改首页游戏遮罩 2021-01-28 14:45:14 +08:00
7ec4ba6582 Merge branch 'hotfix-v4.6.0-270-crash' into 'release'
修改sentry上的一些闪退问题

See merge request halo/assistant-android!89
2021-01-28 14:19:11 +08:00
190434a855 完成存储权限弹窗优化(0127测试) https://git.ghzs.com/pm/halo-app-issues/-/issues/1128 2021-01-28 09:50:57 +08:00
4b82e40eb4 修复启动页在 MIUI 上的显示问题 2021-01-27 18:13:42 +08:00
d8e858ddb8 统一 toast 管理 2021-01-27 18:11:11 +08:00
9b8c8f328d 修改禅道bug 204、206、208 2021-01-27 17:47:12 +08:00
lyr
bebdbe64cd Merge remote-tracking branch 'origin/hotfix-v4.6.0-270-crash' into hotfix-v4.6.0-270-crash
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/WeiBoShareActivity.java
#	app/src/main/java/com/gh/gamecenter/adapter/viewholder/DetailViewHolder.java
2021-01-27 17:36:34 +08:00
lyr
a0555d6ffc 1.修复由于游戏获取Apk为空导致的闪退问题;
2.修复回答编辑-上传图片时,图片路径返回为空导致的闪退问题;
3.修复绑定手机冲突时手机位数不够11位导致的闪退问题;
4.修复微博分享时icon为空导致的闪退问题;
5.修复关于页面-检查更新,更新信息返回后执行刷新页面操作,但页面已经销毁导致的闪退问题;
6.修复WebFragment(网页)下载跳转到自带浏览器,但页面还未添加到Activity导致的闪退问题
2021-01-27 17:31:46 +08:00
d6674efe4a 光环助手V4.7.0-外部应用跳转光环助手功能优化(修改数据上报) https://git.ghzs.com/pm/halo-app-issues/-/issues/1130 2021-01-27 16:09:25 +08:00
20235c6908 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-01-27 15:55:43 +08:00
bc155d580c 光环助手V4.7.0-外部应用跳转光环助手功能优化(修改数据上报) https://git.ghzs.com/pm/halo-app-issues/-/issues/1130 2021-01-27 15:55:14 +08:00
9cf852b490 1.修改视频流闪退问题
2.修改游戏详情顶部视频闪退问题
3.修改上报闪退日志闪退问题
4.修改游戏详情点击下载按钮闪退问题
5.修改我的预约页面数组越界问题
6.修改发布提问选择图片闪退问题
7.修改用户主页视频tab下拉刷新过程中滑动页面闪退问题
8.修改点击下载按钮数组越界问题
9.修改游戏详情即将被销毁点击更多操作弹窗闪退问题
10.修改Activity被销毁关闭下载模拟器弹窗闪退问题
11.修改Activity被销毁关闭游戏标签弹窗闪退问题
12.修改视频流下载游戏闪退问题
13.修改游戏详情点击分享闪退问题
14.修改分享微博闪退问题
15.修改图片预览闪退问题
2021-01-27 15:39:06 +08:00
f32bc837b4 Merge branch 'feature-issues1129' into 'dev-4.7.0'
完成更新推送弹窗新增官网下载地址 https://git.ghzs.com/pm/halo-app-issues/-/issues/1129

See merge request halo/assistant-android!88
2021-01-27 11:32:11 +08:00
36efcdb75a 完成更新推送弹窗新增官网下载地址 https://git.ghzs.com/pm/halo-app-issues/-/issues/1129 2021-01-27 11:31:26 +08:00
a232ef9d03 Merge branch 'hotfix-v4.6.0-270-gid' into 'release'
修复应用重建时 gid 为空的问题

See merge request halo/assistant-android!87
2021-01-27 10:37:19 +08:00
05b90bd5b4 修复应用重建时 gid 为空的问题 2021-01-26 17:41:25 +08:00
bc382e6f31 完成禅道bug 200、202 2021-01-26 17:04:15 +08:00
116d71bb9c Merge branch 'feature-issues1130' into 'dev-4.7.0'
Feature issues1130

See merge request halo/assistant-android!86
2021-01-26 16:17:29 +08:00
2aadb78301 Feature issues1130 2021-01-26 16:17:29 +08:00
lyr
22801846e4 Merge remote-tracking branch 'origin/dev-4.7.0' into dev-4.7.0 2021-01-26 15:31:44 +08:00
lyr
f9f6886250 优化模拟器卸载操作 2021-01-26 15:31:34 +08:00
30c8d4db02 Merge branch 'dev-4.7.0' of git.ghzs.com:halo/assistant-android into dev-4.7.0 2021-01-26 15:28:30 +08:00
08a5ea100e 光环前端优化汇总(2021年1月)(3,7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1138 2021-01-26 15:28:12 +08:00
db33ed0d0d 还原进程限制代码 2021-01-26 12:00:49 +08:00
fd424b0204 将接收到安装完成事件后的操作从 DownloadService 移至 DownloadManger 2021-01-26 11:54:20 +08:00
lyr
c1bf0a628f 光环前端优化汇总(2021年1月)第1、2点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1138 2021-01-26 11:25:42 +08:00
086bf75314 复用下载线程 2021-01-25 18:24:53 +08:00
af31fdc562 Merge branch 'feature-issues1124' into 'dev-4.7.0'
完成专题合集首页图片优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1124

See merge request halo/assistant-android!85
2021-01-25 17:12:49 +08:00
99e0a2e0a2 完成专题合集首页图片优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1124 2021-01-25 17:11:56 +08:00
069d0db00e 删除无用资源和代码 2021-01-25 16:48:43 +08:00
68f1343c93 Merge branch 'feature-startup_ad' into 'dev-4.7.0'
Feature startup ad

See merge request halo/assistant-android!84
2021-01-25 12:01:28 +08:00
fbe25c9099 完成启动图功能强化 (附带冷启动速度优化) https://git.ghzs.com/pm/halo-app-issues/-/issues/1131 2021-01-25 12:01:28 +08:00
9b893bcb42 Merge branch 'feature-issues1122' into 'dev-4.7.0'
Feature issues1122

See merge request halo/assistant-android!83
2021-01-25 09:45:15 +08:00
5182d4cd8d 光环助手V4.7.0-首页游戏功能强化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1122 2021-01-25 09:45:15 +08:00
lyr
cce077de96 光环助手V4.7.0-新分类精选页功能优化 二、前端功能-第3点 pm/halo-app-issues#1123 2021-01-21 10:01:28 +08:00
lyr
03092a0ec1 光环助手V4.7.0-新分类精选页功能优化 二、前端功能-第3点 pm/halo-app-issues#1123 (漏传部分) 2021-01-21 10:01:28 +08:00
lyr
4906ecc06b 光环助手V4.7.0-新分类精选页功能优化 二、前端功能-第3点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1123#note_86465 2021-01-21 10:01:27 +08:00
lyr
7a831ce646 光环助手V4.7.0-新分类精选页功能优化 二(1、2)https://git.ghzs.com/pm/halo-app-issues/-/issues/1123 2021-01-21 10:01:27 +08:00
290a26c4e2 完善权限跳转逻辑 2021-01-20 14:21:59 +08:00
571080b1e5 Merge branch 'feature-issues1128' into 'dev-4.7.0'
Feature issues1128

See merge request halo/assistant-android!81
2021-01-20 11:36:18 +08:00
d9567138ee 完成存储权限弹窗优化(权限应用场景跳转地址暂缺) pm/halo-app-issues#1128 2021-01-20 11:36:18 +08:00
4a31f74dcb Merge branch 'hotfix-v4.6.0-270-generic_crash' into 'release'
修复一些闪退

See merge request halo/assistant-android!80
2021-01-20 11:25:11 +08:00
79cf96b161 Merge branch 'hotfix-v4.6.0-270-crash' into 'release'
1.处理个人主页由于个人信息未初始化导致的闪退;2.处理绑定手机由于ServiceId未初始化导致的闪退

See merge request halo/assistant-android!79
2021-01-20 11:23:30 +08:00
7fdd4dbf04 修复一些闪退
1. 修复部分设备获取 ROM 信息时的闪退
2. 修复启动时预约游戏弹窗页面重建时的闪退
2021-01-20 11:22:28 +08:00
lyr
8eeeb5de60 1.处理个人主页由于个人信息未初始化导致的闪退;2.处理绑定手机由于ServiceId未初始化导致的闪退 2021-01-20 10:34:19 +08:00
150109cef4 修复镜像游戏的包 id 与原游戏 id 不一时出现的按钮状态不变更问题 2021-01-19 17:10:18 +08:00
7e7cc38d6d 优化全局图片加载 2021-01-19 17:04:14 +08:00
b3df30680e 更新头条推广SDK 2021-01-19 14:56:00 +08:00
f73048d3a5 Merge branch 'release' into dev-4.7.0
# Conflicts:
#	app/src/main/java/com/gh/common/exposure/ExposureManager.kt
#	app/src/main/java/com/gh/common/videolog/VideoRecordUtils.kt
#	app/src/main/res/drawable-nodpi/splash_01.webp
2021-01-19 12:02:39 +08:00
c540c14e1c Merge branch 'hotfix-v4.6.0-270-install_status' into 'release'
修复已安装的应用下载按钮状态更新不及时的问题

See merge request halo/assistant-android!77
2021-01-18 09:21:50 +08:00
f216bf4097 Merge branch 'feature-bbs' into dev-4.7.0 2021-01-18 09:13:45 +08:00
a42bcbd160 增加问题草稿功能 2021-01-15 16:30:06 +08:00
bc6786b21e 修复已安装的应用下载按钮状态更新不及时的问题 2021-01-15 11:51:28 +08:00
47e357143c 资源图片批量转webp,删掉部分废弃代码和文件 2021-01-14 17:05:14 +08:00
74eb8ad0e8 Merge branch 'feature-reduce_memory_usage' into 'dev-4.7.0'
优化线程使用

See merge request halo/assistant-android!75
2021-01-13 17:24:18 +08:00
d2fc927ffb 优化线程管理,减少闲置线程数量 2021-01-13 17:24:18 +08:00
db4b049591 Add new gdt account 2021-01-13 15:56:57 +08:00
lyr
86db2b5688 优化模拟器游戏列表加载 2021-01-13 14:46:14 +08:00
baa1c9f9e5 Merge branch 'dev-4.7.0' into feature-bbs 2021-01-13 11:38:03 +08:00
be3a3e82af 修改游戏详情预约闪退问题 2021-01-13 11:34:12 +08:00
120ea26455 编辑帖子/回答上传图片取消加载弹窗,改为占位图 2021-01-13 11:22:40 +08:00
fc476ca889 Merge remote-tracking branch 'origin/dev' into dev 2021-01-13 09:11:59 +08:00
fc36ce6a03 更换启动引导图 https://git.ghzs.com/pm/halo-app-issues/-/issues/1136 2021-01-13 09:11:49 +08:00
0a1d330c2f 修复自动搜索页面的下载按钮复用问题 2021-01-12 15:32:53 +08:00
lyr
e5ec42576c 光环助手V4.7.0-积分体系(第2期)--积分第1期优化 第3点 https://git.ghzs.com/pm/halo-app-issues/-/issues/1127 2021-01-12 14:35:32 +08:00
lyr
b57f74a411 修改非全屏WebFragment标题栏被状态栏遮盖的问题 2021-01-12 12:00:12 +08:00
cb3884ea2e 【光环助手V_4.7.0】论坛交互优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1135 2021-01-12 10:02:30 +08:00
lyr
0cb3184e35 解决禅道问题179(【光能】光能数值的显示问题) 2021-01-11 16:39:09 +08:00
lyr
76c5009fbc 1.修复模拟器游戏页-全选删除整个列表之后由于数组越界导致的闪退问题;2.修改模拟器列表初次加载卡顿问题 2021-01-11 10:13:30 +08:00
lyr
312ec153f1 修复更新版本后模拟器游戏记录被清空问题 2021-01-08 18:39:12 +08:00
1ef79df4c5 Merge branch 'release' into 'dev'
Fix crash

See merge request halo/assistant-android!74
2021-01-08 11:11:08 +08:00
d7a809f8a6 Merge branch 'hotfix-v4.5.4-254-database' into 'release'
捕抓数据库满了的时候的异常

See merge request halo/assistant-android!73
2021-01-08 11:10:42 +08:00
ebadeeb873 捕抓数据库满了的时候的异常 2021-01-08 11:08:14 +08:00
55155ee8ae Merge branch 'release' into 'dev'
捕抓部分 SqliteFullException

See merge request halo/assistant-android!72
2021-01-08 10:26:40 +08:00
be98a6b6d6 Merge branch 'hotfix-v4.5.4-254-database' into 'release'
捕抓视频日志数据库满了的时候的异常

See merge request halo/assistant-android!71
2021-01-08 10:04:22 +08:00
e2f396f8c7 捕抓视频日志数据库满了的时候的异常 2021-01-08 10:03:26 +08:00
2a8024368a 优化通用列表对比实现,提高性能 2021-01-07 16:16:46 +08:00
0cd025b42d 更新依赖 2021-01-07 16:11:40 +08:00
lyr
d6adda78c4 修复禅道问题182(【模拟器游戏】模拟器游戏显示问题) 2021-01-07 11:56:54 +08:00
99708d7801 Merge branch 'dev-4.6.0' into 'dev'
Feature 4.6.0

See merge request halo/assistant-android!70
2021-01-07 10:00:38 +08:00
584986a9d5 Merge branch 'release' into dev-4.6.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/game/columncollection/detail/ColumnCollectionDetailActivity.kt
2021-01-07 09:55:42 +08:00
507da05841 Merge remote-tracking branch 'origin/hotfix-v4.5.4-254-crash' into release
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/simulatorgame/SimulatorGameListFragment.kt
2021-01-07 09:52:22 +08:00
72a8fe4764 将下载文件夹的名字从 gh-download 改成 gh-files,避免被华为手机管家清理 2021-01-07 09:30:30 +08:00
lyr
f9f23f6324 WebFragment和光能屋-顶部栏适配小米型号手机 2021-01-06 18:31:38 +08:00
e2964f93c7 完成曝光数据统计优化(20210106测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1087 2021-01-06 18:04:30 +08:00
cb2a9a3ca0 修改模拟器游戏下载完成后数据上传错误 2021-01-06 17:43:38 +08:00
lyr
ea68941bb9 修改模拟器列表页由于复用产生的错误点击问题 2021-01-06 14:58:15 +08:00
lyr
b598d331d4 修复光能任务列表的空指针错误 2021-01-06 14:33:41 +08:00
lyr
d7b876bfed 1.修复未安装QQ情况下分享图片闪退问题;2.修复缺少专题ID时进入专题合集页面闪退问题;3.修复由于"Fragment未添加到Activity时"进入个人主页闪退问题;4.修复文章编辑页-上传图片时,由于返回的文件路径为空导致创建图片文件失败的闪退问题;5.修复模拟器游戏页由于数组越界导致的闪退问题;6.修复进入WebFragment(即网页)时,由于获取参数为空导致的闪退问题 2021-01-06 10:53:00 +08:00
lyr
6aca60080a 修复光能中心底部和透明导航栏重叠问题 2021-01-06 10:22:07 +08:00
59d76688b9 优化全局预览图->大图的显示 2021-01-06 09:53:03 +08:00
8e5e677228 完成曝光数据统计优化(20201230测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1087 2021-01-05 17:41:00 +08:00
dd1bfcaddd 正式接口切换到 4.6.0,测试接口更改 host 2021-01-05 15:47:02 +08:00
f4c66e1ab7 Merge branch 'hotfix-4.5.4-crash' into 'release'
修改sentry闪退问题

See merge request halo/assistant-android!66
2021-01-05 15:44:44 +08:00
6ebdf6e42a 1.修改选择头像闪退问题
2.修改视频评论闪退问题
3.修改模拟游戏列表闪退问题
4.尝试修改5.0系统手机找不到vector资源文件问题
5.修改用户信息修改页面闪退问题
2021-01-05 15:44:44 +08:00
4c01dd442b Merge branch 'hotfix-v4.5.4-254-generic_crash' into release
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/category/CategoryDirectoryFragment.kt
2021-01-05 15:17:57 +08:00
ac226e3301 修复闪退
1. 修复旧分类页偶发的空指针闪退
2. 修复多进程判断偶发的空指针闪退
3. 捕抓调用关闭 WebView 安全浏览 API 闪退异常
4. 捕抓通用列表刷新时多线程读写造成的数组越界
2021-01-05 15:14:34 +08:00
lyr
569f1b8cf1 微调"光能中心"UI 2021-01-05 09:38:17 +08:00
lyr
46f5da88a6 光环助手V4.6.0-积分体系(第1期)--光能屋 20201230UI测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1062#note_83290 和 光环助手V4.6.0-新增[光能中心-订单管理] 1231-补充1 https://git.ghzs.com/pm/halo-app-issues/-/issues/1061#note_83483 2021-01-04 18:36:10 +08:00
f5b876b018 Merge branch 'hotfix-v4.5.4-254-generic_crash' into 'release'
修复闪退

See merge request halo/assistant-android!63
2021-01-04 18:12:34 +08:00
c5df856023 修复闪退
1. 修复部分机器获取 rom 名字时的闪退
2. 修复下载管理偶发的闪退
3. 修复上报安装应用列表偶发的闪退
4. 修复回答详情数据异常时点击收藏的闪退
2021-01-04 18:05:38 +08:00
lyr
4f775847dc 还原"我的光环"页面,点击右上角便签可进入新的"我的光环"页面 2021-01-04 14:35:23 +08:00
lyr
f53710fdf5 光环助手V4.6.0-新增[光能中心-任务管理] 1230测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1063#note_83291 2021-01-04 11:26:16 +08:00
80c56def08 Merge branch 'release' into 'dev-4.6.0'
Fix crash

See merge request halo/assistant-android!62
2020-12-31 18:19:27 +08:00
a1d88d999e Merge branch 'hotfix-v4.5.4-254-intent_crash' into 'release'
捕抓跳转外部浏览器 intent 异常

See merge request halo/assistant-android!61
2020-12-31 18:18:52 +08:00
fbc6f6adaa Merge branch 'hotfix-4.5.4-crash' into 'release'
修改sentry闪退问题

See merge request halo/assistant-android!59
2020-12-31 18:17:28 +08:00
e7c55b2467 捕抓跳转外部浏览器 intent 异常 2020-12-31 18:15:50 +08:00
2c45bb1da9 1.处理更换头像挂件闪退问题
2.添加游戏投稿选择文件失败toast提示
2020-12-31 18:09:51 +08:00
173825dc74 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-31 09:15:54 +08:00
a63e28809b 修改禅道问题65 2020-12-31 09:15:26 +08:00
0eb8cb6e66 修复卸载应用时下载安装按钮状态变更可能不及时的问题 2020-12-30 21:22:18 +08:00
e1514e2b25 Merge branch 'release' into dev-4.6.0 2020-12-30 18:15:47 +08:00
bef0da821b 去掉下载管理-已安装列表的多线程读写 2020-12-30 18:14:53 +08:00
54741729f3 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-30 18:09:00 +08:00
e2640c22f8 修改论坛信息流图片计数标签显示规则 2020-12-30 18:08:38 +08:00
2f16d5ba99 修复游戏详情页提示弹窗的弹出异常 2020-12-30 17:48:56 +08:00
1d5301f887 修复我的关注页的取消关注列表更新问题,修复在部分设备上的游戏详情页触摸切换tab不灵敏问题 2020-12-30 17:24:52 +08:00
cc09c702f7 Merge branch 'release' into dev-4.6.0 2020-12-30 16:50:43 +08:00
6b2a88766e 修复下载管理-已安装列表的闪退问题 2020-12-30 16:49:48 +08:00
lyr
0eb629fe67 1.修复漏上报"下载游戏"、"玩游戏"、"分享游戏详情"光能任务的问题;2.前端取消上报"注册"和"登录"光能任务 2020-12-30 15:29:56 +08:00
44c398c9fb Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-30 15:17:32 +08:00
96122c5919 【光环助手V4.6.0】论坛发布流程优化(1229测试反馈2) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-30 15:17:13 +08:00
bd4c438036 修复前端测试汇总(20201229测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1098 2020-12-30 15:04:38 +08:00
adc8648f5a 完成曝光数据统计优化(20201229测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1087 2020-12-30 11:34:05 +08:00
8eb3922a33 【光环助手V4.6.0】论坛发布流程优化(201230-UI测试问题汇总) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-30 11:03:59 +08:00
30743bb015 Merge remote-tracking branch 'origin/release' into dev-4.6.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/servers/add/AddKaiFuActivity.kt
#	app/src/main/java/com/gh/gamecenter/servers/patch/PatchKaifuActivity.kt
#	app/src/main/java/com/gh/gamecenter/video/upload/view/UploadVideoActivity.kt
#	dependencies.gradle
#	libraries/LGLibrary
2020-12-30 10:38:11 +08:00
853f51ad0e Merge branch 'hotfix-v4.5.4-254-generic_crash' into 'release'
处理闪退

See merge request halo/assistant-android!57
2020-12-30 10:30:15 +08:00
763aad99ee 1. 修复视频编辑封面页偶发的初始化异常闪退
2. 修复我的游戏评论数据变更时的数组越界闪退
3. 修复部分设备跳转查看应用使用情况时的闪退
2020-12-30 10:28:03 +08:00
2ed2317b02 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-29 19:47:39 +08:00
0663f3eb2b 【光环助手V4.6.0】论坛发布流程优化(1229测试反馈1,2,3,5,6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-29 19:47:20 +08:00
11a2eeae7a 修复帖子评论详情父评论点赞不同步的问题 2020-12-29 18:21:40 +08:00
lyr
8c2619bb22 Merge remote-tracking branch 'origin/dev-4.6.0' into dev-4.6.0 2020-12-29 17:13:07 +08:00
lyr
46320fe07d 光环助手V4.6.0-积分体系(第1期)--光能中心 20201229UI测试-图1/2 https://git.ghzs.com/pm/halo-app-issues/-/issues/1065#note_82979 和 光能助手V4.6.0-积分体系(第1期)-- 我的光环优化 20201229UI测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1066#note_82978 2020-12-29 17:12:54 +08:00
1ee933b115 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-29 17:12:51 +08:00
1d9152ca1a 完成禅道问题56,66 2020-12-29 17:12:34 +08:00
lyr
81ebf21bb9 光环前端优化汇总(2020年12月)20201229补充分类精选页问题 https://git.ghzs.com/pm/halo-app-issues/-/issues/1098#note_82895 2020-12-29 16:17:15 +08:00
dd4bf98f28 补充记录曝光位置页面 https://git.ghzs.com/pm/halo-app-issues/-/issues/1087 2020-12-29 15:59:15 +08:00
ba3a06da43 完成光环前端优化汇总(20201226测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1098 2020-12-29 15:12:53 +08:00
6fe096383c 更新依赖库 2020-12-29 15:10:32 +08:00
d16f99958f 【光环助手V4.6.0】论坛发布流程优化(201226-UI测试问题汇总2-4) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-29 15:05:19 +08:00
5fa3aa42dc Merge remote-tracking branch 'origin/hotfix-4.5.4-254-crash' into release
# Conflicts:
#	libraries/LGLibrary
2020-12-29 14:20:50 +08:00
lyr
f4f28a2daa 解决禅道问题139(【光能中心】兑换区--“前往光能屋查看更多”悬浮窗出现机制) 2020-12-29 12:55:56 +08:00
3c1cd14bdc Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-29 11:52:47 +08:00
6dcc251312 光环助手V4.6.0-游戏预约功能(第三期)(1226测试1) https://git.ghzs.com/pm/halo-app-issues/-/issues/1082 2020-12-29 11:52:29 +08:00
f692aa4bff 修复曝光数据统计优化(20201228测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1087 2020-12-29 11:14:44 +08:00
7cde55ebe0 1.修改webview页面闪退问题
2.修改已安装列表闪退问题
3.修改通知权限弹窗闪退问题
4.修改Activity已关闭后web页面延迟alert而导致的闪退问题
5.修改点击视频流更多按钮闪退问题
6.修改AppUncaughtHandler捕获异常后关闭所有Activity闪退问题
7.修改一些数组越界问题
2020-12-29 10:43:35 +08:00
lyr
94eba08af4 解决禅道问题127(【光能中心】商品分类重复显示问题)、135(【光能屋】滑动界面,光能屋模块的背景问题)、139(【光能中心】兑换区--“前往光能屋查看更多”悬浮窗出现机制) 2020-12-29 10:18:14 +08:00
2bf207661c 补充安全隐患排查的更改 https://git.ghzs.com/pm/halo-app-issues/-/issues/1093 2020-12-29 10:03:21 +08:00
2fff7f60c2 完成前端优化汇总(20201225测试问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1098 2020-12-28 17:51:28 +08:00
lyr
1b3c93ebb5 解决禅道问题133(【手机登录】多次点击第三方登录会弹出多个登录界面)、135(【光能屋】滑动界面,光能屋模块的背景问题) 2020-12-28 17:13:06 +08:00
lyr
5b529b044e 光环助手V4.6.0-积分体系(第1期)--光能中心(2020-12-28 产品测试APP端问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1065#note_82696 和 光环助手V4.6.0-积分体系(第1期)--光能屋(2020-12-28 产品测试APP端问题)https://git.ghzs.com/pm/halo-app-issues/-/issues/1062#note_82698 2020-12-28 16:10:39 +08:00
91f417f479 Merge branch 'hotfix-v4.5.4-254-generic_crash' into 'release'
处理简单闪退

See merge request halo/assistant-android!55
2020-12-28 16:03:56 +08:00
fc0bf595bd 1. 修复偶发的点击安装按钮空指针闪退
2. 修复停留在微信登录页面时下载完成触发的闪退
3. 修复游戏详情页面自定义栏目展开按钮在部分设备上的数组越界闪退
4. 修复分类详情列表页面偶发的空指针闪退
5. 修复游戏管理游戏下载页面的数组越界闪退
6. 修复游戏评论回复页面偶发的空指针闪退
7. 修复通用评论页(视频、答案和文章)列表偶发的空指针闪退
8. 修复问题详情页面偶发的空指针闪退
9. 修复历史修改版本页面偶发的空指针闪退
10. 修复新增开服页面偶发的空指针闪退
11. 修复修改开服页面偶发的空指针闪退
12. 修复视频合集页面偶发的空指针闪退
13. 修复上传视频页面偶发的空指针闪退
14. 修复全局页面点返回键偶发的闪退
15. 修复反馈页面偶发的空指针闪退
16. 修复微博分享页面偶发的空指针闪退
17. 修复游戏新闻搜索页面偶发的空指针闪退
18. 修复全局下载进度更新偶发的闪退
2020-12-28 15:58:32 +08:00
lyr
c0f6499577 光环助手V4.6.0-积分体系(第1期)--光能中心 20201225UI测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1065#note_82546 和 光环助手V4.6.0-积分体系(第1期)--光能屋 20201225UI测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1062#note_82549 2020-12-28 14:32:12 +08:00
b9bef384f1 还原正常安装 Intent 2020-12-28 11:37:02 +08:00
58bfae2fdb 【光环助手V4.6.0】论坛首页信息流UI展示规则优化(201228-UI测试问题汇总) https://git.ghzs.com/pm/halo-app-issues/-/issues/1052 2020-12-28 11:04:40 +08:00
d2c2fff884 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-28 09:46:23 +08:00
ab178c47b5 【光环助手V4.6.0】论坛发布流程优化(遗留UI问题) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-28 09:46:04 +08:00
40be004376 适配登录错误码 https://git.ghzs.com/pm/halo-app-issues/-/issues/1095 2020-12-27 11:57:09 +08:00
0b501c9ce1 下载管理"安装/继续下载"补充权限申请 2020-12-27 10:56:12 +08:00
cff82379f5 升级版本至 4.5.4 2020-12-26 19:08:25 +08:00
825503bd58 Merge branch 'hotfix-v4.5.3-253-multidex' into 'release'
处理分 dex 问题

See merge request halo/assistant-android!54
2020-12-26 19:05:58 +08:00
77a715a4bd 处理分 dex 问题 2020-12-26 19:01:04 +08:00
78b233c506 预览长图默认显示顶部区域 2020-12-26 15:31:08 +08:00
a6e799bb9a 【光环助手V4.6.0】论坛首页信息流UI展示规则优化(201225-UI测试问题汇总3) https://git.ghzs.com/pm/halo-app-issues/-/issues/1052 2020-12-26 14:41:54 +08:00
b65992099a Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-26 14:09:35 +08:00
3d1a5f52bf debug环境开启严格模式 2020-12-26 14:09:15 +08:00
82d0d5745c 【光环助手V4.6.0】论坛发布流程优化(201226-UI测试问题汇总) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-26 14:07:47 +08:00
fe7968cb32 Merge branch 'feature-issues1087' into 'dev-4.6.0'
https://git.ghzs.com/pm/halo-app-issues/-/issues/1087

See merge request halo/assistant-android!53
2020-12-26 11:49:59 +08:00
94c503af74 完成部分曝光统计优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1087 2020-12-26 11:49:59 +08:00
bdd9815ffc Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-25 17:31:41 +08:00
693acceca8 光环助手V4.6.0-游戏预约功能(第三期)(201222-UI测试问题汇总) https://git.ghzs.com/pm/halo-app-issues/-/issues/1082 2020-12-25 17:31:23 +08:00
lyr
90971bc299 Merge remote-tracking branch 'origin/dev-4.6.0' into dev-4.6.0 2020-12-25 16:45:07 +08:00
lyr
e5b3613348 光能助手V4.6.0-积分体系(第1期)-- 我的光环优化 - 20201225UI测试 https://git.ghzs.com/pm/halo-app-issues/-/issues/1066#note_82491 2020-12-25 16:44:52 +08:00
652ca8f69c 1.修改帖子图片计数标签 2.修改帖子提交按钮 2020-12-25 16:43:07 +08:00
f15e351c1e Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-25 16:21:02 +08:00
47c81c3dac 【光环助手V4.6.0】论坛首页信息流UI展示规则优化(201225-UI测试问题汇总) https://git.ghzs.com/pm/halo-app-issues/-/issues/1052 2020-12-25 16:20:32 +08:00
7453a61e4f Merge remote-tracking branch 'origin/dev-4.6.0' into dev-4.6.0 2020-12-25 15:57:42 +08:00
911f6397e8 处理合并冲突 2020-12-25 15:54:52 +08:00
lyr
8d21a4f774 修复光能模块若干bug 2020-12-25 15:53:02 +08:00
991c54b680 Merge remote-tracking branch 'origin/release' into dev-4.6.0
# Conflicts:
#	app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerViewModel.kt
#	dependencies.gradle
2020-12-25 15:47:51 +08:00
48dcb5089b Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-25 15:40:31 +08:00
461efe7101 【光环助手V4.6.0】论坛发布流程优化(201224-UI测试问题汇总) https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-25 15:40:13 +08:00
lyr
0735161a20 光环前端优化汇总(2020年12月)7/8 https://git.ghzs.com/pm/halo-app-issues/-/issues/1098 2020-12-24 16:53:44 +08:00
093c1e2b15 【光环助手V4.6.0】论坛首页信息流UI展示规则优化(1224需求说明) https://git.ghzs.com/pm/halo-app-issues/-/issues/1052 2020-12-24 16:36:01 +08:00
7d0e02c899 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-24 16:08:22 +08:00
8294913f04 【光环助手V4.6.0】论坛发布流程优化201223-UI测试问题汇总1-9 https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-24 16:07:56 +08:00
lyr
e811c4f90b 增加web页跳转光能屋的JS调用方法;优化光能模块 2020-12-24 11:27:38 +08:00
69e248b389 版本更改为 4.5.3 2020-12-24 11:12:37 +08:00
43f6bfc4c2 Merge branch 'hotfix-v4.5.2-252-updateLogic' into 'release'
Hotfix v4.5.2 252 update logic

See merge request halo/assistant-android!52
2020-12-24 11:09:56 +08:00
c8b81ab56a 优化模拟器游戏在下载按钮中的判断逻辑 2020-12-24 11:08:34 +08:00
aa41fd98e8 重置下载按钮的文字显示优先级 2020-12-24 10:49:49 +08:00
lyr
ff6c4c2de9 光能值最多显示9999+ 2020-12-24 09:23:02 +08:00
lyr
136761d2f7 光能模块web页的url增加"from=ghzs",以此标志是从助手跳转 2020-12-23 18:18:17 +08:00
9374fc5264 将部分数据库操作移动到工作线程 2020-12-23 18:11:27 +08:00
lyr
324aaa5056 统一修改光能模块命名 2020-12-23 18:03:51 +08:00
f22afbd819 修复横向专题推荐图标复用问题 2020-12-23 17:47:22 +08:00
e149231cb2 完成对上报 IMEI 和 ANDROID_ID 的简单转码 https://git.ghzs.com/pm/halo-app-issues/-/issues/1093 2020-12-23 17:12:23 +08:00
lyr
9c8155ddf8 1.添加跳转光能中心的JS调用方法;2.客服通知增加"订单中心"和"光能记录"类型 2020-12-23 17:07:23 +08:00
dd95419a36 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-23 15:03:49 +08:00
870b10dd13 【光环助手V4.6.0】论坛首页信息流UI展示规则优化(201222-UI测试问题汇总1-7) https://git.ghzs.com/pm/halo-app-issues/-/issues/1052 2020-12-23 15:02:53 +08:00
lyr
637b426649 优化光能模块 2020-12-23 14:13:54 +08:00
ee8ce87e28 Merge branch 'hotfix-v4.5.2-252-crash' into 'release'
处理一些数组越界闪退

See merge request halo/assistant-android!51
2020-12-23 10:58:40 +08:00
a944a7f730 处理一些数组越界闪退 2020-12-23 10:55:37 +08:00
lyr
fdbb16b45f 修复光能模块若干bug 2020-12-23 10:11:25 +08:00
lyr
29bc098dcf Merge branch 'feature-guangneng' into dev-4.6.0 2020-12-22 18:05:24 +08:00
lyr
b2433cf13a 光环助手V4.6.0-积分体系(第1期)--光能中心(首页部分)https://git.ghzs.com/pm/halo-app-issues/-/issues/1065 和 光环助手V4.6.0-积分体系(第1期)--光能屋(首页部分)https://git.ghzs.com/pm/halo-app-issues/-/issues/1062 2020-12-22 17:15:32 +08:00
e908e23bb2 sentry 事件别名去掉编译时间 2020-12-22 17:02:07 +08:00
1a4dc827b5 Merge branch 'dev-4.6.0' of git.ghzs.com:halo/assistant-android into dev-4.6.0 2020-12-22 16:27:31 +08:00
9fd5e65fa2 添加测试用打包脚本 2020-12-22 16:25:56 +08:00
5405dcd30e 更新 compileSDK版本至30 2020-12-22 16:08:40 +08:00
5c35f7fe5d 完成镜像游戏上报标记字段 https://git.ghzs.com/pm/halo-app-issues/-/issues/1085 2020-12-22 14:54:46 +08:00
fc907a398f 完成光环前端优化汇总(12月)(1,2,4,6) https://git.ghzs.com/pm/halo-app-issues/-/issues/1098 2020-12-22 11:02:50 +08:00
1b8dc6eba0 优化ImageContainerView 2020-12-22 10:26:38 +08:00
c17a36c866 修改大家都在玩分割线不显示问题 2020-12-21 18:31:48 +08:00
a29cf832f1 修复详情页大家都在玩推荐图标显示问题 2020-12-21 18:05:53 +08:00
c9a628a5e9 修改提问、发帖子论坛图标不显示问题 2020-12-21 17:45:07 +08:00
165ba01afd Merge branch 'hotfix-v4.5.2-252-crash' into 'release'
修复v4.5.2版本闪退问题

See merge request halo/assistant-android!50
2020-12-21 16:36:23 +08:00
a39a8c2cce 1.修复视频流滑动闪退问题 2.修复WebView闪退问题 2020-12-21 16:02:00 +08:00
4002f138bb Merge branch 'feature-issues1082' into dev-4.6.0 2020-12-21 15:09:15 +08:00
48fdb38902 光环助手V4.6.0-游戏预约功能(第三期)https://git.ghzs.com/pm/halo-app-issues/-/issues/1082 2020-12-21 15:07:53 +08:00
38a3120ea1 添加应用内游戏详情url拦截 https://git.ghzs.com/pm/halo-app-issues/-/issues/1103 2020-12-21 14:33:55 +08:00
c0e370dfd2 修复横向专题的热门推荐图标显示问题 2020-12-21 14:31:31 +08:00
e4765089fa Merge branch 'hotfix-v4.5.2-252-genericCrash' into 'release'
修复一些空指针和数组越界闪退

See merge request halo/assistant-android!49
2020-12-21 11:23:06 +08:00
721e73ca1e Merge branch 'hotfix-v4.5.2-252-sentry' into 'release'
缩窄 Sentry ANR 的上报条件避免日志轰炸

See merge request halo/assistant-android!48
2020-12-21 11:19:15 +08:00
e406c90027 修复一些空指针和数组越界闪退 2020-12-21 11:17:34 +08:00
e7c4886219 缩窄 Sentry ANR 的上报条件避免日志轰炸 2020-12-21 10:15:20 +08:00
80a2e4f336 修复点击预览图片position错误问题 2020-12-17 17:24:19 +08:00
9f5940c6f6 【光环助手V4.6.0】论坛首页信息流UI展示规则优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1052 2020-12-17 16:33:14 +08:00
d01fda44b3 版本号改为 4.6.0 2020-12-15 09:44:59 +08:00
2020033bc0 测试环境API版本改为v4d6d0 2020-12-11 09:29:34 +08:00
dc2c8e590c Merge branch 'feature-bbs_publish' into dev-4.6.0 2020-12-11 09:19:20 +08:00
b115db51e9 【光环助手V4.6.0】论坛发布流程优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1086 2020-12-10 18:22:54 +08:00
lyr
7d194c7078 光能助手V4.6.0-积分体系(第1期)-- 我的光环优化 https://git.ghzs.com/pm/halo-app-issues/-/issues/1066 2020-12-08 10:31:13 +08:00
3915 changed files with 184080 additions and 50205 deletions

1
.gitignore vendored
View File

@ -7,5 +7,6 @@ local.properties
captures/
build/
release-app/
test-app/
scripts/apk-channel/
app/src/test/java/com/gh/gamecenter

5
.gitmodules vendored
View File

@ -1,4 +1,7 @@
[submodule "libraries/LGLibrary"]
path = libraries/LGLibrary
url = git@gitlab.ghzs.com:android/common-library.git
url = git@git.ghzs.com:android/common-library.git
branch = master
[submodule "assistant_flutter"]
path = assistant_flutter
url = git@git.ghzs.com:halo/android/flutter-module.git

View File

@ -1,69 +1,68 @@
# 光环助手Android客户端
### APK打包配置
### 概述
* 使用[ApkChannelPackage](https://github.com/ltlovezh/ApkChannelPackage)的方案
* 打包命令,视情况使用:
光环助手Android客户端目前使用 Kotlin 作为主要开发语言,以 MVVM 作为参考架构模式进行开发
> 打包Tinker基准包`./scripts/tinker_release_base.sh`
### 约束
> 以Tinker基准包打渠道包`./scripts/tinker_release_channel.sh`
为编写易读易维护且较健壮的代码,可参考以下约束
> 以Tinker基准包打补丁包`./scripts/tinker_release_patch.sh`
1. 尽量将逻辑代码放置于 ViewModel 中View 中只执行 UI 操作
2. 尽量使 View 在被销毁之后仍能恢复状态,处理方式可参考 [保存界面状态](https://developer.android.com/topic/libraries/architecture/saving-states)
3. 尽量参考原有文件结构及命名规范,即以 大模块 - 小模块 的形式生成包关系
4. 遵循最小改动原则,在提交代码前务必先检查变动的代码,尽量以可控的变动规模来构成一个 commit ,以便日后追踪问题
5. 代码规范可参考 [AOSP Java 风格](https://source.android.com/setup/contribute/code-style)
6. 尽量使用 Kotlin 来写新文件
7. 尽量不要使用 DataBinding因为回影响编译性能
8. Commit 前请确保不带入非项目必须文件,可手动修改 [.gitignore](https://stackoverflow.com/questions/8527597/how-do-i-ignore-files-in-a-directory-in-git) 文件忽略
9. 新页面请勿使用 ButterKnife 来进行 View 获取和绑定,请使用 ViewBinding
10. No AsyncTask!
### 混淆配置
### 公用部分
* 配置文件Android默认配置+proguard-rules.txt等
* 参考libraries下每个项目独立的配置文件`proguard-project.txt`
本项目使用 LiveData 实现了一个简单通用的基础列表分页功能,具体可见 `ListFragment`, `ListViewModel` 等类,理想情况下只需少量代码即可新建一个简单分页列表
### apk大小优化
### 首次拉取项目代码
* 限制resConfig资源集
* 开启ShrinkResources
* 开启混淆使用minifyEnabled(仅在release开启
* pngquant对png压缩、png/jpg->webp(未尝试)
`git clone -b dev git@git.ghzs.com:halo/android/assistant-android.git --recursive`
### git 版本管理
本项目使用简化版的 git flow 来管理分支,细节请看 [光环安卓简单 git 规范](https://git.ghzs.com/halo/android/assistant-android/-/wikis/%E5%85%89%E7%8E%AF%E5%AE%89%E5%8D%93%E7%AE%80%E5%8D%95-git-%E8%A7%84%E8%8C%83)
### API 环境配置
本项目使用 Build Variants 来切换 API 环境
* internal 为测试环境
* publish 为正式环境
### 图片资源配置
* 新增图片资源时,默认只添加最高规格的 xxxhdpi 文件
* 新增图片资源时,需要将其转换为 .webp 格式 (包括含透明图层的图片默认质量为90%) (转换后体积变大的文件除外)
### 第三方appkey等配置
* 修改`gradle.properties`文件将各种key填入其中实现统一管理
* 通过gradle文件内的resValue/buildConfigField/manifestPlaceHolder方式实现编译期间修改具体情况请参考``./build.gradle``和``./app/build.gradle``配置
### 拉取代码步骤
### 混淆配置
1. 拉取主项目代码: `git clone -b dev git@gitlab.ghzhushou.com:halo/assistant-android.git`
2. 初始化公用类库: `bash ./scripts/submodules_init.sh`
* 本项目使用了微信的 [AndResGuard](https://github.com/shwenzhang/AndResGuard) 作为资源混淆压缩方案,新增需要使用 `getIdentifier` 获取的资源文件时需要添加至白名单
* 本项目默认使用 R8 作为混淆工具,往 proguard-rules.txt 添加 proguard 新配置项时请检查可用性(如语法等)
### submodule管理方式(只拉取master)
### APK打包配置
* 提交代码需要cd到submodule文件夹去做修改
* 更新远端代码,`bash ./scripts/submodules_update.sh`
* 本项目使用了 [VasDolly](https://github.com/Tencent/VasDolly) 作为渠道包实现方案
* 打包命令,具体参数请见相应文件:
> 打内部测试包:`./scripts/test_build.sh`
> 打正式发布包:`./scripts/build_with_simple_backup.sh`
### TODO
* GSON 序列化用统一一个, GsonUtil fromJson
* CleanApkAdapter 转化字符串size工具函数 比如SpeedUtils
* getString 解决 字符串hardcode问题
* ~~Adapter 里面clicklistener 用接口传参将点击操作委托给controller~~
* ~~Adapter ViewHolder的功能部分重写到ViewHolder类本身~~
* ~~activity 统一入口未完成(外部入口相关)去除多余activity使用统一toolbar~~
* ~~release / debug compile不同的类库不需要再做什么开关~~
* ~~Toolbar分离有图形按钮/没有图形按钮~~
### TODO Since 3.1
- 解决 Utils 工具类引发的内存泄漏问题
- 把原有 EventBus 的消息 Type 统一到一个文件内
- 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
- ~~将 ListViewModel 所对应的 ListRepository 合并到 ListViewModel 中~~
- 依照光环助手界面功能以大模块 - 小模块的方式去修改包结构,包内文件建议以包名摘要作为前缀
- ~~使用 RxJava 的 Debounce 和 Map 操作优化搜索触发机制 参考资料:[1](https://proandroiddev.com/building-an-autocompleting-edittext-using-rxjava-f69c5c3f5a40),[2](https://medium.com/@kurtisnusbaum/rxandroid-basics-part-2-6e877af352)~~
- ~~把 ListViewModel 的数据结构类型转换方式换为抽象方法,让继承的类实现,避免出现无响应的问题~~
- ~~rxjava2 如果接口返回为空 会发生异常:java.lang.NullPointerException: Null is not a valid element (答案编辑) 解决方法->com.gh.gamecenter.retrofit.Response~~
- constraintLayout 1.1.2 导致布局出现异常(问题编辑标签选择弹窗)
- 搞清楚 GameManager 的用途,看能不能去掉
- 重构一下 MainActivity
* 把原有 EventBus 的消息 Type 统一一个文件内
* 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
* 重构 MainActivity

View File

@ -1,27 +1,21 @@
// This comment exists for a reason, do not delete
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' // kotlin
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
// apkChannelPackage
apply plugin: 'channel'
apply plugin: 'AndResGuard'
import groovy.xml.XmlUtil
//apply from: 'tinker-support.gradle'
android {
androidExtensions {
experimental = true
}
dataBinding {
enabled = true
}
viewBinding {
enabled = true
buildFeatures {
viewBinding = true
dataBinding = true
}
compileOptions {
@ -36,6 +30,20 @@ android {
dexOptions {
// jumboMode = true
javaMaxHeapSize "4g"
preDexLibraries true
maxProcessCount 8
}
aaptOptions {
additionalParameters "--no-version-vectors"
}
kapt {
useBuildCache = true
javacOptions {
// 增加注解处理器的最大错误次数,默认为 100
option("-Xmaxerrs", 500)
}
}
defaultConfig {
@ -48,7 +56,12 @@ android {
}
ndk {
abiFilters "armeabi-v7a"
// 如果不添加 `arm64` 调用系统的 PackageManager 的方法读取安装包信息的时候会出现 native 层闪退,草
// 添加了 `arm64` 以后部分 5.0 的设备会报用错 so 的问题,
// couldn't find DSO to load: libimagepipeline.so caused by: dlopen failed: "/data/data/com.gh.gamecenter/lib-main/libimagepipeline.so" is 64-bit instead of 32-bit result: 0
// 以 OPPO R7PLUS 为例,明明设备是骁龙 615ARMv8-64 bit 的设备却不支持 arm64 的 abi限制了只使用 java 后还是报错,只有 5.05.1 设备无法复现 : (
// 惊了
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
}
renderscriptTargetApi 18
@ -68,29 +81,24 @@ android {
/**
* All third-party appid/appkey
*/
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "NEW_API_HOST", "\"${NEW_API_HOST}\""
buildConfigField "String", "WECHAT_APPID", "\"${WECHAT_APPID}\""
buildConfigField "String", "WECHAT_SECRET", "\"${WECHAT_SECRET}\""
buildConfigField "String", "TENCENT_APPID", "\"${TENCENT_APPID}\""
buildConfigField "String", "WEIBO_APPKEY", "\"${WEIBO_APPKEY}\""
buildConfigField "String", "MTA_APPKEY", "\"${MTA_APPKEY}\""
buildConfigField "String", "TD_APPID", "\"${TD_APPID}\""
buildConfigField "String", "LETO_APPID", "\"${LETO_APPID}\""
buildConfigField "String", "TTAD_APPID", "\"${TTAD_APPID}\""
buildConfigField "String", "DOUYIN_CLIENTKEY", "\"${DOUYIN_CLIENTKEY}\""
buildConfigField "String", "DOUYIN_CLIENTSECRET", "\"${DOUYIN_CLIENTSECRET}\""
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
buildConfigField "String", "MEIZUPUSH_APPID", "\"${MEIZUPUSH_APPID}\""
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${MEIZUPUSH_APPKEY}\""
resValue "string", "huawei_push_appid", "appid=${HUAWEI_PUSH_APPID}"
buildConfigField "String", "QUICK_LOGIN_APPID", "\"${QUICK_LOGIN_APPID}\""
buildConfigField "String", "QUICK_LOGIN_APPKEY", "\"${QUICK_LOGIN_APPKEY}\""
/**
* Build Time 供区分 jenkins 打包时间用
*/
buildConfigField "long", "BUILD_TIME", "0"
}
// gradle 2.2以上默认同时启用v1和v2优先用于Android N
@ -131,6 +139,15 @@ android {
}
}
// Ignore useless variant
variantFilter { variant ->
def names = variant.flavors*.name
def isDebugType = variant.buildType.name == "debug"
if ((names.contains("tea") || name.contains("gdt")) && isDebugType) {
setIgnore(true)
}
}
flavorDimensions("env")
sourceSets {
@ -149,72 +166,44 @@ android {
}
productFlavors {
// publish release host
publish {
dimension "env"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
}
// internal test dev host
internal {
dimension "env"
versionNameSuffix "-debug"
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
buildConfigField "String", "DEV_API_HOST", "\"${DEV_API_HOST}\""
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_DEV_API_HOST}\""
}
buildConfigField "String", "SENSITIVE_API_HOST", "\"${DEV_API_HOST}\""
// publish release host˛
publish {
dimension "env"
buildConfigField "String", "UMENG_APPKEY", "\"${DEV_UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEV_UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${DEV_BUGLY_APPID}\""
buildConfigField "String", "DEV_API_HOST", "\"${API_HOST}\""
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_API_HOST}\""
}
tea {
dimension "env"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "DEV_API_HOST", "\"${API_HOST}\""
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
manifestPlaceholders.put("APPLOG_SCHEME", "rangersapplog.byAx6uYt".toLowerCase())
}
gdt {
dimension "env"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
buildConfigField "String", "DEV_API_HOST", "\"${API_HOST}\""
buildConfigField "String", "NEW_DEV_API_HOST", "\"${NEW_API_HOST}\""
}
}
}
// apkChannelPackage
channel {
//多渠道包的输出目录默认为new File(project.buildDir,"channel")
baseOutputDir = new File(project.buildDir, "channel")
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
apkNameFormat = '${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
}
rebuildChannel {
// baseDebugApk = 已有Debug APK
// baseReleaseApk = 已有Release APK
// //默认为new File(project.buildDir, "rebuildChannel/debug")
// debugOutputDir = Debug渠道包输出目录
// //默认为new File(project.buildDir, "rebuildChannel/release")
// releaseOutputDir = Release渠道包输出目录
lintOptions {
// For flutter release build, see https://github.com/flutter/flutter/issues/58247
checkReleaseBuilds false
}
}
repositories {
@ -231,14 +220,12 @@ dependencies {
testImplementation 'junit:junit:4.12'
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
debugImplementation "com.facebook.stetho:stetho:${stetho}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stetho}"
debugImplementation "com.squareup.okhttp3:logging-interceptor:${okHttp}"
debugImplementation "com.gu.android:toolargetool:${toolargetool}"
debugImplementation "com.github.nichbar:WhatTheStack:$whatTheStack"
// debugImplementation "com.gu.android:toolargetool:${toolargetool}" // 需要使用调试时才启用
debugImplementation "com.github.nichbar:WhatTheStack:${whatTheStack}"
implementation "androidx.core:core:${core}"
implementation "androidx.fragment:fragment:${fragment}"
implementation "androidx.core:core-ktx:${core}"
implementation "androidx.fragment:fragment-ktx:${fragment}"
implementation "androidx.multidex:multidex:${multiDex}"
implementation "androidx.appcompat:appcompat:${appCompat}"
implementation "androidx.cardview:cardview:${cardView}"
@ -252,6 +239,7 @@ dependencies {
implementation "androidx.room:room-rxjava2:${room}"
implementation "androidx.core:core-ktx:${ktx}"
implementation "androidx.viewpager2:viewpager2:${viewpager2}"
implementation "androidx.webkit:webkit:${webkit}"
kapt "androidx.room:room-compiler:${room}"
implementation "com.google.android.material:material:${material}"
@ -259,8 +247,10 @@ dependencies {
implementation "com.kyleduo.switchbutton:library:${switchButton}"
implementation "com.facebook.fresco:fresco:${fresco}"
implementation "com.facebook.fresco:animated-gif:${fresco}"
implementation "com.facebook.fresco:animated-gif-lite:${fresco}"
implementation "com.facebook.fresco:animated-drawable:${fresco}"
implementation "com.facebook.fresco:animated-webp:${fresco}"
implementation "com.facebook.fresco:webpsupport:${fresco}"
implementation "com.squareup.okhttp3:okhttp:${okHttp}"
@ -289,11 +279,6 @@ dependencies {
implementation "com.daimajia.swipelayout:library:${swipeLayout}"
implementation "com.sina.weibo.sdk:core:${weiboSDK}"
// bugly with tinker support
// implementation "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
implementation "com.google.android:flexbox:${flexbox}"
implementation "pub.devrel:easypermissions:${easypermissions}"
@ -313,11 +298,11 @@ dependencies {
implementation "com.squareup.picasso:picasso:${picasso}"
// for video streaming
implementation("com.shuyu:gsyVideoPlayer-java:$gsyVideo", {
implementation("com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:$gsyVideo", {
exclude module: "gsyvideoplayer-androidvideocache"
exclude group: "tv.danmaku.ijk.media"
})
implementation "com.shuyu:GSYVideoPlayer-exo2:$gsyVideo"
implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-exo_player2:$gsyVideo"
implementation "android.arch.work:work-runtime:${workManager}"
@ -325,39 +310,50 @@ dependencies {
implementation "com.github.tbruyelle:rxpermissions:${rxPermissions}"
implementation "com.ethanhua:skeleton:${skeleton}"
implementation "io.supercharge:shimmerlayout:${shimmerlayout}"
implementation "com.lg:skeleton:${skeleton}"
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
implementation "com.walkud.rom.checker:RomChecker:${romChecker}"
implementation "com.github.nichbar:AndroidRomChecker:${romChecker}"
debugImplementation "com.github.nichbar.chucker:library:$chucker"
releaseImplementation "com.github.nichbar.chucker:library-no-op:$chucker"
teaImplementation "com.bytedance.applog:RangersAppLog-Lite-cn:$bytedanceApplog"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-china-external:$bytedanceAweme"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-common:$bytedanceAweme"
debugImplementation "com.github.nichbar.chucker:library:${chucker}"
releaseImplementation "com.github.nichbar.chucker:library-no-op:${chucker}"
teaImplementation "com.bytedance.applog:RangersAppLog-Lite-cn:${bytedanceApplog}"
implementation "com.aliyun.dpa:oss-android-sdk:${oss}"
implementation "com.airbnb.android:lottie:$lottie"
implementation "com.airbnb.android:lottie:${lottie}"
implementation "net.lingala.zip4j:zip4j:${zip4j}"
implementation "io.sentry:sentry-android:$sentry"
implementation "io.sentry:sentry-android:4.3.0"
implementation("com.github.piasy:BigImageViewer:$bigImageViewer", {
implementation("com.github.piasy:BigImageViewer:${bigImageViewer}", {
exclude group: 'com.squareup.okhttp3'
exclude group: 'androidx.swiperefreshlayout'
exclude group: 'com.github.bumptech.glide'
exclude group: 'com.facebook.fresco'
})
implementation "com.github.PhilJay:MPAndroidChart:${chart}"
implementation "com.lahm.library:easy-protector-release:${easyProtector}"
implementation "com.github.hsiafan:apk-parser:${apkParser}"
implementation "org.nanohttpd:nanohttpd:${nanohttpd}"
implementation "com.aliyun.openservices:aliyun-log-android-sdk:${aliyunLog}"
implementation "com.lg:easyfloat:${easyFloat}"
implementation "io.github.florent37:shapeofview:${shapeOfView}"
implementation "io.github.sinaweibosdk:core:${weiboSDK}"
implementation "com.lg:gid:1.3.0"
implementation "com.louiscad.splitties:splitties-fun-pack-android-base-with-views-dsl:${splitties}"
compileOnly "com.github.axen1314.lancet:lancet-base:$lancet_version"
implementation project(':libraries:LGLibrary')
// implementation project(':libraries:MTA')
implementation project(':libraries:QQShare')
// implementation project(':libraries:TalkingData')
// implementation project(':libraries:UmengPush')
// implementation project(':libraries:WechatShare')
// implementation project(':libraries:im')
implementation project(':libraries:Matisse')
}
File propFile = file('sign.properties')
@ -417,6 +413,111 @@ if (propFile.exists()) {
// }.each { t -> t.dependsOn generateMetaJson }
//}
andResGuard {
mappingFile = null
use7zip = true
useSign = true
// 打开这个开关会keep住所有资源的原始路径只混淆资源的名字
keepRoot = false
// 设置这个值会把arsc name列混淆成相同的名字减少string常量池的大小
fixedResName = "arg"
// 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源
mergeDuplicatedRes = true
whiteList = [
"R.drawable.icon",
"R.drawable.ic_bar_back",
"R.drawable.toolbar_search_icon",
"R.drawable.bg_notification_answer_style_1",
"R.drawable.bg_notification_answer_style_2",
"R.drawable.bg_notification_article_style_1",
"R.drawable.bg_notification_article_style_2",
"R.drawable.bg_notification_feedback_style_1",
"R.drawable.bg_notification_feedback_style_2",
"R.drawable.bg_notification_gift_style_1",
"R.drawable.bg_notification_gift_style_2",
"R.drawable.bg_notification_login_style_1",
"R.drawable.bg_notification_login_style_2",
"R.drawable.bg_notification_question_style_1",
"R.drawable.bg_notification_question_style_2",
"R.drawable.bg_notification_rating_style_1",
"R.drawable.bg_notification_rating_style_2",
"R.drawable.bg_notification_reserve_game_style_1",
"R.drawable.bg_notification_reserve_game_style_2",
"R.drawable.bg_notification_video_style_1",
"R.drawable.bg_notification_video_style_2",
"R.drawable.ic_search_no_1",
"R.drawable.ic_search_no_2",
"R.drawable.ic_search_no_3",
"R.drawable.ic_search_no_4",
"R.drawable.ic_search_no_5",
"R.drawable.ic_search_no_6",
"R.drawable.ic_search_no_7",
"R.drawable.ic_search_no_8",
"R.drawable.ic_search_no_9",
"R.drawable.ic_search_no_10",
"R.drawable.ic_search_no_11",
"R.drawable.ic_search_no_12",
"R.drawable.ic_search_no_13",
"R.drawable.ic_search_no_14",
"R.drawable.ic_search_no_15",
"R.drawable.ic_search_no_16",
"R.drawable.ic_search_no_17",
"R.drawable.ic_search_no_18",
"R.drawable.ic_search_no_19",
"R.drawable.ic_search_no_20",
"R.drawable.ic_recommend_activity",
"R.drawable.ic_recommend_discount",
"R.drawable.ic_recommend_function",
"R.drawable.ic_recommend_gift",
"R.drawable.ic_recommend_role",
"R.drawable.login_btn_bg",
"R.drawable.ic_quick_login_check",
"R.drawable.ic_quick_login_uncheck",
"R.anim.anim_auth_in",
"R.anim.anim_auth_out",
"R.id.download_speed",
"R.id.download_percentage",
"R.id.comment",
"R.id.vote",
"R.id.watermark_hint",
"R.id.watermark_sb",
"R.id.bottomShareIv",
"R.id.bottomShareTv",
"R.id.recommendStarPref",
"R.id.recommendStar",
"R.drawable.help_search_delete",
"R.drawable.suggest_type_normal",
"R.drawable.suggest_type_crash",
"R.drawable.suggest_type_game_question",
"R.drawable.suggest_type_game_collect",
"R.drawable.suggest_type_function_suggest",
"R.drawable.suggest_type_article_collect",
"R.drawable.suggest_type_copyright",
"R.drawable.help_result_empty",
"R.drawable.news_comment_detail_read",
"R.drawable.news_comment_detail_comment",
"R.drawable.news_comment_detail_share",
"R.drawable.ic_libao",
"R.drawable.ic_link",
"R.drawable.concern_message_icon",
"R.drawable.reuse_blank_hint",
"R.drawable.ic_concern",
"R.drawable.concern_down",
"R.drawable.concern_up",
"R.drawable.ic_libao_more",
"R.drawable.ic_libao_delete",
"R.drawable.ic_dialog_close"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.20'
}
}
project.afterEvaluate {
def variants = null
@ -448,36 +549,46 @@ project.afterEvaluate {
if (manifestFile == null || !manifestFile.exists()) {
return
}
String[] configChanges = [
"density",
"fontScale",
"keyboard",
"keyboardHidden",
"layoutDirection",
"locale",
"mcc",
"mnc",
"navigation",
"orientation",
"screenLayout",
"screenSize",
"smallestScreenSize",
"touchscreen",
"uiMode"]
def parser = new XmlSlurper(false, true)
def manifest = parser.parse(manifestFile)
def app = manifest.'application'[0]
app.'activity'.each { act ->
String value = act.attributes()['android:configChanges']
if (value == null || value.isEmpty()) {
value = "keyboardHidden|orientation|screenSize|screenLayout|density|fontScale|locale"
if (value == null) value = ""
configChanges.eachWithIndex { config, index ->
if (index != configChanges.length - 1) {
value += config + "|"
} else {
value += config
}
}
act.attributes()['androidconfigChanges'] = value
} else {
String[] valueSplit = value.split("\\|")
if (!valueSplit.contains("keyboardHidden")) {
value += "|keyboardHidden"
}
if (!valueSplit.contains("orientation")) {
value += "|orientation"
}
if (!valueSplit.contains("screenSize")) {
value += "|screenSize"
}
if (!valueSplit.contains("screenLayout")) {
value += "|screenLayout"
}
if (!valueSplit.contains("density")) {
value += "|density"
}
if (!valueSplit.contains("fontScale")) {
value += "|fontScale"
}
if (!valueSplit.contains("locale")) {
value += "|locale"
println configChanges
configChanges.eachWithIndex { config, index ->
if (!valueSplit.contains(config)) {
value += ("|" + config)
}
}
act.attributes()['android:configChanges'] = value
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,271 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#--------- remove logs start ----------------
-assumenosideeffects class com.lightgame.config.CommonDebug {
private static String getLogTag(...);
private static String getMethodName();
public static void logMethodName(...);
public static void logParams(...);
public static void logFields(...);
public static void logMethodWithParams(...);
}
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
#-dontoptimize
#--------- remove logs end ----------------
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-dontwarn InnerClasses
# OrmLite uses reflection
-keep class com.j256.**
-keepclassmembers class com.j256.** { *; }
-keep enum com.j256.**
-keepclassmembers enum com.j256.** { *; }
-keep interface com.j256.**
-keepclassmembers interface com.j256.** { *; }
-dontwarn com.j256.**
#okhttp3
-dontwarn com.squareup.okhttp3.**
-dontwarn okio.**
-keep class com.squareup.okhttp3.** { *;}
# stetho
-keep class com.facebook.stetho.** { *; }
-dontwarn com.facebook.stetho.**
# Retrofit 2.2
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# rxjava
-keep class rx.schedulers.Schedulers {
public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
public <methods>;
}
-keep class rx.schedulers.TestScheduler {
public <methods>;
}
-keep class rx.schedulers.Schedulers {
public static ** test();
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
long producerNode;
long consumerNode;
}
-dontwarn rx.internal.util.**
## AutoScrollViewPager
-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
-dontwarn cn.trinea.android.**
## butterknife
# Retain generated class which implement Unbinder.
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
#
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
## is used to reflectively look up the generated ViewBinding.
#-keep class butterknife.*
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }
-keepnames class * { @butterknife.InjectView *;}
-dontwarn butterknife.Views$InjectViewProcessor
-dontwarn com.gc.materialdesign.views.**
# eventbus
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# weiboSdk
-keep class com.sina.weibo.sdk.** { *; }
-dontwarn android.webkit.WebView
-dontwarn android.webkit.WebViewClient
# app models
-keep class com.gh.common.view.** {*;}
-keep class com.gh.gamecenter.db.info.** {*;}
-keep class com.gh.gamecenter.entity.** {*;}
-keep class com.gh.gamecenter.qa.entity.** {*;}
-keep class com.gh.gamecenter.retrofit.** {*;}
-keep class com.gh.gamecenter.eventbus.** {*;}
-keep class com.gh.gamecenter.video.detail.** {*;}
-keep class * extends rx.Subscriber
#---------------------------------webview------------------------------------
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
}
#----------------------------------------------------------------------------
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers enum * { *; }
##---------------End: proguard configuration for Gson ----------
# ------ bugly ---------
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# easypermission
-keepclassmembers class * {
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
}
# 重命名文件为SourceFile再配合mapping符号表可以拿到真实的类名
-renamesourcefileattribute SourceFile
# 保留源文件行号
-keepattributes SourceFile,LineNumberTable
-ignorewarnings
-keep @androidx.annotation.Keep class *
-keepclassmembers class ** {
@androidx.annotation.Keep *;
}
-keep class com.gh.loghub.** { *; }
### greenDAO 3
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
-keep class org.greenrobot.greendao.** { *; }
# If you do not use SQLCipher:
-dontwarn org.greenrobot.greendao.database.**
# If you do not use RxJava:
-dontwarn rx.**
-dontwarn org.greenrobot.greendao.rx.**
-dontwarn org.greenrobot.greendao.**
### fastJson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
-keepattributes Annotation
### 广点通
-dontwarn com.qq.gdt.action.**
-keep class com.qq.gdt.action.** {*;}
### AndroidX
-keep class androidx.core.app.CoreComponentFactory { *; }
#阿里云上传
-keep class com.alibaba.sdk.android.oss.** { *; }
-dontwarn okio.**
-dontwarn org.apache.commons.codec.binary.**
#视频相关
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.**
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.utils.**
-keep class tv.danmaku.ijk.** { *; }
-dontwarn tv.danmaku.ijk.**
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#穿山甲
-keep class com.bytedance.sdk.openadsdk.** { *; }
-keep public interface com.bytedance.sdk.openadsdk.downloadnew.** {*;}
-keep class com.pgl.sys.ces.* {*;}
-keep class com.gyf.immersionbar.* {*;}
-dontwarn com.gyf.immersionbar.**
-keep class com.taobao.securityjni.**{*;}
-keep class com.taobao.wireless.security.**{*;}
-keep class com.ut.secbody.**{*;}
-keep class com.taobao.dp.**{*;}
-keep class com.alibaba.wireless.security.**{*;}
-keep class com.alibaba.sdk.android.**{*;}
-keep class com.ut.**{*;}
-keep class com.ta.**{*;}
-keep class com.gh.gamecenter.GdtHelper { *; }
-keep class com.gh.gamecenter.TeaHelper { *; }

View File

@ -1,20 +1,3 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#--------- remove logs start ----------------
-assumenosideeffects class com.lightgame.config.CommonDebug {
@ -26,126 +9,67 @@
public static void logMethodWithParams(...);
}
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
#-dontoptimize
-assumenosideeffects class com.lightgame.utils.Utils {
public static void log(...);
}
#--------- remove logs end ----------------
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-dontwarn InnerClasses
# TODO Dicard sourceFile in final release build but remain in internal build.
-renamesourcefileattribute SourceFile
# Keep Attribute
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,SourceFile,LineNumberTable
# OrmLite uses reflection
-keep class com.j256.**
-keepclassmembers class com.j256.** { *; }
-keep enum com.j256.**
-keepclassmembers enum com.j256.** { *; }
-keep interface com.j256.**
-keepclassmembers interface com.j256.** { *; }
# OrmLite
-keep class com.j256.*
-keepclassmembers class com.j256.* { *; }
-keep enum com.j256.*
-keepclassmembers enum com.j256.* { *; }
-keep interface com.j256.*
-keepclassmembers interface com.j256.* { *; }
-dontwarn com.j256.**
#okhttp3
-dontwarn com.squareup.okhttp3.**
-dontwarn okio.**
-keep class com.squareup.okhttp3.** { *;}
# stetho
-keep class com.facebook.stetho.** { *; }
-dontwarn com.facebook.stetho.**
# Retrofit 2.2
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# rxjava
-keep class rx.schedulers.Schedulers {
public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
public <methods>;
}
-keep class rx.schedulers.TestScheduler {
public <methods>;
}
-keep class rx.schedulers.Schedulers {
public static ** test();
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
long producerNode;
long consumerNode;
}
-dontwarn rx.internal.util.**
## AutoScrollViewPager
-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
### AutoScrollViewPager
-keep class cn.trinea.android.* { *; }
-keepclassmembers class cn.trinea.android.* { *; }
-dontwarn cn.trinea.android.**
## butterknife
# Retain generated class which implement Unbinder.
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
#
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
## is used to reflectively look up the generated ViewBinding.
#-keep class butterknife.*
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
### Butterknife
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
-keep class butterknife.*
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }
-keepnames class * { @butterknife.InjectView *;}
-dontwarn butterknife.Views$InjectViewProcessor
-dontwarn com.gc.materialdesign.views.**
# eventbus
-keepattributes *Annotation*
-keepclassmembers class ** {
### eventbus
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
### Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# weiboSdk
### weiboSdk
-keep class com.sina.weibo.sdk.** { *; }
-dontwarn android.webkit.WebView
-dontwarn android.webkit.WebViewClient
# app models
-keep class com.gh.common.view.** {*;}
-keep class com.gh.gamecenter.db.info.** {*;}
-keep class com.gh.gamecenter.entity.** {*;}
-keep class com.gh.gamecenter.qa.entity.** {*;}
-keep class com.gh.gamecenter.retrofit.** {*;}
-keep class com.gh.gamecenter.eventbus.** {*;}
-keep class * extends rx.Subscriber
### wechatSdk
### TODO 这里用 com.tencent.*{*;} 不起效?但其它地方可以?
-keep class com.tencent.**{*;}
#---------------------------------webview------------------------------------
### app models
-keep class com.gh.common.view.* {*;}
-keep class com.gh.gamecenter.db.info.* {*;}
-keep class com.gh.gamecenter.entity.* {*;}
-keep class com.gh.gamecenter.qa.entity.* {*;}
-keep class com.gh.gamecenter.retrofit.* {*;}
-keep class com.gh.gamecenter.eventbus.* {*;}
-keep class com.gh.gamecenter.video.detail.* {*;}
-keep class com.gh.gamecenter.home.gamecollection.* {*;}
###
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
@ -153,93 +77,38 @@
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
}
#----------------------------------------------------------------------------
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers enum * { *; }
##---------------End: proguard configuration for Gson ----------
# ------ bugly ---------
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# easypermission
### easypermission
-keepclassmembers class * {
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
}
# 重命名文件为SourceFile再配合mapping符号表可以拿到真实的类名
-renamesourcefileattribute SourceFile
# 保留源文件行号
-keepattributes SourceFile,LineNumberTable
# TODO What's this ?
-ignorewarnings
### Keep Annotation
-keep @androidx.annotation.Keep class *
-keepclassmembers class ** {
-keepclassmembers class * {
@androidx.annotation.Keep *;
}
-keep class com.gh.loghub.** { *; }
### greenDAO 3
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
-keep class org.greenrobot.greendao.** { *; }
# If you do not use SQLCipher:
-dontwarn org.greenrobot.greendao.database.**
# If you do not use RxJava:
-dontwarn rx.**
-dontwarn org.greenrobot.greendao.rx.**
-dontwarn org.greenrobot.greendao.**
### fastJson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
-keepattributes Annotation
### 广点通
-dontwarn com.qq.gdt.action.**
-keep class com.qq.gdt.action.** {*;}
-keep class com.qq.gdt.action.* {*;}
### AndroidX
-keep class androidx.core.app.CoreComponentFactory { *; }
#阿里云上传
-keep class com.alibaba.sdk.android.oss.** { *; }
### 阿里云上传
-keep class com.alibaba.sdk.android.oss.* { *; }
-dontwarn okio.**
-dontwarn org.apache.commons.codec.binary.**
#视频相关
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
### 视频相关
-keep class com.shuyu.gsyvideoplayer.video.* { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.**
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
-keep class com.shuyu.gsyvideoplayer.video.base.* { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
-keep class com.shuyu.gsyvideoplayer.utils.* { *; }
-dontwarn com.shuyu.gsyvideoplayer.utils.**
-keep class tv.danmaku.ijk.** { *; }
-keep class tv.danmaku.ijk.* { *; }
-dontwarn tv.danmaku.ijk.**
-keep public class * extends android.view.View{
*** get*();
@ -249,23 +118,34 @@
public <init>(android.content.Context, android.util.AttributeSet, int);
}
#穿山甲
-keep class com.bytedance.sdk.openadsdk.** { *; }
-keep public interface com.bytedance.sdk.openadsdk.downloadnew.** {*;}
-keep class com.pgl.sys.ces.* {*;}
-keep class com.gyf.immersionbar.* {*;}
-dontwarn com.gyf.immersionbar.**
-keep class com.taobao.securityjni.**{*;}
-keep class com.taobao.wireless.security.**{*;}
-keep class com.ut.secbody.**{*;}
-keep class com.taobao.dp.**{*;}
-keep class com.alibaba.wireless.security.**{*;}
-keep class com.alibaba.sdk.android.**{*;}
-keep class com.ut.**{*;}
-keep class com.ta.**{*;}
-keep class com.alibaba.sdk.android.*{*;}
-keep class com.ut.*{*;}
-keep class com.ta.*{*;}
### GDT & TEA
-keep class com.gh.gamecenter.GdtHelper { *; }
-keep class com.gh.gamecenter.TeaHelper { *; }
-keep class com.gh.gamecenter.TeaHelper { *; }
### 阿里云日志
-keep class com.aliyun.sls.android.producer.* { *; }
-keep interface com.aliyun.sls.android.producer.* { *; }
### 中国移动一键登录
-dontwarn com.cmic.sso.sdk.**
-keep class com.cmic.sso.sdk.* { *; }
### EasyFloat
-keep class com.lzf.easyfloat.* {*;}
### 避免 WebChromeClient 被混淆
-keepclassmembers class * extends android.webkit.WebChromeClient{
public void openFileChooser(...);
}
# Flutter模块
-keep class com.gh.common.util.DirectUtils {
public static void directToQa(...);
public static void directToQaCollection(...);
public static void directToFeedback(...);
}

View File

@ -2,8 +2,6 @@ package com.gh.gamecenter;
import android.app.Application;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
@ -17,9 +15,6 @@ import okhttp3.logging.HttpLoggingInterceptor;
public class Injection {
public static boolean appInit(Application application) {
// init stetho
Stetho.initializeWithDefaults(application);
// 监控Bundle大小,预防溢出(需要调试的时候再开启吧!)
// TooLargeTool.startLogging(application);
return true;
@ -30,7 +25,6 @@ public class Injection {
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addNetworkInterceptor(interceptor);
builder.addNetworkInterceptor(new StethoInterceptor());
return builder;
}

View File

@ -31,6 +31,8 @@ object GdtHelper {
GDTAction.init(application, "1111012969", "9d3d9da5b0948a317c03d08f14d445dc")
} else if (channel == "GH_729") {
GDTAction.init(application, "1111013063", "f53dabf458a356b101d99fc4069eb7f1")
} else if (channel == "GH_765") {
GDTAction.init(application, "1111327925", "588d503f0990f98f9b2394fbb795c570")
} else {
GDTAction.init(application, "1110680399", "f5ddaafbf520d7d7385499232a408d0a")
}

View File

@ -21,23 +21,17 @@
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 允许应用程序改变Wi-Fi连接状态 -->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- 允许应用程序打开系统窗口,显示其他应用程序 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 创建快捷方式的权限 -->
<!-- 允许应用程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<!-- 允许应用程序快捷方式 -->
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<!-- bugly with tinker -->
<!-- <uses-permission android:name="android.permission.READ_LOGS" />-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!--可选,穿山甲提供“获取地理位置权限”和“不给予地理位置权限,开发者传入地理位置参数”两种方式上报用户位置,两种方式均可不选,添加位置权限或参数将帮助投放定位广告-->
<!--请注意:无论通过何种方式提供给穿山甲用户地理位置,均需向用户声明地理位置权限将应用于穿山甲广告投放,穿山甲不强制获取地理位置信息-->
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
<!-- 如果有视频相关的广告且使用textureView播放请务必添加否则黑屏 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -47,9 +41,11 @@
com.shuyu.gsyvideoplayer.armv7a,
com.shuyu.gsyvideoplayer.x86,
com.shuyu.gsy.base,
shuyu.com.androidvideocache,
com.google.android.exoplayer2,
tv.danmaku.ijk.media.exo2,
pl.droidsonroids.gif" />
pl.droidsonroids.gif,
com.lzf.easyfloat" />
<!-- 去掉 SDK 一些流氓权限 -->
<uses-permission
@ -70,16 +66,21 @@
android:icon="@mipmap/logo"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:theme="@style/AppCompatTheme.APP"
tools:replace="android:allowBackup"
tools:replace="android:name,android:allowBackup"
tools:targetApi="n">
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
<!--android:launchMode = "singleTask"-->
<!-- 不让 sentry 读取系统事件 -->
<meta-data
android:name="io.sentry.breadcrumbs.system-events"
android:value="false" />
<activity
android:name="com.gh.gamecenter.SplashScreenActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
@ -106,7 +107,6 @@
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<!--android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" 退出时屏幕抖动 -->
<activity
android:name="com.gh.gamecenter.ImageViewerActivity"
android:theme="@style/Theme.Transparent" />
@ -164,6 +164,10 @@
android:name="com.gh.gamecenter.WebActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.SingletonWebActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.FullScreenWebActivity"
android:screenOrientation="portrait" />
@ -469,7 +473,7 @@
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.VideoActivity"
android:name="com.gh.gamecenter.qa.editor.LocalMediaActivity"
android:screenOrientation="portrait" />
<activity
@ -507,6 +511,12 @@
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name=".qa.dialog.ChooseForumActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="com.gh.gamecenter.video.detail.VideoDetailActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
@ -546,10 +556,6 @@
android:name=".forum.select.ForumSelectActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.follow.ForumMyFollowActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.detail.ForumDetailActivity"
android:screenOrientation="portrait" />
@ -558,6 +564,10 @@
android:name=".forum.moderator.ModeratorListActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.moderator.ApplyModeratorActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.label.VideoLabelActivity"
android:screenOrientation="portrait" />
@ -579,10 +589,6 @@
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
<activity
android:name=".personalhome.excellentcomments.ExcellentCommentsActivity"
android:screenOrientation="portrait" />
<activity
android:name=".simulatorgame.SimulatorGameActivity"
android:screenOrientation="portrait" />
@ -603,6 +609,100 @@
android:name=".forum.search.ForumOrUserSearchActivity"
android:screenOrientation="portrait" />
<activity
android:name=".energy.EnergyCenterActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<activity
android:name=".energy.EnergyHouseActivity"
android:screenOrientation="portrait" />
<activity
android:name=".personal.NewPersonalActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.questions.draft.QuestionDraftActivity"
android:screenOrientation="portrait" />
<activity
android:name=".servers.GameServerTestActivity"
android:screenOrientation="portrait" />
<activity
android:name=".category2.CategoryV2Activity"
android:screenOrientation="portrait" />
<activity
android:name=".personal.DeliveryInfoActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.editor.PreviewVideoActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.video.publish.VideoPublishActivity"
android:screenOrientation="portrait" />
<activity
android:name=".setting.GameDownloadSettingActivity"
android:screenOrientation="portrait" />
<activity
android:name=".setting.VideoSettingActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.video.detail.ForumVideoDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.videomanager.VideoDraftActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.questions.newdetail.NewQuestionDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.editor.FullScreenVideoActivity"
android:screenOrientation="landscape"
android:theme="@style/AppFullScreenTheme" />
<activity
android:name=".forum.list.ForumListActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.answer.detail.SimpleAnswerDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".game.commoncollection.detail.CommonCollectionDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamecollection.detail.GameCollectionDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamecollection.detail.GameCollectionPosterActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.cmic.sso.sdk.activity.LoginAuthActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.Dialog" />
<activity
android:name=".home.skip.PackageSkipActivity"
android:screenOrientation="portrait" />
<!-- &lt;!&ndash; 使用小米/华为推送弹窗功能提高推送成功率&ndash;&gt;-->
<!-- <activity-->
<!-- android:name="com.gh.gamecenter.PushProxyActivity"-->
@ -630,6 +730,39 @@
</intent-filter>
</activity>
<activity
android:name="com.gh.gamecenter.teenagermode.TeenagerModeActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamecollection.publish.GameCollectionEditActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamecollection.choose.ChooseGamesActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamecollection.choose.AddGamesActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamecollection.mine.MyGameCollectionActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamecollection.square.GameCollectionSquareActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamecollection.tag.GameCollectionTagSelectActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.editor.InsertGameCollectionWrapperActivity"
android:screenOrientation="portrait" />
<activity
android:name="${applicationId}.wxapi.WXEntryActivity"
android:exported="true"
@ -654,6 +787,11 @@
android:resource="@xml/provider_paths" />
</provider>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
<receiver
android:name="com.gh.gamecenter.receiver.DownloadReceiver"
android:exported="false">

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
package androidx.swiperefreshlayout.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
public class ViewPagerSwipeRefreshLayout extends SwipeRefreshLayout {
private float startY;
private float startX;
// 记录viewPager是否拖拽的标记
private boolean mIsVpDragger;
private final int mTouchSlop;
public ViewPagerSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 记录手指按下的位置
startY = ev.getY();
startX = ev.getX();
// 初始化标记
mIsVpDragger = false;
break;
case MotionEvent.ACTION_MOVE:
// 如果viewpager正在拖拽中那么不拦截它的事件直接return false
if(mIsVpDragger) {
return false;
}
// 获取当前手指位置
float endY = ev.getY();
float endX = ev.getX();
float distanceX = Math.abs(endX - startX);
float distanceY = Math.abs(endY - startY);
// 如果X轴位移大于Y轴位移那么将事件交给viewPager处理。
if(distanceX > mTouchSlop && distanceX > distanceY) {
mIsVpDragger = true;
return false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 初始化标记
mIsVpDragger = false;
break;
}
// 如果是Y轴位移大于X轴事件交给swipeRefreshLayout处理。
return super.onInterceptTouchEvent(ev);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,550 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.List;
import java.util.Set;
/**
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
* generated.
*
* <p><h3>Operating Model</h3>
*
* The abstract operating model is that there is an input APK which is being signed, thus producing
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
* APK and the output APK may be the same file. Because this engine does not deal with reading and
* writing files, it can handle all of these scenarios.
*
* <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
* This may be more efficient than signing the APK using a new instance of the engine. See
* <a href="#incremental">Incremental Operation</a>.
*
* <p>In the engine's operating model, a signed APK is produced as follows.
* <ol>
* <li>JAR entries to be signed are output,</li>
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
* output,</li>
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
* to the output.</li>
* </ol>
*
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
* the client to hard-code the aspects of APK signing which determine which parts of input must be
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
* APK.
*
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
* steps:
* <ol>
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
* for signing multiple APKs.</li>
* <li>Locate the input APK's APK Signing Block and provide it to
* {@link #inputApkSigningBlock(DataSource)}.</li>
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
* whether this entry should be output. The engine may request to inspect the entry.</li>
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
* inspect the entry.</li>
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
* that additional JAR entries are output. These entries comprise the output APK's JAR
* signature.</li>
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
* invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
* output APK's APK Signature Scheme v2 signature.</li>
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
* confirm that the output APK is signed.</li>
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
* engine free any resources it no longer needs.
* </ol>
*
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
* expected to perform all requested tasks before proceeding to the next stage of signing. See
* documentation of each method about the deadlines for performing the tasks requested by the
* method.
*
* <p><h3 id="incremental">Incremental Operation</h3></a>
*
* The engine supports incremental operation where a signed APK is produced, then modified and
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
* by the developer. Re-signing may be more efficient than signing from scratch.
*
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
* APK.
*
* <p><h3>Output-only Operation</h3>
*
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
* not invoked. In this mode, the engine has less control over output because it cannot request that
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
* signed and will report an error if cannot do so.
*
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
*/
public interface ApkSignerEngine extends Closeable {
default void setExecutor(RunnablesExecutor executor) {
throw new UnsupportedOperationException("setExecutor method is not implemented");
}
/**
* Initializes the signer engine with the data already present in the apk (if any). There
* might already be data that can be reused if the entries has not been changed.
*
* @param manifestBytes
* @param entryNames
* @return set of entry names which were processed by the engine during the initialization, a
* subset of entryNames
*/
default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
throw new UnsupportedOperationException("initWith method is not implemented");
}
/**
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
*
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
* guaranteed to not be used by the engine after this method terminates.
*
* @throws IOException if an I/O error occurs while reading the APK Signing Block
* @throws ApkFormatException if the APK Signing Block is malformed
* @throws IllegalStateException if this engine is closed
*/
void inputApkSigningBlock(DataSource apkSigningBlock)
throws IOException, ApkFormatException, IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
*
* <p>When an input entry is updated/changed, it's OK to not invoke
* {@link #inputJarEntryRemoved(String)} before invoking this method.
*
* @return instructions about how to proceed with this entry
*
* @throws IllegalStateException if this engine is closed
*/
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was output.
*
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
* data requested by the engine.
*
* <p>When an already output entry is updated/changed, it's OK to not invoke
* {@link #outputJarEntryRemoved(String)} before invoking this method.
*
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
* invoked.
*
* @throws IllegalStateException if this engine is closed
*/
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
*
* @return output policy of this JAR entry. The policy indicates how this input entry affects
* the output APK. The client of this engine should use this information to determine
* how the removal of this input APK's JAR entry affects the output APK.
*
* @throws IllegalStateException if this engine is closed
*/
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
throws IllegalStateException;
/**
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
*
* @throws IllegalStateException if this engine is closed
*/
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
/**
* Indicates to this engine that all JAR entries have been output.
*
* @return request to add JAR signature to the output or {@code null} if there is no need to add
* a JAR signature. The request will contain additional JAR entries to be output. The
* request must be fulfilled before
* {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
*
* @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
* from producing a valid signature. For example, if the engine uses the provided
* {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws SignatureException if an error occurred while generating a signature
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries, or if the engine is closed
*/
OutputJarSignatureRequest outputJarEntries()
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
SignatureException, IllegalStateException;
/**
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
*
* <p>The provided data sources are guaranteed to not be used by the engine after this method
* terminates.
*
* @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
* DataSource)}.
*
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
* the ZIP entries. In a well-formed archive, this section starts at the start of the
* archive and extends all the way to the ZIP Central Directory.
* @param zipCentralDirectory ZIP Central Directory section
* @param zipEocd ZIP End of Central Directory (EoCD) record
*
* @return request to add an APK Signing Block to the output or {@code null} if the output must
* not contain an APK Signing Block. The request must be fulfilled before
* {@link #outputDone()} is invoked.
*
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
* engine from producing a valid signature. For example, if the APK Signing Block
* provided to the engine is malformed.
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws SignatureException if an error occurred while generating a signature
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries or to output JAR signature, or if the engine is closed
*/
@Deprecated
OutputApkSigningBlockRequest outputZipSections(
DataSource zipEntries,
DataSource zipCentralDirectory,
DataSource zipEocd)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException, IllegalStateException;
/**
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
*
* <p>The provided data sources are guaranteed to not be used by the engine after this method
* terminates.
*
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
* the ZIP entries. In a well-formed archive, this section starts at the start of the
* archive and extends all the way to the ZIP Central Directory.
* @param zipCentralDirectory ZIP Central Directory section
* @param zipEocd ZIP End of Central Directory (EoCD) record
*
* @return request to add an APK Signing Block to the output or {@code null} if the output must
* not contain an APK Signing Block. The request must be fulfilled before
* {@link #outputDone()} is invoked.
*
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
* engine from producing a valid signature. For example, if the APK Signing Block
* provided to the engine is malformed.
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws SignatureException if an error occurred while generating a signature
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries or to output JAR signature, or if the engine is closed
*/
OutputApkSigningBlockRequest2 outputZipSections2(
DataSource zipEntries,
DataSource zipCentralDirectory,
DataSource zipEocd)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
InvalidKeyException, SignatureException, IllegalStateException;
/**
* Indicates to this engine that the signed APK was output.
*
* <p>This does not change the output APK. The method helps the client confirm that the current
* output is signed.
*
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
* entries or to output signatures, or if the engine is closed
*/
void outputDone() throws IllegalStateException;
/**
* Generates a V4 signature proto and write to output file.
*
* @param data Input data to calculate a verity hash tree and hash root
* @param outputFile To store the serialized V4 Signature.
* @param ignoreFailures Whether any failures will be silently ignored.
* @throws InvalidKeyException if a signature could not be generated because a signing key is
* not suitable for generating the signature
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
* cryptographic algorithm implementation is missing
* @throws SignatureException if an error occurred while generating a signature
* @throws IOException if protobuf fails to be serialized and written to file
*/
void signV4(DataSource data, File outputFile, boolean ignoreFailures)
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
/**
* Checks if the signing configuration provided to the engine is capable of creating a
* SourceStamp.
*/
default boolean isEligibleForSourceStamp() {
return false;
}
/** Generates the digest of the certificate used to sign the source stamp. */
default byte[] generateSourceStampCertificateDigest() throws SignatureException {
return new byte[0];
}
/**
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
* engine is OK.
*
* <p>This does not change the output APK. For example, if the output APK is not yet fully
* signed, it will remain so after this method terminates.
*/
@Override
void close();
/**
* Instructions about how to handle an input APK's JAR entry.
*
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
* invoked.
*/
public static class InputJarEntryInstructions {
private final OutputPolicy mOutputPolicy;
private final InspectJarEntryRequest mInspectJarEntryRequest;
/**
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
* output policy and without a request to inspect the entry.
*/
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
this(outputPolicy, null);
}
/**
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
* output mode and with the provided request to inspect the entry.
*
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
* need to inspect the entry.
*/
public InputJarEntryInstructions(
OutputPolicy outputPolicy,
InspectJarEntryRequest inspectJarEntryRequest) {
mOutputPolicy = outputPolicy;
mInspectJarEntryRequest = inspectJarEntryRequest;
}
/**
* Returns the output policy for this entry.
*/
public OutputPolicy getOutputPolicy() {
return mOutputPolicy;
}
/**
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
* inspect the entry.
*/
public InspectJarEntryRequest getInspectJarEntryRequest() {
return mInspectJarEntryRequest;
}
/**
* Output policy for an input APK's JAR entry.
*/
public static enum OutputPolicy {
/** Entry must not be output. */
SKIP,
/** Entry should be output. */
OUTPUT,
/** Entry will be output by the engine. The client can thus ignore this input entry. */
OUTPUT_BY_ENGINE,
}
}
/**
* Request to inspect the specified JAR entry.
*
* <p>The entry's uncompressed data must be provided to the data sink returned by
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
* must be invoked.
*/
interface InspectJarEntryRequest {
/**
* Returns the data sink into which the entry's uncompressed data should be sent.
*/
DataSink getDataSink();
/**
* Indicates that entry's data has been provided in full.
*/
void done();
/**
* Returns the name of the JAR entry.
*/
String getEntryName();
}
/**
* Request to add JAR signature (aka v1 signature) to the output APK.
*
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
* which {@link #done()} must be invoked.
*/
interface OutputJarSignatureRequest {
/**
* Returns JAR entries that must be added to the output APK.
*/
List<JarEntry> getAdditionalJarEntries();
/**
* Indicates that the JAR entries contained in this request were added to the output APK.
*/
void done();
/**
* JAR entry.
*/
public static class JarEntry {
private final String mName;
private final byte[] mData;
/**
* Constructs a new {@code JarEntry} with the provided name and data.
*
* @param data uncompressed data of the entry. Changes to this array will not be
* reflected in {@link #getData()}.
*/
public JarEntry(String name, byte[] data) {
mName = name;
mData = data.clone();
}
/**
* Returns the name of this ZIP entry.
*/
public String getName() {
return mName;
}
/**
* Returns the uncompressed data of this JAR entry.
*/
public byte[] getData() {
return mData.clone();
}
}
}
/**
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
* signature(s) of the APK are contained in this block.
*
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
* accordingly, and then {@link #done()} must be invoked.
*
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
* contained in this request.
*
* @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
*/
@Deprecated
interface OutputApkSigningBlockRequest {
/**
* Returns the APK Signing Block.
*/
byte[] getApkSigningBlock();
/**
* Indicates that the APK Signing Block was output as requested.
*/
void done();
}
/**
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
* signature(s) of the APK are contained in this block.
*
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
* output APK such that the block is immediately before the ZIP Central Directory. Immediately
* before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
* {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
* ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
* must be invoked.
*
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
* contained in this request.
*/
interface OutputApkSigningBlockRequest2 {
/**
* Returns the APK Signing Block.
*/
byte[] getApkSigningBlock();
/**
* Indicates that the APK Signing Block was output as requested.
*/
void done();
/**
* Returns the number of 0x00 bytes the caller must place immediately before APK Signing
* Block.
*/
int getPaddingSizeBeforeApkSigningBlock();
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
/**
* This class is intended as a lightweight representation of an APK signature verification issue
* where the client does not require the additional textual details provided by a subclass.
*/
public class ApkVerificationIssue {
/* The V2 signer(s) could not be read from the V2 signature block */
public static final int V2_SIG_MALFORMED_SIGNERS = 1;
/* A V2 signature block exists without any V2 signers */
public static final int V2_SIG_NO_SIGNERS = 2;
/* Failed to parse a signer's block in the V2 signature block */
public static final int V2_SIG_MALFORMED_SIGNER = 3;
/* Failed to parse the signer's signature record in the V2 signature block */
public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
/* The V2 signer contained no signatures */
public static final int V2_SIG_NO_SIGNATURES = 5;
/* The V2 signer's certificate could not be parsed */
public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
/* No signing certificates exist for the V2 signer */
public static final int V2_SIG_NO_CERTIFICATES = 7;
/* Failed to parse the V2 signer's digest record */
public static final int V2_SIG_MALFORMED_DIGEST = 8;
/* The V3 signer(s) could not be read from the V3 signature block */
public static final int V3_SIG_MALFORMED_SIGNERS = 9;
/* A V3 signature block exists without any V3 signers */
public static final int V3_SIG_NO_SIGNERS = 10;
/* Failed to parse a signer's block in the V3 signature block */
public static final int V3_SIG_MALFORMED_SIGNER = 11;
/* Failed to parse the signer's signature record in the V3 signature block */
public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
/* The V3 signer contained no signatures */
public static final int V3_SIG_NO_SIGNATURES = 13;
/* The V3 signer's certificate could not be parsed */
public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
/* No signing certificates exist for the V3 signer */
public static final int V3_SIG_NO_CERTIFICATES = 15;
/* Failed to parse the V3 signer's digest record */
public static final int V3_SIG_MALFORMED_DIGEST = 16;
/* The source stamp signer contained no signatures */
public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
/* The source stamp signer's certificate could not be parsed */
public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
/* The source stamp contains a signature produced using an unknown algorithm */
public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
/* Failed to parse the signer's signature in the source stamp signature block */
public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
/* The source stamp's signature block failed verification */
public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
/* An exception was encountered when verifying the source stamp */
public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
/* The certificate digest in the APK does not match the expected digest */
public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
/*
* The APK contains a source stamp signature block without a corresponding stamp certificate
* digest in the APK contents.
*/
public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
/*
* The APK does not contain the source stamp certificate digest file nor the source stamp
* signature block.
*/
public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
/*
* None of the signatures provided by the source stamp were produced with a known signature
* algorithm.
*/
public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
/*
* The source stamp signer's certificate in the signing block does not match the certificate in
* the APK.
*/
public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
/* The APK could not be properly parsed due to a ZIP or APK format exception */
public static final int MALFORMED_APK = 28;
/* An unexpected exception was caught when attempting to verify the APK's signatures */
public static final int UNEXPECTED_EXCEPTION = 29;
/* The APK contains the certificate digest file but does not contain a stamp signature block */
public static final int SOURCE_STAMP_SIG_MISSING = 30;
/* Source stamp block contains a malformed attribute. */
public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
/* Source stamp block contains an unknown attribute. */
public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
/**
* Failed to parse the SigningCertificateLineage structure in the source stamp
* attributes section.
*/
public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
/**
* The source stamp certificate does not match the terminal node in the provided
* proof-of-rotation structure describing the stamp certificate history.
*/
public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
/**
* The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
* with signature(s) that did not verify.
*/
public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
/** No V1 / jar signing signature blocks were found in the APK. */
public static final int JAR_SIG_NO_SIGNATURES = 36;
/** An exception was encountered when parsing the V1 / jar signer in the signature block. */
public static final int JAR_SIG_PARSE_EXCEPTION = 37;
private final int mIssueId;
private final String mFormat;
private final Object[] mParams;
/**
* Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
* {@code params}.
*/
public ApkVerificationIssue(String format, Object... params) {
mIssueId = -1;
mFormat = format;
mParams = params;
}
/**
* Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
* params}.
*/
public ApkVerificationIssue(int issueId, Object... params) {
mIssueId = issueId;
mFormat = null;
mParams = params;
}
/**
* Returns the numeric ID for this issue.
*/
public int getIssueId() {
return mIssueId;
}
/**
* Returns the optional parameters for this issue.
*/
public Object[] getParams() {
return mParams;
}
@Override
public String toString() {
// If this instance was created by a subclass with a format string then return the same
// formatted String as the subclass.
if (mFormat != null) {
return String.format(mFormat, mParams);
}
StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
for (Object param : mParams) {
result.append(", ").append(param.toString());
}
return result.toString();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.v1.V1SchemeConstants;
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
/**
* Exports internally defined constants to allow clients to reference these values without relying
* on internal code.
*/
public class Constants {
private Constants() {}
public static final int VERSION_SOURCE_STAMP = 0;
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
public static final int V1_SOURCE_STAMP_BLOCK_ID =
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
public static final int V2_SOURCE_STAMP_BLOCK_ID =
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
import java.io.IOException;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Hints {
/**
* Name of hint pattern asset file in APK.
*/
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
/**
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
*/
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
private static int clampToInt(long value) {
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
}
public static final class ByteRange {
final long start;
final long end;
public ByteRange(long start, long end) {
this.start = start;
this.end = end;
}
}
public static final class PatternWithRange {
final Pattern pattern;
final long offset;
final long size;
public PatternWithRange(String pattern) {
this.pattern = Pattern.compile(pattern);
this.offset= 0;
this.size = Long.MAX_VALUE;
}
public PatternWithRange(String pattern, long offset, long size) {
this.pattern = Pattern.compile(pattern);
this.offset = offset;
this.size = size;
}
public Matcher matcher(CharSequence input) {
return this.pattern.matcher(input);
}
public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
if (rangeIn.end - rangeIn.start < this.offset) {
return null;
}
long rangeOutStart = rangeIn.start + this.offset;
long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
this.size);
return new ByteRange(rangeOutStart,
rangeOutStart + rangeOutSize);
}
}
/**
* Create a blob of bytes that PinnerService understands as a
* sequence of byte ranges to pin.
*/
public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
DataOutputStream out = new DataOutputStream(bos);
try {
for (ByteRange pinByteRange : pinByteRanges) {
out.writeInt(clampToInt(pinByteRange.start));
out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
}
} catch (IOException ex) {
throw new AssertionError("impossible", ex);
}
return bos.toByteArray();
}
public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
try {
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
String[] fields = line.split(" ");
if (fields.length == 1) {
pinPatterns.add(new PatternWithRange(fields[0]));
} else if (fields.length == 3) {
long start = Long.parseLong(fields[1]);
long end = Long.parseLong(fields[2]);
pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
} else {
throw new AssertionError("bad pin pattern line " + line);
}
}
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("UTF-8 must be supported", ex);
}
return pinPatterns;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,882 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig;
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtilsLite;
import com.android.apksig.internal.apk.ApkSigResult;
import com.android.apksig.internal.apk.ApkSignerInfo;
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.SignatureNotFoundException;
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.zip.ZipFormatException;
import com.android.apksig.zip.ZipSections;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* APK source stamp verifier intended only to verify the validity of the stamp signature.
*
* <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
* when obtaining the digests for verification. This verifier should only be used in cases where
* another mechanism has already been used to verify the APK signatures.
*/
public class SourceStampVerifier {
private final File mApkFile;
private final DataSource mApkDataSource;
private final int mMinSdkVersion;
private final int mMaxSdkVersion;
private SourceStampVerifier(
File apkFile,
DataSource apkDataSource,
int minSdkVersion,
int maxSdkVersion) {
mApkFile = apkFile;
mApkDataSource = apkDataSource;
mMinSdkVersion = minSdkVersion;
mMaxSdkVersion = maxSdkVersion;
}
/**
* Verifies the APK's source stamp signature and returns the result of the verification.
*
* <p>The APK's source stamp can be considered verified if the result's {@link
* Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
* resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
* can be obtained as follows:
* <ul>
* <li>Obtain the generic errors via {@link Result#getErrors()}
* <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
* query for any errors with {@link Result.SignerInfo#getErrors()}
* <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
* query for any errors with {@link Result.SignerInfo#getErrors()}
* <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
* for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
* </ul>
*/
public SourceStampVerifier.Result verifySourceStamp() {
return verifySourceStamp(null);
}
/**
* Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
* the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
* of the verification.
*
* <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
* if present, without verifying the actual source stamp certificate used to sign the source
* stamp. This can be used to verify an APK contains a properly signed source stamp without
* verifying a particular signer.
*
* @see #verifySourceStamp()
*/
public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
Closeable in = null;
try {
DataSource apk;
if (mApkDataSource != null) {
apk = mApkDataSource;
} else if (mApkFile != null) {
RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
in = f;
apk = DataSources.asDataSource(f, 0, f.length());
} else {
throw new IllegalStateException("APK not provided");
}
return verifySourceStamp(apk, expectedCertDigest);
} catch (IOException e) {
Result result = new Result();
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
return result;
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {
}
}
}
}
/**
* Verifies the provided {@code apk}'s source stamp signature, including verification of the
* SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
* returns the result of the verification.
*
* @see #verifySourceStamp(String)
*/
private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
String expectedCertDigest) {
Result result = new Result();
try {
ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
// Attempt to obtain the source stamp's certificate digest from the APK.
List<CentralDirectoryRecord> cdRecords =
ZipUtils.parseZipCentralDirectory(apk, zipSections);
CentralDirectoryRecord sourceStampCdRecord = null;
for (CentralDirectoryRecord cdRecord : cdRecords) {
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
sourceStampCdRecord = cdRecord;
break;
}
}
// If the source stamp's certificate digest is not available within the APK then the
// source stamp cannot be verified; check if a source stamp signing block is in the
// APK's signature block to determine the appropriate status to return.
if (sourceStampCdRecord == null) {
boolean stampSigningBlockFound;
try {
ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
stampSigningBlockFound = true;
} catch (SignatureNotFoundException e) {
stampSigningBlockFound = false;
}
result.addVerificationError(stampSigningBlockFound
? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
: ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
return result;
}
// Verify that the contents of the source stamp certificate digest match the expected
// value, if provided.
byte[] sourceStampCertificateDigest =
LocalFileRecord.getUncompressedData(
apk,
sourceStampCdRecord,
zipSections.getZipCentralDirectoryOffset());
if (expectedCertDigest != null) {
String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
sourceStampCertificateDigest);
if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
result.addVerificationError(
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
actualCertDigest, expectedCertDigest);
return result;
}
}
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
new HashMap<>();
if (mMaxSdkVersion >= AndroidSdkVersion.P) {
SignatureInfo signatureInfo;
try {
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
} catch (SignatureNotFoundException e) {
signatureInfo = null;
}
if (signatureInfo != null) {
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
ContentDigestAlgorithm.class);
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
apkContentDigests, result);
signatureSchemeApkContentDigests.put(
VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
}
}
if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
signatureSchemeApkContentDigests.isEmpty())) {
SignatureInfo signatureInfo;
try {
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
} catch (SignatureNotFoundException e) {
signatureInfo = null;
}
if (signatureInfo != null) {
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
ContentDigestAlgorithm.class);
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
apkContentDigests, result);
signatureSchemeApkContentDigests.put(
VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
}
}
if (mMinSdkVersion < AndroidSdkVersion.N
|| signatureSchemeApkContentDigests.isEmpty()) {
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
apkContentDigests);
}
ApkSigResult sourceStampResult =
V2SourceStampVerifier.verify(
apk,
zipSections,
sourceStampCertificateDigest,
signatureSchemeApkContentDigests,
mMinSdkVersion,
mMaxSdkVersion);
result.mergeFrom(sourceStampResult);
return result;
} catch (ApkFormatException | IOException | ZipFormatException e) {
result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
} catch (NoSuchAlgorithmException e) {
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
} catch (SignatureNotFoundException e) {
result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
}
return result;
}
/**
* Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
* {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
public static void parseSigners(
ByteBuffer apkSignatureSchemeBlock,
int apkSigSchemeVersion,
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
Result result) {
boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
// Both the V2 and V3 signature blocks contain the following:
// * length-prefixed sequence of length-prefixed signers
ByteBuffer signers;
try {
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
} catch (ApkFormatException e) {
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
return;
}
if (!signers.hasRemaining()) {
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
return;
}
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
}
while (signers.hasRemaining()) {
Result.SignerInfo signerInfo = new Result.SignerInfo();
if (isV2Block) {
result.addV2Signer(signerInfo);
} else {
result.addV3Signer(signerInfo);
}
try {
ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
parseSigner(
signer,
apkSigSchemeVersion,
certFactory,
apkContentDigests,
signerInfo);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addVerificationWarning(
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
return;
}
}
}
/**
* Parses the provided signer block and populates the {@code result}.
*
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
* integrity of the APK.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
private static void parseSigner(
ByteBuffer signerBlock,
int apkSigSchemeVersion,
CertificateFactory certFactory,
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
Result.SignerInfo signerInfo)
throws ApkFormatException {
boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
// Both the V2 and V3 signer blocks contain the following:
// * length-prefixed signed data
// * length-prefixed sequence of length-prefixed digests:
// * uint32: signature algorithm ID
// * length-prefixed bytes: digest of contents
// * length-prefixed sequence of certificates:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
// Parse the digests block
while (digests.hasRemaining()) {
try {
ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
int sigAlgorithmId = digest.getInt();
byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
continue;
}
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addVerificationWarning(
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
return;
}
}
// Parse the certificates block
if (certificates.hasRemaining()) {
byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(encodedCert));
} catch (CertificateException e) {
signerInfo.addVerificationWarning(
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
return;
}
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
// form. Without this, getEncoded may return a different form from what was stored in
// the signature. This is because some X509Certificate(Factory) implementations
// re-encode certificates.
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
signerInfo.setSigningCertificate(certificate);
}
if (signerInfo.getSigningCertificate() == null) {
signerInfo.addVerificationWarning(
isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
: ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
return;
}
}
/**
* Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
* V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
* returned.
*
* <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
* will be updated to include a warning, but the source stamp verification can still proceed.
*/
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
List<CentralDirectoryRecord> cdRecords,
DataSource apk,
ZipSections zipSections,
Result result)
throws IOException, ApkFormatException {
CentralDirectoryRecord manifestCdRecord = null;
List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
ContentDigestAlgorithm.class);
for (CentralDirectoryRecord cdRecord : cdRecords) {
String cdRecordName = cdRecord.getName();
if (cdRecordName == null) {
continue;
}
if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
manifestCdRecord = cdRecord;
continue;
}
if (cdRecordName.startsWith("META-INF/")
&& (cdRecordName.endsWith(".RSA")
|| cdRecordName.endsWith(".DSA")
|| cdRecordName.endsWith(".EC"))) {
signatureBlockRecords.add(cdRecord);
}
}
if (manifestCdRecord == null) {
// No JAR signing manifest file found. For SourceStamp verification, returning an empty
// digest is enough since this would affect the final digest signed by the stamp, and
// thus an empty digest will invalidate that signature.
return v1ContentDigest;
}
if (signatureBlockRecords.isEmpty()) {
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
} else {
for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
for (Certificate certificate : certFactory.generateCertificates(
new ByteArrayInputStream(signatureBlockBytes))) {
// If multiple certificates are found within the signature block only the
// first is used as the signer of this block.
if (certificate instanceof X509Certificate) {
Result.SignerInfo signerInfo = new Result.SignerInfo();
signerInfo.setSigningCertificate((X509Certificate) certificate);
result.addV1Signer(signerInfo);
break;
}
}
} catch (CertificateException e) {
// Log a warning for the parsing exception but still proceed with the stamp
// verification.
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
signatureBlockRecord.getName(), e);
break;
} catch (ZipFormatException e) {
throw new ApkFormatException("Failed to read APK", e);
}
}
}
try {
byte[] manifestBytes =
LocalFileRecord.getUncompressedData(
apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
v1ContentDigest.put(
ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
return v1ContentDigest;
} catch (ZipFormatException e) {
throw new ApkFormatException("Failed to read APK", e);
}
}
/**
* Result of verifying the APK's source stamp signature; this signature can only be considered
* verified if {@link #isVerified()} returns true.
*/
public static class Result {
private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
mV2SchemeSigners, mV3SchemeSigners);
private SourceStampInfo mSourceStampInfo;
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
private boolean mVerified;
void addVerificationError(int errorId, Object... params) {
mErrors.add(new ApkVerificationIssue(errorId, params));
}
void addVerificationWarning(int warningId, Object... params) {
mWarnings.add(new ApkVerificationIssue(warningId, params));
}
private void addV1Signer(SignerInfo signerInfo) {
mV1SchemeSigners.add(signerInfo);
}
private void addV2Signer(SignerInfo signerInfo) {
mV2SchemeSigners.add(signerInfo);
}
private void addV3Signer(SignerInfo signerInfo) {
mV3SchemeSigners.add(signerInfo);
}
/**
* Returns {@code true} if the APK's source stamp signature
*/
public boolean isVerified() {
return mVerified;
}
private void mergeFrom(ApkSigResult source) {
switch (source.signatureSchemeVersion) {
case Constants.VERSION_SOURCE_STAMP:
mVerified = source.verified;
if (!source.mSigners.isEmpty()) {
mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
}
break;
default:
throw new IllegalArgumentException(
"Unknown ApkSigResult Signing Block Scheme Id "
+ source.signatureSchemeVersion);
}
}
/**
* Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
* provided APK.
*/
public List<SignerInfo> getV1SchemeSigners() {
return mV1SchemeSigners;
}
/**
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
* provided APK.
*/
public List<SignerInfo> getV2SchemeSigners() {
return mV2SchemeSigners;
}
/**
* Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
* provided APK.
*/
public List<SignerInfo> getV3SchemeSigners() {
return mV3SchemeSigners;
}
/**
* Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
* APK, or null if the source stamp signature verification failed before the stamp signature
* block could be fully parsed.
*/
public SourceStampInfo getSourceStampInfo() {
return mSourceStampInfo;
}
/**
* Returns {@code true} if an error was encountered while verifying the APK.
*
* <p>Any error prevents the APK from being considered verified.
*/
public boolean containsErrors() {
if (!mErrors.isEmpty()) {
return true;
}
for (List<SignerInfo> signers : mAllSchemeSigners) {
for (SignerInfo signer : signers) {
if (signer.containsErrors()) {
return true;
}
}
}
if (mSourceStampInfo != null) {
if (mSourceStampInfo.containsErrors()) {
return true;
}
}
return false;
}
/**
* Returns the errors encountered while verifying the APK's source stamp.
*/
public List<ApkVerificationIssue> getErrors() {
return mErrors;
}
/**
* Returns the warnings encountered while verifying the APK's source stamp.
*/
public List<ApkVerificationIssue> getWarnings() {
return mWarnings;
}
/**
* Returns all errors for this result, including any errors from signature scheme signers
* and the source stamp.
*/
public List<ApkVerificationIssue> getAllErrors() {
List<ApkVerificationIssue> errors = new ArrayList<>();
errors.addAll(mErrors);
for (List<SignerInfo> signers : mAllSchemeSigners) {
for (SignerInfo signer : signers) {
errors.addAll(signer.getErrors());
}
}
if (mSourceStampInfo != null) {
errors.addAll(mSourceStampInfo.getErrors());
}
return errors;
}
/**
* Returns all warnings for this result, including any warnings from signature scheme
* signers and the source stamp.
*/
public List<ApkVerificationIssue> getAllWarnings() {
List<ApkVerificationIssue> warnings = new ArrayList<>();
warnings.addAll(mWarnings);
for (List<SignerInfo> signers : mAllSchemeSigners) {
for (SignerInfo signer : signers) {
warnings.addAll(signer.getWarnings());
}
}
if (mSourceStampInfo != null) {
warnings.addAll(mSourceStampInfo.getWarnings());
}
return warnings;
}
/**
* Contains information about an APK's signer and any errors encountered while parsing the
* corresponding signature block.
*/
public static class SignerInfo {
private X509Certificate mSigningCertificate;
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
void setSigningCertificate(X509Certificate signingCertificate) {
mSigningCertificate = signingCertificate;
}
void addVerificationError(int errorId, Object... params) {
mErrors.add(new ApkVerificationIssue(errorId, params));
}
void addVerificationWarning(int warningId, Object... params) {
mWarnings.add(new ApkVerificationIssue(warningId, params));
}
/**
* Returns the current signing certificate used by this signer.
*/
public X509Certificate getSigningCertificate() {
return mSigningCertificate;
}
/**
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
* encountered during processing of this signer's signature block.
*/
public List<ApkVerificationIssue> getErrors() {
return mErrors;
}
/**
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
* encountered during processing of this signer's signature block.
*/
public List<ApkVerificationIssue> getWarnings() {
return mWarnings;
}
/**
* Returns {@code true} if any errors were encountered while parsing this signer's
* signature block.
*/
public boolean containsErrors() {
return !mErrors.isEmpty();
}
}
/**
* Contains information about an APK's source stamp and any errors encountered while
* parsing the stamp signature block.
*/
public static class SourceStampInfo {
private final List<X509Certificate> mCertificates;
private final List<X509Certificate> mCertificateLineage;
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
/*
* Since this utility is intended just to verify the source stamp, and the source stamp
* currently only logs warnings to prevent failing the APK signature verification, treat
* all warnings as errors. If the stamp verification is updated to log errors this
* should be set to false to ensure only errors trigger a failure verifying the source
* stamp.
*/
private static final boolean mWarningsAsErrors = true;
private SourceStampInfo(ApkSignerInfo result) {
mCertificates = result.certs;
mCertificateLineage = result.certificateLineage;
mErrors.addAll(result.getErrors());
mWarnings.addAll(result.getWarnings());
}
/**
* Returns the SourceStamp's signing certificate or {@code null} if not available. The
* certificate is guaranteed to be available if no errors were encountered during
* verification (see {@link #containsErrors()}.
*
* <p>This certificate contains the SourceStamp's public key.
*/
public X509Certificate getCertificate() {
return mCertificates.isEmpty() ? null : mCertificates.get(0);
}
/**
* Returns a {@code List} of {@link X509Certificate} instances representing the source
* stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
* if the stamp's signing certificate has not been rotated.
*/
public List<X509Certificate> getCertificatesInLineage() {
return mCertificateLineage;
}
/**
* Returns whether any errors were encountered during the source stamp verification.
*/
public boolean containsErrors() {
return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
}
/**
* Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
* encountered during source stamp verification.
*/
public List<ApkVerificationIssue> getErrors() {
if (!mWarningsAsErrors) {
return mErrors;
}
List<ApkVerificationIssue> result = new ArrayList<>();
result.addAll(mErrors);
result.addAll(mWarnings);
return result;
}
/**
* Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
* were encountered during source stamp verification.
*/
public List<ApkVerificationIssue> getWarnings() {
return mWarnings;
}
}
}
/**
* Builder of {@link SourceStampVerifier} instances.
*
* <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
* verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
* queried to determine the APK's minimum supported level, so the caller should specify a lower
* bound with {@link #setMinCheckedPlatformVersion(int)}.
*/
public static class Builder {
private final File mApkFile;
private final DataSource mApkDataSource;
private int mMinSdkVersion = 1;
private int mMaxSdkVersion = Integer.MAX_VALUE;
/**
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
* apk}.
*/
public Builder(File apk) {
if (apk == null) {
throw new NullPointerException("apk == null");
}
mApkFile = apk;
mApkDataSource = null;
}
/**
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
* apk}.
*/
public Builder(DataSource apk) {
if (apk == null) {
throw new NullPointerException("apk == null");
}
mApkDataSource = apk;
mApkFile = null;
}
/**
* Sets the oldest Android platform version for which the APK's source stamp is verified.
*
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
* on all Android platforms starting from the platform version with the provided {@code
* minSdkVersion}. The upper end of the platform versions range can be modified via
* {@link #setMaxCheckedPlatformVersion(int)}.
*
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
*/
public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
mMinSdkVersion = minSdkVersion;
return this;
}
/**
* Sets the newest Android platform version for which the APK's source stamp is verified.
*
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
* on all platform versions up to and including the proviced {@code maxSdkVersion}. The
* lower end of the platform versions range can be modified via {@link
* #setMinCheckedPlatformVersion(int)}.
*
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
* @see #setMinCheckedPlatformVersion(int)
*/
public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
mMaxSdkVersion = maxSdkVersion;
return this;
}
/**
* Returns a {@link SourceStampVerifier} initialized according to the configuration of this
* builder.
*/
public SourceStampVerifier build() {
return new SourceStampVerifier(
mApkFile,
mApkDataSource,
mMinSdkVersion,
mMaxSdkVersion);
}
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
* well-formed ZIP archive, in which case {@link #getCause()} will return a
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
* multiple ZIP entries with the same name.
*/
public class ApkFormatException extends Exception {
private static final long serialVersionUID = 1L;
public ApkFormatException(String message) {
super(message);
}
public ApkFormatException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that no APK Signing Block was found in an APK.
*/
public class ApkSigningBlockNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
public ApkSigningBlockNotFoundException(String message) {
super(message);
}
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,670 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* APK utilities.
*/
public abstract class ApkUtils {
/**
* Name of the Android manifest ZIP entry in APKs.
*/
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
/** Name of the SourceStamp certificate hash ZIP entry in APKs. */
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
private ApkUtils() {}
/**
* Finds the main ZIP sections of the provided APK.
*
* @throws IOException if an I/O error occurred while reading the APK
* @throws ZipFormatException if the APK is malformed
*/
public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
return new ZipSections(
zipSections.getZipCentralDirectoryOffset(),
zipSections.getZipCentralDirectorySizeBytes(),
zipSections.getZipCentralDirectoryRecordCount(),
zipSections.getZipEndOfCentralDirectoryOffset(),
zipSections.getZipEndOfCentralDirectory());
}
/**
* Information about the ZIP sections of an APK.
*/
public static class ZipSections extends com.android.apksig.zip.ZipSections {
public ZipSections(
long centralDirectoryOffset,
long centralDirectorySizeBytes,
int centralDirectoryRecordCount,
long eocdOffset,
ByteBuffer eocd) {
super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
eocdOffset, eocd);
}
}
/**
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
* Directory record.
*
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
* be between {@code 0} and {@code 2^32 - 1} inclusive.
*/
public static void setZipEocdCentralDirectoryOffset(
ByteBuffer zipEndOfCentralDirectory, long offset) {
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
eocd.order(ByteOrder.LITTLE_ENDIAN);
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
}
/**
* Updates the length of EOCD comment.
*
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
*/
public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
eocd.order(ByteOrder.LITTLE_ENDIAN);
ZipUtils.updateZipEocdCommentLen(eocd);
}
/**
* Returns the APK Signing Block of the provided {@code apk}.
*
* @throws ApkFormatException if the APK is not a valid ZIP archive
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
* </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk)
throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
ApkUtils.ZipSections inputZipSections;
try {
inputZipSections = ApkUtils.findZipSections(apk);
} catch (ZipFormatException e) {
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
}
return findApkSigningBlock(apk, inputZipSections);
}
/**
* Returns the APK Signing Block of the provided APK.
*
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
* </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
zipSections);
return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
/**
* Constructs a new {@code ApkSigningBlock}.
*
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
* Signing Block inside the APK file
* @param contents contents of the APK Signing Block
*/
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
super(startOffsetInApk, contents);
}
}
/**
* Returns the contents of the APK's {@code AndroidManifest.xml}.
*
* @throws IOException if an I/O error occurs while reading the APK
* @throws ApkFormatException if the APK is malformed
*/
public static ByteBuffer getAndroidManifest(DataSource apk)
throws IOException, ApkFormatException {
ZipSections zipSections;
try {
zipSections = findZipSections(apk);
} catch (ZipFormatException e) {
throw new ApkFormatException("Not a valid ZIP archive", e);
}
List<CentralDirectoryRecord> cdRecords =
V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
CentralDirectoryRecord androidManifestCdRecord = null;
for (CentralDirectoryRecord cdRecord : cdRecords) {
if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
androidManifestCdRecord = cdRecord;
break;
}
}
if (androidManifestCdRecord == null) {
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
}
DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
try {
return ByteBuffer.wrap(
LocalFileRecord.getUncompressedData(
lfhSection, androidManifestCdRecord, lfhSection.size()));
} catch (ZipFormatException e) {
throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
}
}
/**
* Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
*/
private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
/**
* Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
*/
private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
/**
* Android resource ID of the {@code android:targetSandboxVersion} attribute in
* AndroidManifest.xml.
*/
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
/**
* Android resource ID of the {@code android:targetSdkVersion} attribute in
* AndroidManifest.xml.
*/
private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
/**
* Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
*/
private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
private static final String MANIFEST_ELEMENT_TAG = "manifest";
/**
* Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
*/
private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
/**
* Returns the lowest Android platform version (API Level) supported by an APK with the
* provided {@code AndroidManifest.xml}.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws MinSdkVersionException if an error occurred while determining the API Level
*/
public static int getMinSdkVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws MinSdkVersionException {
// IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
// uses-sdk elements which are children of the top-level manifest element. uses-sdk element
// declares the minimum supported platform version using the android:minSdkVersion attribute
// whose default value is 1.
// For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
// is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
// effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
try {
// If no uses-sdk elements are encountered, Android accepts the APK. We treat this
// scenario as though the minimum supported API Level is 1.
int result = 1;
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 2)
&& ("uses-sdk".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
// In each uses-sdk element, minSdkVersion defaults to 1
int minSdkVersion = 1;
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_INT:
minSdkVersion = parser.getAttributeIntValue(i);
break;
case AndroidBinXmlParser.VALUE_TYPE_STRING:
minSdkVersion =
getMinSdkVersionForCodename(
parser.getAttributeStringValue(i));
break;
default:
throw new MinSdkVersionException(
"Unable to determine APK's minimum supported Android"
+ ": unsupported value type in "
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ " minSdkVersion"
+ ". Only integer values supported.");
}
break;
}
}
result = Math.max(result, minSdkVersion);
}
eventType = parser.next();
}
return result;
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new MinSdkVersionException(
"Unable to determine APK's minimum supported Android platform version"
+ ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
e);
}
}
private static class CodenamesLazyInitializer {
/**
* List of platform codename (first letter of) to API Level mappings. The list must be
* sorted by the first letter. For codenames not in the list, the assumption is that the API
* Level is incremented by one for every increase in the codename's first letter.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
new Pair[] {
Pair.of('C', 2),
Pair.of('D', 3),
Pair.of('E', 4),
Pair.of('F', 7),
Pair.of('G', 8),
Pair.of('H', 10),
Pair.of('I', 13),
Pair.of('J', 15),
Pair.of('K', 18),
Pair.of('L', 20),
Pair.of('M', 22),
Pair.of('N', 23),
Pair.of('O', 25),
};
private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
new ByFirstComparator();
private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
@Override
public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
char c1 = o1.getFirst();
char c2 = o2.getFirst();
return c1 - c2;
}
}
}
/**
* Returns the API Level corresponding to the provided platform codename.
*
* <p>This method is pessimistic. It returns a value one lower than the API Level with which the
* platform is actually released (e.g., 23 for N which was released as API Level 24). This is
* because new features which first appear in an API Level are not available in the early days
* of that platform version's existence, when the platform only has a codename. Moreover, this
* method currently doesn't differentiate between initial and MR releases, meaning API Level
* returned for MR releases may be more than one lower than the API Level with which the
* platform version is actually released.
*
* @throws CodenameMinSdkVersionException if the {@code codename} is not supported
*/
static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
// Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
// We only look at the first letter of the codename as this is the most important letter.
if ((firstChar >= 'A') && (firstChar <= 'Z')) {
Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
int searchResult =
Arrays.binarySearch(
sortedCodenamesFirstCharToApiLevel,
Pair.of(firstChar, null), // second element of the pair is ignored here
CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
if (searchResult >= 0) {
// Exact match -- searchResult is the index of the matching element
return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
}
// Not an exact match -- searchResult is negative and is -(insertion index) - 1.
// The element at insertionIndex - 1 (if present) is smaller than firstChar and the
// element at insertionIndex (if present) is greater than firstChar.
int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
if (insertionIndex == 0) {
// 'A' or 'B' -- never released to public
return 1;
} else {
// The element at insertionIndex - 1 is the newest older codename.
// API Level bumped by at least 1 for every change in the first letter of codename
Pair<Character, Integer> newestOlderCodenameMapping =
sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
}
}
throw new CodenameMinSdkVersionException(
"Unable to determine APK's minimum supported Android platform version"
+ " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
+ "'s minSdkVersion: \"" + codename + "\"",
codename);
}
/**
* Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
* See the {@code android:debuggable} attribute of the {@code application} element.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws ApkFormatException if the manifest is malformed
*/
public static boolean getDebuggableFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
// "application" element which is a child of the top-level manifest element. The debuggable
// attribute of this application element is coerced to a boolean value. If there is no
// application element or if it doesn't declare the debuggable attribute, the package is
// considered not debuggable.
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 2)
&& ("application".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
case AndroidBinXmlParser.VALUE_TYPE_STRING:
case AndroidBinXmlParser.VALUE_TYPE_INT:
String value = parser.getAttributeStringValue(i);
return ("true".equals(value))
|| ("TRUE".equals(value))
|| ("1".equals(value));
case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
// References to resources are not supported on purpose. The
// reason is that the resolved value depends on the resource
// configuration (e.g, MNC/MCC, locale, screen density) used
// at resolution time. As a result, the same APK may appear as
// debuggable in one situation and as non-debuggable in another
// situation. Such APKs may put users at risk.
throw new ApkFormatException(
"Unable to determine whether APK is debuggable"
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ " android:debuggable attribute references a"
+ " resource. References are not supported for"
+ " security reasons. Only constant boolean,"
+ " string and int values are supported.");
default:
throw new ApkFormatException(
"Unable to determine whether APK is debuggable"
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
+ " android:debuggable attribute uses"
+ " unsupported value type. Only boolean,"
+ " string and int values are supported.");
}
}
}
// This application element does not declare the debuggable attribute
return false;
}
eventType = parser.next();
}
// No application element found
return false;
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Unable to determine whether APK is debuggable: malformed binary resource: "
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
e);
}
}
/**
* Returns the package name of the APK according to its {@code AndroidManifest.xml} or
* {@code null} if package name is not declared. See the {@code package} attribute of the
* {@code manifest} element.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*
* @throws ApkFormatException if the manifest is malformed
*/
public static String getPackageNameFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
// manifest element. Interestingly, as opposed to most other attributes, Android Package
// Manager looks up this attribute by its name rather than by its resource ID.
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (parser.getDepth() == 1)
&& ("manifest".equals(parser.getName()))
&& (parser.getNamespace().isEmpty())) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if ("package".equals(parser.getAttributeName(i))
&& (parser.getNamespace().isEmpty())) {
return parser.getAttributeStringValue(i);
}
}
// No "package" attribute found
return null;
}
eventType = parser.next();
}
// No manifest element found
return null;
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Unable to determine APK package name: malformed binary resource: "
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
e);
}
}
/**
* Returns the security sandbox version targeted by an APK with the provided
* {@code AndroidManifest.xml}.
*
* <p>If the security sandbox version is not specified in the manifest a default value of 1 is
* returned.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*/
public static int getTargetSandboxVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) {
try {
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
} catch (ApkFormatException e) {
// An ApkFormatException indicates the target sandbox is not specified in the manifest;
// return a default value of 1.
return 1;
}
}
/**
* Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
*
* <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
* value is specified then a value of 1 is returned.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
*/
public static int getTargetSdkVersionFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) {
// If the targetSdkVersion is not specified then the platform will use the value of the
// minSdkVersion; if neither is specified then the platform will use a value of 1.
int minSdkVersion = 1;
try {
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
} catch (ApkFormatException e) {
// Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
// element is not specified at all.
}
androidManifestContents.rewind();
try {
minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
} catch (ApkFormatException e) {
// Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
// the uses-sdk element is not specified at all.
}
return minSdkVersion;
}
/**
* Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
*
* <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
* integer an ApkFormatException is thrown.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
* @throws ApkFormatException if an error occurred while determining the versionCode, or if the
* versionCode attribute value is not available.
*/
public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
throws ApkFormatException {
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
}
/**
* Returns the versionCode and versionCodeMajor of the APK according to its {@code
* AndroidManifest.xml} combined together as a single long value.
*
* <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
* 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
*
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
* resource format
* @throws ApkFormatException if an error occurred while determining the version, or if the
* versionCode attribute value is not available.
*/
public static long getLongVersionCodeFromBinaryAndroidManifest(
ByteBuffer androidManifestContents) throws ApkFormatException {
// If the versionCode is not found then allow the ApkFormatException to be thrown to notify
// the caller that the versionCode is not available.
int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
long versionCodeMajor = 0;
try {
androidManifestContents.rewind();
versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
} catch (ApkFormatException e) {
// This is expected if the versionCodeMajor has not been defined for the APK; in this
// case the return value is just the versionCode.
}
return (versionCodeMajor << 32) | versionCode;
}
/**
* Returns the integer value of the requested {@code attributeId} in the specified {@code
* elementName} from the provided {@code androidManifestContents} in binary Android resource
* format.
*
* @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
* if the requested attribute is not found.
*/
private static int getAttributeValueFromBinaryAndroidManifest(
ByteBuffer androidManifestContents, String elementName, int attributeId)
throws ApkFormatException {
if (elementName == null) {
throw new NullPointerException("elementName cannot be null");
}
try {
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
int eventType = parser.getEventType();
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
&& (elementName.equals(parser.getName()))) {
for (int i = 0; i < parser.getAttributeCount(); i++) {
if (parser.getAttributeNameResourceId(i) == attributeId) {
int valueType = parser.getAttributeValueType(i);
switch (valueType) {
case AndroidBinXmlParser.VALUE_TYPE_INT:
case AndroidBinXmlParser.VALUE_TYPE_STRING:
return parser.getAttributeIntValue(i);
default:
throw new ApkFormatException(
"Unsupported value type, " + valueType
+ ", for attribute " + String.format("0x%08X",
attributeId) + " under element " + elementName);
}
}
}
}
eventType = parser.next();
}
throw new ApkFormatException(
"Failed to determine APK's " + elementName + " attribute "
+ String.format("0x%08X", attributeId) + " value");
} catch (AndroidBinXmlParser.XmlParserException e) {
throw new ApkFormatException(
"Unable to determine value for attribute " + String.format("0x%08X",
attributeId) + " under element " + elementName
+ "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
}
}
public static byte[] computeSha256DigestBytes(byte[] data) {
return ApkUtilsLite.computeSha256DigestBytes(data);
}
}

View File

@ -0,0 +1,199 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.internal.zip.ZipUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import com.android.apksig.zip.ZipSections;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Lightweight version of the ApkUtils for clients that only require a subset of the utility
* functionality.
*/
public class ApkUtilsLite {
private ApkUtilsLite() {}
/**
* Finds the main ZIP sections of the provided APK.
*
* @throws IOException if an I/O error occurred while reading the APK
* @throws ZipFormatException if the APK is malformed
*/
public static ZipSections findZipSections(DataSource apk)
throws IOException, ZipFormatException {
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
if (eocdAndOffsetInFile == null) {
throw new ZipFormatException("ZIP End of Central Directory record not found");
}
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
long eocdOffset = eocdAndOffsetInFile.getSecond();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
if (cdStartOffset > eocdOffset) {
throw new ZipFormatException(
"ZIP Central Directory start offset out of range: " + cdStartOffset
+ ". ZIP End of Central Directory offset: " + eocdOffset);
}
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
long cdEndOffset = cdStartOffset + cdSizeBytes;
if (cdEndOffset > eocdOffset) {
throw new ZipFormatException(
"ZIP Central Directory overlaps with End of Central Directory"
+ ". CD end: " + cdEndOffset
+ ", EoCD start: " + eocdOffset);
}
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
return new ZipSections(
cdStartOffset,
cdSizeBytes,
cdRecordCount,
eocdOffset,
eocdBuf);
}
// See https://source.android.com/security/apksigning/v2.html
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
/**
* Returns the APK Signing Block of the provided APK.
*
* @throws IOException if an I/O error occurs
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
* </a>
*/
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
throws IOException, ApkSigningBlockNotFoundException {
// FORMAT (see https://source.android.com/security/apksigning/v2.html):
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes payload
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
long centralDirEndOffset =
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
if (centralDirEndOffset != eocdStartOffset) {
throw new ApkSigningBlockNotFoundException(
"ZIP Central Directory is not immediately followed by End of Central Directory"
+ ". CD end: " + centralDirEndOffset
+ ", EoCD start: " + eocdStartOffset);
}
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
throw new ApkSigningBlockNotFoundException(
"APK too small for APK Signing Block. ZIP Central Directory offset: "
+ centralDirStartOffset);
}
// Read the magic and offset in file from the footer section of the block:
// * uint64: size of block
// * 16 bytes: magic
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
footer.order(ByteOrder.LITTLE_ENDIAN);
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
throw new ApkSigningBlockNotFoundException(
"No APK Signing Block before ZIP Central Directory");
}
// Read and compare size fields
long apkSigBlockSizeInFooter = footer.getLong(0);
if ((apkSigBlockSizeInFooter < footer.capacity())
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
}
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
long apkSigBlockOffset = centralDirStartOffset - totalSize;
if (apkSigBlockOffset < 0) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block offset out of range: " + apkSigBlockOffset);
}
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
throw new ApkSigningBlockNotFoundException(
"APK Signing Block sizes in header and footer do not match: "
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
}
return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
}
/**
* Information about the location of the APK Signing Block inside an APK.
*/
public static class ApkSigningBlock {
private final long mStartOffsetInApk;
private final DataSource mContents;
/**
* Constructs a new {@code ApkSigningBlock}.
*
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
* Signing Block inside the APK file
* @param contents contents of the APK Signing Block
*/
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
mStartOffsetInApk = startOffsetInApk;
mContents = contents;
}
/**
* Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
*/
public long getStartOffset() {
return mStartOffsetInApk;
}
/**
* Returns the data source which provides the full contents of the APK Signing Block,
* including its footer.
*/
public DataSource getContents() {
return mContents;
}
}
public static byte[] computeSha256DigestBytes(byte[] data) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 is not found", e);
}
messageDigest.update(data);
return messageDigest.digest();
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that there was an issue determining the minimum Android platform version supported by
* an APK because the version is specified as a codename, rather than as API Level number, and the
* codename is in an unexpected format.
*/
public class CodenameMinSdkVersionException extends MinSdkVersionException {
private static final long serialVersionUID = 1L;
/** Encountered codename. */
private final String mCodename;
/**
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
* codename.
*/
public CodenameMinSdkVersionException(String message, String codename) {
super(message);
mCodename = codename;
}
/**
* Returns the codename.
*/
public String getCodename() {
return mCodename;
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.apk;
/**
* Indicates that there was an issue determining the minimum Android platform version supported by
* an APK.
*/
public class MinSdkVersionException extends ApkFormatException {
private static final long serialVersionUID = 1L;
/**
* Constructs a new {@code MinSdkVersionException} with the provided message.
*/
public MinSdkVersionException(String message) {
super(message);
}
/**
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
*/
public MinSdkVersionException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,869 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
*
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
* {@link #getAttributeNameResourceId(int)}.
*/
public class AndroidBinXmlParser {
/** Event: start of document. */
public static final int EVENT_START_DOCUMENT = 1;
/** Event: end of document. */
public static final int EVENT_END_DOCUMENT = 2;
/** Event: start of an element. */
public static final int EVENT_START_ELEMENT = 3;
/** Event: end of an document. */
public static final int EVENT_END_ELEMENT = 4;
/** Attribute value type is not supported by this parser. */
public static final int VALUE_TYPE_UNSUPPORTED = 0;
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
public static final int VALUE_TYPE_STRING = 1;
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
public static final int VALUE_TYPE_INT = 2;
/**
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
*/
public static final int VALUE_TYPE_REFERENCE = 3;
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
public static final int VALUE_TYPE_BOOLEAN = 4;
private static final long NO_NAMESPACE = 0xffffffffL;
private final ByteBuffer mXml;
private StringPool mStringPool;
private ResourceMap mResourceMap;
private int mDepth;
private int mCurrentEvent = EVENT_START_DOCUMENT;
private String mCurrentElementName;
private String mCurrentElementNamespace;
private int mCurrentElementAttributeCount;
private List<Attribute> mCurrentElementAttributes;
private ByteBuffer mCurrentElementAttributesContents;
private int mCurrentElementAttrSizeBytes;
/**
* Constructs a new parser for the provided document.
*/
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
xml.order(ByteOrder.LITTLE_ENDIAN);
Chunk resXmlChunk = null;
while (xml.hasRemaining()) {
Chunk chunk = Chunk.get(xml);
if (chunk == null) {
break;
}
if (chunk.getType() == Chunk.TYPE_RES_XML) {
resXmlChunk = chunk;
break;
}
}
if (resXmlChunk == null) {
throw new XmlParserException("No XML chunk in file");
}
mXml = resXmlChunk.getContents();
}
/**
* Returns the depth of the current element. Outside of the root of the document the depth is
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
* is decremented by {@code 1} after each {@code end element} event.
*/
public int getDepth() {
return mDepth;
}
/**
* Returns the type of the current event. See {@code EVENT_...} constants.
*/
public int getEventType() {
return mCurrentEvent;
}
/**
* Returns the local name of the current element or {@code null} if the current event does not
* pertain to an element.
*/
public String getName() {
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
return null;
}
return mCurrentElementName;
}
/**
* Returns the namespace of the current element or {@code null} if the current event does not
* pertain to an element. Returns an empty string if the element is not associated with a
* namespace.
*/
public String getNamespace() {
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
return null;
}
return mCurrentElementNamespace;
}
/**
* Returns the number of attributes of the element associated with the current event or
* {@code -1} if no element is associated with the current event.
*/
public int getAttributeCount() {
if (mCurrentEvent != EVENT_START_ELEMENT) {
return -1;
}
return mCurrentElementAttributeCount;
}
/**
* Returns the resource ID corresponding to the name of the specified attribute of the current
* element or {@code 0} if the name is not associated with a resource ID.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public int getAttributeNameResourceId(int index) throws XmlParserException {
return getAttribute(index).getNameResourceId();
}
/**
* Returns the name of the specified attribute of the current element.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public String getAttributeName(int index) throws XmlParserException {
return getAttribute(index).getName();
}
/**
* Returns the name of the specified attribute of the current element or an empty string if
* the attribute is not associated with a namespace.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public String getAttributeNamespace(int index) throws XmlParserException {
return getAttribute(index).getNamespace();
}
/**
* Returns the value type of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event
* @throws XmlParserException if a parsing error is occurred
*/
public int getAttributeValueType(int index) throws XmlParserException {
int type = getAttribute(index).getValueType();
switch (type) {
case Attribute.TYPE_STRING:
return VALUE_TYPE_STRING;
case Attribute.TYPE_INT_DEC:
case Attribute.TYPE_INT_HEX:
return VALUE_TYPE_INT;
case Attribute.TYPE_REFERENCE:
return VALUE_TYPE_REFERENCE;
case Attribute.TYPE_INT_BOOLEAN:
return VALUE_TYPE_BOOLEAN;
default:
return VALUE_TYPE_UNSUPPORTED;
}
}
/**
* Returns the integer value of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event.
* @throws XmlParserException if a parsing error is occurred
*/
public int getAttributeIntValue(int index) throws XmlParserException {
return getAttribute(index).getIntValue();
}
/**
* Returns the boolean value of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event.
* @throws XmlParserException if a parsing error is occurred
*/
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
return getAttribute(index).getBooleanValue();
}
/**
* Returns the string value of the specified attribute of the current element. See
* {@code VALUE_TYPE_...} constants.
*
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
* {@code start element} event.
* @throws XmlParserException if a parsing error is occurred
*/
public String getAttributeStringValue(int index) throws XmlParserException {
return getAttribute(index).getStringValue();
}
private Attribute getAttribute(int index) {
if (mCurrentEvent != EVENT_START_ELEMENT) {
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
}
if (index < 0) {
throw new IndexOutOfBoundsException("index must be >= 0");
}
if (index >= mCurrentElementAttributeCount) {
throw new IndexOutOfBoundsException(
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
}
parseCurrentElementAttributesIfNotParsed();
return mCurrentElementAttributes.get(index);
}
/**
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
*/
public int next() throws XmlParserException {
// Decrement depth if the previous event was "end element".
if (mCurrentEvent == EVENT_END_ELEMENT) {
mDepth--;
}
// Read events from document, ignoring events that we don't report to caller. Stop at the
// earliest event which we report to caller.
while (mXml.hasRemaining()) {
Chunk chunk = Chunk.get(mXml);
if (chunk == null) {
break;
}
switch (chunk.getType()) {
case Chunk.TYPE_STRING_POOL:
if (mStringPool != null) {
throw new XmlParserException("Multiple string pools not supported");
}
mStringPool = new StringPool(chunk);
break;
case Chunk.RES_XML_TYPE_START_ELEMENT:
{
if (mStringPool == null) {
throw new XmlParserException(
"Named element encountered before string pool");
}
ByteBuffer contents = chunk.getContents();
if (contents.remaining() < 20) {
throw new XmlParserException(
"Start element chunk too short. Need at least 20 bytes. Available: "
+ contents.remaining() + " bytes");
}
long nsId = getUnsignedInt32(contents);
long nameId = getUnsignedInt32(contents);
int attrStartOffset = getUnsignedInt16(contents);
int attrSizeBytes = getUnsignedInt16(contents);
int attrCount = getUnsignedInt16(contents);
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
contents.position(0);
if (attrStartOffset > contents.remaining()) {
throw new XmlParserException(
"Attributes start offset out of bounds: " + attrStartOffset
+ ", max: " + contents.remaining());
}
if (attrEndOffset > contents.remaining()) {
throw new XmlParserException(
"Attributes end offset out of bounds: " + attrEndOffset
+ ", max: " + contents.remaining());
}
mCurrentElementName = mStringPool.getString(nameId);
mCurrentElementNamespace =
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
mCurrentElementAttributeCount = attrCount;
mCurrentElementAttributes = null;
mCurrentElementAttrSizeBytes = attrSizeBytes;
mCurrentElementAttributesContents =
sliceFromTo(contents, attrStartOffset, attrEndOffset);
mDepth++;
mCurrentEvent = EVENT_START_ELEMENT;
return mCurrentEvent;
}
case Chunk.RES_XML_TYPE_END_ELEMENT:
{
if (mStringPool == null) {
throw new XmlParserException(
"Named element encountered before string pool");
}
ByteBuffer contents = chunk.getContents();
if (contents.remaining() < 8) {
throw new XmlParserException(
"End element chunk too short. Need at least 8 bytes. Available: "
+ contents.remaining() + " bytes");
}
long nsId = getUnsignedInt32(contents);
long nameId = getUnsignedInt32(contents);
mCurrentElementName = mStringPool.getString(nameId);
mCurrentElementNamespace =
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
mCurrentEvent = EVENT_END_ELEMENT;
mCurrentElementAttributes = null;
mCurrentElementAttributesContents = null;
return mCurrentEvent;
}
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
if (mResourceMap != null) {
throw new XmlParserException("Multiple resource maps not supported");
}
mResourceMap = new ResourceMap(chunk);
break;
default:
// Unknown chunk type -- ignore
break;
}
}
mCurrentEvent = EVENT_END_DOCUMENT;
return mCurrentEvent;
}
private void parseCurrentElementAttributesIfNotParsed() {
if (mCurrentElementAttributes != null) {
return;
}
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
int startPosition = i * mCurrentElementAttrSizeBytes;
ByteBuffer attr =
sliceFromTo(
mCurrentElementAttributesContents,
startPosition,
startPosition + mCurrentElementAttrSizeBytes);
long nsId = getUnsignedInt32(attr);
long nameId = getUnsignedInt32(attr);
attr.position(attr.position() + 7); // skip ignored fields
int valueType = getUnsignedInt8(attr);
long valueData = getUnsignedInt32(attr);
mCurrentElementAttributes.add(
new Attribute(
nsId,
nameId,
valueType,
(int) valueData,
mStringPool,
mResourceMap));
}
}
private static class Attribute {
private static final int TYPE_REFERENCE = 1;
private static final int TYPE_STRING = 3;
private static final int TYPE_INT_DEC = 0x10;
private static final int TYPE_INT_HEX = 0x11;
private static final int TYPE_INT_BOOLEAN = 0x12;
private final long mNsId;
private final long mNameId;
private final int mValueType;
private final int mValueData;
private final StringPool mStringPool;
private final ResourceMap mResourceMap;
private Attribute(
long nsId,
long nameId,
int valueType,
int valueData,
StringPool stringPool,
ResourceMap resourceMap) {
mNsId = nsId;
mNameId = nameId;
mValueType = valueType;
mValueData = valueData;
mStringPool = stringPool;
mResourceMap = resourceMap;
}
public int getNameResourceId() {
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
}
public String getName() throws XmlParserException {
return mStringPool.getString(mNameId);
}
public String getNamespace() throws XmlParserException {
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
}
public int getValueType() {
return mValueType;
}
public int getIntValue() throws XmlParserException {
switch (mValueType) {
case TYPE_REFERENCE:
case TYPE_INT_DEC:
case TYPE_INT_HEX:
case TYPE_INT_BOOLEAN:
return mValueData;
default:
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
}
}
public boolean getBooleanValue() throws XmlParserException {
switch (mValueType) {
case TYPE_INT_BOOLEAN:
return mValueData != 0;
default:
throw new XmlParserException(
"Cannot coerce to boolean: value type " + mValueType);
}
}
public String getStringValue() throws XmlParserException {
switch (mValueType) {
case TYPE_STRING:
return mStringPool.getString(mValueData & 0xffffffffL);
case TYPE_INT_DEC:
return Integer.toString(mValueData);
case TYPE_INT_HEX:
return "0x" + Integer.toHexString(mValueData);
case TYPE_INT_BOOLEAN:
return Boolean.toString(mValueData != 0);
case TYPE_REFERENCE:
return "@" + Integer.toHexString(mValueData);
default:
throw new XmlParserException(
"Cannot coerce to string: value type " + mValueType);
}
}
}
/**
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
* contents.
*/
private static class Chunk {
public static final int TYPE_STRING_POOL = 1;
public static final int TYPE_RES_XML = 3;
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
static final int HEADER_MIN_SIZE_BYTES = 8;
private final int mType;
private final ByteBuffer mHeader;
private final ByteBuffer mContents;
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
mType = type;
mHeader = header;
mContents = contents;
}
public ByteBuffer getContents() {
ByteBuffer result = mContents.slice();
result.order(mContents.order());
return result;
}
public ByteBuffer getHeader() {
ByteBuffer result = mHeader.slice();
result.order(mHeader.order());
return result;
}
public int getType() {
return mType;
}
/**
* Consumes the chunk located at the current position of the input and returns the chunk
* or {@code null} if there is no chunk left in the input.
*
* @throws XmlParserException if the chunk is malformed
*/
public static Chunk get(ByteBuffer input) throws XmlParserException {
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
// Android ignores the last chunk if its header is too big to fit into the file
input.position(input.limit());
return null;
}
int originalPosition = input.position();
int type = getUnsignedInt16(input);
int headerSize = getUnsignedInt16(input);
long chunkSize = getUnsignedInt32(input);
long chunkRemaining = chunkSize - 8;
if (chunkRemaining > input.remaining()) {
// Android ignores the last chunk if it's too big to fit into the file
input.position(input.limit());
return null;
}
if (headerSize < HEADER_MIN_SIZE_BYTES) {
throw new XmlParserException(
"Malformed chunk: header too short: " + headerSize + " bytes");
} else if (headerSize > chunkSize) {
throw new XmlParserException(
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
+ chunkSize + " bytes");
}
int contentStartPosition = originalPosition + headerSize;
long chunkEndPosition = originalPosition + chunkSize;
Chunk chunk =
new Chunk(
type,
sliceFromTo(input, originalPosition, contentStartPosition),
sliceFromTo(input, contentStartPosition, chunkEndPosition));
input.position((int) chunkEndPosition);
return chunk;
}
}
/**
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
*/
private static class StringPool {
private static final int FLAG_UTF8 = 1 << 8;
private final ByteBuffer mChunkContents;
private final ByteBuffer mStringsSection;
private final int mStringCount;
private final boolean mUtf8Encoded;
private final Map<Integer, String> mCachedStrings = new HashMap<>();
/**
* Constructs a new string pool from the provided chunk.
*
* @throws XmlParserException if a parsing error occurred
*/
public StringPool(Chunk chunk) throws XmlParserException {
ByteBuffer header = chunk.getHeader();
int headerSizeBytes = header.remaining();
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
if (header.remaining() < 20) {
throw new XmlParserException(
"XML chunk's header too short. Required at least 20 bytes. Available: "
+ header.remaining() + " bytes");
}
long stringCount = getUnsignedInt32(header);
if (stringCount > Integer.MAX_VALUE) {
throw new XmlParserException("Too many strings: " + stringCount);
}
mStringCount = (int) stringCount;
long styleCount = getUnsignedInt32(header);
if (styleCount > Integer.MAX_VALUE) {
throw new XmlParserException("Too many styles: " + styleCount);
}
long flags = getUnsignedInt32(header);
long stringsStartOffset = getUnsignedInt32(header);
long stylesStartOffset = getUnsignedInt32(header);
ByteBuffer contents = chunk.getContents();
if (mStringCount > 0) {
int stringsSectionStartOffsetInContents =
(int) (stringsStartOffset - headerSizeBytes);
int stringsSectionEndOffsetInContents;
if (styleCount > 0) {
// Styles section follows the strings section
if (stylesStartOffset < stringsStartOffset) {
throw new XmlParserException(
"Styles offset (" + stylesStartOffset + ") < strings offset ("
+ stringsStartOffset + ")");
}
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
} else {
stringsSectionEndOffsetInContents = contents.remaining();
}
mStringsSection =
sliceFromTo(
contents,
stringsSectionStartOffsetInContents,
stringsSectionEndOffsetInContents);
} else {
mStringsSection = ByteBuffer.allocate(0);
}
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
mChunkContents = contents;
}
/**
* Returns the string located at the specified {@code 0}-based index in this pool.
*
* @throws XmlParserException if the string does not exist or cannot be decoded
*/
public String getString(long index) throws XmlParserException {
if (index < 0) {
throw new XmlParserException("Unsuported string index: " + index);
} else if (index >= mStringCount) {
throw new XmlParserException(
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
}
int idx = (int) index;
String result = mCachedStrings.get(idx);
if (result != null) {
return result;
}
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
if (offsetInStringsSection >= mStringsSection.capacity()) {
throw new XmlParserException(
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
+ ", max: " + (mStringsSection.capacity() - 1));
}
mStringsSection.position((int) offsetInStringsSection);
result =
(mUtf8Encoded)
? getLengthPrefixedUtf8EncodedString(mStringsSection)
: getLengthPrefixedUtf16EncodedString(mStringsSection);
mCachedStrings.put(idx, result);
return result;
}
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
throws XmlParserException {
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
// of supported values is 0 to 0x7fffffff inclusive.
int lengthChars = getUnsignedInt16(encoded);
if ((lengthChars & 0x8000) != 0) {
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
}
if (lengthChars > Integer.MAX_VALUE / 2) {
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
}
int lengthBytes = lengthChars * 2;
byte[] arr;
int arrOffset;
if (encoded.hasArray()) {
arr = encoded.array();
arrOffset = encoded.arrayOffset() + encoded.position();
encoded.position(encoded.position() + lengthBytes);
} else {
arr = new byte[lengthBytes];
arrOffset = 0;
encoded.get(arr);
}
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
// array of bytes is NULL terminated.
if ((arr[arrOffset + lengthBytes] != 0)
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
}
try {
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-16LE character encoding not supported", e);
}
}
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
throws XmlParserException {
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
// supported values is 0 to 0x7fff inclusive.
// Skip UTF-16 encoded length (in uint16s)
int lengthBytes = getUnsignedInt8(encoded);
if ((lengthBytes & 0x80) != 0) {
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
}
// Read UTF-8 encoded length (in bytes)
lengthBytes = getUnsignedInt8(encoded);
if ((lengthBytes & 0x80) != 0) {
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
}
byte[] arr;
int arrOffset;
if (encoded.hasArray()) {
arr = encoded.array();
arrOffset = encoded.arrayOffset() + encoded.position();
encoded.position(encoded.position() + lengthBytes);
} else {
arr = new byte[lengthBytes];
arrOffset = 0;
encoded.get(arr);
}
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
// of bytes is NULL terminated.
if (arr[arrOffset + lengthBytes] != 0) {
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
}
try {
return new String(arr, arrOffset, lengthBytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 character encoding not supported", e);
}
}
}
/**
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
* map.
*/
private static class ResourceMap {
private final ByteBuffer mChunkContents;
private final int mEntryCount;
/**
* Constructs a new resource map from the provided chunk.
*
* @throws XmlParserException if a parsing error occurred
*/
public ResourceMap(Chunk chunk) throws XmlParserException {
mChunkContents = chunk.getContents().slice();
mChunkContents.order(chunk.getContents().order());
// Each entry of the map is four bytes long, containing the int32 resource ID.
mEntryCount = mChunkContents.remaining() / 4;
}
/**
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
* {@code 0} if the index is out of range.
*/
public int getResourceId(long index) {
if ((index < 0) || (index >= mEntryCount)) {
return 0;
}
int idx = (int) index;
// Each entry of the map is four bytes long, containing the int32 resource ID.
return mChunkContents.getInt(idx * 4);
}
}
/**
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
* buffer's byte order.
*/
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
return sliceFromTo(source, (int) start, (int) end);
}
/**
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
* buffer's byte order.
*/
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
int originalLimit = source.limit();
int originalPosition = source.position();
try {
source.position(0);
source.limit(end);
source.position(start);
ByteBuffer result = source.slice();
result.order(source.order());
return result;
} finally {
source.position(0);
source.limit(originalLimit);
source.position(originalPosition);
}
}
private static int getUnsignedInt8(ByteBuffer buffer) {
return buffer.get() & 0xff;
}
private static int getUnsignedInt16(ByteBuffer buffer) {
return buffer.getShort() & 0xffff;
}
private static long getUnsignedInt32(ByteBuffer buffer) {
return buffer.getInt() & 0xffffffffL;
}
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
return buffer.getInt(position) & 0xffffffffL;
}
/**
* Indicates that an error occurred while parsing a document.
*/
public static class XmlParserException extends Exception {
private static final long serialVersionUID = 1L;
public XmlParserException(String message) {
super(message);
}
public XmlParserException(String message, Throwable cause) {
super(message, cause);
}
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.ApkVerificationIssue;
import java.util.ArrayList;
import java.util.List;
/**
* Base implementation of an APK signature verification result.
*/
public class ApkSigResult {
public final int signatureSchemeVersion;
/** Whether the APK's Signature Scheme signature verifies. */
public boolean verified;
public final List<ApkSignerInfo> mSigners = new ArrayList<>();
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
public ApkSigResult(int signatureSchemeVersion) {
this.signatureSchemeVersion = signatureSchemeVersion;
}
/**
* Returns {@code true} if this result encountered errors during verification.
*/
public boolean containsErrors() {
if (!mErrors.isEmpty()) {
return true;
}
if (!mSigners.isEmpty()) {
for (ApkSignerInfo signer : mSigners) {
if (signer.containsErrors()) {
return true;
}
}
}
return false;
}
/**
* Returns {@code true} if this result encountered warnings during verification.
*/
public boolean containsWarnings() {
if (!mWarnings.isEmpty()) {
return true;
}
if (!mSigners.isEmpty()) {
for (ApkSignerInfo signer : mSigners) {
if (signer.containsWarnings()) {
return true;
}
}
}
return false;
}
/**
* Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
* issueId} and {@code params}.
*/
public void addError(int issueId, Object... parameters) {
mErrors.add(new ApkVerificationIssue(issueId, parameters));
}
/**
* Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
* issueId} and {@code params}.
*/
public void addWarning(int issueId, Object... parameters) {
mWarnings.add(new ApkVerificationIssue(issueId, parameters));
}
/**
* Returns the errors encountered during verification.
*/
public List<? extends ApkVerificationIssue> getErrors() {
return mErrors;
}
/**
* Returns the warnings encountered during verification.
*/
public List<? extends ApkVerificationIssue> getWarnings() {
return mWarnings;
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.ApkVerificationIssue;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
/**
* Base implementation of an APK signer.
*/
public class ApkSignerInfo {
public int index;
public List<X509Certificate> certs = new ArrayList<>();
public List<X509Certificate> certificateLineage = new ArrayList<>();
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
/**
* Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
* issueId} and {@code params}.
*/
public void addError(int issueId, Object... params) {
mErrors.add(new ApkVerificationIssue(issueId, params));
}
/**
* Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
* issueId} and {@code params}.
*/
public void addWarning(int issueId, Object... params) {
mWarnings.add(new ApkVerificationIssue(issueId, params));
}
/**
* Returns {@code true} if any errors were encountered during verification for this signer.
*/
public boolean containsErrors() {
return !mErrors.isEmpty();
}
/**
* Returns {@code true} if any warnings were encountered during verification for this signer.
*/
public boolean containsWarnings() {
return !mWarnings.isEmpty();
}
/**
* Returns the errors encountered during verification for this signer.
*/
public List<? extends ApkVerificationIssue> getErrors() {
return mErrors;
}
/**
* Returns the warnings encountered during verification for this signer.
*/
public List<? extends ApkVerificationIssue> getWarnings() {
return mWarnings;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,393 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtilsLite;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipSections;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
* utility functionality.
*/
public class ApkSigningBlockUtilsLite {
private ApkSigningBlockUtilsLite() {}
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
/**
* Returns the APK Signature Scheme block contained in the provided APK file for the given ID
* and the additional information relevant for verifying the block against the file.
*
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
* block ID.
*
* @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
* @throws IOException if an I/O error occurs while reading the APK
*/
public static SignatureInfo findSignature(
DataSource apk, ZipSections zipSections, int blockId)
throws IOException, SignatureNotFoundException {
// Find the APK Signing Block.
DataSource apkSigningBlock;
long apkSigningBlockOffset;
try {
ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
ApkUtilsLite.findApkSigningBlock(apk, zipSections);
apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
apkSigningBlock = apkSigningBlockInfo.getContents();
} catch (ApkSigningBlockNotFoundException e) {
throw new SignatureNotFoundException(e.getMessage(), e);
}
ByteBuffer apkSigningBlockBuf =
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
// Find the APK Signature Scheme Block inside the APK Signing Block.
ByteBuffer apkSignatureSchemeBlock =
findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
return new SignatureInfo(
apkSignatureSchemeBlock,
apkSigningBlockOffset,
zipSections.getZipCentralDirectoryOffset(),
zipSections.getZipEndOfCentralDirectoryOffset(),
zipSections.getZipEndOfCentralDirectory());
}
public static ByteBuffer findApkSignatureSchemeBlock(
ByteBuffer apkSigningBlock,
int blockId) throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes pairs
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException(
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount
+ " size out of range: " + lenLong);
}
int len = (int) lenLong;
int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount + " size out of range: " + len
+ ", available: " + pairs.remaining());
}
int id = pairs.getInt();
if (id == blockId) {
return getByteBuffer(pairs, len - 4);
}
pairs.position(nextEntryPos);
}
throw new SignatureNotFoundException(
"No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
}
public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
}
}
/**
* Returns the subset of signatures which are expected to be verified by at least one Android
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
* guaranteed to contain at least one signature.
*
* <p>Each Android platform version typically verifies exactly one signature from the provided
* {@code signatures} set. This method returns the set of these signatures collected over all
* requested platform versions. As a result, the result may contain more than one signature.
*
* @throws NoApkSupportedSignaturesException if no supported signatures were
* found for an Android platform version in the range.
*/
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
List<T> signatures, int minSdkVersion, int maxSdkVersion)
throws NoApkSupportedSignaturesException {
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
}
/**
* Returns the subset of signatures which are expected to be verified by at least one Android
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
* guaranteed to contain at least one signature.
*
* <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
* signature within the signing block using the standard JCA.
*
* <p>Each Android platform version typically verifies exactly one signature from the provided
* {@code signatures} set. This method returns the set of these signatures collected over all
* requested platform versions. As a result, the result may contain more than one signature.
*
* @throws NoApkSupportedSignaturesException if no supported signatures were
* found for an Android platform version in the range.
*/
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
List<T> signatures, int minSdkVersion, int maxSdkVersion,
boolean onlyRequireJcaSupport) throws
NoApkSupportedSignaturesException {
// Pick the signature with the strongest algorithm at all required SDK versions, to mimic
// Android's behavior on those versions.
//
// Here we assume that, once introduced, a signature algorithm continues to be supported in
// all future Android versions. We also assume that the better-than relationship between
// algorithms is exactly the same on all Android platform versions (except that older
// platforms might support fewer algorithms). If these assumption are no longer true, the
// logic here will need to change accordingly.
Map<Integer, T>
bestSigAlgorithmOnSdkVersion = new HashMap<>();
int minProvidedSignaturesVersion = Integer.MAX_VALUE;
for (T sig : signatures) {
SignatureAlgorithm sigAlgorithm = sig.algorithm;
int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
: sigAlgorithm.getMinSdkVersion();
if (sigMinSdkVersion > maxSdkVersion) {
continue;
}
if (sigMinSdkVersion < minProvidedSignaturesVersion) {
minProvidedSignaturesVersion = sigMinSdkVersion;
}
T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
if ((candidate == null)
|| (compareSignatureAlgorithm(
sigAlgorithm, candidate.algorithm) > 0)) {
bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
}
}
// Must have some supported signature algorithms for minSdkVersion.
if (minSdkVersion < minProvidedSignaturesVersion) {
throw new NoApkSupportedSignaturesException(
"Minimum provided signature version " + minProvidedSignaturesVersion +
" > minSdkVersion " + minSdkVersion);
}
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
throw new NoApkSupportedSignaturesException("No supported signature");
}
List<T> signaturesToVerify =
new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
Collections.sort(
signaturesToVerify,
(sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
return signaturesToVerify;
}
/**
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
*/
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
}
/**
* Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
* if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
*/
private static int compareContentDigestAlgorithm(
ContentDigestAlgorithm alg1,
ContentDigestAlgorithm alg2) {
switch (alg1) {
case CHUNKED_SHA256:
switch (alg2) {
case CHUNKED_SHA256:
return 0;
case CHUNKED_SHA512:
case VERITY_CHUNKED_SHA256:
return -1;
default:
throw new IllegalArgumentException("Unknown alg2: " + alg2);
}
case CHUNKED_SHA512:
switch (alg2) {
case CHUNKED_SHA256:
case VERITY_CHUNKED_SHA256:
return 1;
case CHUNKED_SHA512:
return 0;
default:
throw new IllegalArgumentException("Unknown alg2: " + alg2);
}
case VERITY_CHUNKED_SHA256:
switch (alg2) {
case CHUNKED_SHA256:
return 1;
case VERITY_CHUNKED_SHA256:
return 0;
case CHUNKED_SHA512:
return -1;
default:
throw new IllegalArgumentException("Unknown alg2: " + alg2);
}
default:
throw new IllegalArgumentException("Unknown alg1: " + alg1);
}
}
/**
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
* buffer's byte order.
*/
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
if (start < 0) {
throw new IllegalArgumentException("start: " + start);
}
if (end < start) {
throw new IllegalArgumentException("end < start: " + end + " < " + start);
}
int capacity = source.capacity();
if (end > source.capacity()) {
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
}
int originalLimit = source.limit();
int originalPosition = source.position();
try {
source.position(0);
source.limit(end);
source.position(start);
ByteBuffer result = source.slice();
result.order(source.order());
return result;
} finally {
source.position(0);
source.limit(originalLimit);
source.position(originalPosition);
}
}
/**
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
* position of this buffer.
*
* <p>This method reads the next {@code size} bytes at this buffer's current position,
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
* {@code size}.
*/
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
if (size < 0) {
throw new IllegalArgumentException("size: " + size);
}
int originalLimit = source.limit();
int position = source.position();
int limit = position + size;
if ((limit < position) || (limit > originalLimit)) {
throw new BufferUnderflowException();
}
source.limit(limit);
try {
ByteBuffer result = source.slice();
result.order(source.order());
source.position(limit);
return result;
} finally {
source.limit(originalLimit);
}
}
public static String toHex(byte[] value) {
StringBuilder sb = new StringBuilder(value.length * 2);
int len = value.length;
for (int i = 0; i < len; i++) {
int hi = (value[i] & 0xff) >>> 4;
int lo = value[i] & 0x0f;
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
}
return sb.toString();
}
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
if (source.remaining() < 4) {
throw new ApkFormatException(
"Remaining buffer too short to contain length of length-prefixed field"
+ ". Remaining: " + source.remaining());
}
int len = source.getInt();
if (len < 0) {
throw new IllegalArgumentException("Negative length");
} else if (len > source.remaining()) {
throw new ApkFormatException(
"Length-prefixed field longer than remaining buffer"
+ ". Field length: " + len + ", remaining: " + source.remaining());
}
return getByteBuffer(source, len);
}
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
int len = buf.getInt();
if (len < 0) {
throw new ApkFormatException("Negative length");
} else if (len > buf.remaining()) {
throw new ApkFormatException(
"Underflow while reading length-prefixed value. Length: " + len
+ ", available: " + buf.remaining());
}
byte[] result = new byte[len];
buf.get(result);
return result;
}
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
List<Pair<Integer, byte[]>> sequence) {
int resultSize = 0;
for (Pair<Integer, byte[]> element : sequence) {
resultSize += 12 + element.getSecond().length;
}
ByteBuffer result = ByteBuffer.allocate(resultSize);
result.order(ByteOrder.LITTLE_ENDIAN);
for (Pair<Integer, byte[]> element : sequence) {
byte[] second = element.getSecond();
result.putInt(8 + second.length);
result.putInt(element.getFirst());
result.putInt(second.length);
result.put(second);
}
return result.array();
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/**
* Base implementation of a supported signature for an APK.
*/
public class ApkSupportedSignature {
public final SignatureAlgorithm algorithm;
public final byte[] signature;
/**
* Constructs a new supported signature using the provided {@code algorithm} and {@code
* signature} bytes.
*/
public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
this.algorithm = algorithm;
this.signature = signature;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/** APK Signature Scheme v2 content digest algorithm. */
public enum ContentDigestAlgorithm {
/** SHA2-256 over 1 MB chunks. */
CHUNKED_SHA256(1, "SHA-256", 256 / 8),
/** SHA2-512 over 1 MB chunks. */
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
/** SHA2-256 over 4 KB chunks for APK verity. */
VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
/** Non-chunk SHA2-256. */
SHA256(4, "SHA-256", 256 / 8);
private final int mId;
private final String mJcaMessageDigestAlgorithm;
private final int mChunkDigestOutputSizeBytes;
private ContentDigestAlgorithm(
int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
mId = id;
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
}
/** Returns the ID of the digest algorithm used on the APK. */
public int getId() {
return mId;
}
/**
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
* chunks by this content digest algorithm.
*/
String getJcaMessageDigestAlgorithm() {
return mJcaMessageDigestAlgorithm;
}
/** Returns the size (in bytes) of the digest of a chunk of content. */
int getChunkDigestOutputSizeBytes() {
return mChunkDigestOutputSizeBytes;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/**
* Base exception that is thrown when there are no signatures that support the full range of
* requested platform versions.
*/
public class NoApkSupportedSignaturesException extends Exception {
public NoApkSupportedSignaturesException(String message) {
super(message);
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.Pair;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
/**
* APK Signing Block signature algorithm.
*/
public enum SignatureAlgorithm {
// TODO reserve the 0x0000 ID to mean null
/**
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
* digested using SHA2-256 in 1 MB chunks.
*/
RSA_PSS_WITH_SHA256(
0x0101,
ContentDigestAlgorithm.CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA/PSS",
new PSSParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
AndroidSdkVersion.N,
AndroidSdkVersion.M),
/**
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
* digested using SHA2-512 in 1 MB chunks.
*/
RSA_PSS_WITH_SHA512(
0x0102,
ContentDigestAlgorithm.CHUNKED_SHA512,
"RSA",
Pair.of(
"SHA512withRSA/PSS",
new PSSParameterSpec(
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
AndroidSdkVersion.N,
AndroidSdkVersion.M),
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
RSA_PKCS1_V1_5_WITH_SHA256(
0x0103,
ContentDigestAlgorithm.CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
RSA_PKCS1_V1_5_WITH_SHA512(
0x0104,
ContentDigestAlgorithm.CHUNKED_SHA512,
"RSA",
Pair.of("SHA512withRSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
ECDSA_WITH_SHA256(
0x0201,
ContentDigestAlgorithm.CHUNKED_SHA256,
"EC",
Pair.of("SHA256withECDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.HONEYCOMB),
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
ECDSA_WITH_SHA512(
0x0202,
ContentDigestAlgorithm.CHUNKED_SHA512,
"EC",
Pair.of("SHA512withECDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.HONEYCOMB),
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
DSA_WITH_SHA256(
0x0301,
ContentDigestAlgorithm.CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/**
* DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
* deterministically according to RFC 6979.
*/
DETDSA_WITH_SHA256(
0x0301,
ContentDigestAlgorithm.CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDetDSA", null),
AndroidSdkVersion.N,
AndroidSdkVersion.INITIAL_RELEASE),
/**
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
* in little endian) construct the final digest.
*/
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
0x0421,
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"RSA",
Pair.of("SHA256withRSA", null),
AndroidSdkVersion.P,
AndroidSdkVersion.INITIAL_RELEASE),
/**
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
* endian) construct the final digest.
*/
VERITY_ECDSA_WITH_SHA256(
0x0423,
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"EC",
Pair.of("SHA256withECDSA", null),
AndroidSdkVersion.P,
AndroidSdkVersion.HONEYCOMB),
/**
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
* endian) construct the final digest.
*/
VERITY_DSA_WITH_SHA256(
0x0425,
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
"DSA",
Pair.of("SHA256withDSA", null),
AndroidSdkVersion.P,
AndroidSdkVersion.INITIAL_RELEASE);
private final int mId;
private final String mJcaKeyAlgorithm;
private final ContentDigestAlgorithm mContentDigestAlgorithm;
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
private final int mMinSdkVersion;
private final int mJcaSigAlgMinSdkVersion;
SignatureAlgorithm(int id,
ContentDigestAlgorithm contentDigestAlgorithm,
String jcaKeyAlgorithm,
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
int minSdkVersion,
int jcaSigAlgMinSdkVersion) {
mId = id;
mContentDigestAlgorithm = contentDigestAlgorithm;
mJcaKeyAlgorithm = jcaKeyAlgorithm;
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
mMinSdkVersion = minSdkVersion;
mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
}
/**
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
*/
public int getId() {
return mId;
}
/**
* Returns the content digest algorithm associated with this signature algorithm.
*/
public ContentDigestAlgorithm getContentDigestAlgorithm() {
return mContentDigestAlgorithm;
}
/**
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
*/
public String getJcaKeyAlgorithm() {
return mJcaKeyAlgorithm;
}
/**
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
* (or null if not needed) to parameterize the {@code Signature}.
*/
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
return mJcaSignatureAlgAndParams;
}
public int getMinSdkVersion() {
return mMinSdkVersion;
}
/**
* Returns the minimum SDK version that supports the JCA signature algorithm.
*/
public int getJcaSigAlgMinSdkVersion() {
return mJcaSigAlgMinSdkVersion;
}
public static SignatureAlgorithm findById(int id) {
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
if (alg.getId() == id) {
return alg;
}
}
return null;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
import java.nio.ByteBuffer;
/**
* APK Signature Scheme block and additional information relevant to verifying the signatures
* contained in the block against the file.
*/
public class SignatureInfo {
/** Contents of APK Signature Scheme block. */
public final ByteBuffer signatureBlock;
/** Position of the APK Signing Block in the file. */
public final long apkSigningBlockOffset;
/** Position of the ZIP Central Directory in the file. */
public final long centralDirOffset;
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
public final long eocdOffset;
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
public final ByteBuffer eocd;
public SignatureInfo(
ByteBuffer signatureBlock,
long apkSigningBlockOffset,
long centralDirOffset,
long eocdOffset,
ByteBuffer eocd) {
this.signatureBlock = signatureBlock;
this.apkSigningBlockOffset = apkSigningBlockOffset;
this.centralDirOffset = centralDirOffset;
this.eocdOffset = eocdOffset;
this.eocd = eocd;
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk;
/**
* Base exception that is thrown when the APK is not signed with the requested signature scheme.
*/
public class SignatureNotFoundException extends Exception {
public SignatureNotFoundException(String message) {
super(message);
}
public SignatureNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
public class SourceStampCertificateLineage {
private final static int FIRST_VERSION = 1;
private final static int CURRENT_VERSION = FIRST_VERSION;
/**
* Deserializes the binary representation of a SourceStampCertificateLineage. Also
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
* parent.
*/
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
throws IOException {
List<SigningCertificateNode> result = new ArrayList<>();
int nodeCount = 0;
if (inputBytes == null || !inputBytes.hasRemaining()) {
return null;
}
ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
}
// FORMAT (little endian):
// * uint32: version code
// * sequence of length-prefixed (uint32): nodes
// * length-prefixed bytes: signed data
// * length-prefixed bytes: certificate
// * uint32: signature algorithm id
// * uint32: flags
// * uint32: signature algorithm id (used by to sign next cert in lineage)
// * length-prefixed bytes: signature over above signed data
X509Certificate lastCert = null;
int lastSigAlgorithmId = 0;
try {
int version = inputBytes.getInt();
if (version != CURRENT_VERSION) {
// we only have one version to worry about right now, so just check it
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+ " different than any of which we are aware");
}
HashSet<X509Certificate> certHistorySet = new HashSet<>();
while (inputBytes.hasRemaining()) {
nodeCount++;
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
int flags = nodeBytes.getInt();
int sigAlgorithmId = nodeBytes.getInt();
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
if (lastCert != null) {
// Use previous level cert to verify current level
String jcaSignatureAlgorithm =
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
PublicKey publicKey = lastCert.getPublicKey();
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
if (!sig.verify(signature)) {
throw new SecurityException("Unable to verify signature of certificate #"
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+ " SourceStampCertificateLineage object");
}
}
signedData.rewind();
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
int signedSigAlgorithm = signedData.getInt();
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ nodeBytes + " when verifying SourceStampCertificateLineage object");
}
lastCert = (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(encodedCert));
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
if (certHistorySet.contains(lastCert)) {
throw new SecurityException("Encountered duplicate entries in "
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
+ "signing certificates should be unique");
}
certHistorySet.add(lastCert);
lastSigAlgorithmId = sigAlgorithmId;
result.add(new SigningCertificateNode(
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
}
} catch(ApkFormatException | BufferUnderflowException e){
throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
} catch(NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e){
throw new SecurityException(
"Failed to verify signature over signed data for certificate #" + nodeCount
+ " when parsing SourceStampCertificateLineage object", e);
} catch(CertificateException e){
throw new SecurityException("Failed to decode certificate #" + nodeCount
+ " when parsing SourceStampCertificateLineage object", e);
}
return result;
}
/**
* Represents one signing certificate in the SourceStampCertificateLineage, which
* generally means it is/was used at some point to sign source stamps.
*/
public static class SigningCertificateNode {
public SigningCertificateNode(
X509Certificate signingCert,
SignatureAlgorithm parentSigAlgorithm,
SignatureAlgorithm sigAlgorithm,
byte[] signature,
int flags) {
this.signingCert = signingCert;
this.parentSigAlgorithm = parentSigAlgorithm;
this.sigAlgorithm = sigAlgorithm;
this.signature = signature;
this.flags = flags;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SigningCertificateNode)) return false;
SigningCertificateNode that = (SigningCertificateNode) o;
if (!signingCert.equals(that.signingCert)) return false;
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
if (sigAlgorithm != that.sigAlgorithm) return false;
if (!Arrays.equals(signature, that.signature)) return false;
if (flags != that.flags) return false;
// we made it
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
result = prime * result +
((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
result = prime * result + Arrays.hashCode(signature);
result = prime * result + flags;
return result;
}
/**
* the signing cert for this node. This is part of the data signed by the parent node.
*/
public final X509Certificate signingCert;
/**
* the algorithm used by this node's parent to bless this data. Its ID value is part of
* the data signed by the parent node. {@code null} for first node.
*/
public final SignatureAlgorithm parentSigAlgorithm;
/**
* the algorithm used by this node to bless the next node's data. Its ID value is part
* of the signed data of the next node. {@code null} for the last node.
*/
public SignatureAlgorithm sigAlgorithm;
/**
* signature over the signed data (above). The signature is from this node's parent
* signing certificate, which should correspond to the signing certificate used to sign an
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
*/
public final byte[] signature;
/**
* the flags detailing how the platform should treat this signing cert
*/
public int flags;
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
/** Constants used for source stamp signing and verification. */
public class SourceStampConstants {
private SourceStampConstants() {}
public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
}

View File

@ -0,0 +1,348 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
import com.android.apksig.ApkVerificationIssue;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSignerInfo;
import com.android.apksig.internal.apk.ApkSupportedSignature;
import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import java.io.ByteArrayInputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Source Stamp verifier.
*
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
*
* <p>The stamp is part of the APK that is protected by the signing block.
*
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
* block.
*/
class SourceStampVerifier {
/** Hidden constructor to prevent instantiation. */
private SourceStampVerifier() {
}
/**
* Parses the SourceStamp block and populates the {@code result}.
*
* <p>This verifies signatures over digest provided.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
* maxSdkVersion]} range.
*/
public static void verifyV1SourceStamp(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
ApkSignerInfo result,
byte[] apkDigest,
byte[] sourceStampCertificateDigest,
int minSdkVersion,
int maxSdkVersion)
throws ApkFormatException, NoSuchAlgorithmException {
X509Certificate sourceStampCertificate =
verifySourceStampCertificate(
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
if (result.containsWarnings() || result.containsErrors()) {
return;
}
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
verifySourceStampSignature(
apkDigest,
minSdkVersion,
maxSdkVersion,
sourceStampCertificate,
apkDigestSignatures,
result);
}
/**
* Parses the SourceStamp block and populates the {@code result}.
*
* <p>This verifies signatures over digest of multiple signature schemes provided.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
* maxSdkVersion]} range.
*/
public static void verifyV2SourceStamp(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
ApkSignerInfo result,
Map<Integer, byte[]> signatureSchemeApkDigests,
byte[] sourceStampCertificateDigest,
int minSdkVersion,
int maxSdkVersion)
throws ApkFormatException, NoSuchAlgorithmException {
X509Certificate sourceStampCertificate =
verifySourceStampCertificate(
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
if (result.containsWarnings() || result.containsErrors()) {
return;
}
// Parse signed signature schemes block.
ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
while (signedSignatureSchemes.hasRemaining()) {
ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
int signatureSchemeId = signedSignatureScheme.getInt();
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
}
for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
signatureSchemeApkDigests.entrySet()) {
if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
return;
}
verifySourceStampSignature(
signatureSchemeApkDigest.getValue(),
minSdkVersion,
maxSdkVersion,
sourceStampCertificate,
signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
result);
if (result.containsWarnings() || result.containsErrors()) {
return;
}
}
if (sourceStampBlockData.hasRemaining()) {
// The stamp block contains some additional attributes.
ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
stampAttributeData.get(stampAttributeBytes);
stampAttributeData.flip();
verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
sourceStampCertificate, stampAttributeDataSignatures, result);
if (result.containsErrors() || result.containsWarnings()) {
return;
}
parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
}
}
private static X509Certificate verifySourceStampCertificate(
ByteBuffer sourceStampBlockData,
CertificateFactory certFactory,
byte[] sourceStampCertificateDigest,
ApkSignerInfo result)
throws NoSuchAlgorithmException, ApkFormatException {
// Parse the SourceStamp certificate.
byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
X509Certificate sourceStampCertificate;
try {
sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(sourceStampEncodedCertificate));
} catch (CertificateException e) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
return null;
}
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
// form. Without this, getEncoded may return a different form from what was stored in
// the signature. This is because some X509Certificate(Factory) implementations
// re-encode certificates.
sourceStampCertificate =
new GuaranteedEncodedFormX509Certificate(
sourceStampCertificate, sourceStampEncodedCertificate);
result.certs.add(sourceStampCertificate);
// Verify the SourceStamp certificate found in the signing block is the same as the
// SourceStamp certificate found in the APK.
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(sourceStampEncodedCertificate);
byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
result.addWarning(
ApkVerificationIssue
.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
toHex(sourceStampBlockCertificateDigest),
toHex(sourceStampCertificateDigest));
return null;
}
return sourceStampCertificate;
}
private static void verifySourceStampSignature(
byte[] data,
int minSdkVersion,
int maxSdkVersion,
X509Certificate sourceStampCertificate,
ByteBuffer signatures,
ApkSignerInfo result) {
// Parse the signatures block and identify supported signatures
int signatureCount = 0;
List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
byte[] sigBytes = readLengthPrefixedByteArray(signature);
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addWarning(
ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
sigAlgorithmId);
continue;
}
supportedSignatures.add(
new ApkSupportedSignature(signatureAlgorithm, sigBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addWarning(
ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
if (supportedSignatures.isEmpty()) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
return;
}
// Verify signatures over digests using the SourceStamp's certificate.
List<ApkSupportedSignature> signaturesToVerify;
try {
signaturesToVerify =
getSignaturesToVerify(
supportedSignatures, minSdkVersion, maxSdkVersion, true);
} catch (NoApkSupportedSignaturesException e) {
// To facilitate debugging capture the signature algorithms and resulting exception in
// the warning.
StringBuilder signatureAlgorithms = new StringBuilder();
for (ApkSupportedSignature supportedSignature : supportedSignatures) {
if (signatureAlgorithms.length() > 0) {
signatureAlgorithms.append(", ");
}
signatureAlgorithms.append(supportedSignature.algorithm);
}
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
signatureAlgorithms.toString(), e);
return;
}
for (ApkSupportedSignature signature : signaturesToVerify) {
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
PublicKey publicKey = sourceStampCertificate.getPublicKey();
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(data);
byte[] sigBytes = signature.signature;
if (!sig.verify(sigBytes)) {
result.addWarning(
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
return;
}
} catch (InvalidKeyException
| InvalidAlgorithmParameterException
| SignatureException
| NoSuchAlgorithmException e) {
result.addWarning(
ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
}
private static void parseStampAttributes(ByteBuffer stampAttributeData,
X509Certificate sourceStampCertificate, ApkSignerInfo result)
throws ApkFormatException {
ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
int stampAttributeCount = 0;
while (stampAttributes.hasRemaining()) {
stampAttributeCount++;
try {
ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
int id = attribute.getInt();
byte[] value = ByteBufferUtils.toByteArray(attribute);
if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
readStampCertificateLineage(value, sourceStampCertificate, result);
} else {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
}
} catch (ApkFormatException | BufferUnderflowException e) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
stampAttributeCount);
return;
}
}
}
private static void readStampCertificateLineage(byte[] lineageBytes,
X509Certificate sourceStampCertificate, ApkSignerInfo result) {
try {
// SourceStampCertificateLineage is verified when built
List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
SourceStampCertificateLineage.readSigningCertificateLineage(
ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
for (int i = 0; i < nodes.size(); i++) {
result.certificateLineage.add(nodes.get(i).signingCert);
}
// Make sure that the last cert in the chain matches this signer cert
if (!sourceStampCertificate.equals(
result.certificateLineage.get(result.certificateLineage.size() - 1))) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
}
} catch (SecurityException e) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
} catch (IllegalArgumentException e) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
} catch (Exception e) {
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
}
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.util.Pair;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* SourceStamp signer.
*
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
*
* <p>The stamp is part of the APK that is protected by the signing block.
*
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
* block.
*
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
*/
public abstract class V1SourceStampSigner {
public static final int V1_SOURCE_STAMP_BLOCK_ID =
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V1SourceStampSigner() {}
public static Pair<byte[], Integer> generateSourceStampBlock(
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
if (sourceStampSignerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
}
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
SourceStampBlock sourceStampBlock = new SourceStampBlock();
try {
sourceStampBlock.stampCertificate =
sourceStampSignerConfig.certificates.get(0).getEncoded();
} catch (CertificateEncodingException e) {
throw new SignatureException(
"Retrieving the encoded form of the stamp certificate failed", e);
}
byte[] digestBytes =
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
sourceStampBlock.signedDigests =
ApkSigningBlockUtils.generateSignaturesOverData(
sourceStampSignerConfig, digestBytes);
// FORMAT:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
byte[] sourceStampSignerBlock =
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
sourceStampBlock.stampCertificate,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
sourceStampBlock.signedDigests),
});
// FORMAT:
// * length-prefixed stamp block.
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
}
private static final class SourceStampBlock {
public byte[] stampCertificate;
public List<Pair<Integer, byte[]>> signedDigests;
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
import com.android.apksig.ApkVerifier;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Source Stamp verifier.
*
* <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
*/
public abstract class V1SourceStampVerifier {
/** Hidden constructor to prevent instantiation. */
private V1SourceStampVerifier() {}
/**
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
* The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
* {@code true}. If verification fails, the result will contain errors -- see {@link
* ApkSigningBlockUtils.Result#getErrors()}.
*
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
* found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
DataSource apk,
ApkUtils.ZipSections zipSections,
byte[] sourceStampCertificateDigest,
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException,
ApkSigningBlockUtils.SignatureNotFoundException {
ApkSigningBlockUtils.Result result =
new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(
apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
verify(
signatureInfo.signatureBlock,
sourceStampCertificateDigest,
apkContentDigests,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
* result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
* more information about the contract of this method.
*/
private static void verify(
ByteBuffer sourceStampBlock,
byte[] sourceStampCertificateDigest,
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result)
throws NoSuchAlgorithmException {
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
result.signers.add(signerInfo);
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
ByteBuffer sourceStampBlockData =
ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
byte[] digestBytes =
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
getApkDigests(apkContentDigests));
SourceStampVerifier.verifyV1SourceStamp(
sourceStampBlockData,
certFactory,
signerInfo,
digestBytes,
sourceStampCertificateDigest,
minSdkVersion,
maxSdkVersion);
result.verified = !result.containsErrors() && !result.containsWarnings();
} catch (CertificateException e) {
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
}
}
private static List<Pair<Integer, byte[]>> getApkDigests(
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
apkContentDigests.entrySet()) {
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
}
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
return digests;
}
}

View File

@ -0,0 +1,219 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.util.Pair;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* SourceStamp signer.
*
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
*
* <p>The stamp is part of the APK that is protected by the signing block.
*
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
* block.
*
* <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
*/
public abstract class V2SourceStampSigner {
public static final int V2_SOURCE_STAMP_BLOCK_ID =
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V2SourceStampSigner() {
}
public static Pair<byte[], Integer> generateSourceStampBlock(
SignerConfig sourceStampSignerConfig,
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
if (sourceStampSignerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
// Extract the digests for signature schemes.
List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
getSignedDigestsFor(
VERSION_APK_SIGNATURE_SCHEME_V3,
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
getSignedDigestsFor(
VERSION_APK_SIGNATURE_SCHEME_V2,
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
getSignedDigestsFor(
VERSION_JAR_SIGNATURE_SCHEME,
signatureSchemeDigestInfos,
sourceStampSignerConfig,
signatureSchemeDigests);
Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
SourceStampBlock sourceStampBlock = new SourceStampBlock();
try {
sourceStampBlock.stampCertificate =
sourceStampSignerConfig.certificates.get(0).getEncoded();
} catch (CertificateEncodingException e) {
throw new SignatureException(
"Retrieving the encoded form of the stamp certificate failed", e);
}
sourceStampBlock.signedDigests = signatureSchemeDigests;
sourceStampBlock.stampAttributes = encodeStampAttributes(
generateStampAttributes(sourceStampSignerConfig.mSigningCertificateLineage));
sourceStampBlock.signedStampAttributes =
ApkSigningBlockUtils.generateSignaturesOverData(sourceStampSignerConfig,
sourceStampBlock.stampAttributes);
// FORMAT:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
// * uint32: signature scheme id
// * length-prefixed bytes: signed digests for the respective signature scheme
// * length-prefixed bytes: encoded stamp attributes
// * length-prefixed sequence of length-prefixed signed stamp attributes:
// * uint32: signature algorithm id
// * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
byte[] sourceStampSignerBlock =
encodeAsSequenceOfLengthPrefixedElements(
new byte[][]{
sourceStampBlock.stampCertificate,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
sourceStampBlock.signedDigests),
sourceStampBlock.stampAttributes,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
sourceStampBlock.signedStampAttributes),
});
// FORMAT:
// * length-prefixed stamp block.
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
}
private static void getSignedDigestsFor(
int signatureSchemeVersion,
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos,
SignerConfig sourceStampSignerConfig,
List<Pair<Integer, byte[]>> signatureSchemeDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (!signatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
return;
}
Map<ContentDigestAlgorithm, byte[]> digestInfo =
signatureSchemeDigestInfos.get(signatureSchemeVersion);
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
}
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
// FORMAT:
// * length-prefixed sequence of length-prefixed digests:
// * uint32: digest algorithm id
// * length-prefixed bytes: digest of the respective digest algorithm
byte[] digestBytes =
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
// FORMAT:
// * length-prefixed sequence of length-prefixed signed digests:
// * uint32: signature algorithm id
// * length-prefixed bytes: signed digest for the respective signature algorithm
List<Pair<Integer, byte[]>> signedDigest =
ApkSigningBlockUtils.generateSignaturesOverData(
sourceStampSignerConfig, digestBytes);
// FORMAT:
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
// * uint32: signature scheme id
// * length-prefixed bytes: signed digests for the respective signature scheme
signatureSchemeDigests.add(
Pair.of(
signatureSchemeVersion,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signedDigest)));
}
private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
int payloadSize = 0;
for (byte[] attributeValue : stampAttributes.values()) {
// Pair size + Attribute ID + Attribute value
payloadSize += 4 + 4 + attributeValue.length;
}
// FORMAT (little endian):
// * length-prefixed bytes: pair
// * uint32: ID
// * bytes: value
ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(payloadSize);
for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
// Pair size
result.putInt(4 + stampAttribute.getValue().length);
result.putInt(stampAttribute.getKey());
result.put(stampAttribute.getValue());
}
return result.array();
}
private static Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
if (lineage != null) {
stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
lineage.encodeSigningCertificateLineage());
}
return stampAttributes;
}
private static final class SourceStampBlock {
public byte[] stampCertificate;
public List<Pair<Integer, byte[]>> signedDigests;
// Optional stamp attributes that are not required for verification.
public byte[] stampAttributes;
public List<Pair<Integer, byte[]>> signedStampAttributes;
}
}

View File

@ -0,0 +1,154 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.stamp;
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
import com.android.apksig.ApkVerificationIssue;
import com.android.apksig.Constants;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigResult;
import com.android.apksig.internal.apk.ApkSignerInfo;
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.SignatureNotFoundException;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipSections;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Source Stamp verifier.
*
* <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
*/
public abstract class V2SourceStampVerifier {
/** Hidden constructor to prevent instantiation. */
private V2SourceStampVerifier() {}
/**
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
* The APK must be considered verified only if {@link ApkSigResult#verified} is
* {@code true}. If verification fails, the result will contain errors -- see {@link
* ApkSigResult#getErrors()}.
*
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws SignatureNotFoundException if no SourceStamp signatures are
* found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigResult verify(
DataSource apk,
ZipSections zipSections,
byte[] sourceStampCertificateDigest,
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
ApkSigResult result =
new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
SignatureInfo signatureInfo =
ApkSigningBlockUtilsLite.findSignature(
apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
verify(
signatureInfo.signatureBlock,
sourceStampCertificateDigest,
signatureSchemeApkContentDigests,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
* result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
* more information about the contract of this method.
*/
private static void verify(
ByteBuffer sourceStampBlock,
byte[] sourceStampCertificateDigest,
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
int minSdkVersion,
int maxSdkVersion,
ApkSigResult result)
throws NoSuchAlgorithmException {
ApkSignerInfo signerInfo = new ApkSignerInfo();
result.mSigners.add(signerInfo);
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
ByteBuffer sourceStampBlockData =
ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
SourceStampVerifier.verifyV2SourceStamp(
sourceStampBlockData,
certFactory,
signerInfo,
getSignatureSchemeDigests(signatureSchemeApkContentDigests),
sourceStampCertificateDigest,
minSdkVersion,
maxSdkVersion);
result.verified = !result.containsErrors() && !result.containsWarnings();
} catch (CertificateException e) {
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
}
}
private static Map<Integer, byte[]> getSignatureSchemeDigests(
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
Map<Integer, byte[]> digests = new HashMap<>();
for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
List<Pair<Integer, byte[]>> apkDigests =
getApkDigests(signatureSchemeApkContentDigest.getValue());
digests.put(
signatureSchemeApkContentDigest.getKey(),
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
}
return digests;
}
private static List<Pair<Integer, byte[]>> getApkDigests(
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
apkContentDigests.entrySet()) {
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
}
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
return digests;
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v1;
import java.util.Comparator;
/**
* Digest algorithm used with JAR signing (aka v1 signing scheme).
*/
public enum DigestAlgorithm {
/** SHA-1 */
SHA1("SHA-1"),
/** SHA2-256 */
SHA256("SHA-256");
private final String mJcaMessageDigestAlgorithm;
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
}
/**
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
* algorithm.
*/
String getJcaMessageDigestAlgorithm() {
return mJcaMessageDigestAlgorithm;
}
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
@Override
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
switch (a1) {
case SHA1:
switch (a2) {
case SHA1:
return 0;
case SHA256:
return -1;
}
throw new RuntimeException("Unsupported algorithm: " + a2);
case SHA256:
switch (a2) {
case SHA1:
return 1;
case SHA256:
return 0;
}
throw new RuntimeException("Unsupported algorithm: " + a2);
default:
throw new RuntimeException("Unsupported algorithm: " + a1);
}
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v1;
/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
public class V1SchemeConstants {
private V1SchemeConstants() {}
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
"X-Android-APK-Signed";
}

View File

@ -0,0 +1,580 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v1;
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.asn1.Asn1EncodingException;
import com.android.apksig.internal.jar.ManifestWriter;
import com.android.apksig.internal.jar.SignatureFileWriter;
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
import com.android.apksig.internal.util.Pair;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* APK signer which uses JAR signing (aka v1 signing scheme).
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
*/
public abstract class V1SchemeSigner {
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
new Attributes.Name("Created-By");
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
/**
* Signer configuration.
*/
public static class SignerConfig {
/** Name. */
public String name;
/** Private key. */
public PrivateKey privateKey;
/**
* Certificates, with the first certificate containing the public key corresponding to
* {@link #privateKey}.
*/
public List<X509Certificate> certificates;
/**
* Digest algorithm used for the signature.
*/
public DigestAlgorithm signatureDigestAlgorithm;
/**
* If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
*/
public boolean deterministicDsaSigning;
}
/** Hidden constructor to prevent instantiation. */
private V1SchemeSigner() {}
/**
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
* AndroidManifest.xml minSdkVersion attribute)
*
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
* JAR signing (aka v1 signature scheme)
*/
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
// Prior to API Level 18, only SHA-1 can be used with RSA.
if (minSdkVersion < 18) {
return DigestAlgorithm.SHA1;
}
return DigestAlgorithm.SHA256;
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// Prior to API Level 21, only SHA-1 can be used with DSA
if (minSdkVersion < 21) {
return DigestAlgorithm.SHA1;
} else {
return DigestAlgorithm.SHA256;
}
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
if (minSdkVersion < 18) {
throw new InvalidKeyException(
"ECDSA signatures only supported for minSdkVersion 18 and higher");
}
return DigestAlgorithm.SHA256;
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
/**
* Returns a safe version of the provided signer name.
*/
public static String getSafeSignerName(String name) {
if (name.isEmpty()) {
throw new IllegalArgumentException("Empty name");
}
// According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
// name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
StringBuilder result = new StringBuilder();
char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
char c = nameCharsUpperCase[i];
if (((c >= 'A') && (c <= 'Z'))
|| ((c >= '0') && (c <= '9'))
|| (c == '-')
|| (c == '_')) {
result.append(c);
} else {
result.append('_');
}
}
return result.toString();
}
/**
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
*/
private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
throws NoSuchAlgorithmException {
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
return MessageDigest.getInstance(jcaAlgorithm);
}
/**
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
* algorithm.
*/
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
return digestAlgorithm.getJcaMessageDigestAlgorithm();
}
/**
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
* manifest.
*/
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
// Entries which represent directories sould not be listed in the manifest.
if (entryName.endsWith("/")) {
return false;
}
// Entries outside of META-INF must be listed in the manifest.
if (!entryName.startsWith("META-INF/")) {
return true;
}
// Entries in subdirectories of META-INF must be listed in the manifest.
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
return true;
}
// Ignored file names (case-insensitive) in META-INF directory:
// MANIFEST.MF
// *.SF
// *.RSA
// *.DSA
// *.EC
// SIG-*
String fileNameLowerCase =
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
if (("manifest.mf".equals(fileNameLowerCase))
|| (fileNameLowerCase.endsWith(".sf"))
|| (fileNameLowerCase.endsWith(".rsa"))
|| (fileNameLowerCase.endsWith(".dsa"))
|| (fileNameLowerCase.endsWith(".ec"))
|| (fileNameLowerCase.startsWith("sig-"))) {
return false;
}
return true;
}
/**
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
* JAR entries which need to be added to the APK as part of the signature.
*
* @param signerConfigs signer configurations, one for each signer. At least one signer config
* must be provided.
*
* @throws ApkFormatException if the source manifest is malformed
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
* missing
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
* cannot be used in general
* @throws SignatureException if an error occurs when computing digests of generating
* signatures
*/
public static List<Pair<String, byte[]>> sign(
List<SignerConfig> signerConfigs,
DigestAlgorithm jarEntryDigestAlgorithm,
Map<String, byte[]> jarEntryDigests,
List<Integer> apkSigningSchemeIds,
byte[] sourceManifestBytes,
String createdBy)
throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
CertificateException, SignatureException {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
OutputManifestFile manifest =
generateManifestFile(
jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
return signManifest(
signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
}
/**
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
* JAR entries which need to be added to the APK as part of the signature.
*
* @param signerConfigs signer configurations, one for each signer. At least one signer config
* must be provided.
*
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
* cannot be used in general
* @throws SignatureException if an error occurs when computing digests of generating
* signatures
*/
public static List<Pair<String, byte[]>> signManifest(
List<SignerConfig> signerConfigs,
DigestAlgorithm digestAlgorithm,
List<Integer> apkSigningSchemeIds,
String createdBy,
OutputManifestFile manifest)
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
SignatureException {
if (signerConfigs.isEmpty()) {
throw new IllegalArgumentException("At least one signer config must be provided");
}
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
List<Pair<String, byte[]>> signatureJarEntries =
new ArrayList<>(2 * signerConfigs.size() + 1);
byte[] sfBytes =
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
for (SignerConfig signerConfig : signerConfigs) {
String signerName = signerConfig.name;
byte[] signatureBlock;
try {
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
} catch (InvalidKeyException e) {
throw new InvalidKeyException(
"Failed to sign using signer \"" + signerName + "\"", e);
} catch (CertificateException e) {
throw new CertificateException(
"Failed to sign using signer \"" + signerName + "\"", e);
} catch (SignatureException e) {
throw new SignatureException(
"Failed to sign using signer \"" + signerName + "\"", e);
}
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
String signatureBlockFileName =
"META-INF/" + signerName + "."
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
signatureJarEntries.add(
Pair.of(signatureBlockFileName, signatureBlock));
}
signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
return signatureJarEntries;
}
/**
* Returns the names of JAR entries which this signer will produce as part of v1 signature.
*/
public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
for (SignerConfig signerConfig : signerConfigs) {
String signerName = signerConfig.name;
result.add("META-INF/" + signerName + ".SF");
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
String signatureBlockFileName =
"META-INF/" + signerName + "."
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
result.add(signatureBlockFileName);
}
result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
return result;
}
/**
* Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
* input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
*/
public static OutputManifestFile generateManifestFile(
DigestAlgorithm jarEntryDigestAlgorithm,
Map<String, byte[]> jarEntryDigests,
byte[] sourceManifestBytes) throws ApkFormatException {
Manifest sourceManifest = null;
if (sourceManifestBytes != null) {
try {
sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
} catch (IOException e) {
throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
}
}
ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
Attributes mainAttrs = new Attributes();
// Copy the main section from the source manifest (if provided). Otherwise use defaults.
// NOTE: We don't output our own Created-By header because this signer did not create the
// JAR/APK being signed -- the signer only adds signatures to the already existing
// JAR/APK.
if (sourceManifest != null) {
mainAttrs.putAll(sourceManifest.getMainAttributes());
} else {
mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
}
try {
ManifestWriter.writeMainSection(manifestOut, mainAttrs);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
}
List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
Collections.sort(sortedEntryNames);
SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
for (String entryName : sortedEntryNames) {
checkEntryNameValid(entryName);
byte[] entryDigest = jarEntryDigests.get(entryName);
Attributes entryAttrs = new Attributes();
entryAttrs.putValue(
entryDigestAttributeName,
Base64.getEncoder().encodeToString(entryDigest));
ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
byte[] sectionBytes;
try {
ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
sectionBytes = sectionOut.toByteArray();
manifestOut.write(sectionBytes);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
}
invidualSectionsContents.put(entryName, sectionBytes);
}
OutputManifestFile result = new OutputManifestFile();
result.contents = manifestOut.toByteArray();
result.mainSectionAttributes = mainAttrs;
result.individualSectionsContents = invidualSectionsContents;
return result;
}
private static void checkEntryNameValid(String name) throws ApkFormatException {
// JAR signing spec says CR, LF, and NUL are not permitted in entry names
// CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
// is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
// issues when parsing using C and C++ like languages.
for (char c : name.toCharArray()) {
if ((c == '\r') || (c == '\n') || (c == 0)) {
throw new ApkFormatException(
String.format(
"Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
(int) c,
name));
}
}
}
public static class OutputManifestFile {
public byte[] contents;
public SortedMap<String, byte[]> individualSectionsContents;
public Attributes mainSectionAttributes;
}
private static byte[] generateSignatureFile(
List<Integer> apkSignatureSchemeIds,
DigestAlgorithm manifestDigestAlgorithm,
String createdBy,
OutputManifestFile manifest) throws NoSuchAlgorithmException {
Manifest sf = new Manifest();
Attributes mainAttrs = sf.getMainAttributes();
mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
if (!apkSignatureSchemeIds.isEmpty()) {
// Add APK Signature Scheme v2 (and newer) signature stripping protection.
// This attribute indicates that this APK is supposed to have been signed using one or
// more APK-specific signature schemes in addition to the standard JAR signature scheme
// used by this code. APK signature verifier should reject the APK if it does not
// contain a signature for the signature scheme the verifier prefers out of this set.
StringBuilder attrValue = new StringBuilder();
for (int id : apkSignatureSchemeIds) {
if (attrValue.length() > 0) {
attrValue.append(", ");
}
attrValue.append(String.valueOf(id));
}
mainAttrs.put(
SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
attrValue.toString());
}
// Add main attribute containing the digest of MANIFEST.MF.
MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
mainAttrs.putValue(
getManifestDigestAttributeName(manifestDigestAlgorithm),
Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
SignatureFileWriter.writeMainSection(out, mainAttrs);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory .SF file", e);
}
String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
for (Map.Entry<String, byte[]> manifestSection
: manifest.individualSectionsContents.entrySet()) {
String sectionName = manifestSection.getKey();
byte[] sectionContents = manifestSection.getValue();
byte[] sectionDigest = md.digest(sectionContents);
Attributes attrs = new Attributes();
attrs.putValue(
entryDigestAttributeName,
Base64.getEncoder().encodeToString(sectionDigest));
try {
SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
} catch (IOException e) {
throw new RuntimeException("Failed to write in-memory .SF file", e);
}
}
// A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
// cause a spurious IOException to be thrown if the length of the signature file is a
// multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
try {
SignatureFileWriter.writeSectionDelimiter(out);
} catch (IOException e) {
throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
}
}
return out.toByteArray();
}
/**
* Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
* signing configuration.
*/
private static byte[] generateSignatureBlock(
SignerConfig signerConfig, byte[] signatureFileBytes)
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
SignatureException {
// Obtain relevant bits of signing configuration
List<X509Certificate> signerCerts = signerConfig.certificates;
X509Certificate signingCert = signerCerts.get(0);
PublicKey publicKey = signingCert.getPublicKey();
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
Pair<String, AlgorithmIdentifier> signatureAlgs =
getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
signerConfig.deterministicDsaSigning);
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
// Generate the cryptographic signature of the signature file
byte[] signatureBytes;
try {
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
signature.initSign(signerConfig.privateKey);
signature.update(signatureFileBytes);
signatureBytes = signature.sign();
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
} catch (SignatureException e) {
throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
}
// Verify the signature against the public key in the signing certificate
try {
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
signature.initVerify(publicKey);
signature.update(signatureFileBytes);
if (!signature.verify(signatureBytes)) {
throw new SignatureException("Signature did not verify");
}
} catch (InvalidKeyException e) {
throw new InvalidKeyException(
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ " public key from certificate",
e);
} catch (SignatureException e) {
throw new SignatureException(
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
+ " public key from certificate",
e);
}
AlgorithmIdentifier digestAlgorithmId =
getSignerInfoDigestAlgorithmOid(digestAlgorithm);
AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
try {
return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
signatureBytes,
null,
signerCerts, digestAlgorithmId,
signatureAlgorithmId);
} catch (Asn1EncodingException | CertificateEncodingException ex) {
throw new SignatureException("Failed to encode signature block");
}
}
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
return "SHA1-Digest";
case SHA256:
return "SHA-256-Digest";
default:
throw new IllegalArgumentException(
"Unexpected content digest algorithm: " + digestAlgorithm);
}
}
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
return "SHA1-Digest-Manifest";
case SHA256:
return "SHA-256-Digest-Manifest";
default:
throw new IllegalArgumentException(
"Unexpected content digest algorithm: " + digestAlgorithm);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v2;
/** Constants used by the V2 Signature Scheme signing and verification. */
public class V2SchemeConstants {
private V2SchemeConstants() {}
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
}

View File

@ -0,0 +1,321 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v2;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* APK Signature Scheme v2 signer.
*
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V2SchemeSigner {
/*
* The two main goals of APK Signature Scheme v2 are:
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
* cover every byte of the APK being signed.
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
* only a minimal amount of APK parsing before the signature is verified, thus completely
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
* employing a hash tree.
*
* The generated signature block is wrapped into an APK Signing Block and inserted into the
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
* extensibility. For example, a future signature scheme could insert its signatures there as
* well. The contract of the APK Signing Block is that all contents outside of the block must be
* protected by signatures inside the block.
*/
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
/** Hidden constructor to prevent instantiation. */
private V2SchemeSigner() {}
/**
* Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
* provided key.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
* AndroidManifest.xml minSdkVersion attribute).
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
* Signature Scheme v2
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
// deterministic signatures which make life easier for OTA updates (fewer files
// changed when deterministic signature schemes are used).
// Pick a digest which is no weaker than the key.
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
if (modulusLengthBits <= 3072) {
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
}
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(
deterministicDsaSigning ?
SignatureAlgorithm.DETDSA_WITH_SHA256 :
SignatureAlgorithm.DSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
// Pick a digest which is no weaker than the key.
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
if (keySizeBits <= 256) {
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
}
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs,
boolean v3SigningEnabled)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
signerConfigs, v3SigningEnabled, null);
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV2Block(
RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs,
boolean v3SigningEnabled,
List<byte[]> preservedV2SignerBlocks)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
generateApkSignatureSchemeV2Block(
digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
preservedV2SignerBlocks),
digestInfo.getSecond());
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
List<SignerConfig> signerConfigs,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
boolean v3SigningEnabled,
List<byte[]> preservedV2SignerBlocks)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
signerBlocks.addAll(preservedV2SignerBlocks);
}
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
} catch (SignatureException e) {
throw new SignatureException("Signer #" + signerNumber + " failed", e);
}
signerBlocks.add(signerBlock);
}
return Pair.of(
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
private static byte[] generateSignerBlock(
SignerConfig signerConfig,
Map<ContentDigestAlgorithm, byte[]> contentDigests,
boolean v3SigningEnabled)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
byte[] encodedPublicKey = encodePublicKey(publicKey);
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
try {
signedData.certificates = encodeCertificates(signerConfig.certificates);
} catch (CertificateEncodingException e) {
throw new SignatureException("Failed to encode certificates", e);
}
List<Pair<Integer, byte[]>> digests =
new ArrayList<>(signerConfig.signatureAlgorithms.size());
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
contentDigestAlgorithm
+ " content digest for "
+ signatureAlgorithm
+ " not computed");
}
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
}
signedData.digests = digests;
signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled);
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
// FORMAT:
// * length-prefixed sequence of length-prefixed digests:
// * uint32: signature algorithm ID
// * length-prefixed bytes: digest of contents
// * length-prefixed sequence of certificates:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
// * length-prefixed sequence of length-prefixed additional attributes:
// * uint32: ID
// * (length - 4) bytes: value
signer.signedData =
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signedData.digests),
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
signedData.additionalAttributes,
new byte[0],
});
signer.publicKey = encodedPublicKey;
signer.signatures = new ArrayList<>();
signer.signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
// FORMAT:
// * length-prefixed signed data
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
return encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
signer.signedData,
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signer.signatures),
signer.publicKey,
});
}
private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
if (v3SigningEnabled) {
// FORMAT (little endian):
// * length-prefixed bytes: attribute pair
// * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
// * uint32: value - 3 (v3 signature scheme id) in this case
int payloadSize = 4 + 4 + 4;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(payloadSize - 4);
result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
return result.array();
} else {
return new byte[0];
}
}
private static final class V2SignatureSchemeBlock {
private static final class Signer {
public byte[] signedData;
public List<Pair<Integer, byte[]>> signatures;
public byte[] publicKey;
}
private static final class SignedData {
public List<Pair<Integer, byte[]>> digests;
public List<byte[]> certificates;
public byte[] additionalAttributes;
}
}
}

View File

@ -0,0 +1,466 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v2;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* APK Signature Scheme v2 verifier.
*
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V2SchemeVerifier {
/** Hidden constructor to prevent instantiation. */
private V2SchemeVerifier() {}
/**
* Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
* verification. The APK must be considered verified only if
* {@link ApkSigningBlockUtils.Result#verified} is
* {@code true}. If verification fails, the result will contain errors -- see
* {@link ApkSigningBlockUtils.Result#getErrors()}.
*
* <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
* If the APK's signature is expected to not verify on any of the specified platform versions,
* this method returns a result with one or more errors and whose
* {@code Result.verified == false}, or this method throws an exception.
*
* @throws ApkFormatException if the APK is malformed
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2
* signatures are found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
RunnablesExecutor executor,
DataSource apk,
ApkUtils.ZipSections zipSections,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundSigSchemeIds,
int minSdkVersion,
int maxSdkVersion)
throws IOException, ApkFormatException, NoSuchAlgorithmException,
ApkSigningBlockUtils.SignatureNotFoundException {
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
apk.slice(
signatureInfo.centralDirOffset,
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
ByteBuffer eocd = signatureInfo.eocd;
verify(executor,
beforeApkSigningBlock,
signatureInfo.signatureBlock,
centralDir,
eocd,
supportedApkSigSchemeNames,
foundSigSchemeIds,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's v2 signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
* Set, int, int)} for more information about the contract of this method.
*
* @param result result populated by this method with interesting information about the APK,
* such as information about signers, and verification errors and warnings.
*/
private static void verify(
RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
ByteBuffer apkSignatureSchemeV2Block,
DataSource centralDir,
ByteBuffer eocd,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundSigSchemeIds,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result)
throws IOException, NoSuchAlgorithmException {
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
parseSigners(
apkSignatureSchemeV2Block,
contentDigestsToVerify,
supportedApkSigSchemeNames,
foundSigSchemeIds,
minSdkVersion,
maxSdkVersion,
result);
if (result.containsErrors()) {
return;
}
ApkSigningBlockUtils.verifyIntegrity(
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
if (!result.containsErrors()) {
result.verified = true;
}
}
/**
* Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding
* {@code signerInfos} of the provided {@code result}.
*
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
* However, this does not verify the integrity of the rest of the APK but rather simply reports
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
public static void parseSigners(
ByteBuffer apkSignatureSchemeV2Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
ByteBuffer signers;
try {
signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
} catch (ApkFormatException e) {
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
return;
}
if (!signers.hasRemaining()) {
result.addError(Issue.V2_SIG_NO_SIGNERS);
return;
}
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
}
int signerCount = 0;
while (signers.hasRemaining()) {
int signerIndex = signerCount;
signerCount++;
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
signerInfo.index = signerIndex;
result.signers.add(signerInfo);
try {
ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
parseSigner(
signer,
certFactory,
signerInfo,
contentDigestsToVerify,
supportedApkSigSchemeNames,
foundApkSigSchemeIds,
minSdkVersion,
maxSdkVersion);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
return;
}
}
}
/**
* Parses the provided signer block and populates the {@code result}.
*
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
* integrity of the APK.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
private static void parseSigner(
ByteBuffer signerBlock,
CertificateFactory certFactory,
ApkSigningBlockUtils.Result.SignerInfo result,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
Map<Integer, String> supportedApkSigSchemeNames,
Set<Integer> foundApkSigSchemeIds,
int minSdkVersion,
int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
signedData.flip();
result.signedData = signedDataBytes;
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
// Parse the signatures block and identify supported signatures
int signatureCount = 0;
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
result.signatures.add(
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
sigAlgorithmId, sigBytes));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
continue;
}
supportedSignatures.add(
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
if (result.signatures.isEmpty()) {
result.addError(Issue.V2_SIG_NO_SIGNATURES);
return;
}
// Verify signatures over signed-data block using the public key
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
try {
signaturesToVerify =
ApkSigningBlockUtils.getSignaturesToVerify(
supportedSignatures, minSdkVersion, maxSdkVersion);
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
return;
}
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
PublicKey publicKey;
try {
publicKey =
KeyFactory.getInstance(keyAlgorithm).generatePublic(
new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
return;
}
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
signedData.position(0);
sig.update(signedData);
byte[] sigBytes = signature.signature;
if (!sig.verify(sigBytes)) {
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
return;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
// At least one signature over signedData has verified. We can now parse signed-data.
signedData.position(0);
ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
// Parse the certificates block
int certificateIndex = -1;
while (certificates.hasRemaining()) {
certificateIndex++;
byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
} catch (CertificateException e) {
result.addError(
Issue.V2_SIG_MALFORMED_CERTIFICATE,
certificateIndex,
certificateIndex + 1,
e);
return;
}
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
// form. Without this, getEncoded may return a different form from what was stored in
// the signature. This is because some X509Certificate(Factory) implementations
// re-encode certificates.
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
result.certs.add(certificate);
}
if (result.certs.isEmpty()) {
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
return;
}
X509Certificate mainCertificate = result.certs.get(0);
byte[] certificatePublicKeyBytes;
try {
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
mainCertificate.getPublicKey());
} catch (InvalidKeyException e) {
System.out.println("Caught an exception encoding the public key: " + e);
e.printStackTrace();
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
}
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
result.addError(
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
ApkSigningBlockUtils.toHex(publicKeyBytes));
return;
}
// Parse the digests block
int digestCount = 0;
while (digests.hasRemaining()) {
digestCount++;
try {
ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
int sigAlgorithmId = digest.getInt();
byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
result.contentDigests.add(
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
sigAlgorithmId, digestBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
return;
}
}
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
}
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
}
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
result.addError(
Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
sigAlgsFromSignaturesRecord,
sigAlgsFromDigestsRecord);
return;
}
// Parse the additional attributes block.
int additionalAttributeCount = 0;
Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
while (additionalAttributes.hasRemaining()) {
additionalAttributeCount++;
try {
ByteBuffer attribute =
ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes);
int id = attribute.getInt();
byte[] value = ByteBufferUtils.toByteArray(attribute);
result.additionalAttributes.add(
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
switch (id) {
case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
// stripping protection added when signing with a newer scheme
int foundId = ByteBuffer.wrap(value).order(
ByteOrder.LITTLE_ENDIAN).getInt();
if (supportedApkSigSchemeIds.contains(foundId)) {
supportedExpectedApkSigSchemeIds.add(foundId);
} else {
result.addWarning(
Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
}
break;
default:
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
}
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
return;
}
}
// make sure that all known IDs indicated in stripping protection have already verified
for (int id : supportedExpectedApkSigSchemeIds) {
if (!foundApkSigSchemeIds.contains(id)) {
String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
result.addError(
Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
result.index,
apkSigSchemeName);
}
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v3;
/** Constants used by the V3 Signature Scheme signing and verification. */
public class V3SchemeConstants {
private V3SchemeConstants() {}
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
}

View File

@ -0,0 +1,332 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* APK Signature Scheme v3 signer.
*
* <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
* Signature Scheme v2 goals.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
* <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
* SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
* it can prove the new siging certificate was signed by the old.
*/
public abstract class V3SchemeSigner {
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
/** Hidden constructor to prevent instantiation. */
private V3SchemeSigner() {}
/**
* Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
* provided key.
*
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
* AndroidManifest.xml minSdkVersion attribute).
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
* Signature Scheme v3
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = signingKey.getAlgorithm();
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
// deterministic signatures which make life easier for OTA updates (fewer files
// changed when deterministic signature schemes are used).
// Pick a digest which is no weaker than the key.
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
if (modulusLengthBits <= 3072) {
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
}
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
// DSA is supported only with SHA-256.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(
deterministicDsaSigning ?
SignatureAlgorithm.DETDSA_WITH_SHA256 :
SignatureAlgorithm.DSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
}
return algorithms;
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
// Pick a digest which is no weaker than the key.
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
if (keySizeBits <= 256) {
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
List<SignatureAlgorithm> algorithms = new ArrayList<>();
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
if (verityEnabled) {
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
}
return algorithms;
} else {
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
// digest being the weak link. SHA-512 is the next strongest supported digest.
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
}
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
generateApkSignatureSchemeV3Block(
RunnablesExecutor executor,
DataSource beforeCentralDir,
DataSource centralDir,
DataSource eocd,
List<SignerConfig> signerConfigs)
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
SignatureException {
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
ApkSigningBlockUtils.computeContentDigests(
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()),
digestInfo.getSecond());
}
public static byte[] generateV3SignerAttribute(
SigningCertificateLineage signingCertificateLineage) {
// FORMAT (little endian):
// * length-prefixed bytes: attribute pair
// * uint32: ID
// * bytes: value - encoded V3 SigningCertificateLineage
byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
int payloadSize = 4 + 4 + encodedLineage.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(4 + encodedLineage.length);
result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
result.put(encodedLineage);
return result.array();
}
private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
// FORMAT:
// * length-prefixed sequence of length-prefixed signer blocks.
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
int signerNumber = 0;
for (SignerConfig signerConfig : signerConfigs) {
signerNumber++;
byte[] signerBlock;
try {
signerBlock = generateSignerBlock(signerConfig, contentDigests);
} catch (InvalidKeyException e) {
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
} catch (SignatureException e) {
throw new SignatureException("Signer #" + signerNumber + " failed", e);
}
signerBlocks.add(signerBlock);
}
return Pair.of(
encodeAsSequenceOfLengthPrefixedElements(
new byte[][] {
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
}),
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
private static byte[] generateSignerBlock(
SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
byte[] encodedPublicKey = encodePublicKey(publicKey);
V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
try {
signedData.certificates = encodeCertificates(signerConfig.certificates);
} catch (CertificateEncodingException e) {
throw new SignatureException("Failed to encode certificates", e);
}
List<Pair<Integer, byte[]>> digests =
new ArrayList<>(signerConfig.signatureAlgorithms.size());
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
if (contentDigest == null) {
throw new RuntimeException(
contentDigestAlgorithm
+ " content digest for "
+ signatureAlgorithm
+ " not computed");
}
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
}
signedData.digests = digests;
signedData.minSdkVersion = signerConfig.minSdkVersion;
signedData.maxSdkVersion = signerConfig.maxSdkVersion;
signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
signer.signedData = encodeSignedData(signedData);
signer.minSdkVersion = signerConfig.minSdkVersion;
signer.maxSdkVersion = signerConfig.maxSdkVersion;
signer.publicKey = encodedPublicKey;
signer.signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
return encodeSigner(signer);
}
private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
byte[] signatures =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signer.signatures));
byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey);
// FORMAT:
// * length-prefixed signed data
// * uint32: minSdkVersion
// * uint32: maxSdkVersion
// * length-prefixed sequence of length-prefixed signatures:
// * uint32: signature algorithm ID
// * length-prefixed bytes: signature of signed data
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(signedData);
result.putInt(signer.minSdkVersion);
result.putInt(signer.maxSdkVersion);
result.put(signatures);
result.put(publicKey);
return result.array();
}
private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
byte[] digests =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
signedData.digests));
byte[] certs =
encodeAsLengthPrefixedElement(
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
// FORMAT:
// * length-prefixed sequence of length-prefixed digests:
// * uint32: signature algorithm ID
// * length-prefixed bytes: digest of contents
// * length-prefixed sequence of certificates:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
// * uint-32: minSdkVersion
// * uint-32: maxSdkVersion
// * length-prefixed sequence of length-prefixed additional attributes:
// * uint32: ID
// * (length - 4) bytes: value
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
// * length-prefixed roof-of-rotation structure
int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(digests);
result.put(certs);
result.putInt(signedData.minSdkVersion);
result.putInt(signedData.maxSdkVersion);
result.put(attributes);
return result.array();
}
private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
if (signerConfig.mSigningCertificateLineage == null) {
return new byte[0];
}
return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage);
}
private static final class V3SignatureSchemeBlock {
private static final class Signer {
public byte[] signedData;
public int minSdkVersion;
public int maxSdkVersion;
public List<Pair<Integer, byte[]>> signatures;
public byte[] publicKey;
}
private static final class SignedData {
public List<Pair<Integer, byte[]>> digests;
public List<byte[]> certificates;
public int minSdkVersion;
public int maxSdkVersion;
public byte[] additionalAttributes;
}
}
}

View File

@ -0,0 +1,522 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.SigningCertificateLineage;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.util.ByteBufferUtils;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.RunnablesExecutor;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* APK Signature Scheme v3 verifier.
*
* <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
* single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
* uncompressed contents of ZIP entries.
*
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
*/
public abstract class V3SchemeVerifier {
/** Hidden constructor to prevent instantiation. */
private V3SchemeVerifier() {}
/**
* Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
* verification. The APK must be considered verified only if
* {@link ApkSigningBlockUtils.Result#verified} is
* {@code true}. If verification fails, the result will contain errors -- see
* {@link ApkSigningBlockUtils.Result#getErrors()}.
*
* <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
* If the APK's signature is expected to not verify on any of the specified platform versions,
* this method returns a result with one or more errors and whose
* {@code Result.verified == false}, or this method throws an exception.
*
* @throws ApkFormatException if the APK is malformed
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
* required cryptographic algorithm implementation is missing
* @throws SignatureNotFoundException if no APK Signature Scheme v3
* signatures are found
* @throws IOException if an I/O error occurs when reading the APK
*/
public static ApkSigningBlockUtils.Result verify(
RunnablesExecutor executor,
DataSource apk,
ApkUtils.ZipSections zipSections,
int minSdkVersion,
int maxSdkVersion)
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
apk.slice(
signatureInfo.centralDirOffset,
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
ByteBuffer eocd = signatureInfo.eocd;
// v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported
// platforms
if (minSdkVersion < AndroidSdkVersion.P) {
minSdkVersion = AndroidSdkVersion.P;
}
verify(executor,
beforeApkSigningBlock,
signatureInfo.signatureBlock,
centralDir,
eocd,
minSdkVersion,
maxSdkVersion,
result);
return result;
}
/**
* Verifies the provided APK's v3 signatures and outputs the results into the provided
* {@code result}. APK is considered verified only if there are no errors reported in the
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
* int)} for more information about the contract of this method.
*
* @param result result populated by this method with interesting information about the APK,
* such as information about signers, and verification errors and warnings.
*/
private static void verify(
RunnablesExecutor executor,
DataSource beforeApkSigningBlock,
ByteBuffer apkSignatureSchemeV3Block,
DataSource centralDir,
ByteBuffer eocd,
int minSdkVersion,
int maxSdkVersion,
ApkSigningBlockUtils.Result result)
throws IOException, NoSuchAlgorithmException {
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result);
if (result.containsErrors()) {
return;
}
ApkSigningBlockUtils.verifyIntegrity(
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
// longest SigningCertificateHistory, if present, corresponds to the newest platform
// versions
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) {
sortedSigners.put(signer.minSdkVersion, signer);
}
// first make sure there is neither overlap nor holes
int firstMin = 0;
int lastMax = 0;
int lastLineageSize = 0;
// while we're iterating through the signers, build up the list of lineages
List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size());
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
int currentMin = signer.minSdkVersion;
int currentMax = signer.maxSdkVersion;
if (firstMin == 0) {
// first round sets up our basis
firstMin = currentMin;
} else {
if (currentMin != lastMax + 1) {
result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
break;
}
}
lastMax = currentMax;
// also, while we're here, make sure that the lineage sizes only increase
if (signer.signingCertificateLineage != null) {
int currLineageSize = signer.signingCertificateLineage.size();
if (currLineageSize < lastLineageSize) {
result.addError(Issue.V3_INCONSISTENT_LINEAGES);
break;
}
lastLineageSize = currLineageSize;
lineages.add(signer.signingCertificateLineage);
}
}
// make sure we support our desired sdk ranges
if (firstMin > minSdkVersion || lastMax < maxSdkVersion) {
result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
}
try {
result.signingCertificateLineage =
SigningCertificateLineage.consolidateLineages(lineages);
} catch (IllegalArgumentException e) {
result.addError(Issue.V3_INCONSISTENT_LINEAGES);
}
if (!result.containsErrors()) {
result.verified = true;
}
}
/**
* Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
* {@code signerInfos} of the provided {@code result}.
*
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
* However, this does not verify the integrity of the rest of the APK but rather simply reports
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
public static void parseSigners(
ByteBuffer apkSignatureSchemeV3Block,
Set<ContentDigestAlgorithm> contentDigestsToVerify,
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
ByteBuffer signers;
try {
signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block);
} catch (ApkFormatException e) {
result.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
return;
}
if (!signers.hasRemaining()) {
result.addError(Issue.V3_SIG_NO_SIGNERS);
return;
}
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
}
int signerCount = 0;
while (signers.hasRemaining()) {
int signerIndex = signerCount;
signerCount++;
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
new ApkSigningBlockUtils.Result.SignerInfo();
signerInfo.index = signerIndex;
result.signers.add(signerInfo);
try {
ByteBuffer signer = getLengthPrefixedSlice(signers);
parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
} catch (ApkFormatException | BufferUnderflowException e) {
signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
return;
}
}
}
/**
* Parses the provided signer block and populates the {@code result}.
*
* <p>This verifies signatures over {@code signed-data} contained in this block, as well as
* the data contained therein, but does not verify the integrity of the rest of the APK. To
* facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
* These digests can then be used to verify the integrity of the APK.
*
* <p>This method adds one or more errors to the {@code result} if a verification error is
* expected to be encountered on an Android platform version in the
* {@code [minSdkVersion, maxSdkVersion]} range.
*/
private static void parseSigner(
ByteBuffer signerBlock,
CertificateFactory certFactory,
ApkSigningBlockUtils.Result.SignerInfo result,
Set<ContentDigestAlgorithm> contentDigestsToVerify)
throws ApkFormatException, NoSuchAlgorithmException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
byte[] signedDataBytes = new byte[signedData.remaining()];
signedData.get(signedDataBytes);
signedData.flip();
result.signedData = signedDataBytes;
int parsedMinSdkVersion = signerBlock.getInt();
int parsedMaxSdkVersion = signerBlock.getInt();
result.minSdkVersion = parsedMinSdkVersion;
result.maxSdkVersion = parsedMaxSdkVersion;
if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
result.addError(
Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
}
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
// Parse the signatures block and identify supported signatures
int signatureCount = 0;
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = getLengthPrefixedSlice(signatures);
int sigAlgorithmId = signature.getInt();
byte[] sigBytes = readLengthPrefixedByteArray(signature);
result.signatures.add(
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
sigAlgorithmId, sigBytes));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
continue;
}
// TODO consider dropping deprecated signatures for v3 or modifying
// getSignaturesToVerify (called below)
supportedSignatures.add(
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
return;
}
}
if (result.signatures.isEmpty()) {
result.addError(Issue.V3_SIG_NO_SIGNATURES);
return;
}
// Verify signatures over signed-data block using the public key
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
try {
signaturesToVerify =
ApkSigningBlockUtils.getSignaturesToVerify(
supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
return;
}
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
PublicKey publicKey;
try {
publicKey =
KeyFactory.getInstance(keyAlgorithm).generatePublic(
new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
return;
}
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
signedData.position(0);
sig.update(signedData);
byte[] sigBytes = signature.signature;
if (!sig.verify(sigBytes)) {
result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
return;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return;
}
}
// At least one signature over signedData has verified. We can now parse signed-data.
signedData.position(0);
ByteBuffer digests = getLengthPrefixedSlice(signedData);
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
int signedMinSdkVersion = signedData.getInt();
if (signedMinSdkVersion != parsedMinSdkVersion) {
result.addError(
Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
parsedMinSdkVersion,
signedMinSdkVersion);
}
int signedMaxSdkVersion = signedData.getInt();
if (signedMaxSdkVersion != parsedMaxSdkVersion) {
result.addError(
Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
parsedMaxSdkVersion,
signedMaxSdkVersion);
}
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
// Parse the certificates block
int certificateIndex = -1;
while (certificates.hasRemaining()) {
certificateIndex++;
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
} catch (CertificateException e) {
result.addError(
Issue.V3_SIG_MALFORMED_CERTIFICATE,
certificateIndex,
certificateIndex + 1,
e);
return;
}
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
// form. Without this, getEncoded may return a different form from what was stored in
// the signature. This is because some X509Certificate(Factory) implementations
// re-encode certificates.
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
result.certs.add(certificate);
}
if (result.certs.isEmpty()) {
result.addError(Issue.V3_SIG_NO_CERTIFICATES);
return;
}
X509Certificate mainCertificate = result.certs.get(0);
byte[] certificatePublicKeyBytes;
try {
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(mainCertificate.getPublicKey());
} catch (InvalidKeyException e) {
System.out.println("Caught an exception encoding the public key: " + e);
e.printStackTrace();
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
}
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
result.addError(
Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
ApkSigningBlockUtils.toHex(publicKeyBytes));
return;
}
// Parse the digests block
int digestCount = 0;
while (digests.hasRemaining()) {
digestCount++;
try {
ByteBuffer digest = getLengthPrefixedSlice(digests);
int sigAlgorithmId = digest.getInt();
byte[] digestBytes = readLengthPrefixedByteArray(digest);
result.contentDigests.add(
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
sigAlgorithmId, digestBytes));
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
return;
}
}
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
}
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
}
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
result.addError(
Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
sigAlgsFromSignaturesRecord,
sigAlgsFromDigestsRecord);
return;
}
// Parse the additional attributes block.
int additionalAttributeCount = 0;
while (additionalAttributes.hasRemaining()) {
additionalAttributeCount++;
try {
ByteBuffer attribute =
getLengthPrefixedSlice(additionalAttributes);
int id = attribute.getInt();
byte[] value = ByteBufferUtils.toByteArray(attribute);
result.additionalAttributes.add(
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
try {
// SigningCertificateLineage is verified when built
result.signingCertificateLineage =
SigningCertificateLineage.readFromV3AttributeValue(value);
// make sure that the last cert in the chain matches this signer cert
SigningCertificateLineage subLineage =
result.signingCertificateLineage.getSubLineage(result.certs.get(0));
if (result.signingCertificateLineage.size() != subLineage.size()) {
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
}
} catch (SecurityException e) {
result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
} catch (IllegalArgumentException e) {
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
} catch (Exception e) {
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
}
} else {
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
}
} catch (ApkFormatException | BufferUnderflowException e) {
result.addError(
Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
return;
}
}
}
}

View File

@ -0,0 +1,314 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v3;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
/**
* APK Signer Lineage.
*
* <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
* the validity of its descendant. Each additional descendant represents a new identity that can be
* used to sign an APK, and each generation has accompanying attributes which represent how the
* APK would like to view the older signing certificates, specifically how they should be trusted in
* certain situations.
*
* <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
* the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
* Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
* allow upgrades to the new certificate.
*
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
*/
public class V3SigningCertificateLineage {
private final static int FIRST_VERSION = 1;
private final static int CURRENT_VERSION = FIRST_VERSION;
/**
* Deserializes the binary representation of an {@link V3SigningCertificateLineage}. Also
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
* parent.
*/
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
throws IOException {
List<SigningCertificateNode> result = new ArrayList<>();
int nodeCount = 0;
if (inputBytes == null || !inputBytes.hasRemaining()) {
return null;
}
ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes);
// FORMAT (little endian):
// * uint32: version code
// * sequence of length-prefixed (uint32): nodes
// * length-prefixed bytes: signed data
// * length-prefixed bytes: certificate
// * uint32: signature algorithm id
// * uint32: flags
// * uint32: signature algorithm id (used by to sign next cert in lineage)
// * length-prefixed bytes: signature over above signed data
X509Certificate lastCert = null;
int lastSigAlgorithmId = 0;
try {
int version = inputBytes.getInt();
if (version != CURRENT_VERSION) {
// we only have one version to worry about right now, so just check it
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
+ " different than any of which we are aware");
}
HashSet<X509Certificate> certHistorySet = new HashSet<>();
while (inputBytes.hasRemaining()) {
nodeCount++;
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
int flags = nodeBytes.getInt();
int sigAlgorithmId = nodeBytes.getInt();
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
if (lastCert != null) {
// Use previous level cert to verify current level
String jcaSignatureAlgorithm =
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
PublicKey publicKey = lastCert.getPublicKey();
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
if (!sig.verify(signature)) {
throw new SecurityException("Unable to verify signature of certificate #"
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
+ " V3SigningCertificateLineage object");
}
}
signedData.rewind();
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
int signedSigAlgorithm = signedData.getInt();
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ nodeBytes + " when verifying V3SigningCertificateLineage object");
}
lastCert = X509CertificateUtils.generateCertificate(encodedCert);
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
if (certHistorySet.contains(lastCert)) {
throw new SecurityException("Encountered duplicate entries in "
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
+ "signing certificates should be unique");
}
certHistorySet.add(lastCert);
lastSigAlgorithmId = sigAlgorithmId;
result.add(new SigningCertificateNode(
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
}
} catch(ApkFormatException | BufferUnderflowException e){
throw new IOException("Failed to parse V3SigningCertificateLineage object", e);
} catch(NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e){
throw new SecurityException(
"Failed to verify signature over signed data for certificate #" + nodeCount
+ " when parsing V3SigningCertificateLineage object", e);
} catch(CertificateException e){
throw new SecurityException("Failed to decode certificate #" + nodeCount
+ " when parsing V3SigningCertificateLineage object", e);
}
return result;
}
/**
* encode the in-memory representation of this {@code V3SigningCertificateLineage}
*/
public static byte[] encodeSigningCertificateLineage(
List<SigningCertificateNode> signingCertificateLineage) {
// FORMAT (little endian):
// * version code
// * sequence of length-prefixed (uint32): nodes
// * length-prefixed bytes: signed data
// * length-prefixed bytes: certificate
// * uint32: signature algorithm id
// * uint32: flags
// * uint32: signature algorithm id (used by to sign next cert in lineage)
List<byte[]> nodes = new ArrayList<>();
for (SigningCertificateNode node : signingCertificateLineage) {
nodes.add(encodeSigningCertificateNode(node));
}
byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes);
// add the version code (uint32) on top of the encoded nodes
int payloadSize = 4 + encodedSigningCertificateLineage.length;
ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize);
encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN);
encodedWithVersion.putInt(CURRENT_VERSION);
encodedWithVersion.put(encodedSigningCertificateLineage);
return encodedWithVersion.array();
}
public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) {
// FORMAT (little endian):
// * length-prefixed bytes: signed data
// * length-prefixed bytes: certificate
// * uint32: signature algorithm id
// * uint32: flags
// * uint32: signature algorithm id (used by to sign next cert in lineage)
// * length-prefixed bytes: signature over signed data
int parentSigAlgorithmId = 0;
if (node.parentSigAlgorithm != null) {
parentSigAlgorithmId = node.parentSigAlgorithm.getId();
}
int sigAlgorithmId = 0;
if (node.sigAlgorithm != null) {
sigAlgorithmId = node.sigAlgorithm.getId();
}
byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId);
byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature);
int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(prefixedSignedData);
result.putInt(node.flags);
result.putInt(sigAlgorithmId);
result.put(prefixedSignature);
return result.array();
}
public static byte[] encodeSignedData(X509Certificate certificate, int flags) {
try {
byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded());
int payloadSize = 4 + prefixedCertificate.length;
ByteBuffer result = ByteBuffer.allocate(payloadSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.put(prefixedCertificate);
result.putInt(flags);
return encodeAsLengthPrefixedElement(result.array());
} catch (CertificateEncodingException e) {
throw new RuntimeException(
"Failed to encode V3SigningCertificateLineage certificate", e);
}
}
/**
* Represents one signing certificate in the {@link V3SigningCertificateLineage}, which
* generally means it is/was used at some point to sign the same APK of the others in the
* lineage.
*/
public static class SigningCertificateNode {
public SigningCertificateNode(
X509Certificate signingCert,
SignatureAlgorithm parentSigAlgorithm,
SignatureAlgorithm sigAlgorithm,
byte[] signature,
int flags) {
this.signingCert = signingCert;
this.parentSigAlgorithm = parentSigAlgorithm;
this.sigAlgorithm = sigAlgorithm;
this.signature = signature;
this.flags = flags;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SigningCertificateNode)) return false;
SigningCertificateNode that = (SigningCertificateNode) o;
if (!signingCert.equals(that.signingCert)) return false;
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
if (sigAlgorithm != that.sigAlgorithm) return false;
if (!Arrays.equals(signature, that.signature)) return false;
if (flags != that.flags) return false;
// we made it
return true;
}
@Override
public int hashCode() {
int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
result = 31 * result + Arrays.hashCode(signature);
return result;
}
/**
* the signing cert for this node. This is part of the data signed by the parent node.
*/
public final X509Certificate signingCert;
/**
* the algorithm used by the this node's parent to bless this data. Its ID value is part of
* the data signed by the parent node. {@code null} for first node.
*/
public final SignatureAlgorithm parentSigAlgorithm;
/**
* the algorithm used by the this nodeto bless the next node's data. Its ID value is part
* of the signed data of the next node. {@code null} for the last node.
*/
public SignatureAlgorithm sigAlgorithm;
/**
* signature over the signed data (above). The signature is from this node's parent
* signing certificate, which should correspond to the signing certificate used to sign an
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
*/
public final byte[] signature;
/**
* the flags detailing how the platform should treat this signing cert
*/
public int flags;
}
}

View File

@ -0,0 +1,357 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.apk.SignatureInfo;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.util.Pair;
import com.android.apksig.util.DataSource;
import com.android.apksig.zip.ZipFormatException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
* installation. And optional verity tree - has to be present during session commit.
* <p>
* The fields:
* <p>
* 1. hashingInfo - verity root hash and hashing info,
* 2. signingInfo - certificate, public key and signature,
* For more details see V4Signature.
* </p>
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
* <p>
* TODO(schfan): Add v4 unit tests
*/
public abstract class V4SchemeSigner {
/**
* Hidden constructor to prevent instantiation.
*/
private V4SchemeSigner() {
}
/**
* Based on a public key, return a signing algorithm that supports verity.
*/
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
int minSdkVersion, boolean apkSigningBlockPaddingSupported,
boolean deterministicDsaSigning)
throws InvalidKeyException {
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
signingKey, minSdkVersion,
apkSigningBlockPaddingSupported, deterministicDsaSigning);
// Keeping only supported algorithms.
for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
final SignatureAlgorithm algorithm = iter.next();
if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
iter.remove();
}
}
return algorithms;
}
/**
* Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
* output file.
*/
public static void generateV4Signature(
DataSource apkContent, SignerConfig signerConfig, File outputFile)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
try (final OutputStream output = new FileOutputStream(outputFile)) {
pair.getFirst().writeTo(output);
V4Signature.writeBytes(output, pair.getSecond());
} catch (IOException e) {
outputFile.delete();
throw e;
}
}
/** Generate v4 signature and hash tree for a given APK. */
public static Pair<V4Signature, byte[]> generateV4Signature(
DataSource apkContent,
SignerConfig signerConfig)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
// Salt has to stay empty for fs-verity compatibility.
final byte[] salt = null;
// Not used by apksigner.
final byte[] additionalData = null;
final long fileSize = apkContent.size();
// Obtaining first supported digest from v2/v3 blocks (SHA256 or SHA512).
final byte[] apkDigest = getApkDigest(apkContent);
// Obtaining the merkle tree and the root hash in verity format.
ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
final ContentDigestAlgorithm verityContentDigestAlgorithm =
verityContentDigestInfo.contentDigestAlgorithm;
final byte[] rootHash = verityContentDigestInfo.rootHash;
final byte[] tree = verityContentDigestInfo.tree;
final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
verityContentDigestAlgorithm);
final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
salt, rootHash);
// Generating SigningInfo and combining everything into V4Signature.
final V4Signature signature;
try {
signature = generateSignature(signerConfig, hashingInfo, apkDigest, additionalData,
fileSize);
} catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
throw new InvalidKeyException("Signer failed", e);
}
return Pair.of(signature, tree);
}
private static V4Signature generateSignature(
SignerConfig signerConfig,
V4Signature.HashingInfo hashingInfo,
byte[] apkDigest, byte[] additionaData, long fileSize)
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
CertificateEncodingException {
if (signerConfig.certificates.isEmpty()) {
throw new SignatureException("No certificates configured for signer");
}
if (signerConfig.certificates.size() != 1) {
throw new CertificateEncodingException("Should only have one certificate");
}
// Collecting data for signing.
final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
final byte[] encodedCertificate = encodedCertificates.get(0);
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
encodedCertificate, additionaData, publicKey.getEncoded(), -1, null);
final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo,
signingInfoNoSignature);
// Signing.
final List<Pair<Integer, byte[]>> signatures =
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
if (signatures.size() != 1) {
throw new SignatureException("Should only be one signature generated");
}
final int signatureAlgorithmId = signatures.get(0).getFirst();
final byte[] signature = signatures.get(0).getSecond();
final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest,
encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId,
signature);
return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
signingInfo.toByteArray());
}
// Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type.
private static byte[] getApkDigest(DataSource apk) throws IOException {
ApkUtils.ZipSections zipSections;
try {
zipSections = ApkUtils.findZipSections(apk);
} catch (ZipFormatException e) {
throw new IOException("Malformed APK: not a ZIP archive", e);
}
final SignatureException v3Exception;
try {
return getBestV3Digest(apk, zipSections);
} catch (SignatureException e) {
v3Exception = e;
}
final SignatureException v2Exception;
try {
return getBestV2Digest(apk, zipSections);
} catch (SignatureException e) {
v2Exception = e;
}
throw new IOException(
"Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
+ v2Exception);
}
private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections)
throws SignatureException {
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
try {
final SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result);
final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
result);
} catch (Exception e) {
throw new SignatureException("Failed to extract and parse v3 block", e);
}
if (result.signers.size() != 1) {
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
}
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
if (signer.containsErrors()) {
throw new SignatureException("Parsing failed: " + signer.getErrors());
}
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
result.signers.get(0).contentDigests;
return pickBestDigest(contentDigests);
}
private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
throws SignatureException {
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
try {
final SignatureInfo signatureInfo =
ApkSigningBlockUtils.findSignature(apk, zipSections,
APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
V2SchemeVerifier.parseSigners(
apkSignatureSchemeV2Block,
contentDigestsToVerify,
Collections.emptyMap(),
foundApkSigSchemeIds,
Integer.MAX_VALUE,
Integer.MAX_VALUE,
result);
} catch (Exception e) {
throw new SignatureException("Failed to extract and parse v2 block", e);
}
if (result.signers.size() != 1) {
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
}
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
if (signer.containsErrors()) {
throw new SignatureException("Parsing failed: " + signer.getErrors());
}
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
signer.contentDigests;
return pickBestDigest(contentDigests);
}
private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
if (contentDigests == null || contentDigests.isEmpty()) {
throw new SignatureException("Should have at least one digest");
}
int bestAlgorithmOrder = -1;
byte[] bestDigest = null;
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
final SignatureAlgorithm signatureAlgorithm =
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
final ContentDigestAlgorithm contentDigestAlgorithm =
signatureAlgorithm.getContentDigestAlgorithm();
if (!isSupported(contentDigestAlgorithm, true)) {
continue;
}
final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
if (bestAlgorithmOrder < algorithmOrder) {
bestAlgorithmOrder = algorithmOrder;
bestDigest = contentDigest.getValue();
}
}
if (bestDigest == null) {
throw new SignatureException("Failed to find a supported digest in the source APK");
}
return bestDigest;
}
// Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest
// verification in framework works.
public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
switch (contentDigestAlgorithm) {
case CHUNKED_SHA256:
return 0;
case VERITY_CHUNKED_SHA256:
return 1;
case CHUNKED_SHA512:
return 2;
default:
return -1;
}
}
private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
boolean forV3Digest) {
if (contentDigestAlgorithm == null) {
return false;
}
if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
|| contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
|| (forV3Digest
&& contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
return true;
}
return false;
}
private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
throws NoSuchAlgorithmException {
switch (algorithm) {
case VERITY_CHUNKED_SHA256:
return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
default:
throw new NoSuchAlgorithmException(
"Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
}
}
}

View File

@ -0,0 +1,250 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v4;
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
import com.android.apksig.ApkVerifier;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
import com.android.apksig.internal.apk.SignatureAlgorithm;
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
import com.android.apksig.internal.util.X509CertificateUtils;
import com.android.apksig.util.DataSource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
/**
* APK Signature Scheme V4 verifier.
* <p>
* Verifies the serialized V4Signature file against an APK.
*/
public abstract class V4SchemeVerifier {
/**
* Hidden constructor to prevent instantiation.
*/
private V4SchemeVerifier() {
}
/**
* <p>
* The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
* signature block against the raw root hash bytes in the proto field 3) verifies that the raw
* root hash matches with the actual hash tree root of the give APK 4) if the file contains a
* verity tree, verifies that it matches with the actual verity tree computed from the given
* APK.
* </p>
*/
public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
throws IOException, NoSuchAlgorithmException {
final V4Signature signature;
final byte[] tree;
try (InputStream input = new FileInputStream(v4SignatureFile)) {
signature = V4Signature.readFrom(input);
tree = V4Signature.readBytes(input);
}
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
if (signature == null) {
result.addError(Issue.V4_SIG_NO_SIGNATURES,
"Signature file does not contain a v4 signature.");
return result;
}
if (signature.version != V4Signature.CURRENT_VERSION) {
result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
V4Signature.CURRENT_VERSION);
}
V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
signature.hashingInfo);
V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
signature.signingInfo);
final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo);
// First, verify the signature over signedData.
ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock(
signingInfo, signedData);
result.signers.add(signerInfo);
if (result.containsErrors()) {
return result;
}
// Second, check if the root hash and the tree are correct.
verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
if (!result.containsErrors()) {
result.verified = true;
}
return result;
}
/**
* Parses the provided signature block and populates the {@code result}.
* <p>
* This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
* contained in the signature block. This method adds one or more errors to the {@code result}.
*/
private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
V4Signature.SigningInfo signingInfo,
final byte[] signedData) throws NoSuchAlgorithmException {
final ApkSigningBlockUtils.Result.SignerInfo result =
new ApkSigningBlockUtils.Result.SignerInfo();
result.index = 0;
final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
final byte[] sigBytes = signingInfo.signature;
result.signatures.add(
new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
if (signatureAlgorithm == null) {
result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
return result;
}
String jcaSignatureAlgorithm =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
final byte[] publicKeyBytes = signingInfo.publicKey;
PublicKey publicKey;
try {
publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
new X509EncodedKeySpec(publicKeyBytes));
} catch (Exception e) {
result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
return result;
}
try {
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
if (!sig.verify(sigBytes)) {
result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
return result;
}
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
} catch (InvalidKeyException | InvalidAlgorithmParameterException
| SignatureException e) {
result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
return result;
}
if (signingInfo.certificate == null) {
result.addError(Issue.V4_SIG_NO_CERTIFICATE);
return result;
}
final X509Certificate certificate;
try {
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
// form. Without this, getEncoded may return a different form from what was stored in
// the signature. This is because some X509Certificate(Factory) implementations
// re-encode certificates.
certificate = new GuaranteedEncodedFormX509Certificate(
X509CertificateUtils.generateCertificate(signingInfo.certificate),
signingInfo.certificate);
} catch (CertificateException e) {
result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
return result;
}
result.certs.add(certificate);
byte[] certificatePublicKeyBytes;
try {
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
certificate.getPublicKey());
} catch (InvalidKeyException e) {
System.out.println("Caught an exception encoding the public key: " + e);
e.printStackTrace();
certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
}
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
result.addError(
Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
ApkSigningBlockUtils.toHex(publicKeyBytes));
return result;
}
// Add apk digest from the file to the result.
ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
result.contentDigests.add(contentDigest);
return result;
}
private static void verifyRootHashAndTree(DataSource apkContent,
ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
final byte[] actualDigest = actualContentDigestInfo.rootHash;
final byte[] actualTree = actualContentDigestInfo.tree;
if (!Arrays.equals(expectedDigest, actualDigest)) {
signerInfo.addError(
ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
algorithm,
toHex(expectedDigest),
toHex(actualDigest));
return;
}
// Only check verity tree if it is not empty
if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
signerInfo.addError(
ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
algorithm,
toHex(expectedDigest),
toHex(actualDigest));
return;
}
signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.apk.v4;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class V4Signature {
public static final int CURRENT_VERSION = 2;
public static final int HASHING_ALGORITHM_SHA256 = 1;
public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
public static class HashingInfo {
public final int hashAlgorithm; // only 1 == SHA256 supported
public final byte log2BlockSize; // only 12 (block size 4096) supported now
public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
public final byte[] rawRootHash; // salted digest of the first Merkle tree page
HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
this.hashAlgorithm = hashAlgorithm;
this.log2BlockSize = log2BlockSize;
this.salt = salt;
this.rawRootHash = rawRootHash;
}
static HashingInfo fromByteArray(byte[] bytes) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
final int hashAlgorithm = buffer.getInt();
final byte log2BlockSize = buffer.get();
byte[] salt = readBytes(buffer);
byte[] rawRootHash = readBytes(buffer);
return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
}
byte[] toByteArray() {
final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
+ bytesSize(this.rawRootHash);
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(this.hashAlgorithm);
buffer.put(this.log2BlockSize);
writeBytes(buffer, this.salt);
writeBytes(buffer, this.rawRootHash);
return buffer.array();
}
}
public static class SigningInfo {
public final byte[] apkDigest; // used to match with the corresponding APK
public final byte[] certificate; // ASN.1 DER form
public final byte[] additionalData; // a free-form binary data blob
public final byte[] publicKey; // ASN.1 DER, must match the certificate
public final int signatureAlgorithmId; // see the APK v2 doc for the list
public final byte[] signature;
SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
this.apkDigest = apkDigest;
this.certificate = certificate;
this.additionalData = additionalData;
this.publicKey = publicKey;
this.signatureAlgorithmId = signatureAlgorithmId;
this.signature = signature;
}
static SigningInfo fromByteArray(byte[] bytes) throws IOException {
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
byte[] apkDigest = readBytes(buffer);
byte[] certificate = readBytes(buffer);
byte[] additionalData = readBytes(buffer);
byte[] publicKey = readBytes(buffer);
int signatureAlgorithmId = buffer.getInt();
byte[] signature = readBytes(buffer);
return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
signatureAlgorithmId, signature);
}
byte[] toByteArray() {
final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
+ bytesSize(this.signature);
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
writeBytes(buffer, this.apkDigest);
writeBytes(buffer, this.certificate);
writeBytes(buffer, this.additionalData);
writeBytes(buffer, this.publicKey);
buffer.putInt(this.signatureAlgorithmId);
writeBytes(buffer, this.signature);
return buffer.array();
}
}
public final int version; // Always 2 for now.
public final byte[] hashingInfo;
public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later.
V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) {
this.version = version;
this.hashingInfo = hashingInfo;
this.signingInfo = signingInfo;
}
static V4Signature readFrom(InputStream stream) throws IOException {
final int version = readIntLE(stream);
if (version != CURRENT_VERSION) {
throw new IOException("Invalid signature version.");
}
final byte[] hashingInfo = readBytes(stream);
final byte[] signingInfo = readBytes(stream);
return new V4Signature(version, hashingInfo, signingInfo);
}
public void writeTo(OutputStream stream) throws IOException {
writeIntLE(stream, this.version);
writeBytes(stream, this.hashingInfo);
writeBytes(stream, this.signingInfo);
}
static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
final int size =
4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
signingInfo.additionalData);
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(size);
buffer.putLong(fileSize);
buffer.putInt(hashingInfo.hashAlgorithm);
buffer.put(hashingInfo.log2BlockSize);
writeBytes(buffer, hashingInfo.salt);
writeBytes(buffer, hashingInfo.rawRootHash);
writeBytes(buffer, signingInfo.apkDigest);
writeBytes(buffer, signingInfo.certificate);
writeBytes(buffer, signingInfo.additionalData);
return buffer.array();
}
// Utility methods.
static int bytesSize(byte[] bytes) {
return 4/*length*/ + (bytes == null ? 0 : bytes.length);
}
static void readFully(InputStream stream, byte[] buffer) throws IOException {
int len = buffer.length;
int n = 0;
while (n < len) {
int count = stream.read(buffer, n, len - n);
if (count < 0) {
throw new EOFException();
}
n += count;
}
}
static int readIntLE(InputStream stream) throws IOException {
final byte[] buffer = new byte[4];
readFully(stream, buffer);
return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
}
static void writeIntLE(OutputStream stream, int v) throws IOException {
final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
stream.write(buffer);
}
static byte[] readBytes(InputStream stream) throws IOException {
try {
final int size = readIntLE(stream);
final byte[] bytes = new byte[size];
readFully(stream, bytes);
return bytes;
} catch (EOFException ignored) {
return null;
}
}
static byte[] readBytes(ByteBuffer buffer) throws IOException {
if (buffer.remaining() < 4) {
throw new EOFException();
}
final int size = buffer.getInt();
if (buffer.remaining() < size) {
throw new EOFException();
}
final byte[] bytes = new byte[size];
buffer.get(bytes);
return bytes;
}
static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
if (bytes == null) {
writeIntLE(stream, 0);
return;
}
writeIntLE(stream, bytes.length);
stream.write(bytes);
}
static void writeBytes(ByteBuffer buffer, byte[] bytes) {
if (bytes == null) {
buffer.putInt(0);
return;
}
buffer.putInt(bytes.length);
buffer.put(bytes);
}
}

View File

@ -0,0 +1,673 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
import com.android.apksig.internal.asn1.ber.BerDataValue;
import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
import com.android.apksig.internal.asn1.ber.BerDataValueReader;
import com.android.apksig.internal.asn1.ber.BerEncoding;
import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
import com.android.apksig.internal.util.ByteBufferUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Parser of ASN.1 BER-encoded structures.
*
* <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class},
* containing fields annotated with {@link Asn1Field}.
*/
public final class Asn1BerParser {
private Asn1BerParser() {}
/**
* Returns the ASN.1 structure contained in the BER encoded input.
*
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
* is advanced to the first position following the end of the consumed structure.
* @param containerClass class describing the structure of the input. The class must meet the
* following requirements:
* <ul>
* <li>The class must be annotated with {@link Asn1Class}.</li>
* <li>The class must expose a public no-arg constructor.</li>
* <li>Member fields of the class which are populated with parsed input must be
* annotated with {@link Asn1Field} and be public and non-final.</li>
* </ul>
*
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
* object
*/
public static <T> T parse(ByteBuffer encoded, Class<T> containerClass)
throws Asn1DecodingException {
BerDataValue containerDataValue;
try {
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Failed to decode top-level data value", e);
}
if (containerDataValue == null) {
throw new Asn1DecodingException("Empty input");
}
return parse(containerDataValue, containerClass);
}
/**
* Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
* that this method does not care whether the tag number of this data structure is
* {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
*
* <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1
* SET may contain duplicate elements.
*
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
* is advanced to the first position following the end of the consumed structure.
* @param elementClass class describing the structure of the values/elements contained in this
* container. The class must meet the following requirements:
* <ul>
* <li>The class must be annotated with {@link Asn1Class}.</li>
* <li>The class must expose a public no-arg constructor.</li>
* <li>Member fields of the class which are populated with parsed input must be
* annotated with {@link Asn1Field} and be public and non-final.</li>
* </ul>
*
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
* object
*/
public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
throws Asn1DecodingException {
BerDataValue containerDataValue;
try {
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Failed to decode top-level data value", e);
}
if (containerDataValue == null) {
throw new Asn1DecodingException("Empty input");
}
return parseSetOf(containerDataValue, elementClass);
}
private static <T> T parse(BerDataValue container, Class<T> containerClass)
throws Asn1DecodingException {
if (container == null) {
throw new NullPointerException("container == null");
}
if (containerClass == null) {
throw new NullPointerException("containerClass == null");
}
Asn1Type dataType = getContainerAsn1Type(containerClass);
switch (dataType) {
case CHOICE:
return parseChoice(container, containerClass);
case SEQUENCE:
{
int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL;
int expectedTagNumber = BerEncoding.getTagNumber(dataType);
if ((container.getTagClass() != expectedTagClass)
|| (container.getTagNumber() != expectedTagNumber)) {
throw new Asn1UnexpectedTagException(
"Unexpected data value read as " + containerClass.getName()
+ ". Expected " + BerEncoding.tagClassAndNumberToString(
expectedTagClass, expectedTagNumber)
+ ", but read: " + BerEncoding.tagClassAndNumberToString(
container.getTagClass(), container.getTagNumber()));
}
return parseSequence(container, containerClass);
}
case UNENCODED_CONTAINER:
return parseSequence(container, containerClass, true);
default:
throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
}
}
private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass)
throws Asn1DecodingException {
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
if (fields.isEmpty()) {
throw new Asn1DecodingException(
"No fields annotated with " + Asn1Field.class.getName()
+ " in CHOICE class " + containerClass.getName());
}
// Check that class + tagNumber don't clash between the choices
for (int i = 0; i < fields.size() - 1; i++) {
AnnotatedField f1 = fields.get(i);
int tagNumber1 = f1.getBerTagNumber();
int tagClass1 = f1.getBerTagClass();
for (int j = i + 1; j < fields.size(); j++) {
AnnotatedField f2 = fields.get(j);
int tagNumber2 = f2.getBerTagNumber();
int tagClass2 = f2.getBerTagClass();
if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) {
throw new Asn1DecodingException(
"CHOICE fields are indistinguishable because they have the same tag"
+ " class and number: " + containerClass.getName()
+ "." + f1.getField().getName()
+ " and ." + f2.getField().getName());
}
}
}
// Instantiate the container object / result
T obj;
try {
obj = containerClass.getConstructor().newInstance();
} catch (IllegalArgumentException | ReflectiveOperationException e) {
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
}
// Set the matching field's value from the data value
for (AnnotatedField field : fields) {
try {
field.setValueFrom(dataValue, obj);
return obj;
} catch (Asn1UnexpectedTagException expected) {
// not a match
}
}
throw new Asn1DecodingException(
"No options of CHOICE " + containerClass.getName() + " matched");
}
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
throws Asn1DecodingException {
return parseSequence(container, containerClass, false);
}
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
boolean isUnencodedContainer) throws Asn1DecodingException {
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
Collections.sort(
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
// Check that there are no fields with the same index
if (fields.size() > 1) {
AnnotatedField lastField = null;
for (AnnotatedField field : fields) {
if ((lastField != null)
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
throw new Asn1DecodingException(
"Fields have the same index: " + containerClass.getName()
+ "." + lastField.getField().getName()
+ " and ." + field.getField().getName());
}
lastField = field;
}
}
// Instantiate the container object / result
T t;
try {
t = containerClass.getConstructor().newInstance();
} catch (IllegalArgumentException | ReflectiveOperationException e) {
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
}
// Parse fields one by one. A complication is that there may be optional fields.
int nextUnreadFieldIndex = 0;
BerDataValueReader elementsReader = container.contentsReader();
while (nextUnreadFieldIndex < fields.size()) {
BerDataValue dataValue;
try {
// if this is the first field of an unencoded container then the entire contents of
// the container should be used when assigning to this field.
if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
dataValue = container;
} else {
dataValue = elementsReader.readDataValue();
}
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Malformed data value", e);
}
if (dataValue == null) {
break;
}
for (int i = nextUnreadFieldIndex; i < fields.size(); i++) {
AnnotatedField field = fields.get(i);
try {
if (field.isOptional()) {
// Optional field -- might not be present and we may thus be trying to set
// it from the wrong tag.
try {
field.setValueFrom(dataValue, t);
nextUnreadFieldIndex = i + 1;
break;
} catch (Asn1UnexpectedTagException e) {
// This field is not present, attempt to use this data value for the
// next / iteration of the loop
continue;
}
} else {
// Mandatory field -- if we can't set its value from this data value, then
// it's an error
field.setValueFrom(dataValue, t);
nextUnreadFieldIndex = i + 1;
break;
}
} catch (Asn1DecodingException e) {
throw new Asn1DecodingException(
"Failed to parse " + containerClass.getName()
+ "." + field.getField().getName(),
e);
}
}
}
return t;
}
// NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness
// of elements -- it's an unordered collection.
@SuppressWarnings("unchecked")
private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass)
throws Asn1DecodingException {
List<T> result = new ArrayList<>();
BerDataValueReader elementsReader = container.contentsReader();
while (true) {
BerDataValue dataValue;
try {
dataValue = elementsReader.readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException("Malformed data value", e);
}
if (dataValue == null) {
break;
}
T element;
if (ByteBuffer.class.equals(elementClass)) {
element = (T) dataValue.getEncodedContents();
} else if (Asn1OpaqueObject.class.equals(elementClass)) {
element = (T) new Asn1OpaqueObject(dataValue.getEncoded());
} else {
element = parse(dataValue, elementClass);
}
result.add(element);
}
return result;
}
private static Asn1Type getContainerAsn1Type(Class<?> containerClass)
throws Asn1DecodingException {
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
if (containerAnnotation == null) {
throw new Asn1DecodingException(
containerClass.getName() + " is not annotated with "
+ Asn1Class.class.getName());
}
switch (containerAnnotation.type()) {
case CHOICE:
case SEQUENCE:
case UNENCODED_CONTAINER:
return containerAnnotation.type();
default:
throw new Asn1DecodingException(
"Unsupported ASN.1 container annotation type: "
+ containerAnnotation.type());
}
}
private static Class<?> getElementType(Field field)
throws Asn1DecodingException, ClassNotFoundException {
String type = field.getGenericType().getTypeName();
int delimiterIndex = type.indexOf('<');
if (delimiterIndex == -1) {
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
}
int startIndex = delimiterIndex + 1;
int endIndex = type.indexOf('>', startIndex);
// TODO: handle comma?
if (endIndex == -1) {
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
}
String elementClassName = type.substring(startIndex, endIndex);
return Class.forName(elementClassName);
}
private static final class AnnotatedField {
private final Field mField;
private final Asn1Field mAnnotation;
private final Asn1Type mDataType;
private final Asn1TagClass mTagClass;
private final int mBerTagClass;
private final int mBerTagNumber;
private final Asn1Tagging mTagging;
private final boolean mOptional;
public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException {
mField = field;
mAnnotation = annotation;
mDataType = annotation.type();
Asn1TagClass tagClass = annotation.cls();
if (tagClass == Asn1TagClass.AUTOMATIC) {
if (annotation.tagNumber() != -1) {
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
} else {
tagClass = Asn1TagClass.UNIVERSAL;
}
}
mTagClass = tagClass;
mBerTagClass = BerEncoding.getTagClass(mTagClass);
int tagNumber;
if (annotation.tagNumber() != -1) {
tagNumber = annotation.tagNumber();
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
tagNumber = -1;
} else {
tagNumber = BerEncoding.getTagNumber(mDataType);
}
mBerTagNumber = tagNumber;
mTagging = annotation.tagging();
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
&& (annotation.tagNumber() == -1)) {
throw new Asn1DecodingException(
"Tag number must be specified when tagging mode is " + mTagging);
}
mOptional = annotation.optional();
}
public Field getField() {
return mField;
}
public Asn1Field getAnnotation() {
return mAnnotation;
}
public boolean isOptional() {
return mOptional;
}
public int getBerTagClass() {
return mBerTagClass;
}
public int getBerTagNumber() {
return mBerTagNumber;
}
public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException {
int readTagClass = dataValue.getTagClass();
if (mBerTagNumber != -1) {
int readTagNumber = dataValue.getTagNumber();
if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) {
throw new Asn1UnexpectedTagException(
"Tag mismatch. Expected: "
+ BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber)
+ ", but found "
+ BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber));
}
} else {
if (readTagClass != mBerTagClass) {
throw new Asn1UnexpectedTagException(
"Tag mismatch. Expected class: "
+ BerEncoding.tagClassToString(mBerTagClass)
+ ", but found "
+ BerEncoding.tagClassToString(readTagClass));
}
}
if (mTagging == Asn1Tagging.EXPLICIT) {
try {
dataValue = dataValue.contentsReader().readDataValue();
} catch (BerDataValueFormatException e) {
throw new Asn1DecodingException(
"Failed to read contents of EXPLICIT data value", e);
}
}
BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue);
}
}
private static class Asn1UnexpectedTagException extends Asn1DecodingException {
private static final long serialVersionUID = 1L;
public Asn1UnexpectedTagException(String message) {
super(message);
}
}
private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException {
if (!encodedOid.hasRemaining()) {
throw new Asn1DecodingException("Empty OBJECT IDENTIFIER");
}
// First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2
long firstComponent = decodeBase128UnsignedLong(encodedOid);
int firstNode = (int) Math.min(firstComponent / 40, 2);
long secondNode = firstComponent - firstNode * 40;
StringBuilder result = new StringBuilder();
result.append(Long.toString(firstNode)).append('.')
.append(Long.toString(secondNode));
// Each consecutive node is encoded as a separate component
while (encodedOid.hasRemaining()) {
long node = decodeBase128UnsignedLong(encodedOid);
result.append('.').append(Long.toString(node));
}
return result.toString();
}
private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException {
if (!encoded.hasRemaining()) {
return 0;
}
long result = 0;
while (encoded.hasRemaining()) {
if (result > Long.MAX_VALUE >>> 7) {
throw new Asn1DecodingException("Base-128 number too large");
}
int b = encoded.get() & 0xff;
result <<= 7;
result |= b & 0x7f;
if ((b & 0x80) == 0) {
return result;
}
}
throw new Asn1DecodingException(
"Truncated base-128 encoded input: missing terminating byte, with highest bit not"
+ " set");
}
private static BigInteger integerToBigInteger(ByteBuffer encoded) {
if (!encoded.hasRemaining()) {
return BigInteger.ZERO;
}
return new BigInteger(ByteBufferUtils.toByteArray(encoded));
}
private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
|| value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
}
return value.intValue();
}
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
BigInteger value = integerToBigInteger(encoded);
if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
|| value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
throw new Asn1DecodingException(
String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
}
return value.longValue();
}
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
throws Asn1DecodingException {
Field[] declaredFields = containerClass.getDeclaredFields();
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
for (Field field : declaredFields) {
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
if (annotation == null) {
continue;
}
if (Modifier.isStatic(field.getModifiers())) {
throw new Asn1DecodingException(
Asn1Field.class.getName() + " used on a static field: "
+ containerClass.getName() + "." + field.getName());
}
AnnotatedField annotatedField;
try {
annotatedField = new AnnotatedField(field, annotation);
} catch (Asn1DecodingException e) {
throw new Asn1DecodingException(
"Invalid ASN.1 annotation on "
+ containerClass.getName() + "." + field.getName(),
e);
}
result.add(annotatedField);
}
return result;
}
private static final class BerToJavaConverter {
private BerToJavaConverter() {}
public static void setFieldValue(
Object obj, Field field, Asn1Type type, BerDataValue dataValue)
throws Asn1DecodingException {
try {
switch (type) {
case SET_OF:
case SEQUENCE_OF:
if (Asn1OpaqueObject.class.equals(field.getType())) {
field.set(obj, convert(type, dataValue, field.getType()));
} else {
field.set(obj, parseSetOf(dataValue, getElementType(field)));
}
return;
default:
field.set(obj, convert(type, dataValue, field.getType()));
break;
}
} catch (ReflectiveOperationException e) {
throw new Asn1DecodingException(
"Failed to set value of " + obj.getClass().getName()
+ "." + field.getName(),
e);
}
}
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
@SuppressWarnings("unchecked")
public static <T> T convert(
Asn1Type sourceType,
BerDataValue dataValue,
Class<T> targetType) throws Asn1DecodingException {
if (ByteBuffer.class.equals(targetType)) {
return (T) dataValue.getEncodedContents();
} else if (byte[].class.equals(targetType)) {
ByteBuffer resultBuf = dataValue.getEncodedContents();
if (!resultBuf.hasRemaining()) {
return (T) EMPTY_BYTE_ARRAY;
}
byte[] result = new byte[resultBuf.remaining()];
resultBuf.get(result);
return (T) result;
} else if (Asn1OpaqueObject.class.equals(targetType)) {
return (T) new Asn1OpaqueObject(dataValue.getEncoded());
}
ByteBuffer encodedContents = dataValue.getEncodedContents();
switch (sourceType) {
case INTEGER:
if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
return (T) Integer.valueOf(integerToInt(encodedContents));
} else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
return (T) Long.valueOf(integerToLong(encodedContents));
} else if (BigInteger.class.equals(targetType)) {
return (T) integerToBigInteger(encodedContents);
}
break;
case OBJECT_IDENTIFIER:
if (String.class.equals(targetType)) {
return (T) oidToString(encodedContents);
}
break;
case UTC_TIME:
case GENERALIZED_TIME:
if (String.class.equals(targetType)) {
return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
}
break;
case BOOLEAN:
// A boolean should be encoded in a single byte with a value of 0 for false and
// any non-zero value for true.
if (boolean.class.equals(targetType)) {
if (encodedContents.remaining() != 1) {
throw new Asn1DecodingException(
"Incorrect encoded size of boolean value: "
+ encodedContents.remaining());
}
boolean result;
if (encodedContents.get() == 0) {
result = false;
} else {
result = true;
}
return (T) new Boolean(result);
}
break;
case SEQUENCE:
{
Asn1Class containerAnnotation =
targetType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
return parseSequence(dataValue, targetType);
}
break;
}
case CHOICE:
{
Asn1Class containerAnnotation =
targetType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
return parseChoice(dataValue, targetType);
}
break;
}
default:
break;
}
throw new Asn1DecodingException(
"Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
}
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Asn1Class {
public Asn1Type type();
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
/**
* Indicates that input could not be decoded into intended ASN.1 structure.
*/
public class Asn1DecodingException extends Exception {
private static final long serialVersionUID = 1L;
public Asn1DecodingException(String message) {
super(message);
}
public Asn1DecodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,596 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
import com.android.apksig.internal.asn1.ber.BerEncoding;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Encoder of ASN.1 structures into DER-encoded form.
*
* <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class},
* containing fields annotated with {@link Asn1Field}.
*/
public final class Asn1DerEncoder {
private Asn1DerEncoder() {}
/**
* Returns the DER-encoded form of the provided ASN.1 structure.
*
* @param container container to be encoded. The container's class must meet the following
* requirements:
* <ul>
* <li>The class must be annotated with {@link Asn1Class}.</li>
* <li>Member fields of the class which are to be encoded must be annotated with
* {@link Asn1Field} and be public.</li>
* </ul>
*
* @throws Asn1EncodingException if the input could not be encoded
*/
public static byte[] encode(Object container) throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
if (containerAnnotation == null) {
throw new Asn1EncodingException(
containerClass.getName() + " not annotated with " + Asn1Class.class.getName());
}
Asn1Type containerType = containerAnnotation.type();
switch (containerType) {
case CHOICE:
return toChoice(container);
case SEQUENCE:
return toSequence(container);
case UNENCODED_CONTAINER:
return toSequence(container, true);
default:
throw new Asn1EncodingException("Unsupported container type: " + containerType);
}
}
private static byte[] toChoice(Object container) throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
List<AnnotatedField> fields = getAnnotatedFields(container);
if (fields.isEmpty()) {
throw new Asn1EncodingException(
"No fields annotated with " + Asn1Field.class.getName()
+ " in CHOICE class " + containerClass.getName());
}
AnnotatedField resultField = null;
for (AnnotatedField field : fields) {
Object fieldValue = getMemberFieldValue(container, field.getField());
if (fieldValue != null) {
if (resultField != null) {
throw new Asn1EncodingException(
"Multiple non-null fields in CHOICE class " + containerClass.getName()
+ ": " + resultField.getField().getName()
+ ", " + field.getField().getName());
}
resultField = field;
}
}
if (resultField == null) {
throw new Asn1EncodingException(
"No non-null fields in CHOICE class " + containerClass.getName());
}
return resultField.toDer();
}
private static byte[] toSequence(Object container) throws Asn1EncodingException {
return toSequence(container, false);
}
private static byte[] toSequence(Object container, boolean omitTag)
throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
List<AnnotatedField> fields = getAnnotatedFields(container);
Collections.sort(
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
if (fields.size() > 1) {
AnnotatedField lastField = null;
for (AnnotatedField field : fields) {
if ((lastField != null)
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
throw new Asn1EncodingException(
"Fields have the same index: " + containerClass.getName()
+ "." + lastField.getField().getName()
+ " and ." + field.getField().getName());
}
lastField = field;
}
}
List<byte[]> serializedFields = new ArrayList<>(fields.size());
int contentLen = 0;
for (AnnotatedField field : fields) {
byte[] serializedField;
try {
serializedField = field.toDer();
} catch (Asn1EncodingException e) {
throw new Asn1EncodingException(
"Failed to encode " + containerClass.getName()
+ "." + field.getField().getName(),
e);
}
if (serializedField != null) {
serializedFields.add(serializedField);
contentLen += serializedField.length;
}
}
if (omitTag) {
byte[] unencodedResult = new byte[contentLen];
int index = 0;
for (byte[] serializedField : serializedFields) {
System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length);
index += serializedField.length;
}
return unencodedResult;
} else {
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
serializedFields.toArray(new byte[0][]));
}
}
private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
return toSequenceOrSetOf(values, elementType, true);
}
private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
return toSequenceOrSetOf(values, elementType, false);
}
private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet)
throws Asn1EncodingException {
List<byte[]> serializedValues = new ArrayList<>(values.size());
for (Object value : values) {
serializedValues.add(JavaToDerConverter.toDer(value, elementType, null));
}
int tagNumber;
if (toSet) {
if (serializedValues.size() > 1) {
Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
}
tagNumber = BerEncoding.TAG_NUMBER_SET;
} else {
tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE;
}
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
serializedValues.toArray(new byte[0][]));
}
/**
* Compares two bytes arrays based on their lexicographic order. Corresponding elements of the
* two arrays are compared in ascending order. Elements at out of range indices are assumed to
* be smaller than the smallest possible value for an element.
*/
private static class ByteArrayLexicographicComparator implements Comparator<byte[]> {
private static final ByteArrayLexicographicComparator INSTANCE =
new ByteArrayLexicographicComparator();
@Override
public int compare(byte[] arr1, byte[] arr2) {
int commonLength = Math.min(arr1.length, arr2.length);
for (int i = 0; i < commonLength; i++) {
int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff);
if (diff != 0) {
return diff;
}
}
return arr1.length - arr2.length;
}
}
private static List<AnnotatedField> getAnnotatedFields(Object container)
throws Asn1EncodingException {
Class<?> containerClass = container.getClass();
Field[] declaredFields = containerClass.getDeclaredFields();
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
for (Field field : declaredFields) {
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
if (annotation == null) {
continue;
}
if (Modifier.isStatic(field.getModifiers())) {
throw new Asn1EncodingException(
Asn1Field.class.getName() + " used on a static field: "
+ containerClass.getName() + "." + field.getName());
}
AnnotatedField annotatedField;
try {
annotatedField = new AnnotatedField(container, field, annotation);
} catch (Asn1EncodingException e) {
throw new Asn1EncodingException(
"Invalid ASN.1 annotation on "
+ containerClass.getName() + "." + field.getName(),
e);
}
result.add(annotatedField);
}
return result;
}
private static byte[] toInteger(int value) {
return toInteger((long) value);
}
private static byte[] toInteger(long value) {
return toInteger(BigInteger.valueOf(value));
}
private static byte[] toInteger(BigInteger value) {
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
value.toByteArray());
}
private static byte[] toBoolean(boolean value) {
// A boolean should be encoded in a single byte with a value of 0 for false and any non-zero
// value for true.
byte[] result = new byte[1];
if (value == false) {
result[0] = 0;
} else {
result[0] = 1;
}
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result);
}
private static byte[] toOid(String oid) throws Asn1EncodingException {
ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
String[] nodes = oid.split("\\.");
if (nodes.length < 2) {
throw new Asn1EncodingException(
"OBJECT IDENTIFIER must contain at least two nodes: " + oid);
}
int firstNode;
try {
firstNode = Integer.parseInt(nodes[0]);
} catch (NumberFormatException e) {
throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
}
if ((firstNode > 6) || (firstNode < 0)) {
throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
}
int secondNode;
try {
secondNode = Integer.parseInt(nodes[1]);
} catch (NumberFormatException e) {
throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
}
if ((secondNode >= 40) || (secondNode < 0)) {
throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
}
int firstByte = firstNode * 40 + secondNode;
if (firstByte > 0xff) {
throw new Asn1EncodingException(
"First two nodes out of range: " + firstNode + "." + secondNode);
}
encodedValue.write(firstByte);
for (int i = 2; i < nodes.length; i++) {
String nodeString = nodes[i];
int node;
try {
node = Integer.parseInt(nodeString);
} catch (NumberFormatException e) {
throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
}
if (node < 0) {
throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
}
if (node <= 0x7f) {
encodedValue.write(node);
continue;
}
if (node < 1 << 14) {
encodedValue.write(0x80 | (node >> 7));
encodedValue.write(node & 0x7f);
continue;
}
if (node < 1 << 21) {
encodedValue.write(0x80 | (node >> 14));
encodedValue.write(0x80 | ((node >> 7) & 0x7f));
encodedValue.write(node & 0x7f);
continue;
}
throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
}
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
encodedValue.toByteArray());
}
private static Object getMemberFieldValue(Object obj, Field field)
throws Asn1EncodingException {
try {
return field.get(obj);
} catch (ReflectiveOperationException e) {
throw new Asn1EncodingException(
"Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
}
}
private static final class AnnotatedField {
private final Field mField;
private final Object mObject;
private final Asn1Field mAnnotation;
private final Asn1Type mDataType;
private final Asn1Type mElementDataType;
private final Asn1TagClass mTagClass;
private final int mDerTagClass;
private final int mDerTagNumber;
private final Asn1Tagging mTagging;
private final boolean mOptional;
public AnnotatedField(Object obj, Field field, Asn1Field annotation)
throws Asn1EncodingException {
mObject = obj;
mField = field;
mAnnotation = annotation;
mDataType = annotation.type();
mElementDataType = annotation.elementType();
Asn1TagClass tagClass = annotation.cls();
if (tagClass == Asn1TagClass.AUTOMATIC) {
if (annotation.tagNumber() != -1) {
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
} else {
tagClass = Asn1TagClass.UNIVERSAL;
}
}
mTagClass = tagClass;
mDerTagClass = BerEncoding.getTagClass(mTagClass);
int tagNumber;
if (annotation.tagNumber() != -1) {
tagNumber = annotation.tagNumber();
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
tagNumber = -1;
} else {
tagNumber = BerEncoding.getTagNumber(mDataType);
}
mDerTagNumber = tagNumber;
mTagging = annotation.tagging();
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
&& (annotation.tagNumber() == -1)) {
throw new Asn1EncodingException(
"Tag number must be specified when tagging mode is " + mTagging);
}
mOptional = annotation.optional();
}
public Field getField() {
return mField;
}
public Asn1Field getAnnotation() {
return mAnnotation;
}
public byte[] toDer() throws Asn1EncodingException {
Object fieldValue = getMemberFieldValue(mObject, mField);
if (fieldValue == null) {
if (mOptional) {
return null;
}
throw new Asn1EncodingException("Required field not set");
}
byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
switch (mTagging) {
case NORMAL:
return encoded;
case EXPLICIT:
return createTag(mDerTagClass, true, mDerTagNumber, encoded);
case IMPLICIT:
int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
if (originalTagNumber == 0x1f) {
throw new Asn1EncodingException("High-tag-number form not supported");
}
if (mDerTagNumber >= 0x1f) {
throw new Asn1EncodingException(
"Unsupported high tag number: " + mDerTagNumber);
}
encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
return encoded;
default:
throw new RuntimeException("Unknown tagging mode: " + mTagging);
}
}
}
private static byte[] createTag(
int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
if (tagNumber >= 0x1f) {
throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
}
// tag class & number fit into the first byte
byte firstIdentifierByte =
(byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
int contentsLength = 0;
for (byte[] c : contents) {
contentsLength += c.length;
}
int contentsPosInResult;
byte[] result;
if (contentsLength < 0x80) {
// Length fits into one byte
contentsPosInResult = 2;
result = new byte[contentsPosInResult + contentsLength];
result[0] = firstIdentifierByte;
result[1] = (byte) contentsLength;
} else {
// Length is represented as multiple bytes
// The low 7 bits of the first byte represent the number of length bytes (following the
// first byte) in which the length is in big-endian base-256 form
if (contentsLength <= 0xff) {
contentsPosInResult = 3;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x81; // 1 length byte
result[2] = (byte) contentsLength;
} else if (contentsLength <= 0xffff) {
contentsPosInResult = 4;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x82; // 2 length bytes
result[2] = (byte) (contentsLength >> 8);
result[3] = (byte) (contentsLength & 0xff);
} else if (contentsLength <= 0xffffff) {
contentsPosInResult = 5;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x83; // 3 length bytes
result[2] = (byte) (contentsLength >> 16);
result[3] = (byte) ((contentsLength >> 8) & 0xff);
result[4] = (byte) (contentsLength & 0xff);
} else {
contentsPosInResult = 6;
result = new byte[contentsPosInResult + contentsLength];
result[1] = (byte) 0x84; // 4 length bytes
result[2] = (byte) (contentsLength >> 24);
result[3] = (byte) ((contentsLength >> 16) & 0xff);
result[4] = (byte) ((contentsLength >> 8) & 0xff);
result[5] = (byte) (contentsLength & 0xff);
}
result[0] = firstIdentifierByte;
}
for (byte[] c : contents) {
System.arraycopy(c, 0, result, contentsPosInResult, c.length);
contentsPosInResult += c.length;
}
return result;
}
private static final class JavaToDerConverter {
private JavaToDerConverter() {}
public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType)
throws Asn1EncodingException {
Class<?> sourceType = source.getClass();
if (Asn1OpaqueObject.class.equals(sourceType)) {
ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
byte[] result = new byte[buf.remaining()];
buf.get(result);
return result;
}
if ((targetType == null) || (targetType == Asn1Type.ANY)) {
return encode(source);
}
switch (targetType) {
case OCTET_STRING:
case BIT_STRING:
byte[] value = null;
if (source instanceof ByteBuffer) {
ByteBuffer buf = (ByteBuffer) source;
value = new byte[buf.remaining()];
buf.slice().get(value);
} else if (source instanceof byte[]) {
value = (byte[]) source;
}
if (value != null) {
return createTag(
BerEncoding.TAG_CLASS_UNIVERSAL,
false,
BerEncoding.getTagNumber(targetType),
value);
}
break;
case INTEGER:
if (source instanceof Integer) {
return toInteger((Integer) source);
} else if (source instanceof Long) {
return toInteger((Long) source);
} else if (source instanceof BigInteger) {
return toInteger((BigInteger) source);
}
break;
case BOOLEAN:
if (source instanceof Boolean) {
return toBoolean((Boolean) (source));
}
break;
case UTC_TIME:
case GENERALIZED_TIME:
if (source instanceof String) {
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false,
BerEncoding.getTagNumber(targetType), ((String) source).getBytes());
}
break;
case OBJECT_IDENTIFIER:
if (source instanceof String) {
return toOid((String) source);
}
break;
case SEQUENCE:
{
Asn1Class containerAnnotation =
sourceType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
return toSequence(source);
}
break;
}
case CHOICE:
{
Asn1Class containerAnnotation =
sourceType.getDeclaredAnnotation(Asn1Class.class);
if ((containerAnnotation != null)
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
return toChoice(source);
}
break;
}
case SET_OF:
return toSetOf((Collection<?>) source, targetElementType);
case SEQUENCE_OF:
return toSequenceOf((Collection<?>) source, targetElementType);
default:
break;
}
throw new Asn1EncodingException(
"Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
}
}
/** ASN.1 DER-encoded {@code NULL}. */
public static final Asn1OpaqueObject ASN1_DER_NULL =
new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
/**
* Indicates that an ASN.1 structure could not be encoded.
*/
public class Asn1EncodingException extends Exception {
private static final long serialVersionUID = 1L;
public Asn1EncodingException(String message) {
super(message);
}
public Asn1EncodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Asn1Field {
/** Index used to order fields in a container. Required for fields of SEQUENCE containers. */
public int index() default 0;
public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
public Asn1Type type();
/** Tagging mode. Default: NORMAL. */
public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
/** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/
public int tagNumber() default -1;
/** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */
public boolean optional() default false;
/** Type of elements. Used only for SET_OF or SEQUENCE_OF. */
public Asn1Type elementType() default Asn1Type.ANY;
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
import java.nio.ByteBuffer;
/**
* Opaque holder of encoded ASN.1 stuff.
*/
public class Asn1OpaqueObject {
private final ByteBuffer mEncoded;
public Asn1OpaqueObject(ByteBuffer encoded) {
mEncoded = encoded.slice();
}
public Asn1OpaqueObject(byte[] encoded) {
mEncoded = ByteBuffer.wrap(encoded);
}
public ByteBuffer getEncoded() {
return mEncoded.slice();
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
public enum Asn1TagClass {
UNIVERSAL,
APPLICATION,
CONTEXT_SPECIFIC,
PRIVATE,
/**
* Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
* automatically.
*/
AUTOMATIC,
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
public enum Asn1Tagging {
NORMAL,
EXPLICIT,
IMPLICIT,
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1;
public enum Asn1Type {
ANY,
CHOICE,
INTEGER,
OBJECT_IDENTIFIER,
OCTET_STRING,
SEQUENCE,
SEQUENCE_OF,
SET_OF,
BIT_STRING,
UTC_TIME,
GENERALIZED_TIME,
BOOLEAN,
// This type can be used to annotate classes that encapsulate ASN.1 structures that are not
// classified as a SEQUENCE or SET.
UNENCODED_CONTAINER
}

View File

@ -0,0 +1,115 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
import java.nio.ByteBuffer;
/**
* ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
*/
public class BerDataValue {
private final ByteBuffer mEncoded;
private final ByteBuffer mEncodedContents;
private final int mTagClass;
private final boolean mConstructed;
private final int mTagNumber;
BerDataValue(
ByteBuffer encoded,
ByteBuffer encodedContents,
int tagClass,
boolean constructed,
int tagNumber) {
mEncoded = encoded;
mEncodedContents = encodedContents;
mTagClass = tagClass;
mConstructed = constructed;
mTagNumber = tagNumber;
}
/**
* Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
* constants.
*/
public int getTagClass() {
return mTagClass;
}
/**
* Returns {@code true} if the content octets of this data value are the complete BER encoding
* of one or more data values, {@code false} if the content octets of this data value directly
* represent the value.
*/
public boolean isConstructed() {
return mConstructed;
}
/**
* Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
* constants.
*/
public int getTagNumber() {
return mTagNumber;
}
/**
* Returns the encoded form of this data value.
*/
public ByteBuffer getEncoded() {
return mEncoded.slice();
}
/**
* Returns the encoded contents of this data value.
*/
public ByteBuffer getEncodedContents() {
return mEncodedContents.slice();
}
/**
* Returns a new reader of the contents of this data value.
*/
public BerDataValueReader contentsReader() {
return new ByteBufferBerDataValueReader(getEncodedContents());
}
/**
* Returns a new reader which returns just this data value. This may be useful for re-reading
* this value in different contexts.
*/
public BerDataValueReader dataValueReader() {
return new ParsedValueReader(this);
}
private static final class ParsedValueReader implements BerDataValueReader {
private final BerDataValue mValue;
private boolean mValueOutput;
public ParsedValueReader(BerDataValue value) {
mValue = value;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
if (mValueOutput) {
return null;
}
mValueOutput = true;
return mValue;
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
/**
* Indicates that an ASN.1 data value being read could not be decoded using
* Basic Encoding Rules (BER).
*/
public class BerDataValueFormatException extends Exception {
private static final long serialVersionUID = 1L;
public BerDataValueFormatException(String message) {
super(message);
}
public BerDataValueFormatException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
/**
* Reader of ASN.1 Basic Encoding Rules (BER) data values.
*
* <p>BER data value reader returns data values, one by one, from a source. The interpretation of
* data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
* the elements of a SEQUENCE value) is left to clients of the reader.
*/
public interface BerDataValueReader {
/**
* Returns the next data value or {@code null} if end of input has been reached.
*
* @throws BerDataValueFormatException if the value being read is malformed.
*/
BerDataValue readDataValue() throws BerDataValueFormatException;
}

View File

@ -0,0 +1,225 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1TagClass;
/**
* ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
*/
public abstract class BerEncoding {
private BerEncoding() {}
/**
* Constructed vs primitive flag in the first identifier byte.
*/
public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
/**
* Tag class: UNIVERSAL
*/
public static final int TAG_CLASS_UNIVERSAL = 0;
/**
* Tag class: APPLICATION
*/
public static final int TAG_CLASS_APPLICATION = 1;
/**
* Tag class: CONTEXT SPECIFIC
*/
public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
/**
* Tag class: PRIVATE
*/
public static final int TAG_CLASS_PRIVATE = 3;
/**
* Tag number: BOOLEAN
*/
public static final int TAG_NUMBER_BOOLEAN = 0x1;
/**
* Tag number: INTEGER
*/
public static final int TAG_NUMBER_INTEGER = 0x2;
/**
* Tag number: BIT STRING
*/
public static final int TAG_NUMBER_BIT_STRING = 0x3;
/**
* Tag number: OCTET STRING
*/
public static final int TAG_NUMBER_OCTET_STRING = 0x4;
/**
* Tag number: NULL
*/
public static final int TAG_NUMBER_NULL = 0x05;
/**
* Tag number: OBJECT IDENTIFIER
*/
public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
/**
* Tag number: SEQUENCE
*/
public static final int TAG_NUMBER_SEQUENCE = 0x10;
/**
* Tag number: SET
*/
public static final int TAG_NUMBER_SET = 0x11;
/**
* Tag number: UTC_TIME
*/
public final static int TAG_NUMBER_UTC_TIME = 0x17;
/**
* Tag number: GENERALIZED_TIME
*/
public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18;
public static int getTagNumber(Asn1Type dataType) {
switch (dataType) {
case INTEGER:
return TAG_NUMBER_INTEGER;
case OBJECT_IDENTIFIER:
return TAG_NUMBER_OBJECT_IDENTIFIER;
case OCTET_STRING:
return TAG_NUMBER_OCTET_STRING;
case BIT_STRING:
return TAG_NUMBER_BIT_STRING;
case SET_OF:
return TAG_NUMBER_SET;
case SEQUENCE:
case SEQUENCE_OF:
return TAG_NUMBER_SEQUENCE;
case UTC_TIME:
return TAG_NUMBER_UTC_TIME;
case GENERALIZED_TIME:
return TAG_NUMBER_GENERALIZED_TIME;
case BOOLEAN:
return TAG_NUMBER_BOOLEAN;
default:
throw new IllegalArgumentException("Unsupported data type: " + dataType);
}
}
public static int getTagClass(Asn1TagClass tagClass) {
switch (tagClass) {
case APPLICATION:
return TAG_CLASS_APPLICATION;
case CONTEXT_SPECIFIC:
return TAG_CLASS_CONTEXT_SPECIFIC;
case PRIVATE:
return TAG_CLASS_PRIVATE;
case UNIVERSAL:
return TAG_CLASS_UNIVERSAL;
default:
throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
}
}
public static String tagClassToString(int typeClass) {
switch (typeClass) {
case TAG_CLASS_APPLICATION:
return "APPLICATION";
case TAG_CLASS_CONTEXT_SPECIFIC:
return "";
case TAG_CLASS_PRIVATE:
return "PRIVATE";
case TAG_CLASS_UNIVERSAL:
return "UNIVERSAL";
default:
throw new IllegalArgumentException("Unsupported type class: " + typeClass);
}
}
public static String tagClassAndNumberToString(int tagClass, int tagNumber) {
String classString = tagClassToString(tagClass);
String numberString = tagNumberToString(tagNumber);
return classString.isEmpty() ? numberString : classString + " " + numberString;
}
public static String tagNumberToString(int tagNumber) {
switch (tagNumber) {
case TAG_NUMBER_INTEGER:
return "INTEGER";
case TAG_NUMBER_OCTET_STRING:
return "OCTET STRING";
case TAG_NUMBER_BIT_STRING:
return "BIT STRING";
case TAG_NUMBER_NULL:
return "NULL";
case TAG_NUMBER_OBJECT_IDENTIFIER:
return "OBJECT IDENTIFIER";
case TAG_NUMBER_SEQUENCE:
return "SEQUENCE";
case TAG_NUMBER_SET:
return "SET";
case TAG_NUMBER_BOOLEAN:
return "BOOLEAN";
case TAG_NUMBER_GENERALIZED_TIME:
return "GENERALIZED TIME";
case TAG_NUMBER_UTC_TIME:
return "UTC TIME";
default:
return "0x" + Integer.toHexString(tagNumber);
}
}
/**
* Returns {@code true} if the provided first identifier byte indicates that the data value uses
* constructed encoding for its contents, or {@code false} if the data value uses primitive
* encoding for its contents.
*/
public static boolean isConstructed(byte firstIdentifierByte) {
return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
}
/**
* Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
* constants.
*/
public static int getTagClass(byte firstIdentifierByte) {
return (firstIdentifierByte & 0xff) >> 6;
}
public static byte setTagClass(byte firstIdentifierByte, int tagClass) {
return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6));
}
/**
* Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
* constants.
*/
public static int getTagNumber(byte firstIdentifierByte) {
return firstIdentifierByte & 0x1f;
}
public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) {
return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
}
}

View File

@ -0,0 +1,208 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
import java.nio.ByteBuffer;
/**
* {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
* values. See {@code X.690} for the encoding.
*/
public class ByteBufferBerDataValueReader implements BerDataValueReader {
private final ByteBuffer mBuf;
public ByteBufferBerDataValueReader(ByteBuffer buf) {
if (buf == null) {
throw new NullPointerException("buf == null");
}
mBuf = buf;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
int startPosition = mBuf.position();
if (!mBuf.hasRemaining()) {
return null;
}
byte firstIdentifierByte = mBuf.get();
int tagNumber = readTagNumber(firstIdentifierByte);
boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException("Missing length");
}
int firstLengthByte = mBuf.get() & 0xff;
int contentsLength;
int contentsOffsetInTag;
if ((firstLengthByte & 0x80) == 0) {
// short form length
contentsLength = readShortFormLength(firstLengthByte);
contentsOffsetInTag = mBuf.position() - startPosition;
skipDefiniteLengthContents(contentsLength);
} else if (firstLengthByte != 0x80) {
// long form length
contentsLength = readLongFormLength(firstLengthByte);
contentsOffsetInTag = mBuf.position() - startPosition;
skipDefiniteLengthContents(contentsLength);
} else {
// indefinite length -- value ends with 0x00 0x00
contentsOffsetInTag = mBuf.position() - startPosition;
contentsLength =
constructed
? skipConstructedIndefiniteLengthContents()
: skipPrimitiveIndefiniteLengthContents();
}
// Create the encoded data value ByteBuffer
int endPosition = mBuf.position();
mBuf.position(startPosition);
int bufOriginalLimit = mBuf.limit();
mBuf.limit(endPosition);
ByteBuffer encoded = mBuf.slice();
mBuf.position(mBuf.limit());
mBuf.limit(bufOriginalLimit);
// Create the encoded contents ByteBuffer
encoded.position(contentsOffsetInTag);
encoded.limit(contentsOffsetInTag + contentsLength);
ByteBuffer encodedContents = encoded.slice();
encoded.clear();
return new BerDataValue(
encoded,
encodedContents,
BerEncoding.getTagClass(firstIdentifierByte),
constructed,
tagNumber);
}
private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
if (tagNumber == 0x1f) {
// high-tag-number form, where the tag number follows this byte in base-128
// big-endian form, where each byte has the highest bit set, except for the last
// byte
return readHighTagNumber();
} else {
// low-tag-number form
return tagNumber;
}
}
private int readHighTagNumber() throws BerDataValueFormatException {
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
// byte
int b;
int result = 0;
do {
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException("Truncated tag number");
}
b = mBuf.get();
if (result > Integer.MAX_VALUE >>> 7) {
throw new BerDataValueFormatException("Tag number too large");
}
result <<= 7;
result |= b & 0x7f;
} while ((b & 0x80) != 0);
return result;
}
private int readShortFormLength(int firstLengthByte) {
return firstLengthByte & 0x7f;
}
private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
// The low 7 bits of the first byte represent the number of bytes (following the first
// byte) in which the length is in big-endian base-256 form
int byteCount = firstLengthByte & 0x7f;
if (byteCount > 4) {
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
}
int result = 0;
for (int i = 0; i < byteCount; i++) {
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException("Truncated length");
}
int b = mBuf.get();
if (result > Integer.MAX_VALUE >>> 8) {
throw new BerDataValueFormatException("Length too large");
}
result <<= 8;
result |= b & 0xff;
}
return result;
}
private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
if (mBuf.remaining() < contentsLength) {
throw new BerDataValueFormatException(
"Truncated contents. Need: " + contentsLength + " bytes, available: "
+ mBuf.remaining());
}
mBuf.position(mBuf.position() + contentsLength);
}
private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00
boolean prevZeroByte = false;
int bytesRead = 0;
while (true) {
if (!mBuf.hasRemaining()) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
}
int b = mBuf.get();
bytesRead++;
if (bytesRead < 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
if (b == 0) {
if (prevZeroByte) {
// End of contents reached -- we've read the value and its terminator 0x00 0x00
return bytesRead - 2;
}
prevZeroByte = true;
} else {
prevZeroByte = false;
}
}
}
private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
// can contain data values which are themselves indefinite length encoded. As a result, we
// must parse the direct children of this data value to correctly skip over the contents of
// this data value.
int startPos = mBuf.position();
while (mBuf.hasRemaining()) {
// Check whether the 0x00 0x00 terminator is at current position
if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) {
int contentsLength = mBuf.position() - startPos;
mBuf.position(mBuf.position() + 2);
return contentsLength;
}
// No luck. This must be a BER-encoded data value -- skip over it by parsing it
readDataValue();
}
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: "
+ (mBuf.position() - startPos) + " bytes read");
}
}

View File

@ -0,0 +1,313 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
* values. See {@code X.690} for the encoding.
*/
public class InputStreamBerDataValueReader implements BerDataValueReader {
private final InputStream mIn;
public InputStreamBerDataValueReader(InputStream in) {
if (in == null) {
throw new NullPointerException("in == null");
}
mIn = in;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
return readDataValue(mIn);
}
/**
* Returns the next data value or {@code null} if end of input has been reached.
*
* @throws BerDataValueFormatException if the value being read is malformed.
*/
@SuppressWarnings("resource")
private static BerDataValue readDataValue(InputStream input)
throws BerDataValueFormatException {
RecordingInputStream in = new RecordingInputStream(input);
try {
int firstIdentifierByte = in.read();
if (firstIdentifierByte == -1) {
// End of input
return null;
}
int tagNumber = readTagNumber(in, firstIdentifierByte);
int firstLengthByte = in.read();
if (firstLengthByte == -1) {
throw new BerDataValueFormatException("Missing length");
}
boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
int contentsLength;
int contentsOffsetInDataValue;
if ((firstLengthByte & 0x80) == 0) {
// short form length
contentsLength = readShortFormLength(firstLengthByte);
contentsOffsetInDataValue = in.getReadByteCount();
skipDefiniteLengthContents(in, contentsLength);
} else if ((firstLengthByte & 0xff) != 0x80) {
// long form length
contentsLength = readLongFormLength(in, firstLengthByte);
contentsOffsetInDataValue = in.getReadByteCount();
skipDefiniteLengthContents(in, contentsLength);
} else {
// indefinite length
contentsOffsetInDataValue = in.getReadByteCount();
contentsLength =
constructed
? skipConstructedIndefiniteLengthContents(in)
: skipPrimitiveIndefiniteLengthContents(in);
}
byte[] encoded = in.getReadBytes();
ByteBuffer encodedContents =
ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
return new BerDataValue(
ByteBuffer.wrap(encoded),
encodedContents,
BerEncoding.getTagClass((byte) firstIdentifierByte),
constructed,
tagNumber);
} catch (IOException e) {
throw new BerDataValueFormatException("Failed to read data value", e);
}
}
private static int readTagNumber(InputStream in, int firstIdentifierByte)
throws IOException, BerDataValueFormatException {
int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
if (tagNumber == 0x1f) {
// high-tag-number form
return readHighTagNumber(in);
} else {
// low-tag-number form
return tagNumber;
}
}
private static int readHighTagNumber(InputStream in)
throws IOException, BerDataValueFormatException {
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
// byte where the highest bit is not set
int b;
int result = 0;
do {
b = in.read();
if (b == -1) {
throw new BerDataValueFormatException("Truncated tag number");
}
if (result > Integer.MAX_VALUE >>> 7) {
throw new BerDataValueFormatException("Tag number too large");
}
result <<= 7;
result |= b & 0x7f;
} while ((b & 0x80) != 0);
return result;
}
private static int readShortFormLength(int firstLengthByte) {
return firstLengthByte & 0x7f;
}
private static int readLongFormLength(InputStream in, int firstLengthByte)
throws IOException, BerDataValueFormatException {
// The low 7 bits of the first byte represent the number of bytes (following the first
// byte) in which the length is in big-endian base-256 form
int byteCount = firstLengthByte & 0x7f;
if (byteCount > 4) {
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
}
int result = 0;
for (int i = 0; i < byteCount; i++) {
int b = in.read();
if (b == -1) {
throw new BerDataValueFormatException("Truncated length");
}
if (result > Integer.MAX_VALUE >>> 8) {
throw new BerDataValueFormatException("Length too large");
}
result <<= 8;
result |= b & 0xff;
}
return result;
}
private static void skipDefiniteLengthContents(InputStream in, int len)
throws IOException, BerDataValueFormatException {
long bytesRead = 0;
while (len > 0) {
int skipped = (int) in.skip(len);
if (skipped <= 0) {
throw new BerDataValueFormatException(
"Truncated definite-length contents: " + bytesRead + " bytes read"
+ ", " + len + " missing");
}
len -= skipped;
bytesRead += skipped;
}
}
private static int skipPrimitiveIndefiniteLengthContents(InputStream in)
throws IOException, BerDataValueFormatException {
// Contents are terminated by 0x00 0x00
boolean prevZeroByte = false;
int bytesRead = 0;
while (true) {
int b = in.read();
if (b == -1) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
}
bytesRead++;
if (bytesRead < 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
if (b == 0) {
if (prevZeroByte) {
// End of contents reached -- we've read the value and its terminator 0x00 0x00
return bytesRead - 2;
}
prevZeroByte = true;
continue;
} else {
prevZeroByte = false;
}
}
}
private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in)
throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
// can contain data values which are indefinite length encoded as well. As a result, we
// must parse the direct children of this data value to correctly skip over the contents of
// this data value.
int readByteCountBefore = in.getReadByteCount();
while (true) {
// We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
// Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
// then check below to see whether it's 0x00 0x00.
BerDataValue dataValue = readDataValue(in);
if (dataValue == null) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: "
+ (in.getReadByteCount() - readByteCountBefore) + " bytes read");
}
if (in.getReadByteCount() <= 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
ByteBuffer encoded = dataValue.getEncoded();
if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
// 0x00 0x00 encountered
return in.getReadByteCount() - readByteCountBefore - 2;
}
}
}
private static class RecordingInputStream extends InputStream {
private final InputStream mIn;
private final ByteArrayOutputStream mBuf;
private RecordingInputStream(InputStream in) {
mIn = in;
mBuf = new ByteArrayOutputStream();
}
public byte[] getReadBytes() {
return mBuf.toByteArray();
}
public int getReadByteCount() {
return mBuf.size();
}
@Override
public int read() throws IOException {
int b = mIn.read();
if (b != -1) {
mBuf.write(b);
}
return b;
}
@Override
public int read(byte[] b) throws IOException {
int len = mIn.read(b);
if (len > 0) {
mBuf.write(b, 0, len);
}
return len;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
len = mIn.read(b, off, len);
if (len > 0) {
mBuf.write(b, off, len);
}
return len;
}
@Override
public long skip(long n) throws IOException {
if (n <= 0) {
return mIn.skip(n);
}
byte[] buf = new byte[4096];
int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
if (len > 0) {
mBuf.write(buf, 0, len);
}
return (len < 0) ? 0 : len;
}
@Override
public int available() throws IOException {
return super.available();
}
@Override
public void close() throws IOException {
super.close();
}
@Override
public synchronized void mark(int readlimit) {}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
@Override
public boolean markSupported() {
return false;
}
}
}

View File

@ -0,0 +1,363 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.jar;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.jar.Attributes;
/**
* JAR manifest and signature file parser.
*
* <p>These files consist of a main section followed by individual sections. Individual sections
* are named, their names referring to JAR entries.
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
*/
public class ManifestParser {
private final byte[] mManifest;
private int mOffset;
private int mEndOffset;
private byte[] mBufferedLine;
/**
* Constructs a new {@code ManifestParser} with the provided input.
*/
public ManifestParser(byte[] data) {
this(data, 0, data.length);
}
/**
* Constructs a new {@code ManifestParser} with the provided input.
*/
public ManifestParser(byte[] data, int offset, int length) {
mManifest = data;
mOffset = offset;
mEndOffset = offset + length;
}
/**
* Returns the remaining sections of this file.
*/
public List<Section> readAllSections() {
List<Section> sections = new ArrayList<>();
Section section;
while ((section = readSection()) != null) {
sections.add(section);
}
return sections;
}
/**
* Returns the next section from this file or {@code null} if end of file has been reached.
*/
public Section readSection() {
// Locate the first non-empty line
int sectionStartOffset;
String attr;
do {
sectionStartOffset = mOffset;
attr = readAttribute();
if (attr == null) {
return null;
}
} while (attr.length() == 0);
List<Attribute> attrs = new ArrayList<>();
attrs.add(parseAttr(attr));
// Read attributes until end of section reached
while (true) {
attr = readAttribute();
if ((attr == null) || (attr.length() == 0)) {
// End of section
break;
}
attrs.add(parseAttr(attr));
}
int sectionEndOffset = mOffset;
int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
return new Section(sectionStartOffset, sectionSizeBytes, attrs);
}
private static Attribute parseAttr(String attr) {
// Name is separated from value by a semicolon followed by a single SPACE character.
// This permits trailing spaces in names and leading and trailing spaces in values.
// Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual
// spaces to be able to parse such obfuscated APKs.
int delimiterIndex = attr.indexOf(": ");
if (delimiterIndex == -1) {
return new Attribute(attr, "");
} else {
return new Attribute(
attr.substring(0, delimiterIndex),
attr.substring(delimiterIndex + ": ".length()));
}
}
/**
* Returns the next attribute or empty {@code String} if end of section has been reached or
* {@code null} if end of input has been reached.
*/
private String readAttribute() {
byte[] bytes = readAttributeBytes();
if (bytes == null) {
return null;
} else if (bytes.length == 0) {
return "";
} else {
return new String(bytes, StandardCharsets.UTF_8);
}
}
/**
* Returns the next attribute or empty array if end of section has been reached or {@code null}
* if end of input has been reached.
*/
private byte[] readAttributeBytes() {
// Check whether end of section was reached during previous invocation
if ((mBufferedLine != null) && (mBufferedLine.length == 0)) {
mBufferedLine = null;
return EMPTY_BYTE_ARRAY;
}
// Read the next line
byte[] line = readLine();
if (line == null) {
// End of input
if (mBufferedLine != null) {
byte[] result = mBufferedLine;
mBufferedLine = null;
return result;
}
return null;
}
// Consume the read line
if (line.length == 0) {
// End of section
if (mBufferedLine != null) {
byte[] result = mBufferedLine;
mBufferedLine = EMPTY_BYTE_ARRAY;
return result;
}
return EMPTY_BYTE_ARRAY;
}
byte[] attrLine;
if (mBufferedLine == null) {
attrLine = line;
} else {
if ((line.length == 0) || (line[0] != ' ')) {
// The most common case: buffered line is a full attribute
byte[] result = mBufferedLine;
mBufferedLine = line;
return result;
}
attrLine = mBufferedLine;
mBufferedLine = null;
attrLine = concat(attrLine, line, 1, line.length - 1);
}
// Everything's buffered in attrLine now. mBufferedLine is null
// Read more lines
while (true) {
line = readLine();
if (line == null) {
// End of input
return attrLine;
} else if (line.length == 0) {
// End of section
mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time
return attrLine;
}
if (line[0] == ' ') {
// Continuation line
attrLine = concat(attrLine, line, 1, line.length - 1);
} else {
// Next attribute
mBufferedLine = line;
return attrLine;
}
}
}
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) {
byte[] result = new byte[arr1.length + length2];
System.arraycopy(arr1, 0, result, 0, arr1.length);
System.arraycopy(arr2, offset2, result, arr1.length, length2);
return result;
}
/**
* Returns the next line (without line delimiter characters) or {@code null} if end of input has
* been reached.
*/
private byte[] readLine() {
if (mOffset >= mEndOffset) {
return null;
}
int startOffset = mOffset;
int newlineStartOffset = -1;
int newlineEndOffset = -1;
for (int i = startOffset; i < mEndOffset; i++) {
byte b = mManifest[i];
if (b == '\r') {
newlineStartOffset = i;
int nextIndex = i + 1;
if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
newlineEndOffset = nextIndex + 1;
break;
}
newlineEndOffset = nextIndex;
break;
} else if (b == '\n') {
newlineStartOffset = i;
newlineEndOffset = i + 1;
break;
}
}
if (newlineStartOffset == -1) {
newlineStartOffset = mEndOffset;
newlineEndOffset = mEndOffset;
}
mOffset = newlineEndOffset;
if (newlineStartOffset == startOffset) {
return EMPTY_BYTE_ARRAY;
}
return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset);
}
/**
* Attribute.
*/
public static class Attribute {
private final String mName;
private final String mValue;
/**
* Constructs a new {@code Attribute} with the provided name and value.
*/
public Attribute(String name, String value) {
mName = name;
mValue = value;
}
/**
* Returns this attribute's name.
*/
public String getName() {
return mName;
}
/**
* Returns this attribute's value.
*/
public String getValue() {
return mValue;
}
}
/**
* Section.
*/
public static class Section {
private final int mStartOffset;
private final int mSizeBytes;
private final String mName;
private final List<Attribute> mAttributes;
/**
* Constructs a new {@code Section}.
*
* @param startOffset start offset (in bytes) of the section in the input file
* @param sizeBytes size (in bytes) of the section in the input file
* @param attrs attributes contained in the section
*/
public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
mStartOffset = startOffset;
mSizeBytes = sizeBytes;
String sectionName = null;
if (!attrs.isEmpty()) {
Attribute firstAttr = attrs.get(0);
if ("Name".equalsIgnoreCase(firstAttr.getName())) {
sectionName = firstAttr.getValue();
}
}
mName = sectionName;
mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
}
public String getName() {
return mName;
}
/**
* Returns the offset (in bytes) at which this section starts in the input.
*/
public int getStartOffset() {
return mStartOffset;
}
/**
* Returns the size (in bytes) of this section in the input.
*/
public int getSizeBytes() {
return mSizeBytes;
}
/**
* Returns this section's attributes, in the order in which they appear in the input.
*/
public List<Attribute> getAttributes() {
return mAttributes;
}
/**
* Returns the value of the specified attribute in this section or {@code null} if this
* section does not contain a matching attribute.
*/
public String getAttributeValue(Attributes.Name name) {
return getAttributeValue(name.toString());
}
/**
* Returns the value of the specified attribute in this section or {@code null} if this
* section does not contain a matching attribute.
*
* @param name name of the attribute. Attribute names are case-insensitive.
*/
public String getAttributeValue(String name) {
for (Attribute attr : mAttributes) {
if (attr.getName().equalsIgnoreCase(name)) {
return attr.getValue();
}
}
return null;
}
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.jar;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.jar.Attributes;
/**
* Producer of {@code META-INF/MANIFEST.MF} file.
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
*/
public abstract class ManifestWriter {
private static final byte[] CRLF = new byte[] {'\r', '\n'};
private static final int MAX_LINE_LENGTH = 70;
private ManifestWriter() {}
public static void writeMainSection(OutputStream out, Attributes attributes)
throws IOException {
// Main section must start with the Manifest-Version attribute.
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
if (manifestVersion == null) {
throw new IllegalArgumentException(
"Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
}
writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
if (attributes.size() > 1) {
SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
writeAttributes(out, namedAttributes);
}
writeSectionDelimiter(out);
}
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
throws IOException {
writeAttribute(out, "Name", name);
if (!attributes.isEmpty()) {
writeAttributes(out, getAttributesSortedByName(attributes));
}
writeSectionDelimiter(out);
}
static void writeSectionDelimiter(OutputStream out) throws IOException {
out.write(CRLF);
}
static void writeAttribute(OutputStream out, Attributes.Name name, String value)
throws IOException {
writeAttribute(out, name.toString(), value);
}
private static void writeAttribute(OutputStream out, String name, String value)
throws IOException {
writeLine(out, name + ": " + value);
}
private static void writeLine(OutputStream out, String line) throws IOException {
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
int offset = 0;
int remaining = lineBytes.length;
boolean firstLine = true;
while (remaining > 0) {
int chunkLength;
if (firstLine) {
// First line
chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
} else {
// Continuation line
out.write(CRLF);
out.write(' ');
chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
}
out.write(lineBytes, offset, chunkLength);
offset += chunkLength;
remaining -= chunkLength;
firstLine = false;
}
out.write(CRLF);
}
static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
for (Map.Entry<Object, Object> attribute : attributesEntries) {
String attrName = attribute.getKey().toString();
String attrValue = attribute.getValue().toString();
namedAttributes.put(attrName, attrValue);
}
return namedAttributes;
}
static void writeAttributes(
OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
String attrName = attribute.getKey();
String attrValue = attribute.getValue();
writeAttribute(out, attrName, attrValue);
}
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.jar;
import java.io.IOException;
import java.io.OutputStream;
import java.util.SortedMap;
import java.util.jar.Attributes;
/**
* Producer of JAR signature file ({@code *.SF}).
*
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
*/
public abstract class SignatureFileWriter {
private SignatureFileWriter() {}
public static void writeMainSection(OutputStream out, Attributes attributes)
throws IOException {
// Main section must start with the Signature-Version attribute.
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
if (signatureVersion == null) {
throw new IllegalArgumentException(
"Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
}
ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
if (attributes.size() > 1) {
SortedMap<String, String> namedAttributes =
ManifestWriter.getAttributesSortedByName(attributes);
namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
ManifestWriter.writeAttributes(out, namedAttributes);
}
writeSectionDelimiter(out);
}
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
throws IOException {
ManifestWriter.writeIndividualSection(out, name, attributes);
}
public static void writeSectionDelimiter(OutputStream out) throws IOException {
ManifestWriter.writeSectionDelimiter(out);
}
}

View File

@ -0,0 +1,463 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.oid;
import com.android.apksig.internal.util.InclusiveIntRange;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OidConstants {
public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
new HashMap<>();
static {
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_RSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_RSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
InclusiveIntRange.fromTo(21, 21));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_DSA,
InclusiveIntRange.from(0));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.from(9));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_DSA,
InclusiveIntRange.from(22));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_DSA,
InclusiveIntRange.from(22));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.from(18));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.from(21));
addSupportedSigAlg(
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
InclusiveIntRange.fromTo(21, 23));
addSupportedSigAlg(
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
InclusiveIntRange.from(21));
}
public static void addSupportedSigAlg(
String digestAlgorithmOid,
String signatureAlgorithmOid,
InclusiveIntRange... supportedApiLevels) {
SUPPORTED_SIG_ALG_OIDS.put(
digestAlgorithmOid + "with" + signatureAlgorithmOid,
Arrays.asList(supportedApiLevels));
}
public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
String digestAlgorithmOid,
String signatureAlgorithmOid) {
List<InclusiveIntRange> result =
SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
return (result != null) ? result : Collections.emptyList();
}
public static class OidToUserFriendlyNameMapper {
private OidToUserFriendlyNameMapper() {}
private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
static {
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
}
public static String getUserFriendlyNameForOid(String oid) {
return OID_TO_USER_FRIENDLY_NAME.get(oid);
}
}
public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
static {
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
}
public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
static {
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
}
private OidConstants() {}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
import com.android.apksig.internal.apk.v1.DigestAlgorithm;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.util.Pair;
import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
/**
* PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class AlgorithmIdentifier {
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
public String algorithm;
@Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
public Asn1OpaqueObject parameters;
public AlgorithmIdentifier() {}
public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
this.algorithm = algorithmOid;
this.parameters = parameters;
}
/**
* Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
* algorithm.
*/
public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
case SHA256:
return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
}
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
}
/**
* Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
* when signing with the specified key and digest algorithm.
*/
public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
throws InvalidKeyException {
String keyAlgorithm = publicKey.getAlgorithm();
String jcaDigestPrefixForSigAlg;
switch (digestAlgorithm) {
case SHA1:
jcaDigestPrefixForSigAlg = "SHA1";
break;
case SHA256:
jcaDigestPrefixForSigAlg = "SHA256";
break;
default:
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withRSA",
new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
AlgorithmIdentifier sigAlgId;
switch (digestAlgorithm) {
case SHA1:
sigAlgId =
new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
break;
case SHA256:
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
// 21 and higher. However, there are two ways to specify their SignedData
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
// the former.
sigAlgId =
new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
break;
default:
throw new IllegalArgumentException(
"Unexpected digest algorithm: " + digestAlgorithm);
}
String signingAlgorithmName =
jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
return Pair.of(signingAlgorithmName, sigAlgId);
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
return Pair.of(
jcaDigestPrefixForSigAlg + "withECDSA",
new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
} else {
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
}
}
public static String getJcaSignatureAlgorithm(
String digestAlgorithmOid,
String signatureAlgorithmOid) throws SignatureException {
// First check whether the signature algorithm OID alone is sufficient
String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
if (result != null) {
return result;
}
// Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
// with signature algorithm OID.
String suffix;
if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
suffix = "RSA";
} else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
suffix = "DSA";
} else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
suffix = "ECDSA";
} else {
throw new SignatureException(
"Unsupported JCA Signature algorithm"
+ " . Digest algorithm: " + digestAlgorithmOid
+ ", signature algorithm: " + signatureAlgorithmOid);
}
String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
// Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
// SHA algorithms.
if (jcaDigestAlg.startsWith("SHA-")) {
jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
}
return jcaDigestAlg + "with" + suffix;
}
public static String getJcaDigestAlgorithm(String oid)
throws SignatureException {
String result = OID_TO_JCA_DIGEST_ALG.get(oid);
if (result == null) {
throw new SignatureException("Unsupported digest algorithm: " + oid);
}
return result;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import java.util.List;
/**
* PKCS #7 {@code Attribute} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class Attribute {
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
public String attrType;
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
public List<Asn1OpaqueObject> attrValues;
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
/**
* PKCS #7 {@code ContentInfo} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class ContentInfo {
@Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
public String contentType;
@Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
public Asn1OpaqueObject content;
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
/**
* PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class EncapsulatedContentInfo {
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
public String contentType;
@Asn1Field(
index = 1,
type = Asn1Type.OCTET_STRING,
tagging = Asn1Tagging.EXPLICIT, tagNumber = 0,
optional = true)
public ByteBuffer content;
public EncapsulatedContentInfo() {}
public EncapsulatedContentInfo(String contentTypeOid) {
contentType = contentTypeOid;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import java.math.BigInteger;
/**
* PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class IssuerAndSerialNumber {
@Asn1Field(index = 0, type = Asn1Type.ANY)
public Asn1OpaqueObject issuer;
@Asn1Field(index = 1, type = Asn1Type.INTEGER)
public BigInteger certificateSerialNumber;
public IssuerAndSerialNumber() {}
public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
this.issuer = issuer;
this.certificateSerialNumber = certificateSerialNumber;
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
/**
* Assorted PKCS #7 constants from RFC 5652.
*/
public abstract class Pkcs7Constants {
private Pkcs7Constants() {}
public static final String OID_DATA = "1.2.840.113549.1.7.1";
public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
/**
* Indicates that an error was encountered while decoding a PKCS #7 structure.
*/
public class Pkcs7DecodingException extends Exception {
private static final long serialVersionUID = 1L;
public Pkcs7DecodingException(String message) {
super(message);
}
public Pkcs7DecodingException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
import java.util.List;
/**
* PKCS #7 {@code SignedData} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class SignedData {
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
public int version;
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
public List<AlgorithmIdentifier> digestAlgorithms;
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
public EncapsulatedContentInfo encapContentInfo;
@Asn1Field(
index = 3,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
optional = true)
public List<Asn1OpaqueObject> certificates;
@Asn1Field(
index = 4,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
optional = true)
public List<ByteBuffer> crls;
@Asn1Field(index = 5, type = Asn1Type.SET_OF)
public List<SignerInfo> signerInfos;
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
/**
* PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.CHOICE)
public class SignerIdentifier {
@Asn1Field(type = Asn1Type.SEQUENCE)
public IssuerAndSerialNumber issuerAndSerialNumber;
@Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
public ByteBuffer subjectKeyIdentifier;
public SignerIdentifier() {}
public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
this.issuerAndSerialNumber = issuerAndSerialNumber;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.pkcs7;
import com.android.apksig.internal.asn1.Asn1Class;
import com.android.apksig.internal.asn1.Asn1Field;
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
import com.android.apksig.internal.asn1.Asn1Type;
import com.android.apksig.internal.asn1.Asn1Tagging;
import java.nio.ByteBuffer;
import java.util.List;
/**
* PKCS #7 {@code SignerInfo} as specified in RFC 5652.
*/
@Asn1Class(type = Asn1Type.SEQUENCE)
public class SignerInfo {
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
public int version;
@Asn1Field(index = 1, type = Asn1Type.CHOICE)
public SignerIdentifier sid;
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
public AlgorithmIdentifier digestAlgorithm;
@Asn1Field(
index = 3,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
optional = true)
public Asn1OpaqueObject signedAttrs;
@Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
public AlgorithmIdentifier signatureAlgorithm;
@Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
public ByteBuffer signature;
@Asn1Field(
index = 6,
type = Asn1Type.SET_OF,
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
optional = true)
public List<Attribute> unsignedAttrs;
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
/**
* Android SDK version / API Level constants.
*/
public abstract class AndroidSdkVersion {
/** Hidden constructor to prevent instantiation. */
private AndroidSdkVersion() {}
/** Android 1.0 */
public static final int INITIAL_RELEASE = 1;
/** Android 2.3. */
public static final int GINGERBREAD = 9;
/** Android 3.0 */
public static final int HONEYCOMB = 11;
/** Android 4.3. The revenge of the beans. */
public static final int JELLY_BEAN_MR2 = 18;
/** Android 4.4. KitKat, another tasty treat. */
public static final int KITKAT = 19;
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
public static final int LOLLIPOP = 21;
/** Android 6.0. M is for Marshmallow! */
public static final int M = 23;
/** Android 7.0. N is for Nougat. */
public static final int N = 24;
/** Android O. */
public static final int O = 26;
/** Android P. */
public static final int P = 28;
/** Android R. */
public static final int R = 30;
}

View File

@ -0,0 +1,240 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.ReadableDataSink;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Growable byte array which can be appended to via {@link DataSink} interface and read from via
* {@link DataSource} interface.
*/
public class ByteArrayDataSink implements ReadableDataSink {
private static final int MAX_READ_CHUNK_SIZE = 65536;
private byte[] mArray;
private int mSize;
public ByteArrayDataSink() {
this(65536);
}
public ByteArrayDataSink(int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException("initial capacity: " + initialCapacity);
}
mArray = new byte[initialCapacity];
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
if (offset < 0) {
// Must perform this check because System.arraycopy below doesn't perform it when
// length == 0
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (offset > buf.length) {
// Must perform this check because System.arraycopy below doesn't perform it when
// length == 0
throw new IndexOutOfBoundsException(
"offset: " + offset + ", buf.length: " + buf.length);
}
if (length == 0) {
return;
}
ensureAvailable(length);
System.arraycopy(buf, offset, mArray, mSize, length);
mSize += length;
}
@Override
public void consume(ByteBuffer buf) throws IOException {
if (!buf.hasRemaining()) {
return;
}
if (buf.hasArray()) {
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
buf.position(buf.limit());
return;
}
ensureAvailable(buf.remaining());
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
while (buf.hasRemaining()) {
int chunkSize = Math.min(buf.remaining(), tmp.length);
buf.get(tmp, 0, chunkSize);
System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
mSize += chunkSize;
}
}
private void ensureAvailable(int minAvailable) throws IOException {
if (minAvailable <= 0) {
return;
}
long minCapacity = ((long) mSize) + minAvailable;
if (minCapacity <= mArray.length) {
return;
}
if (minCapacity > Integer.MAX_VALUE) {
throw new IOException(
"Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
}
int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
mArray = Arrays.copyOf(mArray, newSize);
}
@Override
public long size() {
return mSize;
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
return ByteBuffer.wrap(mArray, (int) offset, size).slice();
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset and size to int.
sink.consume(mArray, (int) offset, (int) size);
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
dest.put(mArray, (int) offset, size);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
}
}
@Override
public DataSource slice(long offset, long size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset and size to int.
return new SliceDataSource((int) offset, (int) size);
}
/**
* Slice of the growable byte array. The slice's offset and size in the array are fixed.
*/
private class SliceDataSource implements DataSource {
private final int mSliceOffset;
private final int mSliceSize;
private SliceDataSource(int offset, int size) {
mSliceOffset = offset;
mSliceSize = size;
}
@Override
public long size() {
return mSliceSize;
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow.
return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow.
dest.put(mArray, (int) (mSliceOffset + offset), size);
}
@Override
public DataSource slice(long offset, long size) {
checkChunkValid(offset, size);
// checkChunkValid combined with the way instances of this class are constructed ensures
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSliceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSliceSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSliceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
+ ")");
}
}
}
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* {@link DataSource} backed by a {@link ByteBuffer}.
*/
public class ByteBufferDataSource implements DataSource {
private final ByteBuffer mBuffer;
private final int mSize;
/**
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
* buffer between the buffer's position and limit.
*/
public ByteBufferDataSource(ByteBuffer buffer) {
this(buffer, true);
}
/**
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
* buffer between the buffer's position and limit.
*/
private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
mBuffer = (sliceRequired) ? buffer.slice() : buffer;
mSize = buffer.remaining();
}
@Override
public long size() {
return mSize;
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) {
checkChunkValid(offset, size);
// checkChunkValid ensures that it's OK to cast offset to int.
int chunkPosition = (int) offset;
int chunkLimit = chunkPosition + size;
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
// and limit fields, to be more specific). We thus use synchronization around these
// state-changing operations to make instances of this class thread-safe.
synchronized (mBuffer) {
// ByteBuffer.limit(int) and .position(int) check that that the position >= limit
// invariant is not broken. Thus, the only way to safely change position and limit
// without caring about their current values is to first set position to 0 or set the
// limit to capacity.
mBuffer.position(0);
mBuffer.limit(chunkLimit);
mBuffer.position(chunkPosition);
return mBuffer.slice();
}
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) {
dest.put(getByteBuffer(offset, size));
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
if ((size < 0) || (size > mSize)) {
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
}
sink.consume(getByteBuffer(offset, (int) size));
}
@Override
public ByteBufferDataSource slice(long offset, long size) {
if ((offset == 0) && (size == mSize)) {
return this;
}
if ((size < 0) || (size > mSize)) {
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
}
return new ByteBufferDataSource(
getByteBuffer(offset, (int) size),
false // no need to slice -- it's already a slice
);
}
private void checkChunkValid(long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + mSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > mSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import com.android.apksig.util.DataSink;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
/**
* Data sink which stores all received data into the associated {@link ByteBuffer}.
*/
public class ByteBufferSink implements DataSink {
private final ByteBuffer mBuffer;
public ByteBufferSink(ByteBuffer buffer) {
mBuffer = buffer;
}
public ByteBuffer getBuffer() {
return mBuffer;
}
@Override
public void consume(byte[] buf, int offset, int length) throws IOException {
try {
mBuffer.put(buf, offset, length);
} catch (BufferOverflowException e) {
throw new IOException(
"Insufficient space in output buffer for " + length + " bytes", e);
}
}
@Override
public void consume(ByteBuffer buf) throws IOException {
int length = buf.remaining();
try {
mBuffer.put(buf);
} catch (BufferOverflowException e) {
throw new IOException(
"Insufficient space in output buffer for " + length + " bytes", e);
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import java.nio.ByteBuffer;
public final class ByteBufferUtils {
private ByteBufferUtils() {}
/**
* Returns the remaining data of the provided buffer as a new byte array and advances the
* position of the buffer to the buffer's limit.
*/
public static byte[] toByteArray(ByteBuffer buf) {
byte[] result = new byte[buf.remaining()];
buf.get(result);
return result;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Utilities for byte arrays and I/O streams.
*/
public final class ByteStreams {
private ByteStreams() {}
/**
* Returns the data remaining in the provided input stream as a byte array
*/
public static byte[] toByteArray(InputStream in) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buf = new byte[16384];
int chunkSize;
while ((chunkSize = in.read(buf)) != -1) {
result.write(buf, 0, chunkSize);
}
return result.toByteArray();
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */
public class ChainedDataSource implements DataSource {
private final DataSource[] mSources;
private final long mTotalSize;
public ChainedDataSource(DataSource... sources) {
mSources = sources;
mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum();
}
@Override
public long size() {
return mTotalSize;
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
if (offset + size > mTotalSize) {
throw new IndexOutOfBoundsException("Requested more than available");
}
for (DataSource src : mSources) {
// Offset is beyond the current source. Skip.
if (offset >= src.size()) {
offset -= src.size();
continue;
}
// If the remaining is enough, finish it.
long remaining = src.size() - offset;
if (remaining >= size) {
src.feed(offset, size, sink);
break;
}
// If the remaining is not enough, consume all.
src.feed(offset, remaining, sink);
size -= remaining;
offset = 0;
}
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
if (offset + size > mTotalSize) {
throw new IndexOutOfBoundsException("Requested more than available");
}
// Skip to the first DataSource we need.
Pair<Integer, Long> firstSource = locateDataSource(offset);
int i = firstSource.getFirst();
offset = firstSource.getSecond();
// Return the current source's ByteBuffer if it fits.
if (offset + size <= mSources[i].size()) {
return mSources[i].getByteBuffer(offset, size);
}
// Otherwise, read into a new buffer.
ByteBuffer buffer = ByteBuffer.allocate(size);
for (; i < mSources.length && buffer.hasRemaining(); i++) {
long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
offset = 0; // may not be zero for the first source, but reset after that.
}
buffer.rewind();
return buffer;
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
feed(offset, size, new ByteBufferSink(dest));
}
@Override
public DataSource slice(long offset, long size) {
// Find the first slice.
Pair<Integer, Long> firstSource = locateDataSource(offset);
int beginIndex = firstSource.getFirst();
long beginLocalOffset = firstSource.getSecond();
DataSource beginSource = mSources[beginIndex];
if (beginLocalOffset + size <= beginSource.size()) {
return beginSource.slice(beginLocalOffset, size);
}
// Add the first slice to chaining, followed by the middle full slices, then the last.
ArrayList<DataSource> sources = new ArrayList<>();
sources.add(beginSource.slice(
beginLocalOffset, beginSource.size() - beginLocalOffset));
Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
int endIndex = lastSource.getFirst();
long endLocalOffset = lastSource.getSecond();
for (int i = beginIndex + 1; i < endIndex; i++) {
sources.add(mSources[i]);
}
sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
return new ChainedDataSource(sources.toArray(new DataSource[0]));
}
/**
* Find the index of DataSource that offset is at.
* @return Pair of DataSource index and the local offset in the DataSource.
*/
private Pair<Integer, Long> locateDataSource(long offset) {
long localOffset = offset;
for (int i = 0; i < mSources.length; i++) {
if (localOffset < mSources[i].size()) {
return Pair.of(i, localOffset);
}
localOffset -= mSources[i].size();
}
throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
", totalSize: " + mTotalSize);
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.security.auth.x500.X500Principal;
/**
* {@link X509Certificate} which delegates all method invocations to the provided delegate
* {@code X509Certificate}.
*/
public class DelegatingX509Certificate extends X509Certificate {
private static final long serialVersionUID = 1L;
private final X509Certificate mDelegate;
public DelegatingX509Certificate(X509Certificate delegate) {
this.mDelegate = delegate;
}
@Override
public Set<String> getCriticalExtensionOIDs() {
return mDelegate.getCriticalExtensionOIDs();
}
@Override
public byte[] getExtensionValue(String oid) {
return mDelegate.getExtensionValue(oid);
}
@Override
public Set<String> getNonCriticalExtensionOIDs() {
return mDelegate.getNonCriticalExtensionOIDs();
}
@Override
public boolean hasUnsupportedCriticalExtension() {
return mDelegate.hasUnsupportedCriticalExtension();
}
@Override
public void checkValidity()
throws CertificateExpiredException, CertificateNotYetValidException {
mDelegate.checkValidity();
}
@Override
public void checkValidity(Date date)
throws CertificateExpiredException, CertificateNotYetValidException {
mDelegate.checkValidity(date);
}
@Override
public int getVersion() {
return mDelegate.getVersion();
}
@Override
public BigInteger getSerialNumber() {
return mDelegate.getSerialNumber();
}
@Override
public Principal getIssuerDN() {
return mDelegate.getIssuerDN();
}
@Override
public Principal getSubjectDN() {
return mDelegate.getSubjectDN();
}
@Override
public Date getNotBefore() {
return mDelegate.getNotBefore();
}
@Override
public Date getNotAfter() {
return mDelegate.getNotAfter();
}
@Override
public byte[] getTBSCertificate() throws CertificateEncodingException {
return mDelegate.getTBSCertificate();
}
@Override
public byte[] getSignature() {
return mDelegate.getSignature();
}
@Override
public String getSigAlgName() {
return mDelegate.getSigAlgName();
}
@Override
public String getSigAlgOID() {
return mDelegate.getSigAlgOID();
}
@Override
public byte[] getSigAlgParams() {
return mDelegate.getSigAlgParams();
}
@Override
public boolean[] getIssuerUniqueID() {
return mDelegate.getIssuerUniqueID();
}
@Override
public boolean[] getSubjectUniqueID() {
return mDelegate.getSubjectUniqueID();
}
@Override
public boolean[] getKeyUsage() {
return mDelegate.getKeyUsage();
}
@Override
public int getBasicConstraints() {
return mDelegate.getBasicConstraints();
}
@Override
public byte[] getEncoded() throws CertificateEncodingException {
return mDelegate.getEncoded();
}
@Override
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchProviderException, SignatureException {
mDelegate.verify(key);
}
@Override
public void verify(PublicKey key, String sigProvider)
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
NoSuchProviderException, SignatureException {
mDelegate.verify(key, sigProvider);
}
@Override
public String toString() {
return mDelegate.toString();
}
@Override
public PublicKey getPublicKey() {
return mDelegate.getPublicKey();
}
@Override
public X500Principal getIssuerX500Principal() {
return mDelegate.getIssuerX500Principal();
}
@Override
public X500Principal getSubjectX500Principal() {
return mDelegate.getSubjectX500Principal();
}
@Override
public List<String> getExtendedKeyUsage() throws CertificateParsingException {
return mDelegate.getExtendedKeyUsage();
}
@Override
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
return mDelegate.getSubjectAlternativeNames();
}
@Override
public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
return mDelegate.getIssuerAlternativeNames();
}
@Override
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
mDelegate.verify(key, sigProvider);
}
}

View File

@ -0,0 +1,191 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.util;
import com.android.apksig.util.DataSink;
import com.android.apksig.util.DataSource;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
*/
public class FileChannelDataSource implements DataSource {
private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
private final FileChannel mChannel;
private final long mOffset;
private final long mSize;
/**
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
* whole file. Changes to the contents of the file, including the size of the file,
* will be visible in this data source.
*/
public FileChannelDataSource(FileChannel channel) {
mChannel = channel;
mOffset = 0;
mSize = -1;
}
/**
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
* specified region of the provided file. Changes to the contents of the file will be visible in
* this data source.
*
* @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
*/
public FileChannelDataSource(FileChannel channel, long offset, long size) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + size);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
mChannel = channel;
mOffset = offset;
mSize = size;
}
@Override
public long size() {
if (mSize == -1) {
try {
return mChannel.size();
} catch (IOException e) {
return 0;
}
} else {
return mSize;
}
}
@Override
public FileChannelDataSource slice(long offset, long size) {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if ((offset == 0) && (size == sourceSize)) {
return this;
}
return new FileChannelDataSource(mChannel, mOffset + offset, size);
}
@Override
public void feed(long offset, long size, DataSink sink) throws IOException {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if (size == 0) {
return;
}
long chunkOffsetInFile = mOffset + offset;
long remaining = size;
ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
while (remaining > 0) {
int chunkSize = (int) Math.min(remaining, buf.capacity());
int chunkRemaining = chunkSize;
buf.limit(chunkSize);
synchronized (mChannel) {
mChannel.position(chunkOffsetInFile);
while (chunkRemaining > 0) {
int read = mChannel.read(buf);
if (read < 0) {
throw new IOException("Unexpected EOF encountered");
}
chunkRemaining -= read;
}
}
buf.flip();
sink.consume(buf);
buf.clear();
chunkOffsetInFile += chunkSize;
remaining -= chunkSize;
}
}
@Override
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
long sourceSize = size();
checkChunkValid(offset, size, sourceSize);
if (size == 0) {
return;
}
if (size > dest.remaining()) {
throw new BufferOverflowException();
}
long offsetInFile = mOffset + offset;
int remaining = size;
int prevLimit = dest.limit();
try {
// FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
// the buffer's limit to avoid reading more than size bytes.
dest.limit(dest.position() + size);
while (remaining > 0) {
int chunkSize;
synchronized (mChannel) {
mChannel.position(offsetInFile);
chunkSize = mChannel.read(dest);
}
offsetInFile += chunkSize;
remaining -= chunkSize;
}
} finally {
dest.limit(prevLimit);
}
}
@Override
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
ByteBuffer result = ByteBuffer.allocate(size);
copyTo(offset, size, result);
result.flip();
return result;
}
private static void checkChunkValid(long offset, long size, long sourceSize) {
if (offset < 0) {
throw new IndexOutOfBoundsException("offset: " + offset);
}
if (size < 0) {
throw new IndexOutOfBoundsException("size: " + size);
}
if (offset > sourceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") > source size (" + sourceSize + ")");
}
long endOffset = offset + size;
if (endOffset < offset) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size + ") overflow");
}
if (endOffset > sourceSize) {
throw new IndexOutOfBoundsException(
"offset (" + offset + ") + size (" + size
+ ") > source size (" + sourceSize +")");
}
}
}

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