Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
167fe89908 | ||
|
|
892b3ece41 | ||
|
|
6dfa056e41 | ||
|
|
1a8afd86be | ||
|
|
e0ef18659b | ||
|
|
52be469b2d | ||
|
|
6895289ec8 | ||
|
|
3e86cbcf60 | ||
|
|
536d294f7b | ||
|
|
8e7888df35 | ||
|
|
79703ce0dd | ||
|
|
25aa08d8e4 | ||
|
|
35a77cd03d | ||
|
|
1198927d85 | ||
|
|
f1c5e7660e | ||
|
|
0f99403702 | ||
|
|
10170a274b | ||
|
|
47630e6620 | ||
|
|
bb5799b414 | ||
|
|
2110e1b102 | ||
|
|
3dfbd102b4 | ||
|
|
0ba6500625 | ||
|
|
2e0fdcf507 | ||
|
|
b72f5315fe | ||
|
|
2c3bafdac3 | ||
|
|
6d0b55ac26 | ||
|
|
e0bd573988 | ||
|
|
3de3046b8a | ||
|
|
7ccc8dd6b6 | ||
|
|
1d0ae20920 | ||
|
|
cbee05d35b | ||
|
|
ad937488a6 | ||
|
|
9f53f1fe4d | ||
|
|
b33dea3cb0 | ||
|
|
35d7bf6e0b | ||
|
|
9af8c85994 | ||
|
|
ea1233f474 | ||
|
|
a9fe136c55 | ||
|
|
0f5d2abaef | ||
|
|
4ce4983ac8 | ||
|
|
9cc4e31ba1 | ||
|
|
b9fb515832 | ||
|
|
46ad431a5c | ||
|
|
c2e6711575 | ||
|
|
c6d2a61652 | ||
|
|
e3bf0e8341 | ||
|
|
70fa2eb821 | ||
|
|
afe94b5dba | ||
|
|
846f83918b | ||
|
|
27caf83ef3 | ||
|
|
24761fe05b | ||
|
|
3726530af9 | ||
|
|
8e0f5dc697 | ||
|
|
24d6b47941 | ||
|
|
7fd3ae0702 | ||
|
|
10caf7bbd8 | ||
|
|
3ee63bdd47 | ||
|
|
2e4f6398f6 | ||
|
|
542eeda11c | ||
|
|
0ced054631 | ||
|
|
2e74222244 | ||
|
|
9f23e4d7b3 | ||
|
|
05abc83e6e | ||
|
|
18ea016014 | ||
|
|
b04264793b | ||
|
|
cb76649fdf | ||
|
|
b19c9fb132 | ||
|
|
aa759f5187 | ||
|
|
ef504d5a71 | ||
|
|
8cfa98e971 | ||
|
|
4577168953 | ||
|
|
ac74a30fcf | ||
|
|
ca802940b4 | ||
|
|
923858caa9 | ||
|
|
68ae37093a | ||
|
|
000aeb920e | ||
|
|
ce8da252c3 | ||
|
|
de6a664729 | ||
|
|
12bf20f656 | ||
|
|
c58493726e | ||
|
|
541e637df0 | ||
|
|
9d570a91d2 | ||
|
|
d54fe52060 | ||
|
|
1eb86355cb | ||
|
|
e48ecac3cd | ||
|
|
850a8fb35c | ||
|
|
edd1ec2cc1 | ||
|
|
622d4055d9 | ||
|
|
36aa37359b | ||
|
|
7c44cd3ebb | ||
|
|
95a3581ce6 | ||
|
|
41b27de058 | ||
|
|
0db262681b | ||
|
|
b75c74096c | ||
|
|
c68c67b192 | ||
|
|
9ffd0971ae | ||
|
|
b276f2f25f | ||
|
|
e36c77c450 | ||
|
|
4c8cb4eb97 | ||
|
|
746abe91ec | ||
|
|
df0dee36c2 | ||
|
|
5af07c5f07 | ||
|
|
ed5dd9af21 | ||
|
|
d1cfafa944 | ||
|
|
b4428dc1dc | ||
|
|
f649687b74 | ||
|
|
905f327031 | ||
|
|
12f857a936 | ||
|
|
01daf4a63e | ||
|
|
cf3769959f | ||
|
|
10e853f968 | ||
|
|
ac6995be8d | ||
|
|
045d6f15a7 | ||
|
|
2d16f1cb57 | ||
|
|
ebe1d451b0 | ||
|
|
7faa4bea0d | ||
|
|
3785d3130c | ||
|
|
9daa1c27c4 | ||
|
|
67c722ed43 | ||
|
|
3c8f1809aa | ||
|
|
c5a3059e31 | ||
|
|
f305a59880 | ||
|
|
aa18e5f2f1 | ||
|
|
ac25b95eae | ||
|
|
a92fa304f1 | ||
|
|
2f3f3d6480 | ||
|
|
0d108393fc | ||
|
|
8d02f8c6a3 | ||
|
|
87d284ad97 | ||
|
|
c7ffdbec93 | ||
|
|
f9aad6ce9c | ||
|
|
7a64fd4c11 | ||
|
|
909af0cbed | ||
|
|
34db6fc79f | ||
|
|
b025f150c9 | ||
|
|
fea6b38c1e | ||
|
|
e2a03427f9 | ||
|
|
654cb5fdd5 | ||
|
|
333419b716 | ||
|
|
c66b1e4622 | ||
|
|
ac55e2f8fc | ||
|
|
24718e7d25 | ||
|
|
72e1fe9621 | ||
|
|
0c7bf96bf5 | ||
|
|
f1dde6c8bc | ||
|
|
3177a8352f | ||
|
|
3cb024b7f5 | ||
|
|
0a09d16500 | ||
|
|
a79b1ad785 | ||
|
|
4388c53929 | ||
|
|
02f000d37b | ||
|
|
7259e5e315 | ||
|
|
7ce744b5af | ||
|
|
57f21c8d39 | ||
|
|
07f27a8438 | ||
|
|
4f4dc55b4e | ||
|
|
8e9cda8e93 | ||
|
|
d5cfabd73e | ||
|
|
4f1b371e75 | ||
|
|
4d8b50912d | ||
|
|
660c717cfb | ||
|
|
70d3d9a5b2 | ||
|
|
464da25ca1 | ||
|
|
c2795120e1 | ||
|
|
b10fb46f1f | ||
|
|
e39897176a | ||
|
|
6e7e6fab74 | ||
|
|
94009b6829 | ||
|
|
289217fefc | ||
|
|
97feab1203 | ||
|
|
a9a3b20eb3 | ||
|
|
7af1e168a4 | ||
|
|
c8bf34e0d7 | ||
|
|
86d2282cd6 | ||
|
|
cfdb23d6dc | ||
|
|
1874ddd15b | ||
|
|
a004c308cc | ||
|
|
863337c789 | ||
|
|
64d91eed33 | ||
|
|
d0c3a3e14d | ||
|
|
464636d87d | ||
|
|
0b0a1f456f | ||
|
|
302030ca65 | ||
|
|
3e63738542 | ||
|
|
d6823c9c8e | ||
|
|
6bc3866660 | ||
|
|
f41f4dd8ba | ||
|
|
cde39812ce | ||
|
|
03c2abc901 | ||
|
|
7646334525 | ||
|
|
f8d1cd3fab | ||
|
|
4ea94be64a | ||
|
|
dc0470e75a | ||
|
|
5d49ecdd7d | ||
|
|
ef2441116d | ||
|
|
207909c7be | ||
|
|
ddb3dd2d95 | ||
|
|
8bb980fe97 | ||
|
|
3c3a201ed8 | ||
|
|
81bb37e929 | ||
|
|
ae34690eb2 | ||
|
|
285b0f5a69 | ||
|
|
a5327fce07 | ||
|
|
a4b750f9cd | ||
|
|
943344d4bb | ||
|
|
79f1b92960 | ||
|
|
575ae38a28 | ||
|
|
f90e2a5c80 | ||
|
|
4d9b2877fe | ||
|
|
4a955947a2 | ||
|
|
7307c136ee | ||
|
|
657e1b1d0f | ||
|
|
43edf11275 | ||
|
|
32ec4c6715 | ||
|
|
84ac57d69e | ||
|
|
328e7745b7 | ||
|
|
ccf22eaef4 | ||
|
|
c89d42f368 | ||
|
|
41192ea7f5 | ||
|
|
7840987a7c | ||
|
|
fcbe0a7f55 | ||
|
|
f12894c95d | ||
|
|
40f139fda2 | ||
|
|
b8507a371e | ||
|
|
e5a71ce906 | ||
|
|
f9e708fe1e | ||
|
|
50ba6f2bd2 | ||
|
|
2484371f8c | ||
|
|
ba675c4212 | ||
|
|
6f9bc3135a | ||
|
|
4475d4d018 | ||
|
|
08e45db49d | ||
|
|
85c76065a5 | ||
|
|
13582c3f1b | ||
|
|
ae941a4955 | ||
|
|
72da7ebfe4 | ||
|
|
8e1bacbfac | ||
|
|
747c19c028 | ||
|
|
aca83fc1ee | ||
|
|
35babd089d | ||
|
|
457ea56aab | ||
|
|
ed22de2360 | ||
|
|
8d386b44bc | ||
|
|
c4c88c40c5 | ||
|
|
e637ad6acb | ||
|
|
3b9e08e1c7 | ||
|
|
4a2aa8a975 | ||
|
|
9c08eda38a | ||
|
|
713d972d16 | ||
|
|
d4b3705a9e | ||
|
|
c543d1540c | ||
|
|
2956ff08bd | ||
|
|
ff09dec4e4 | ||
|
|
5e02479aa5 | ||
|
|
6157b9cf82 | ||
|
|
9260633e26 | ||
|
|
1a2ae6b1a8 | ||
|
|
282b4adaff | ||
|
|
8519b5a096 | ||
|
|
4a00be922e | ||
|
|
31f30cf1b1 | ||
|
|
62737cc32f | ||
|
|
c7306b2358 | ||
|
|
779958dfeb | ||
|
|
d0ca6872b0 | ||
|
|
161f191f3d | ||
|
|
2cfa46ed05 | ||
|
|
005e2b3fa1 | ||
|
|
3826dfb1d4 | ||
|
|
81bb9a852f | ||
|
|
37e60673d7 | ||
|
|
9b618f7ecc | ||
|
|
0ce3ceb900 | ||
|
|
4833226241 | ||
|
|
5cae964a1e | ||
|
|
70eaa35bc9 | ||
|
|
38bd3fede1 | ||
|
|
e1e32a7f5e | ||
|
|
b77d0fb47a | ||
|
|
4076b2f900 | ||
|
|
1bc409b54a | ||
|
|
1247a0ade9 | ||
|
|
2fd688ff9e | ||
|
|
b0274946bf | ||
|
|
b78c132742 | ||
|
|
b33c46de82 | ||
|
|
25c8bba1b0 | ||
|
|
089f5cb7ec | ||
|
|
95a6a9dca6 | ||
|
|
e19759a7e4 | ||
|
|
8f95ffeebf | ||
|
|
98f08feb16 | ||
|
|
50baaffc36 | ||
|
|
66e9f43bba | ||
|
|
0cb13bb6a3 |
No files matched your search
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: 功能申请 / Feature request
|
||||
about: 有个想法 / Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**这个功能是用来解决问题的吗? / Is your feature request related to a problem? Please describe.**
|
||||
描述这个问题。
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**描述你想要的解决方法 / Describe the solution you'd like**
|
||||
描述应通过何种行为来解决这个问题。
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**备注 / Additional context**
|
||||
还有别的想说的吗?
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: Bug 反馈 / Bug report
|
||||
about: 出问题了 / Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**简略描述 / Describe the bug**
|
||||
大概说下是什么问题。
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**如何重现 / To Reproduce**
|
||||
重现的步骤:
|
||||
1. 打开什么什么
|
||||
2. 点击什么什么
|
||||
3. 出现什么什么问题
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**预期行为 / Expected behavior**
|
||||
应该是什么样子的?
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**截图 / Screenshots**
|
||||
能截图的话就发截图,请注意风序良俗。
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**设备型号与 Android 版本 / Device model and Android version**
|
||||
- 设备型号:【比如 Pixel 6 XXXL】
|
||||
- Android 版本:【比如 Android 12.1】
|
||||
- Device model: [e.g. Pixel 6 XXXL]
|
||||
- Android version: [e.g. Android 12.1]
|
||||
|
||||
**备注 / Additional context**
|
||||
还有什么想说的?
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Bug 反馈 / Bug report
|
||||
description: 提交一个问题报告 / Create a bug report to help us improve
|
||||
labels:
|
||||
- "bug"
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: 复现步骤 / Step to reproduce
|
||||
description: |
|
||||
在此处写下复现的方式
|
||||
Describe how to reproduce here
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id : expected
|
||||
attributes:
|
||||
label: 预期行为 / Expected behaviour
|
||||
description: |
|
||||
在此处说明正常情况下应用的预期行为
|
||||
Describe what should happened here
|
||||
placeholder: |
|
||||
它应该XXX...
|
||||
It should be ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: 实际行为 / Actual Behaviour
|
||||
description: |
|
||||
在此处描绘应用的实际行为,最好附上截图
|
||||
Describe what actually happened here, screenshots is better
|
||||
placeholder: |
|
||||
实际上它XXX...
|
||||
Actually it ...
|
||||
[截图] / [Screenshots]
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: EhViewer 版本号 / EhViewer version code
|
||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
||||
placeholder: 四位版本号 / 4 digits version code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: ci
|
||||
attributes:
|
||||
label: EhViewer CI 版本 / EhViewer CI Version
|
||||
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
|
||||
placeholder: https://github.com/asuka-mio/Ehviewer/actions/runs/XXXXXXXXXX
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: Android 系统版本 / Android version
|
||||
description: Android 分支名称 + 版本号 / AOSP fork name + version code
|
||||
placeholder: MIUI 12.5, ArrowOS 12.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: 设备信息 / Device info
|
||||
description: 特殊设备在此填入设备信息 / Put message here if you device is special
|
||||
placeholder: WSA, arm-v7a
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: 应用日志 / App logs
|
||||
description: 您可以通过设置-高级-导出日志来获得日志文件 / You can get logs file in Settings - Advanced - Dump logcat
|
||||
placeholder: 06-15 17:44:53.704 23382 23382 E ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: check
|
||||
attributes:
|
||||
label: 自查步骤 / Verify steps
|
||||
description: |
|
||||
请确认您已经遵守所有必选项
|
||||
Please ensure you have obtained all needed options
|
||||
options:
|
||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||
required: false
|
||||
|
||||
- label: 您已仔细查看并知情 [Q&A](https://github.com/asuka-mio/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/asuka-mio/Ehviewer/issues/61) carefully
|
||||
required: true
|
||||
|
||||
- label: 您已搜索过 [Issue Tracker](https://github.com/asuka-mio/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/asuka-mio/Ehviewer/issues), No duplicate or related open issue has been found
|
||||
required: true
|
||||
|
||||
- label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告,烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs
|
||||
required: true
|
||||
|
||||
- label: 您确保已使用最新 CI 版本测试,并且该问题在最新 CI 版本中并未解决 / This bug have not solved in latest CI version
|
||||
required: true
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: 所有其他问题 / All other questions
|
||||
url: https://github.com/asuka-mio/Ehviewer/discussions
|
||||
about: 转到 Discussions / Turn to discussions
|
||||
@@ -0,0 +1,85 @@
|
||||
name: 功能请求 / Feature request
|
||||
description: 提出一个功能建议 / Suggest an idea for this project
|
||||
labels:
|
||||
- "enhancement"
|
||||
body:
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
label: 需求 / Requirement
|
||||
description: |
|
||||
在此处描述您的需求。这通常会是一个您想要的功能
|
||||
Ddescribe what you need here.
|
||||
placeholder: |
|
||||
我需要 XXX 功能 ...
|
||||
I want ABC feature ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: impl
|
||||
attributes:
|
||||
label: 建议实现 / Suggested implements
|
||||
description: |
|
||||
在此处表述您建议的实现方式。如有可能,UI 类功能请求还请尽量附上图示
|
||||
Describe your suggested implements here, It's recommend to add a photo if you are making a UI feature request.
|
||||
placeholder: |
|
||||
建议在 XX 处添加 XX ...
|
||||
I recommend add ABC feature to DEF...
|
||||
图片(如果有)/ Photos (if exists)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: more
|
||||
attributes:
|
||||
label: 其他细节 / Addition details
|
||||
description: |
|
||||
在此处写下其他您想说的内容
|
||||
Describe addition details here
|
||||
placeholder: |
|
||||
其他有用的信息与附件
|
||||
Additional details and attachments
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: EhViewer 版本号 / EhViewer version code
|
||||
description: 您可以在设置 - 关于处找到版本号 / You can get version code in Settings - About
|
||||
placeholder: 四位版本号 / 4 digits version code
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: ci
|
||||
attributes:
|
||||
label: EhViewer CI 版本 / EhViewer CI Version
|
||||
description: 直接将对应 CI 下载页面网址填入即可 / Simply drop CI download page url here
|
||||
placeholder: https://github.com/asuka-mio/Ehviewer/actions/runs/XXXXXXXXXX
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: check
|
||||
attributes:
|
||||
label: 自查步骤 / Verify steps
|
||||
description: |
|
||||
请确认您已经遵守所有必选项
|
||||
Please ensure you have obtained all needed options
|
||||
options:
|
||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||
required: false
|
||||
|
||||
- label: 您已仔细查看并知情 [Q&A](https://github.com/asuka-mio/Ehviewer/issues/61) 中的内容 / You have checked [Q&A](https://github.com/asuka-mio/Ehviewer/issues/61) carefully
|
||||
required: true
|
||||
|
||||
- label: 您已搜索过 [Issue Tracker](https://github.com/asuka-mio/Ehviewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/asuka-mio/Ehviewer/issues), No duplicate or related open issue has been found
|
||||
required: true
|
||||
|
||||
- label: 您确保这个 Issue 只提及一个功能。如果您有多个功能请求,烦请发起多个 Issue / Ensure there is only one feature request in this issue. Please make mutiply issue for mutiply feature request
|
||||
required: true
|
||||
|
||||
- label: 您确保已使用最新 CI 版本测试,并且该功能在最新 CI 版本中并未按照预期实现 / This feature don't implement correctly in latest CI version
|
||||
required: true
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
name: Sad Panda / 下载太慢 / Slow download speed
|
||||
about: 所有其他问题 / All other questions
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
加入我们的 Telegram 群组,[https://t.me/ehviewer](https://t.me/ehviewer)。温柔[大小]姐姐手把手帮你解决问题。
|
||||
Join our Telegram group, [https://t.me/ehviewer](https://t.me/ehviewer). Administrators will help you solve the problem.
|
||||
@@ -1,57 +1,54 @@
|
||||
name: EhViewer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 1.8
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
- name: Build
|
||||
env:
|
||||
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
|
||||
ALIAS_NAME: ${{ secrets.ALIAS_NAME }}
|
||||
ALIAS_PASS: ${{ secrets.ALIAS_PASS }}
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
./gradlew assembleRelease
|
||||
echo ::set-env name=APK_FILE::$(find app/build/outputs/apk -name "*.apk")
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
|
||||
- name: Update ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
tag_name: 1.7.4_${{ github.run_id }}
|
||||
release_name: 1.7.4_${{ github.run_id }}
|
||||
body: ""
|
||||
draft: false
|
||||
prerelease: true
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: arm64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
|
||||
- name: Update x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||
asset_path: ${{ env.APK_FILE }}
|
||||
asset_name: app-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- uses: actions/upload-artifact@master
|
||||
name: Upload artifact
|
||||
name: x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
|
||||
- name: Update ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: EhViewer
|
||||
path: app/build/outputs/
|
||||
name: armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
|
||||
- name: Update x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/release/app-x86-release.apk
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Java 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Android SDK Tools
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
echo "y" | sudo ${ANDROID_HOME}/tools/bin/sdkmanager --install "ndk;25.0.8775105" --sdk_root=${ANDROID_SDK_ROOT} &> /dev/null
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- run: |
|
||||
git submodule update --init --recursive
|
||||
gradle assembleRelease
|
||||
|
||||
- name: Releases
|
||||
uses: softprops/action-gh-release@v0.1.14
|
||||
with:
|
||||
body: Bump Version
|
||||
files: |
|
||||
app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
app/build/outputs/apk/release/app-x86_64-release.apk
|
||||
app/build/outputs/apk/release/app-x86-release.apk
|
||||
app/build/outputs/apk/release/app-armeabi-v7a-release.apk
|
||||
+7
-4
@@ -1,12 +1,15 @@
|
||||
[submodule "native/src/main/jni/a7zip/p7zip/p7zip"]
|
||||
path = native/src/main/jni/a7zip/p7zip/p7zip
|
||||
url = https://github.com/seven332/p7zip.git
|
||||
[submodule "native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo"]
|
||||
path = native/src/main/jni/image/libjpeg-turbo/libjpeg-turbo
|
||||
url = https://gitlab.com/NekoInverter/libjpeg-turbo.git
|
||||
url = https://github.com/asuka-mio/libjpeg-turbo
|
||||
[submodule "native/src/main/jni/image/giflib/giflib"]
|
||||
path = native/src/main/jni/image/giflib/giflib
|
||||
url = https://gitlab.com/NekoInverter/giflib.git
|
||||
[submodule "native/src/main/jni/image/libpng/libpng"]
|
||||
path = native/src/main/jni/image/libpng/libpng
|
||||
url = https://gitlab.com/NekoInverter/libpng.git
|
||||
[submodule "native/src/main/jni/libarchive"]
|
||||
path = native/src/main/jni/libarchive
|
||||
url = https://github.com/asuka-mio/libarchive
|
||||
[submodule "native/src/main/jni/liblzma"]
|
||||
path = native/src/main/jni/liblzma
|
||||
url = https://git.tukaani.org/xz.git
|
||||
@@ -1,202 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Preamble
|
||||
|
||||
1. Definitions.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
0. Definitions.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
1. Source Code.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
2. Basic Permissions.
|
||||
|
||||
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
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
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.
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
-1840
File diff suppressed because it is too large.
Load diff
@@ -1,22 +1,50 @@
|
||||
### [下载](https://install.appcenter.ms/users/nekoinverter/apps/ehviewer/distribution_groups/public)
|
||||
<h1 align="center">
|
||||
<img src="art/launcher_icon-web.png" width="200">
|
||||
<br>EhViewer<br>
|
||||
</h1>
|
||||
|
||||
[Download](https://install.appcenter.ms/users/nekoinverter/apps/ehviewer/distribution_groups/public)
|
||||
<p align="center">
|
||||
<a href="https://github.com/asuka-mio/Ehviewer/actions">
|
||||
<img src="https://img.shields.io/github/workflow/status/asuka-mio/Ehviewer/EhViewer" alt="Github Actions">
|
||||
</a>
|
||||
<a href="https://github.com/asuka-mio/Ehviewer/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/asuka-mio/Ehviewer" alt="LICENSE">
|
||||
</a>
|
||||
<a href="https://www.codefactor.io/repository/github/asuka-mio/ehviewer">
|
||||
<img src="https://www.codefactor.io/repository/github/asuka-mio/ehviewer/badge" alt="CodeFactor">
|
||||
</a>
|
||||
<a href="https://github.com/asuka-mio/Ehviewer/releases">
|
||||
<img src="https://img.shields.io/github/v/release/asuka-mio/ehviewer?include_prereleases" alt="Release">
|
||||
</a>
|
||||
<a href="https://github.com/asuka-mio/Ehviewer/issues">
|
||||
<img src="https://img.shields.io/github/issues/asuka-mio/ehviewer" alt="Issues">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# EhViewer
|
||||
## Description
|
||||
这是一个 E-Hentai Android 平台的浏览器
|
||||
|
||||

|
||||
已针对性适配 Material Design 3 及其动态取色系统
|
||||
|
||||
动态配色仅供 Android 12 及以上平台使用!
|
||||
|
||||
这是一个 E-Hentai Android 平台的浏览器。
|
||||
|
||||
An E-Hentai Application for Android.
|
||||
|
||||
with Material Design 3 and Dynamic Color Support.
|
||||
|
||||
# Screenshot
|
||||
Dynamic Color only support Android S and above!
|
||||
|
||||
## Download
|
||||
|
||||
[Github 下载](https://github.com/asuka-mio/Ehviewer/releases)
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
# Thanks
|
||||
## Thanks
|
||||
|
||||
本项目受到了诸多开源项目的帮助
|
||||
|
||||
@@ -29,29 +57,26 @@ Here is the libraries
|
||||
- [giflib](http://giflib.sourceforge.net)
|
||||
- [greenDAO](https://github.com/greenrobot/greenDAO)
|
||||
- [jsoup](https://github.com/jhy/jsoup)
|
||||
- [libjpeg-turbo](http://libjpeg-turbo.virtualgl.org/)
|
||||
- [libjpeg-turbo](https://libjpeg-turbo.org/)
|
||||
- [libpng](http://www.libpng.org/pub/png/libpng.html)
|
||||
- [okhttp](https://github.com/square/okhttp)
|
||||
- [roaster](https://github.com/forge/roaster)
|
||||
- [ShowcaseView](https://github.com/amlcurran/ShowcaseView)
|
||||
- [Slabo](https://github.com/TiroTypeworks/Slabo)
|
||||
- [TapTargetView](https://github.com/KeepSafe/TapTargetView)
|
||||
- [TagSoup](http://home.ccil.org/~cowan/tagsoup/)
|
||||
- [libarchive](http://www.libarchive.org/)
|
||||
- [XZ Utils](https://tukaani.org/xz/)
|
||||
|
||||
|
||||
# License
|
||||
## License
|
||||
|
||||
Copyright (C) 2014-2019 Hippo Seven
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
Copyright 2020-2022 NekoInverter
|
||||
Copyright 2022 Tarsin Norbin
|
||||
|
||||
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
|
||||
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
EhViewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
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.
|
||||
You should have received a copy of the GNU General Public License along with EhViewer. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
ic_launcher 图标为 Hippo Seven 所有,所有权利保留
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fasuka-mio%2FEhviewer?ref=badge_large)
|
||||
+29
-58
@@ -5,8 +5,8 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "32.0.0"
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.0"
|
||||
|
||||
Properties localProperties = new Properties()
|
||||
if (project.rootProject.file('local.properties').exists()) {
|
||||
@@ -17,27 +17,25 @@ android {
|
||||
abi {
|
||||
enable = true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64'
|
||||
include 'arm64-v8a', 'x86_64', 'armeabi-v7a', 'x86'
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (localProperties.getProperty("storeFile") != null) {
|
||||
sign {
|
||||
storeFile file(localProperties.getProperty("storeFile"))
|
||||
storePassword localProperties.getProperty("storePassword")
|
||||
keyAlias localProperties.getProperty("keyAlias")
|
||||
keyPassword localProperties.getProperty("keyPassword")
|
||||
}
|
||||
sign {
|
||||
storeFile file(signStoreFile)
|
||||
storePassword signStorePassword
|
||||
keyAlias signkeyAlias
|
||||
keyPassword signkeyPassword
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "io.github.nekoinverter.ehviewer"
|
||||
minSdkVersion 31
|
||||
targetSdkVersion 32
|
||||
versionCode 114516
|
||||
versionName "1.7.26.7"
|
||||
applicationId "moe.tarsin.ehviewer"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 33
|
||||
versionCode 114528
|
||||
versionName "1.7.28.1"
|
||||
resConfigs 'zh', 'zh-rCN', 'zh-rHK', 'zh-rTW', 'es', 'ja', 'ko', 'fr', 'de', 'th'
|
||||
}
|
||||
|
||||
@@ -72,17 +70,11 @@ android {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-rules.pro'
|
||||
if (localProperties.getProperty("storeFile") != null) {
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
signingConfig signingConfigs.sign
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
if (localProperties.getProperty("storeFile") != null) {
|
||||
signingConfig signingConfigs.sign
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +83,7 @@ android {
|
||||
java.srcDirs += 'src/main/java-gen'
|
||||
}
|
||||
}
|
||||
|
||||
namespace 'com.hippo.ehviewer'
|
||||
}
|
||||
|
||||
@@ -99,56 +92,34 @@ tasks.withType(JavaCompile) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.activity:activity:1.4.0'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.activity:activity:1.6.0-alpha05'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0-alpha05'
|
||||
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'androidx.core:core:1.7.0'
|
||||
implementation 'androidx.dynamicanimation:dynamicanimation:1.1.0-alpha03'
|
||||
implementation 'androidx.fragment:fragment:1.4.1'
|
||||
implementation 'androidx.fragment:fragment:1.5.0'
|
||||
implementation "androidx.lifecycle:lifecycle-process:2.5.0"
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
|
||||
implementation 'androidx.webkit:webkit:1.4.0'
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
implementation 'com.google.android.material:material:1.6.0'
|
||||
implementation 'com.google.android.material:material:1.7.0-alpha02'
|
||||
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.13.3'
|
||||
implementation 'com.github.seven332:beerbelly:0.1.4'
|
||||
implementation 'com.github.seven332:glgallery:0.1.2'
|
||||
implementation 'com.github.seven332:glview:0.1.0'
|
||||
implementation 'com.github.seven332:glview-image:0.1.0'
|
||||
implementation 'com.github.seven332:streampipe:0.1.0'
|
||||
implementation 'com.github.seven332:tuxiang:0.1.6'
|
||||
implementation 'com.github.seven332:unifile:9ec57bcd8f'
|
||||
implementation 'com.github.seven332:yorozuya-thread:0.1.1'
|
||||
implementation 'com.github.seven332:yorozuya-collect:0.1.4'
|
||||
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.2'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
|
||||
implementation 'com.takisoft.preferencex:preferencex:1.1.0'
|
||||
implementation 'com.gitlab.NekoInverter:yorozuya:723dc867a9'
|
||||
implementation 'dev.rikka.rikkax.appcompat:appcompat:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.core:core:1.3.4'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.2.0'
|
||||
implementation 'dev.rikka.rikkax.core:core-ktx:1.4.1'
|
||||
implementation 'dev.rikka.rikkax.insets:insets:1.3.0'
|
||||
implementation 'dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0'
|
||||
implementation 'dev.rikka.rikkax.material:material:1.6.6'
|
||||
implementation 'dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1'
|
||||
implementation 'dev.rikka.rikkax.widget:borderview:1.1.0'
|
||||
implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3'
|
||||
implementation 'dev.rikka.rikkax.material:material-preference:2.0.0'
|
||||
implementation 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'
|
||||
implementation 'org.greenrobot:greendao:3.3.0'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.2'
|
||||
implementation projects.native
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'com.github.seven332:glgallery:25893283ca'
|
||||
force 'com.github.seven332:glview:ba6aee61d7'
|
||||
force 'com.github.seven332:glview-image:68d94b0fc2'
|
||||
|
||||
exclude group: 'com.github.seven332', module: 'okhttp'
|
||||
exclude group: 'com.github.seven332', module: 'image'
|
||||
exclude group: 'com.github.seven332', module: 'yorozuya'
|
||||
exclude group: 'androidx.appcompat', module: 'appcompat'
|
||||
}
|
||||
exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat'
|
||||
exclude group: 'dev.rikka.rikkax.material', module: 'material'
|
||||
}
|
||||
Binary file not shown.
@@ -14,16 +14,15 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
@@ -41,14 +40,14 @@
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name="com.hippo.ehviewer.ui.MainActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:banner="@mipmap/ic_leanback_banner"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden">
|
||||
|
||||
<intent-filter>
|
||||
@@ -56,6 +55,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
@@ -91,8 +95,7 @@
|
||||
android:name="com.hippo.ehviewer.ui.GalleryActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/AppTheme.Gallery">
|
||||
android:launchMode="singleTask">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
@@ -1,45 +0,0 @@
|
||||
package androidx.recyclerview.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class BridgeRecyclerView extends RecyclerView {
|
||||
public BridgeRecyclerView(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public BridgeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BridgeRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
|
||||
try {
|
||||
super.scrollStep(dx, dy, consumed);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
|
||||
return super.scrollByInternal(x, y, ev, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
try {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public abstract class BeerBelly<V> {
|
||||
|
||||
private static final String TAG = BeerBelly.class.getSimpleName();
|
||||
private final boolean mHasMemoryCache;
|
||||
private final boolean mHasDiskCache;
|
||||
@Nullable
|
||||
private MemoryCache<V> mMemoryCache;
|
||||
@Nullable
|
||||
private DiskCache<V> mDiskCache;
|
||||
|
||||
public BeerBelly(BeerBellyParams params) {
|
||||
params.isValid();
|
||||
mHasMemoryCache = params.hasMemoryCache;
|
||||
mHasDiskCache = params.hasDiskCache;
|
||||
|
||||
if (mHasMemoryCache) {
|
||||
initMemoryCache(params.memoryCacheMaxSize);
|
||||
}
|
||||
|
||||
if (mHasDiskCache) {
|
||||
initDiskCache(params.diskCacheDir, params.diskCacheMaxSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void initMemoryCache(int maxSize) {
|
||||
mMemoryCache = new MemoryCache<>(maxSize, this);
|
||||
}
|
||||
|
||||
private void initDiskCache(File cacheDir, int maxSize) {
|
||||
// Set up disk cache
|
||||
try {
|
||||
mDiskCache = new DiskCache<>(cacheDir, maxSize, this);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Can't create disk cache", e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract int sizeOf(String key, V value);
|
||||
|
||||
protected abstract void memoryEntryAdded(V value);
|
||||
|
||||
protected abstract void memoryEntryRemoved(boolean evicted, String key, V oldValue, V newValue);
|
||||
|
||||
protected abstract V read(@NonNull InputStreamPipe isPipe);
|
||||
|
||||
protected abstract boolean write(OutputStream os, V value);
|
||||
|
||||
@Nullable
|
||||
public LruCache<String, V> getMemoryCache() {
|
||||
return mMemoryCache;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SimpleDiskCache getDiskCache() {
|
||||
if (mDiskCache != null) {
|
||||
return mDiskCache.mDiskCache;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if have memory cache
|
||||
*
|
||||
* @return true if have memory cache
|
||||
*/
|
||||
public boolean hasMemoryCache() {
|
||||
return mHasMemoryCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if have disk cache
|
||||
*
|
||||
* @return true if have disk cache
|
||||
*/
|
||||
public boolean hasDiskCache() {
|
||||
return mHasDiskCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory cache usage size
|
||||
*
|
||||
* @return {@code -1} for no memory cache
|
||||
*/
|
||||
public int memorySize() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.size();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get memory cache max size
|
||||
*
|
||||
* @return {@code -1} for no memory cache
|
||||
*/
|
||||
public int memoryMaxSize() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.maxSize();
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disk cache usage size
|
||||
*
|
||||
* @return {@code -1L} for no memory cache
|
||||
*/
|
||||
public long diskSize() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disk cache max size
|
||||
*
|
||||
* @return {@code -1L} for no memory cache
|
||||
*/
|
||||
public long diskMaxSize() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.getMaxSize();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from memory cache
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get, null for miss or no memory cache
|
||||
*/
|
||||
public V getFromMemory(@NonNull String key) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
return mMemoryCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from disk cache. Override {@link #read(InputStreamPipe)} to do it
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get, null for miss or no memory cache or get error
|
||||
*/
|
||||
public V getFromDisk(@NonNull String key) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.get(key);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from memory cache and disk cache. If miss in memory cache and
|
||||
* get in disk cache, it will put value from disk cache to memory cache.
|
||||
*
|
||||
* @param key the key to get value
|
||||
* @return the value you get
|
||||
*/
|
||||
public V get(@NonNull String key) {
|
||||
V value = getFromMemory(key);
|
||||
|
||||
if (value != null) {
|
||||
// Get it in memory cache
|
||||
return value;
|
||||
}
|
||||
|
||||
value = getFromDisk(key);
|
||||
|
||||
if (value != null) {
|
||||
// Get it in disk cache
|
||||
putToMemory(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to memory cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return false if no memory cache
|
||||
*/
|
||||
public boolean putToMemory(@NonNull String key, @NonNull V value) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
memoryEntryAdded(value);
|
||||
mMemoryCache.put(key, value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to disk cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return false if no disk cache or get error
|
||||
*/
|
||||
public boolean putToDisk(@NonNull String key, @NonNull V value) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.put(key, value);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put value to memory cache and disk cache
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
public void put(@NonNull String key, @NonNull V value) {
|
||||
putToMemory(key, value);
|
||||
putToDisk(key, value);
|
||||
}
|
||||
|
||||
public void removeFromMemory(@NonNull String key) {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
mMemoryCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFromDisk(@NonNull String key) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(@NonNull String key) {
|
||||
removeFromMemory(key);
|
||||
removeFromDisk(key);
|
||||
}
|
||||
|
||||
public boolean pullFromDiskCache(@NonNull String key, @NonNull OutputStream os) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.pull(key, os);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key
|
||||
* @param is the input stream to store
|
||||
* @return false if no disk cache or get error
|
||||
*/
|
||||
public boolean putRawToDisk(@NonNull String key, @NonNull InputStream is) {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
return mDiskCache.putRaw(key, is);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache
|
||||
*/
|
||||
public void clearMemory() {
|
||||
if (mHasMemoryCache && mMemoryCache != null) {
|
||||
mMemoryCache.evictAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear disk cache
|
||||
*/
|
||||
public void clearDisk() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
if (mHasDiskCache && mDiskCache != null) {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all of the items from the memory cache and lets the system know
|
||||
* now would be a good time to garbage collect
|
||||
*/
|
||||
public void clear() {
|
||||
clearMemory();
|
||||
clearDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
* A holder class that contains cache parameters.
|
||||
*/
|
||||
public static class BeerBellyParams {
|
||||
|
||||
/**
|
||||
* is memory cache available
|
||||
*/
|
||||
public boolean hasMemoryCache = false;
|
||||
/**
|
||||
* the maximum number of bytes the memory cache should use to store
|
||||
*/
|
||||
public int memoryCacheMaxSize = 0;
|
||||
/**
|
||||
* is disk cache available
|
||||
*/
|
||||
public boolean hasDiskCache = false;
|
||||
/**
|
||||
* the dir to store disk cache
|
||||
*/
|
||||
public File diskCacheDir = null;
|
||||
/**
|
||||
* the maximum number of bytes the disk cache should use to store
|
||||
*/
|
||||
public int diskCacheMaxSize = 0;
|
||||
|
||||
/**
|
||||
* Check BeerBellyParams is valid
|
||||
*/
|
||||
public void isValid() throws IllegalStateException {
|
||||
if (!hasMemoryCache && !hasDiskCache) {
|
||||
throw new IllegalStateException("No memory cache and no disk cache. What can I do for you?");
|
||||
}
|
||||
|
||||
if (hasMemoryCache && memoryCacheMaxSize <= 0) {
|
||||
throw new IllegalStateException("Memory cache max size must be bigger than 0.");
|
||||
}
|
||||
|
||||
if (hasDiskCache) {
|
||||
if (diskCacheDir == null) {
|
||||
throw new IllegalStateException("Disk cache dir can't be null.");
|
||||
}
|
||||
if (diskCacheMaxSize <= 0) {
|
||||
throw new IllegalStateException("Disk cache max size must be bigger than 0.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiskCache<E> {
|
||||
|
||||
private static final int IO_BUFFER_SIZE = 8 * 1024;
|
||||
|
||||
private final SimpleDiskCache mDiskCache;
|
||||
private final BeerBelly<E> mParent;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mMaxSize;
|
||||
|
||||
public DiskCache(File cacheDir, int size, BeerBelly<E> parent) throws IOException {
|
||||
mDiskCache = new SimpleDiskCache(cacheDir, size);
|
||||
mParent = parent;
|
||||
|
||||
mCacheDir = cacheDir;
|
||||
mMaxSize = size;
|
||||
}
|
||||
|
||||
public File getCacheDir() {
|
||||
return mCacheDir;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
return mMaxSize;
|
||||
}
|
||||
|
||||
public long size() {
|
||||
return mDiskCache.size();
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
mDiskCache.flush();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mDiskCache.clear();
|
||||
}
|
||||
|
||||
public E get(String key) {
|
||||
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
|
||||
if (isPipe == null) {
|
||||
return null;
|
||||
} else {
|
||||
return mParent.read(isPipe);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean put(String key, E value) {
|
||||
OutputStreamPipe osPipe = mDiskCache.getOutputStreamPipe(key);
|
||||
BufferedOutputStream buffOut = null;
|
||||
try {
|
||||
osPipe.obtain();
|
||||
OutputStream os = osPipe.open();
|
||||
buffOut = new BufferedOutputStream(os, IO_BUFFER_SIZE);
|
||||
return mParent.write(os, value);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
Util.closeQuietly(buffOut);
|
||||
osPipe.close();
|
||||
osPipe.release();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean putRaw(String key, InputStream is) {
|
||||
return mDiskCache.put(key, is);
|
||||
}
|
||||
|
||||
public void remove(String key) {
|
||||
mDiskCache.remove(key);
|
||||
}
|
||||
|
||||
public boolean pull(@NonNull String key, @NonNull OutputStream os) {
|
||||
InputStreamPipe isPipe = mDiskCache.getInputStreamPipe(key);
|
||||
if (isPipe == null) {
|
||||
return false;
|
||||
} else {
|
||||
try {
|
||||
isPipe.obtain();
|
||||
InputStream is = isPipe.open();
|
||||
Util.copy(is, os);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} finally {
|
||||
isPipe.close();
|
||||
isPipe.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MemoryCache<E> extends LruCache<String, E> {
|
||||
|
||||
public BeerBelly<E> mParent;
|
||||
|
||||
public MemoryCache(int maxSize, BeerBelly<E> parent) {
|
||||
super(maxSize);
|
||||
mParent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int sizeOf(String key, E value) {
|
||||
return mParent.sizeOf(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entryRemoved(boolean evicted, String key, E oldValue, E newValue) {
|
||||
mParent.memoryEntryRemoved(evicted, key, oldValue, newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,983 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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.hippo.beerbelly;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A cache that uses a bounded amount of space on a filesystem. Each cache
|
||||
* entry has a string key and a fixed number of values. Each key must match
|
||||
* the regex <strong>[a-z0-9_-]{1,120}</strong>. Values are byte sequences,
|
||||
* accessible as streams or files. Each value must be between {@code 0} and
|
||||
* {@code Integer.MAX_VALUE} bytes in length.
|
||||
*
|
||||
* <p>The cache stores its data in a directory on the filesystem. This
|
||||
* directory must be exclusive to the cache; the cache may delete or overwrite
|
||||
* files from its directory. It is an error for multiple processes to use the
|
||||
* same cache directory at the same time.
|
||||
*
|
||||
* <p>This cache limits the number of bytes that it will store on the
|
||||
* filesystem. When the number of stored bytes exceeds the limit, the cache will
|
||||
* remove entries in the background until the limit is satisfied. The limit is
|
||||
* not strict: the cache may temporarily exceed it while waiting for files to be
|
||||
* deleted. The limit does not include filesystem overhead or the cache
|
||||
* journal so space-sensitive applications should set a conservative limit.
|
||||
*
|
||||
* <p>Clients call {@link #edit} to create or update the values of an entry. An
|
||||
* entry may have only one editor at one time; if a value is not available to be
|
||||
* edited then {@link #edit} will return null.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* Every {@link #edit} call must be matched by a call to {@link Editor#commit}
|
||||
* or {@link Editor#abort}. Committing is atomic: a read observes the full set
|
||||
* of values as they were before or after the commit, but never a mix of values.
|
||||
*
|
||||
* <p>Clients call {@link #get} to read a snapshot of an entry. The read will
|
||||
* observe the value at the time that {@link #get} was called. Updates and
|
||||
* removals after the call do not impact ongoing reads.
|
||||
*
|
||||
* <p>This class is tolerant of some I/O errors. If files are missing from the
|
||||
* filesystem, the corresponding entries will be dropped from the cache. If
|
||||
* an error occurs while writing a cache value, the edit will fail silently.
|
||||
* Callers should handle other problems by catching {@code IOException} and
|
||||
* responding appropriately.
|
||||
*/
|
||||
public final class DiskLruCache implements Closeable {
|
||||
static final String JOURNAL_FILE = "journal";
|
||||
static final String JOURNAL_FILE_TEMP = "journal.tmp";
|
||||
static final String JOURNAL_FILE_BACKUP = "journal.bkp";
|
||||
static final String MAGIC = "libcore.io.DiskLruCache";
|
||||
static final String VERSION_1 = "1";
|
||||
static final long ANY_SEQUENCE_NUMBER = -1;
|
||||
static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
|
||||
static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
|
||||
private static final String CLEAN = "CLEAN";
|
||||
private static final String DIRTY = "DIRTY";
|
||||
private static final String REMOVE = "REMOVE";
|
||||
private static final String READ = "READ";
|
||||
|
||||
/*
|
||||
* This cache uses a journal file named "journal". A typical journal file
|
||||
* looks like this:
|
||||
* libcore.io.DiskLruCache
|
||||
* 1
|
||||
* 100
|
||||
* 2
|
||||
*
|
||||
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
|
||||
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
|
||||
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
|
||||
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
|
||||
* DIRTY 1ab96a171faeeee38496d8b330771a7a
|
||||
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
|
||||
* READ 335c4c6028171cfddfbaae1a9c313c52
|
||||
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
|
||||
*
|
||||
* The first five lines of the journal form its header. They are the
|
||||
* constant string "libcore.io.DiskLruCache", the disk cache's version,
|
||||
* the application's version, the value count, and a blank line.
|
||||
*
|
||||
* Each of the subsequent lines in the file is a record of the state of a
|
||||
* cache entry. Each line contains space-separated values: a state, a key,
|
||||
* and optional state-specific values.
|
||||
* o DIRTY lines track that an entry is actively being created or updated.
|
||||
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
|
||||
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
|
||||
* temporary files may need to be deleted.
|
||||
* o CLEAN lines track a cache entry that has been successfully published
|
||||
* and may be read. A publish line is followed by the lengths of each of
|
||||
* its values.
|
||||
* o READ lines track accesses for LRU.
|
||||
* o REMOVE lines track entries that have been deleted.
|
||||
*
|
||||
* The journal file is appended to as cache operations occur. The journal may
|
||||
* occasionally be compacted by dropping redundant lines. A temporary file named
|
||||
* "journal.tmp" will be used during compaction; that file should be deleted if
|
||||
* it exists when the cache is opened.
|
||||
*/
|
||||
private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// Eat all writes silently. Nom nom.
|
||||
}
|
||||
};
|
||||
/**
|
||||
* This cache uses a single background thread to evict entries.
|
||||
*/
|
||||
final ThreadPoolExecutor executorService =
|
||||
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
|
||||
private final File directory;
|
||||
private final File journalFile;
|
||||
private final File journalFileTmp;
|
||||
private final File journalFileBackup;
|
||||
private final int appVersion;
|
||||
private final int valueCount;
|
||||
private final LinkedHashMap<String, Entry> lruEntries =
|
||||
new LinkedHashMap<>(0, 0.75f, true);
|
||||
private long maxSize;
|
||||
private long size = 0;
|
||||
private Writer journalWriter;
|
||||
private int redundantOpCount;
|
||||
/**
|
||||
* To differentiate between old and current snapshots, each entry is given
|
||||
* a sequence number each time an edit is committed. A snapshot is stale if
|
||||
* its sequence number is not equal to its entry's sequence number.
|
||||
*/
|
||||
private long nextSequenceNumber = 0; private final Callable<Void> cleanupCallable = new Callable<>() {
|
||||
public Void call() throws Exception {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (journalWriter == null) {
|
||||
return null; // Closed.
|
||||
}
|
||||
trimToSize();
|
||||
if (journalRebuildRequired()) {
|
||||
rebuildJournal();
|
||||
redundantOpCount = 0;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
|
||||
this.directory = directory;
|
||||
this.appVersion = appVersion;
|
||||
this.journalFile = new File(directory, JOURNAL_FILE);
|
||||
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
|
||||
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
this.valueCount = valueCount;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cache in {@code directory}, creating a cache if none exists
|
||||
* there.
|
||||
*
|
||||
* @param directory a writable directory
|
||||
* @param valueCount the number of values per cache entry. Must be positive.
|
||||
* @param maxSize the maximum number of bytes this cache should use to store
|
||||
* @throws IOException if reading or writing the cache directory fails
|
||||
*/
|
||||
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
|
||||
throws IOException {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
if (valueCount <= 0) {
|
||||
throw new IllegalArgumentException("valueCount <= 0");
|
||||
}
|
||||
|
||||
// If a bkp file exists, use it instead.
|
||||
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
|
||||
if (backupFile.exists()) {
|
||||
File journalFile = new File(directory, JOURNAL_FILE);
|
||||
// If journal file also exists just delete backup file.
|
||||
if (journalFile.exists()) {
|
||||
backupFile.delete();
|
||||
} else {
|
||||
renameTo(backupFile, journalFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to pick up where we left off.
|
||||
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
if (cache.journalFile.exists()) {
|
||||
try {
|
||||
cache.readJournal();
|
||||
cache.processJournal();
|
||||
return cache;
|
||||
} catch (IOException journalIsCorrupt) {
|
||||
System.out
|
||||
.println("DiskLruCache "
|
||||
+ directory
|
||||
+ " is corrupt: "
|
||||
+ journalIsCorrupt.getMessage()
|
||||
+ ", removing");
|
||||
cache.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new empty cache.
|
||||
directory.mkdirs();
|
||||
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
|
||||
cache.rebuildJournal();
|
||||
return cache;
|
||||
}
|
||||
|
||||
private static void deleteIfExists(File file) throws IOException {
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
|
||||
if (deleteDestination) {
|
||||
deleteIfExists(to);
|
||||
}
|
||||
if (!from.renameTo(to)) {
|
||||
throw new IOException();
|
||||
}
|
||||
}
|
||||
|
||||
private static String inputStreamToString(InputStream in) throws IOException {
|
||||
return Util.readFully(new InputStreamReader(in, Util.UTF_8));
|
||||
}
|
||||
|
||||
private void readJournal() throws IOException {
|
||||
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
|
||||
try {
|
||||
String magic = reader.readLine();
|
||||
String version = reader.readLine();
|
||||
String appVersionString = reader.readLine();
|
||||
String valueCountString = reader.readLine();
|
||||
String blank = reader.readLine();
|
||||
if (!MAGIC.equals(magic)
|
||||
|| !VERSION_1.equals(version)
|
||||
|| !Integer.toString(appVersion).equals(appVersionString)
|
||||
|| !Integer.toString(valueCount).equals(valueCountString)
|
||||
|| !"".equals(blank)) {
|
||||
throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
|
||||
+ valueCountString + ", " + blank + "]");
|
||||
}
|
||||
|
||||
int lineCount = 0;
|
||||
while (true) {
|
||||
try {
|
||||
readJournalLine(reader.readLine());
|
||||
lineCount++;
|
||||
} catch (EOFException endOfJournal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
redundantOpCount = lineCount - lruEntries.size();
|
||||
|
||||
// If we ended on a truncated line, rebuild the journal before appending to it.
|
||||
if (reader.hasUnterminatedLine()) {
|
||||
rebuildJournal();
|
||||
} else {
|
||||
journalWriter = new BufferedWriter(new OutputStreamWriter(
|
||||
new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
} finally {
|
||||
Util.closeQuietly(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private void readJournalLine(String line) throws IOException {
|
||||
int firstSpace = line.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
|
||||
int keyBegin = firstSpace + 1;
|
||||
int secondSpace = line.indexOf(' ', keyBegin);
|
||||
final String key;
|
||||
if (secondSpace == -1) {
|
||||
key = line.substring(keyBegin);
|
||||
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
|
||||
lruEntries.remove(key);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
key = line.substring(keyBegin, secondSpace);
|
||||
}
|
||||
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
}
|
||||
|
||||
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
|
||||
String[] parts = line.substring(secondSpace + 1).split(" ");
|
||||
entry.readable = true;
|
||||
entry.currentEditor = null;
|
||||
entry.setLengths(parts);
|
||||
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
|
||||
entry.currentEditor = new Editor(entry);
|
||||
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
|
||||
// This work was already done by calling lruEntries.get().
|
||||
} else {
|
||||
throw new IOException("unexpected journal line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the initial size and collects garbage as a part of opening the
|
||||
* cache. Dirty entries are assumed to be inconsistent and will be deleted.
|
||||
*/
|
||||
private void processJournal() throws IOException {
|
||||
deleteIfExists(journalFileTmp);
|
||||
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
|
||||
Entry entry = i.next();
|
||||
if (entry.currentEditor == null) {
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
size += entry.lengths[t];
|
||||
}
|
||||
} else {
|
||||
entry.currentEditor = null;
|
||||
for (int t = 0; t < valueCount; t++) {
|
||||
deleteIfExists(entry.getCleanFile(t));
|
||||
deleteIfExists(entry.getDirtyFile(t));
|
||||
}
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new journal that omits redundant information. This replaces the
|
||||
* current journal if it exists.
|
||||
*/
|
||||
private synchronized void rebuildJournal() throws IOException {
|
||||
if (journalWriter != null) {
|
||||
journalWriter.close();
|
||||
}
|
||||
|
||||
try (Writer writer = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) {
|
||||
writer.write(MAGIC);
|
||||
writer.write("\n");
|
||||
writer.write(VERSION_1);
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(appVersion));
|
||||
writer.write("\n");
|
||||
writer.write(Integer.toString(valueCount));
|
||||
writer.write("\n");
|
||||
writer.write("\n");
|
||||
|
||||
for (Entry entry : lruEntries.values()) {
|
||||
if (entry.currentEditor != null) {
|
||||
writer.write(DIRTY + ' ' + entry.key + '\n');
|
||||
} else {
|
||||
writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (journalFile.exists()) {
|
||||
renameTo(journalFile, journalFileBackup, true);
|
||||
}
|
||||
renameTo(journalFileTmp, journalFile, false);
|
||||
journalFileBackup.delete();
|
||||
|
||||
journalWriter = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if it contains the key
|
||||
*/
|
||||
public synchronized boolean contain(String key) {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
return entry != null && entry.readable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a snapshot of the entry named {@code key}, or null if it doesn't
|
||||
* exist is not currently readable. If a value is returned, it is moved to
|
||||
* the head of the LRU queue.
|
||||
*/
|
||||
public synchronized Snapshot get(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Open all streams eagerly to guarantee that we see a single published
|
||||
// snapshot. If we opened streams lazily then the streams could come
|
||||
// from different edits.
|
||||
InputStream[] ins = new InputStream[valueCount];
|
||||
try {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
ins[i] = new FileInputStream(entry.getCleanFile(i));
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// A file must have been deleted manually!
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (ins[i] != null) {
|
||||
Util.closeQuietly(ins[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n'));
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for the entry named {@code key}, or null if another
|
||||
* edit is in progress.
|
||||
*/
|
||||
public Editor edit(String key) throws IOException {
|
||||
return edit(key, ANY_SEQUENCE_NUMBER);
|
||||
}
|
||||
|
||||
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|
||||
|| entry.sequenceNumber != expectedSequenceNumber)) {
|
||||
return null; // Snapshot is stale.
|
||||
}
|
||||
if (entry == null) {
|
||||
entry = new Entry(key);
|
||||
lruEntries.put(key, entry);
|
||||
} else if (entry.currentEditor != null) {
|
||||
return null; // Another edit is in progress.
|
||||
}
|
||||
|
||||
Editor editor = new Editor(entry);
|
||||
entry.currentEditor = editor;
|
||||
|
||||
// Flush the journal before creating files to prevent file leaks.
|
||||
journalWriter.write(DIRTY + ' ' + key + '\n');
|
||||
journalWriter.flush();
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where this cache stores its data.
|
||||
*/
|
||||
public File getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum number of bytes that this cache should use to store
|
||||
* its data.
|
||||
*/
|
||||
public synchronized long getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the maximum number of bytes the cache can store and queues a job
|
||||
* to trim the existing store, if necessary.
|
||||
*/
|
||||
public synchronized void setMaxSize(long maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes currently being used to store the values in
|
||||
* this cache. This may be greater than the max size if a background
|
||||
* deletion is pending.
|
||||
*/
|
||||
public synchronized long size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
|
||||
Entry entry = editor.entry;
|
||||
if (entry.currentEditor != editor) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// If this edit is creating the entry for the first time, every index must have a value.
|
||||
if (success && !entry.readable) {
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
if (!editor.written[i]) {
|
||||
editor.abort();
|
||||
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
|
||||
}
|
||||
if (!entry.getDirtyFile(i).exists()) {
|
||||
editor.abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File dirty = entry.getDirtyFile(i);
|
||||
if (success) {
|
||||
if (dirty.exists()) {
|
||||
File clean = entry.getCleanFile(i);
|
||||
dirty.renameTo(clean);
|
||||
long oldLength = entry.lengths[i];
|
||||
long newLength = clean.length();
|
||||
entry.lengths[i] = newLength;
|
||||
size = size - oldLength + newLength;
|
||||
}
|
||||
} else {
|
||||
deleteIfExists(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
entry.currentEditor = null;
|
||||
if (entry.readable | success) {
|
||||
entry.readable = true;
|
||||
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
|
||||
if (success) {
|
||||
entry.sequenceNumber = nextSequenceNumber++;
|
||||
}
|
||||
} else {
|
||||
lruEntries.remove(entry.key);
|
||||
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
|
||||
}
|
||||
journalWriter.flush();
|
||||
|
||||
if (size > maxSize || journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We only rebuild the journal when it will halve the size of the journal
|
||||
* and eliminate at least 2000 ops.
|
||||
*/
|
||||
private boolean journalRebuildRequired() {
|
||||
final int redundantOpCompactThreshold = 2000;
|
||||
return redundantOpCount >= redundantOpCompactThreshold //
|
||||
&& redundantOpCount >= lruEntries.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the entry for {@code key} if it exists and can be removed. Entries
|
||||
* actively being edited cannot be removed.
|
||||
*
|
||||
* @return true if an entry was removed.
|
||||
*/
|
||||
public synchronized boolean remove(String key) throws IOException {
|
||||
checkNotClosed();
|
||||
validateKey(key);
|
||||
Entry entry = lruEntries.get(key);
|
||||
if (entry == null || entry.currentEditor != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valueCount; i++) {
|
||||
File file = entry.getCleanFile(i);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IOException("failed to delete " + file);
|
||||
}
|
||||
size -= entry.lengths[i];
|
||||
entry.lengths[i] = 0;
|
||||
}
|
||||
|
||||
redundantOpCount++;
|
||||
journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n'));
|
||||
lruEntries.remove(key);
|
||||
|
||||
if (journalRebuildRequired()) {
|
||||
executorService.submit(cleanupCallable);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this cache has been closed.
|
||||
*/
|
||||
public synchronized boolean isClosed() {
|
||||
return journalWriter == null;
|
||||
}
|
||||
|
||||
private void checkNotClosed() {
|
||||
if (journalWriter == null) {
|
||||
throw new IllegalStateException("cache is closed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force buffered operations to the filesystem.
|
||||
*/
|
||||
public synchronized void flush() throws IOException {
|
||||
checkNotClosed();
|
||||
trimToSize();
|
||||
journalWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes this cache. Stored values will remain on the filesystem.
|
||||
*/
|
||||
public synchronized void close() throws IOException {
|
||||
if (journalWriter == null) {
|
||||
return; // Already closed.
|
||||
}
|
||||
for (Entry entry : new ArrayList<>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
|
||||
remove(toEvict.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the cache and deletes all of its stored values. This will delete
|
||||
* all files in the cache directory including files that weren't created by
|
||||
* the cache.
|
||||
*/
|
||||
public void delete() throws IOException {
|
||||
close();
|
||||
Util.deleteContents(directory);
|
||||
}
|
||||
|
||||
private void validateKey(String key) {
|
||||
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalArgumentException("keys must match regex "
|
||||
+ STRING_KEY_PATTERN + ": \"" + key + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot of the values for an entry.
|
||||
*/
|
||||
public final class Snapshot implements Closeable {
|
||||
private final String key;
|
||||
private final long sequenceNumber;
|
||||
private final InputStream[] ins;
|
||||
private final long[] lengths;
|
||||
|
||||
private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
|
||||
this.key = key;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.ins = ins;
|
||||
this.lengths = lengths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an editor for this snapshot's entry, or null if either the
|
||||
* entry has changed since this snapshot was created or if another edit
|
||||
* is in progress.
|
||||
*/
|
||||
public Editor edit() throws IOException {
|
||||
return DiskLruCache.this.edit(key, sequenceNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unbuffered stream with the value for {@code index}.
|
||||
*/
|
||||
public InputStream getInputStream(int index) {
|
||||
return ins[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value for {@code index}.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
return inputStreamToString(getInputStream(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte length of the value for {@code index}.
|
||||
*/
|
||||
public long getLength(int index) {
|
||||
return lengths[index];
|
||||
}
|
||||
|
||||
public void close() {
|
||||
for (InputStream in : ins) {
|
||||
Util.closeQuietly(in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the values for an entry.
|
||||
*/
|
||||
public final class Editor {
|
||||
private final Entry entry;
|
||||
private final boolean[] written;
|
||||
private boolean hasErrors;
|
||||
private boolean committed;
|
||||
|
||||
private Editor(Entry entry) {
|
||||
this.entry = entry;
|
||||
this.written = (entry.readable) ? null : new boolean[valueCount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered input stream to read the last committed value,
|
||||
* or null if no value has been committed.
|
||||
*/
|
||||
public InputStream newInputStream(int index) {
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new FileInputStream(entry.getCleanFile(index));
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last committed value as a string, or null if no value
|
||||
* has been committed.
|
||||
*/
|
||||
public String getString(int index) throws IOException {
|
||||
InputStream in = newInputStream(index);
|
||||
return in != null ? inputStreamToString(in) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new unbuffered output stream to write the value at
|
||||
* {@code index}. If the underlying output stream encounters errors
|
||||
* when writing to the filesystem, this edit will be aborted when
|
||||
* {@link #commit} is called. The returned output stream does not throw
|
||||
* IOExceptions.
|
||||
*/
|
||||
public OutputStream newOutputStream(int index) throws IOException {
|
||||
if (index < 0 || index >= valueCount) {
|
||||
throw new IllegalArgumentException("Expected index " + index + " to "
|
||||
+ "be greater than 0 and less than the maximum value count "
|
||||
+ "of " + valueCount);
|
||||
}
|
||||
synchronized (DiskLruCache.this) {
|
||||
if (entry.currentEditor != this) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (!entry.readable) {
|
||||
written[index] = true;
|
||||
}
|
||||
File dirtyFile = entry.getDirtyFile(index);
|
||||
FileOutputStream outputStream;
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Attempt to recreate the cache directory.
|
||||
directory.mkdirs();
|
||||
try {
|
||||
outputStream = new FileOutputStream(dirtyFile);
|
||||
} catch (FileNotFoundException e2) {
|
||||
// We are unable to recover. Silently eat the writes.
|
||||
return NULL_OUTPUT_STREAM;
|
||||
}
|
||||
}
|
||||
return new FaultHidingOutputStream(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value at {@code index} to {@code value}.
|
||||
*/
|
||||
public void set(int index, String value) throws IOException {
|
||||
Writer writer = null;
|
||||
try {
|
||||
writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
|
||||
writer.write(value);
|
||||
} finally {
|
||||
Util.closeQuietly(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits this edit so it is visible to readers. This releases the
|
||||
* edit lock so another edit may be started on the same key.
|
||||
*/
|
||||
public void commit() throws IOException {
|
||||
if (hasErrors) {
|
||||
completeEdit(this, false);
|
||||
remove(entry.key); // The previous entry is stale.
|
||||
} else {
|
||||
completeEdit(this, true);
|
||||
}
|
||||
committed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aborts this edit. This releases the edit lock so another edit may be
|
||||
* started on the same key.
|
||||
*/
|
||||
public void abort() throws IOException {
|
||||
completeEdit(this, false);
|
||||
}
|
||||
|
||||
public void abortUnlessCommitted() {
|
||||
if (!committed) {
|
||||
try {
|
||||
abort();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FaultHidingOutputStream extends FilterOutputStream {
|
||||
private FaultHidingOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int oneByte) {
|
||||
try {
|
||||
out.write(oneByte);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] buffer, int offset, int length) {
|
||||
try {
|
||||
out.write(buffer, offset, length);
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
out.close();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
try {
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
hasErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class Entry {
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Lengths of this entry's files.
|
||||
*/
|
||||
private final long[] lengths;
|
||||
|
||||
/**
|
||||
* True if this entry has ever been published.
|
||||
*/
|
||||
private boolean readable;
|
||||
|
||||
/**
|
||||
* The ongoing edit or null if this entry is not being edited.
|
||||
*/
|
||||
private Editor currentEditor;
|
||||
|
||||
/**
|
||||
* The sequence number of the most recently committed edit to this entry.
|
||||
*/
|
||||
private long sequenceNumber;
|
||||
|
||||
private Entry(String key) {
|
||||
this.key = key;
|
||||
this.lengths = new long[valueCount];
|
||||
}
|
||||
|
||||
public String getLengths() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (long size : lengths) {
|
||||
result.append(' ').append(size);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lengths using decimal numbers like "10123".
|
||||
*/
|
||||
private void setLengths(String[] strings) throws IOException {
|
||||
if (strings.length != valueCount) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < strings.length; i++) {
|
||||
lengths[i] = Long.parseLong(strings[i]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw invalidLengths(strings);
|
||||
}
|
||||
}
|
||||
|
||||
private IOException invalidLengths(String[] strings) throws IOException {
|
||||
throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
|
||||
}
|
||||
|
||||
public File getCleanFile(int i) {
|
||||
return new File(directory, key + "." + i);
|
||||
}
|
||||
|
||||
public File getDirtyFile(int i) {
|
||||
return new File(directory, key + "." + i + ".tmp");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
/*
|
||||
* Copyright 2015-2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.beerbelly;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.streampipe.InputStreamPipe;
|
||||
import com.hippo.streampipe.OutputStreamPipe;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class SimpleDiskCache {
|
||||
|
||||
private static final String TAG = SimpleDiskCache.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Disk cache is free
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_NONE = 0;
|
||||
|
||||
/**
|
||||
* Disk cache is flushing or clearing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_IN_USE = 1;
|
||||
|
||||
/**
|
||||
* Disk cache is reading or writing
|
||||
*/
|
||||
private static final int STATE_DISK_CACHE_BUSY = 2;
|
||||
|
||||
private final File mCacheDir;
|
||||
private final int mSize;
|
||||
@NonNull
|
||||
private final Map<String, CounterLock> mLockMap = new HashMap<>();
|
||||
@NonNull
|
||||
private final LockPool mLockPool = new LockPool();
|
||||
private int mDiskCacheState;
|
||||
@Nullable
|
||||
private DiskLruCache mDiskLruCache;
|
||||
|
||||
public SimpleDiskCache(File cacheDir, int size) {
|
||||
mCacheDir = cacheDir;
|
||||
mSize = size;
|
||||
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hashing method that changes a string (like a URL) into a hash suitable
|
||||
* for using as a disk filename.
|
||||
*
|
||||
* @param key The key used to store the file
|
||||
*/
|
||||
private static String hashKeyForDisk(final String key) {
|
||||
String cacheKey;
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
digest.update(key.getBytes());
|
||||
cacheKey = bytesToHexString(digest.digest());
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
cacheKey = String.valueOf(key.hashCode());
|
||||
}
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://stackoverflow.com/questions/332079
|
||||
*
|
||||
* @param bytes The bytes to convert.
|
||||
* @return A {@link String} converted from the bytes of a hashable key used
|
||||
* to store a filename on the disk, to hex digits.
|
||||
*/
|
||||
private static String bytesToHexString(final byte[] bytes) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (final byte b : bytes) {
|
||||
final String hex = Integer.toHexString(0xFF & b);
|
||||
if (hex.length() == 1) {
|
||||
builder.append('0');
|
||||
}
|
||||
builder.append(hex);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public synchronized boolean isValid() {
|
||||
return mDiskLruCache != null;
|
||||
}
|
||||
|
||||
private synchronized void init() throws IOException {
|
||||
if (!isValid()) {
|
||||
mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized long size() {
|
||||
if (null != mDiskLruCache) {
|
||||
return mDiskLruCache.size();
|
||||
} else {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void flush() {
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
if (null != mDiskLruCache) {
|
||||
try {
|
||||
mDiskLruCache.flush();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache flush", e);
|
||||
}
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public synchronized boolean clear() {
|
||||
boolean result = true;
|
||||
if (null == mDiskLruCache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for cache available
|
||||
while (mDiskCacheState != STATE_DISK_CACHE_NONE) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_BUSY;
|
||||
|
||||
try {
|
||||
mDiskLruCache.delete();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache clearCache", e);
|
||||
}
|
||||
mDiskLruCache = null;
|
||||
try {
|
||||
init();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "SimpleDiskCache init", e);
|
||||
result = false;
|
||||
}
|
||||
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
return result;
|
||||
}
|
||||
|
||||
private synchronized CounterLock obtainLock(String key) {
|
||||
// Wait for clear over
|
||||
while (mDiskCacheState == STATE_DISK_CACHE_BUSY) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
// Empty
|
||||
}
|
||||
}
|
||||
mDiskCacheState = STATE_DISK_CACHE_IN_USE;
|
||||
|
||||
// Get lock from key
|
||||
CounterLock lock;
|
||||
lock = mLockMap.get(key);
|
||||
if (lock == null) {
|
||||
lock = mLockPool.get();
|
||||
mLockMap.put(key, lock);
|
||||
}
|
||||
lock.obtain();
|
||||
|
||||
return lock;
|
||||
}
|
||||
|
||||
private synchronized void releaseLock(String key, CounterLock lock) {
|
||||
lock.release();
|
||||
if (lock.isFree()) {
|
||||
mLockMap.remove(key);
|
||||
mLockPool.push(lock);
|
||||
}
|
||||
|
||||
if (mLockMap.isEmpty()) {
|
||||
mDiskCacheState = STATE_DISK_CACHE_NONE;
|
||||
notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contain(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.readLock().lock();
|
||||
|
||||
boolean result = mDiskLruCache.contain(diskKey);
|
||||
|
||||
lock.readLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result;
|
||||
try {
|
||||
result = mDiskLruCache.remove(diskKey);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
result = false;
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean put(@NonNull String key, @NonNull InputStream is) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
CounterLock lock = obtainLock(diskKey);
|
||||
if (null == mDiskLruCache) {
|
||||
releaseLock(diskKey, lock);
|
||||
return false;
|
||||
}
|
||||
lock.writeLock().lock();
|
||||
|
||||
boolean result = false;
|
||||
if (isValid()) {
|
||||
result = putToDisk(diskKey, is);
|
||||
}
|
||||
|
||||
lock.writeLock().unlock();
|
||||
releaseLock(diskKey, lock);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean putToDisk(String key, InputStream is) {
|
||||
DiskLruCache.Editor editor = null;
|
||||
OutputStream os = null;
|
||||
boolean completeEdit = false;
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
editor = mDiskLruCache.edit(key);
|
||||
if (editor == null) {
|
||||
// The editor is in progress
|
||||
return false;
|
||||
}
|
||||
|
||||
os = editor.newOutputStream(0);
|
||||
if (os != null) {
|
||||
Util.copy(is, os);
|
||||
completeEdit = true;
|
||||
editor.commit();
|
||||
return true;
|
||||
} else {
|
||||
// Can't get OutputStream
|
||||
completeEdit = true;
|
||||
editor.abort();
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Util.closeQuietly(os);
|
||||
try {
|
||||
if (!completeEdit && editor != null) {
|
||||
editor.abort();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key the key of the target
|
||||
* @return the InputStreamPipe, <code>null</code> for missing
|
||||
*/
|
||||
@Nullable
|
||||
public InputStreamPipe getInputStreamPipe(@NonNull String key) {
|
||||
if (contain(key)) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheInputStreamPipe(diskKey);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public OutputStreamPipe getOutputStreamPipe(@NonNull String key) {
|
||||
String diskKey = hashKeyForDisk(key);
|
||||
return new CacheOutputStreamPipe(diskKey);
|
||||
}
|
||||
|
||||
private class CacheInputStreamPipe implements InputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Snapshot mCurrentSnapshot;
|
||||
|
||||
private CacheInputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.readLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.readLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
InputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentSnapshot != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Snapshot snapshot;
|
||||
snapshot = mDiskLruCache.get(mKey);
|
||||
if (snapshot == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
mCurrentSnapshot = snapshot;
|
||||
return snapshot.getInputStream(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Util.closeQuietly(mCurrentSnapshot);
|
||||
mCurrentSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheOutputStreamPipe implements OutputStreamPipe {
|
||||
|
||||
private final String mKey;
|
||||
private CounterLock mLock;
|
||||
private DiskLruCache.Editor mCurrentEditor;
|
||||
|
||||
private CacheOutputStreamPipe(String key) {
|
||||
mKey = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtain() {
|
||||
if (mLock == null) {
|
||||
mLock = obtainLock(mKey);
|
||||
mLock.writeLock().lock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it first");
|
||||
}
|
||||
|
||||
if (mLock != null) {
|
||||
mLock.writeLock().unlock();
|
||||
releaseLock(mKey, mLock);
|
||||
mLock = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
OutputStream open() throws IOException {
|
||||
if (mLock == null) {
|
||||
throw new IllegalStateException("Please obtain it first");
|
||||
}
|
||||
if (mCurrentEditor != null) {
|
||||
throw new IllegalStateException("Please close it before reopen");
|
||||
}
|
||||
if (null == mDiskLruCache) {
|
||||
throw new IOException("Can't find disk lru cache");
|
||||
}
|
||||
|
||||
DiskLruCache.Editor editor = mDiskLruCache.edit(mKey);
|
||||
if (editor == null) {
|
||||
throw new IOException("Miss the key " + mKey);
|
||||
}
|
||||
|
||||
OutputStream os;
|
||||
try {
|
||||
os = editor.newOutputStream(0);
|
||||
mCurrentEditor = editor;
|
||||
return os;
|
||||
} catch (IOException e) {
|
||||
// Get trouble
|
||||
try {
|
||||
editor.abort();
|
||||
} catch (IOException ex) {
|
||||
// Ignore
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mCurrentEditor != null) {
|
||||
try {
|
||||
mCurrentEditor.commit();
|
||||
} catch (IOException e) {
|
||||
// Empty
|
||||
}
|
||||
mCurrentEditor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CounterLock extends ReentrantReadWriteLock {
|
||||
|
||||
private int mCount;
|
||||
|
||||
public boolean isFree() {
|
||||
return mCount == 0;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (--mCount < 0) {
|
||||
throw new IllegalStateException("Release time is more than occupy time");
|
||||
}
|
||||
}
|
||||
|
||||
public void obtain() {
|
||||
++mCount;
|
||||
}
|
||||
}
|
||||
|
||||
private class LockPool {
|
||||
|
||||
private static final int MAX_SIZE = 10;
|
||||
|
||||
private final CounterLock[] mArray = new CounterLock[MAX_SIZE];
|
||||
private final int mMaxSize = MAX_SIZE;
|
||||
private int mSize;
|
||||
|
||||
public void push(CounterLock lock) {
|
||||
if (lock != null && mSize < mMaxSize) {
|
||||
mArray[mSize++] = lock;
|
||||
}
|
||||
}
|
||||
|
||||
public CounterLock get() {
|
||||
if (mSize > 0) {
|
||||
CounterLock lock = mArray[--mSize];
|
||||
mArray[mSize] = null;
|
||||
return lock;
|
||||
} else {
|
||||
return new CounterLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.hippo.beerbelly;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Buffers input from an {@link InputStream} for reading lines.
|
||||
*
|
||||
* <p>This class is used for buffered reading of lines. For purposes of this class, a line ends
|
||||
* with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated
|
||||
* line at end of input is invalid and will be ignored, the caller may use {@code
|
||||
* hasUnterminatedLine()} to detect it after catching the {@code EOFException}.
|
||||
*
|
||||
* <p>This class is intended for reading input that strictly consists of lines, such as line-based
|
||||
* cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction
|
||||
* with {@link java.io.InputStreamReader} provides similar functionality, this class uses different
|
||||
* end-of-input reporting and a more restrictive definition of a line.
|
||||
*
|
||||
* <p>This class supports only charsets that encode '\r' and '\n' as a single byte with value 13
|
||||
* and 10, respectively, and the representation of no other character contains these values.
|
||||
* We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1.
|
||||
* The default charset is US_ASCII.
|
||||
*/
|
||||
class StrictLineReader implements Closeable {
|
||||
private static final byte CR = (byte) '\r';
|
||||
private static final byte LF = (byte) '\n';
|
||||
|
||||
private final InputStream in;
|
||||
private final Charset charset;
|
||||
|
||||
/*
|
||||
* Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
|
||||
* and the data in the range [pos, end) is buffered for reading. At end of input, if there is
|
||||
* an unterminated line, we set end == -1, otherwise end == pos. If the underlying
|
||||
* {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
|
||||
*/
|
||||
private byte[] buf;
|
||||
private int pos;
|
||||
private int end;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified charset and the default capacity.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, Charset charset) {
|
||||
this(in, 8192, charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code LineReader} with the specified capacity and charset.
|
||||
*
|
||||
* @param in the {@code InputStream} to read data from.
|
||||
* @param capacity the capacity of the buffer.
|
||||
* @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
|
||||
* supported.
|
||||
* @throws NullPointerException if {@code in} or {@code charset} is null.
|
||||
* @throws IllegalArgumentException if {@code capacity} is negative or zero
|
||||
* or the specified charset is not supported.
|
||||
*/
|
||||
public StrictLineReader(InputStream in, int capacity, Charset charset) {
|
||||
if (in == null || charset == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
if (capacity < 0) {
|
||||
throw new IllegalArgumentException("capacity <= 0");
|
||||
}
|
||||
if (!(charset.equals(Util.US_ASCII))) {
|
||||
throw new IllegalArgumentException("Unsupported encoding");
|
||||
}
|
||||
|
||||
this.in = in;
|
||||
this.charset = charset;
|
||||
buf = new byte[capacity];
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the reader by closing the underlying {@code InputStream} and
|
||||
* marking this reader as closed.
|
||||
*
|
||||
* @throws IOException for errors when closing the underlying {@code InputStream}.
|
||||
*/
|
||||
public void close() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf != null) {
|
||||
buf = null;
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
|
||||
* this end of line marker is not included in the result.
|
||||
*
|
||||
* @return the next line from the input.
|
||||
* @throws IOException for underlying {@code InputStream} errors.
|
||||
* @throws EOFException for the end of source stream.
|
||||
*/
|
||||
public String readLine() throws IOException {
|
||||
synchronized (in) {
|
||||
if (buf == null) {
|
||||
throw new IOException("LineReader is closed");
|
||||
}
|
||||
|
||||
// Read more data if we are at the end of the buffered data.
|
||||
// Though it's an error to read after an exception, we will let {@code fillBuf()}
|
||||
// throw again if that happens; thus we need to handle end == -1 as well as end == pos.
|
||||
if (pos >= end) {
|
||||
fillBuf();
|
||||
}
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
|
||||
String res = new String(buf, pos, lineEnd - pos, charset.name());
|
||||
pos = i + 1;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's anticipate up to 80 characters on top of those already read.
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
|
||||
try {
|
||||
return new String(buf, 0, length, charset.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new AssertionError(e); // Since we control the charset this will never happen.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (true) {
|
||||
out.write(buf, pos, end - pos);
|
||||
// Mark unterminated line in case fillBuf throws EOFException or IOException.
|
||||
end = -1;
|
||||
fillBuf();
|
||||
// Try to find LF in the buffered data and return the line if successful.
|
||||
for (int i = pos; i != end; ++i) {
|
||||
if (buf[i] == LF) {
|
||||
if (i != pos) {
|
||||
out.write(buf, pos, i - pos);
|
||||
}
|
||||
pos = i + 1;
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasUnterminatedLine() {
|
||||
return end == -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads new input data into the buffer. Call only with pos == end or end == -1,
|
||||
* depending on the desired outcome if the function throws.
|
||||
*/
|
||||
private void fillBuf() throws IOException {
|
||||
int result = in.read(buf, 0, buf.length);
|
||||
if (result == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
pos = 0;
|
||||
end = result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.beerbelly;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Junk drawer of utility methods.
|
||||
*/
|
||||
final class Util {
|
||||
static final Charset US_ASCII = StandardCharsets.US_ASCII;
|
||||
static final Charset UTF_8 = StandardCharsets.UTF_8;
|
||||
private static final int EOF = -1;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
static String readFully(Reader reader) throws IOException {
|
||||
try (reader) {
|
||||
StringWriter writer = new StringWriter();
|
||||
char[] buffer = new char[1024];
|
||||
int count;
|
||||
while ((count = reader.read(buffer)) != -1) {
|
||||
writer.write(buffer, 0, count);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the contents of {@code dir}. Throws an IOException if any file
|
||||
* could not be deleted, or if {@code dir} is not a readable directory.
|
||||
*/
|
||||
static void deleteContents(File dir) throws IOException {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
throw new IOException("not a readable directory: " + dir);
|
||||
}
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteContents(file);
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new IOException("failed to delete file: " + file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void closeQuietly(/*Auto*/Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy bytes from an <code>InputStream</code> to an
|
||||
* <code>OutputStream</code>.
|
||||
*
|
||||
* @param input the InputStream
|
||||
* @param output the OutputStream
|
||||
* @return the number of bytes copied
|
||||
*/
|
||||
public static long copy(InputStream input, OutputStream output)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
long count = 0;
|
||||
int n;
|
||||
while (EOF != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import com.hippo.conaco.ConacoTask;
|
||||
import com.hippo.conaco.Unikery;
|
||||
import com.hippo.image.ImageBitmap;
|
||||
import com.hippo.image.ImageDrawable;
|
||||
import com.hippo.image.RecycledException;
|
||||
import com.hippo.widget.ObservedTextView;
|
||||
|
||||
public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap>,
|
||||
@@ -127,7 +126,7 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery<ImageBitmap
|
||||
ImageDrawable drawable;
|
||||
try {
|
||||
drawable = new ImageDrawable(value);
|
||||
} catch (RecycledException e) {
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "The ImageBitmap is recycled", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
|
||||
package com.hippo.easyrecyclerview;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
@@ -26,33 +24,20 @@ import android.util.SparseBooleanArray;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.Checkable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.LongSparseArray;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.BridgeRecyclerView;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
/**
|
||||
* Add setChoiceMode for RecyclerView
|
||||
*/
|
||||
// Get some code from twoway-view and AbsListView.
|
||||
public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
|
||||
/**
|
||||
* Represents an invalid position. All valid positions are in the range 0 to 1 less than the
|
||||
* number of items in the current adapter.
|
||||
*/
|
||||
public static final int INVALID_POSITION = -1;
|
||||
public class EasyRecyclerView extends RecyclerView {
|
||||
|
||||
/**
|
||||
* Normal list that does not indicate choices
|
||||
@@ -78,14 +63,8 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
* The list allows multiple choices in custom action
|
||||
*/
|
||||
public static final int CHOICE_MODE_MULTIPLE_CUSTOM = 4;
|
||||
private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
|
||||
// Fling friction
|
||||
private final float mFlingFriction = ViewConfiguration.getScrollFriction();
|
||||
public float pageScrollThreshold;
|
||||
double flingMoveDistanceX;
|
||||
double flingMoveDistanceY;
|
||||
boolean canScrollVertical = true;
|
||||
boolean canScrollHorizontally = true;
|
||||
/**
|
||||
* Wrapper for the multiple choice mode callback; AbsListView needs to perform
|
||||
* a few extra actions around what application code does.
|
||||
@@ -131,30 +110,20 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
private LongSparseArray<Integer> mCheckedIdStates;
|
||||
|
||||
private Adapter<?> mAdapter;
|
||||
private boolean isScrollBefore = false;
|
||||
private int initialTouchX;
|
||||
private int initialTouchY;
|
||||
// A context-specific coefficient adjusted to physical values.
|
||||
private float mPhysicalCoeff;
|
||||
private int defaultOverflowItemScrollKeep;
|
||||
private boolean enableScroll = false;
|
||||
|
||||
public EasyRecyclerView(Context context) {
|
||||
super(context);
|
||||
EasyRecyclerView.this.init(context);
|
||||
setEnableScroll(!Settings.getEInkMode());
|
||||
}
|
||||
|
||||
public EasyRecyclerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
EasyRecyclerView.this.init(context);
|
||||
setEnableScroll(!Settings.getEInkMode());
|
||||
}
|
||||
|
||||
public EasyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
EasyRecyclerView.this.init(context);
|
||||
setEnableScroll(!Settings.getEInkMode());
|
||||
}
|
||||
|
||||
public static void setViewChecked(View view, boolean checked) {
|
||||
@@ -202,18 +171,7 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of items currently selected. This will only be valid
|
||||
* if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
|
||||
*
|
||||
* <p>To determine the specific items that are currently selected, use one of
|
||||
* the <code>getChecked*</code> methods.
|
||||
*
|
||||
* @return The number of items currently selected
|
||||
* @see #getCheckedItemPosition()
|
||||
* @see #getCheckedItemPositions()
|
||||
* @see #getCheckedItemIds()
|
||||
*/
|
||||
|
||||
public int getCheckedItemCount() {
|
||||
return mCheckedItemCount;
|
||||
}
|
||||
@@ -237,22 +195,6 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently checked item. The result is only valid if the choice
|
||||
* mode has been set to {@link #CHOICE_MODE_SINGLE}.
|
||||
*
|
||||
* @return The position of the currently checked item or
|
||||
* {@link #INVALID_POSITION} if nothing is selected
|
||||
* @see #setChoiceMode(int)
|
||||
*/
|
||||
public int getCheckedItemPosition() {
|
||||
if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
|
||||
return mCheckStates.keyAt(0);
|
||||
}
|
||||
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of checked items in the list. The result is only valid if
|
||||
* the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
|
||||
@@ -269,30 +211,6 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the set of checked items ids. The result is only valid if the
|
||||
* choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
|
||||
* has stable IDs. ({@link android.widget.ListAdapter#hasStableIds()} == {@code true})
|
||||
*
|
||||
* @return A new array which contains the id of each checked item in the
|
||||
* list.
|
||||
*/
|
||||
public long[] getCheckedItemIds() {
|
||||
if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) {
|
||||
return new long[0];
|
||||
}
|
||||
|
||||
final LongSparseArray<Integer> idStates = mCheckedIdStates;
|
||||
final int count = idStates.size();
|
||||
final long[] ids = new long[count];
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
ids[i] = idStates.keyAt(i);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
public boolean isInCustomChoice() {
|
||||
return mCustomChoice;
|
||||
}
|
||||
@@ -475,14 +393,6 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
updateOnScreenCheckedViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current choice mode
|
||||
* @see #setChoiceMode(int)
|
||||
*/
|
||||
public int getChoiceMode() {
|
||||
return mChoiceMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the choice behavior for the List. By default, Lists do not have any choice behavior
|
||||
* ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
|
||||
@@ -518,24 +428,6 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the
|
||||
* selection {@link android.view.ActionMode}. Only used when the choice mode is set to
|
||||
* {@link #CHOICE_MODE_MULTIPLE_MODAL}.
|
||||
*
|
||||
* @param listener Listener that will manage the selection mode
|
||||
* @see #setChoiceMode(int)
|
||||
*/
|
||||
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
||||
if (mMultiChoiceModeCallback == null) {
|
||||
mMultiChoiceModeCallback = new MultiChoiceModeWrapper();
|
||||
}
|
||||
mMultiChoiceModeCallback.setWrapped(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listener
|
||||
*/
|
||||
public void setCustomCheckedListener(CustomChoiceListener listener) {
|
||||
mCustomChoiceListener = listener;
|
||||
}
|
||||
@@ -556,299 +448,13 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
|
||||
private void init(Context context) {
|
||||
float scale = context.getResources().getDisplayMetrics().density;
|
||||
final float ppi = scale * 160.0f;
|
||||
mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
|
||||
* 39.37f // inch/meter
|
||||
* ppi
|
||||
* 0.84f; // look and feel tuning
|
||||
// g (m/s^2)
|
||||
// inch/meter
|
||||
// A context-specific coefficient adjusted to physical values.
|
||||
// look and feel tuning
|
||||
pageScrollThreshold = scale * 80;
|
||||
}
|
||||
|
||||
public void setPageScrollThreshold(float pageScrollThreshold) {
|
||||
this.pageScrollThreshold = pageScrollThreshold;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e) {
|
||||
if (enableScroll) {
|
||||
return super.onTouchEvent(e);
|
||||
}
|
||||
boolean superTouchResult = super.onTouchEvent(e);
|
||||
if (superTouchResult) {
|
||||
switch (e.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
initialTouchX = (int) (e.getX() + 0.5f);
|
||||
initialTouchY = (int) (e.getY() + 0.5f);
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (initialTouchX < 0 || initialTouchY < 0) {
|
||||
initialTouchX = (int) (e.getX() + 0.5f);
|
||||
initialTouchY = (int) (e.getY() + 0.5f);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
if (isScrollBefore) {
|
||||
int moveDistanceX = (int) (initialTouchX - e.getX() + flingMoveDistanceX);
|
||||
int moveDistanceY = (int) (initialTouchY - e.getY() + flingMoveDistanceY);
|
||||
onTouchScroll(moveDistanceX, moveDistanceY);
|
||||
initialTouchX = initialTouchY = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
isScrollBefore = getScrollState() == SCROLL_STATE_DRAGGING;
|
||||
}
|
||||
return superTouchResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
|
||||
if (enableScroll) {
|
||||
return super.scrollByInternal(x, y, ev, type);
|
||||
}
|
||||
//disable drag scroll effect,e-ink screen don't need it
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean fling(int velocityX, int velocityY) {
|
||||
if (enableScroll) {
|
||||
return super.fling(velocityX, velocityY);
|
||||
}
|
||||
flingMoveDistanceX = getSplineFlingDistance(velocityX) * Math.signum(velocityX);
|
||||
flingMoveDistanceY = getSplineFlingDistance(velocityY) * Math.signum(velocityY);
|
||||
//disable fling scroll effect,e-ink screen don't need it
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void onTouchScroll(int dx, int dy) {
|
||||
canScrollVertical = true;
|
||||
canScrollHorizontally = true;
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout != null) {
|
||||
if (dy > pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
canScrollVertical = false;
|
||||
onScrollDown();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
canScrollHorizontally = false;
|
||||
onScrollRight();
|
||||
}
|
||||
} else if (dy < -pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
canScrollVertical = false;
|
||||
onScrollUp();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
canScrollHorizontally = false;
|
||||
onScrollLeft();
|
||||
}
|
||||
}
|
||||
|
||||
if (dx > pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
onScrollDown();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
onScrollRight();
|
||||
}
|
||||
} else if (dx < -pageScrollThreshold) {
|
||||
if (canScrollVertical && layout.canScrollVertically()) {
|
||||
onScrollUp();
|
||||
} else if (canScrollHorizontally && layout.canScrollHorizontally()) {
|
||||
onScrollLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollLeft() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int firstVisiblePosition = realLayout.findFirstVisibleItemPosition();
|
||||
ViewHolder firstVisibleItem = findViewHolderForAdapterPosition(firstVisiblePosition);
|
||||
|
||||
if (firstVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item width overscreen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (firstVisibleItem.itemView.getMeasuredWidth() > getMeasuredWidth()) {
|
||||
super.scrollByInternal(-getMeasuredWidth() + defaultOverflowItemScrollKeep, 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (firstVisibleItem.itemView.getMeasuredWidth() == getMeasuredWidth()) {
|
||||
super.scrollByInternal(-getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(firstVisibleItem.itemView.getRight() - getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] firstVisiblePosition = realLayout.findFirstVisibleItemPositions(null);
|
||||
ViewHolder[] firstVisibleItems = new ViewHolder[firstVisiblePosition.length];
|
||||
for (int i = 0; i < firstVisiblePosition.length; i++) {
|
||||
firstVisibleItems[i] = findViewHolderForAdapterPosition(firstVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = Integer.MAX_VALUE;
|
||||
for (ViewHolder firstVisibleItem : firstVisibleItems) {
|
||||
if (firstVisibleItem != null) {
|
||||
if (firstVisibleItem.itemView.getRight() < scrollDistance) {
|
||||
scrollDistance = firstVisibleItem.itemView.getRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollDistance == getMeasuredWidth()) {
|
||||
scrollDistance = 0;
|
||||
}
|
||||
super.scrollByInternal(scrollDistance - getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollRight() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int lastVisiblePosition = realLayout.findLastVisibleItemPosition();
|
||||
ViewHolder lastVisibleItem = findViewHolderForAdapterPosition(lastVisiblePosition);
|
||||
|
||||
if (lastVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item width overflow screen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (lastVisibleItem.itemView.getMeasuredWidth() > getMeasuredWidth()) {
|
||||
super.scrollByInternal(getMeasuredWidth() - defaultOverflowItemScrollKeep, 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (lastVisibleItem.itemView.getMeasuredWidth() == getMeasuredWidth()) {
|
||||
super.scrollByInternal(getMeasuredWidth(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(lastVisibleItem.itemView.getLeft(), 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] lastVisiblePosition = realLayout.findLastVisibleItemPositions(null);
|
||||
ViewHolder[] lastVisibleItems = new ViewHolder[lastVisiblePosition.length];
|
||||
for (int i = 0; i < lastVisiblePosition.length; i++) {
|
||||
lastVisibleItems[i] = findViewHolderForAdapterPosition(lastVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = 0;
|
||||
for (ViewHolder lastVisibleItem : lastVisibleItems) {
|
||||
if (lastVisibleItem != null) {
|
||||
if (lastVisibleItem.itemView.getLeft() > scrollDistance) {
|
||||
scrollDistance = lastVisibleItem.itemView.getLeft();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scrollDistance == 0) {
|
||||
scrollDistance = getMeasuredWidth();
|
||||
}
|
||||
super.scrollByInternal(scrollDistance, 0, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollUp() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int firstVisiblePosition = realLayout.findFirstVisibleItemPosition();
|
||||
ViewHolder firstVisibleItem = findViewHolderForAdapterPosition(firstVisiblePosition);
|
||||
|
||||
if (firstVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item height overscreen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (firstVisibleItem.itemView.getMeasuredHeight() > getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, -getMeasuredHeight() + defaultOverflowItemScrollKeep, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (firstVisibleItem.itemView.getMeasuredHeight() == getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, -getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(0, firstVisibleItem.itemView.getBottom() - getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] firstVisiblePosition = realLayout.findFirstVisibleItemPositions(null);
|
||||
ViewHolder[] firstVisibleItems = new ViewHolder[firstVisiblePosition.length];
|
||||
for (int i = 0; i < firstVisiblePosition.length; i++) {
|
||||
firstVisibleItems[i] = findViewHolderForAdapterPosition(firstVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = Integer.MAX_VALUE;
|
||||
for (ViewHolder firstVisibleItem : firstVisibleItems) {
|
||||
if (firstVisibleItem != null) {
|
||||
if (firstVisibleItem.itemView.getBottom() < scrollDistance) {
|
||||
scrollDistance = firstVisibleItem.itemView.getBaseline();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scrollDistance == getMeasuredWidth()) {
|
||||
scrollDistance = 0;
|
||||
}
|
||||
super.scrollByInternal(0, scrollDistance - getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onScrollDown() {
|
||||
LayoutManager layout = getLayoutManager();
|
||||
if (layout instanceof LinearLayoutManager) {
|
||||
LinearLayoutManager realLayout = (LinearLayoutManager) layout;
|
||||
int lastVisiblePosition = realLayout.findLastVisibleItemPosition();
|
||||
ViewHolder lastVisibleItem = findViewHolderForAdapterPosition(lastVisiblePosition);
|
||||
|
||||
if (lastVisibleItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//when the item height overflow screen, findLastCompletelyVisibleItemPosition always return NO_POSITION
|
||||
if (lastVisibleItem.itemView.getMeasuredHeight() > getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, getMeasuredHeight() - defaultOverflowItemScrollKeep, null, ViewCompat.TYPE_TOUCH);
|
||||
} else if (lastVisibleItem.itemView.getMeasuredHeight() == getMeasuredHeight()) {
|
||||
super.scrollByInternal(0, getMeasuredHeight(), null, ViewCompat.TYPE_TOUCH);
|
||||
} else {
|
||||
super.scrollByInternal(0, lastVisibleItem.itemView.getTop(), null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
} else if (layout instanceof StaggeredGridLayoutManager) {
|
||||
StaggeredGridLayoutManager realLayout = (StaggeredGridLayoutManager) layout;
|
||||
int[] lastVisiblePosition = realLayout.findLastVisibleItemPositions(null);
|
||||
ViewHolder[] lastVisibleItems = new ViewHolder[lastVisiblePosition.length];
|
||||
for (int i = 0; i < lastVisiblePosition.length; i++) {
|
||||
lastVisibleItems[i] = findViewHolderForAdapterPosition(lastVisiblePosition[i]);
|
||||
}
|
||||
int scrollDistance = 0;
|
||||
for (ViewHolder lastVisibleItem : lastVisibleItems) {
|
||||
if (lastVisibleItem != null) {
|
||||
if (lastVisibleItem.itemView.getTop() > scrollDistance) {
|
||||
scrollDistance = lastVisibleItem.itemView.getTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (scrollDistance == 0) {
|
||||
scrollDistance = getMeasuredHeight();
|
||||
}
|
||||
super.scrollByInternal(0, scrollDistance, null, ViewCompat.TYPE_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
defaultOverflowItemScrollKeep = (int) (h * 0.05f);
|
||||
}
|
||||
|
||||
private double getSplineDeceleration(int velocity) {
|
||||
return Math.log(0.35f * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
|
||||
}
|
||||
|
||||
private double getSplineFlingDistance(int velocity) {
|
||||
final double l = getSplineDeceleration(velocity);
|
||||
final double decelMinusOne = DECELERATION_RATE - 1.0;
|
||||
return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
|
||||
}
|
||||
|
||||
public boolean isEnableScroll() {
|
||||
return enableScroll;
|
||||
}
|
||||
|
||||
public void setEnableScroll(boolean enableScroll) {
|
||||
this.enableScroll = enableScroll;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final SavedState ss = new SavedState(super.onSaveInstanceState());
|
||||
@@ -1016,9 +622,6 @@ public class EasyRecyclerView extends BridgeRecyclerView {
|
||||
class MultiChoiceModeWrapper implements MultiChoiceModeListener {
|
||||
private MultiChoiceModeListener mWrapped;
|
||||
|
||||
public void setWrapped(MultiChoiceModeListener wrapped) {
|
||||
mWrapped = wrapped;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean hasWrappedCallback() {
|
||||
|
||||
@@ -35,7 +35,12 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.collection.LruCache;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.hippo.Native;
|
||||
@@ -91,8 +96,6 @@ import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
import rikka.material.app.LocaleDelegate;
|
||||
|
||||
public class EhApplication extends SceneApplication {
|
||||
|
||||
@@ -104,6 +107,8 @@ public class EhApplication extends SceneApplication {
|
||||
private static final boolean DEBUG_PRINT_IMAGE_COUNT = false;
|
||||
private static final long DEBUG_PRINT_INTERVAL = 3000L;
|
||||
|
||||
public static boolean locked = false;
|
||||
|
||||
private static EhApplication instance;
|
||||
|
||||
private final IntIdGenerator mIdGenerator = new IntIdGenerator();
|
||||
@@ -297,14 +302,17 @@ public class EhApplication extends SceneApplication {
|
||||
return application.mFavouriteStatusRouter;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String getDeveloperEmail() {
|
||||
return "nekoworkshop$protonmail.com".replace('$', '@');
|
||||
static class EhlifecycleObserver implements DefaultLifecycleObserver {
|
||||
@Override
|
||||
public void onPause(@NonNull LifecycleOwner owner) {
|
||||
locked = true;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Override
|
||||
public void onCreate() {
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(new EhlifecycleObserver());
|
||||
instance = this;
|
||||
|
||||
Thread.UncaughtExceptionHandler handler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
@@ -341,9 +349,8 @@ public class EhApplication extends SceneApplication {
|
||||
EhDB.mergeOldDB(this);
|
||||
}
|
||||
|
||||
LocaleDelegate.setDefaultLocale(Settings.getLocale());
|
||||
DayNightDelegate.setApplicationContext(this);
|
||||
DayNightDelegate.setDefaultNightMode(Settings.getTheme());
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.create(Settings.getLocale()));
|
||||
AppCompatDelegate.setDefaultNightMode(Settings.getTheme());
|
||||
|
||||
// Do io tasks in new thread
|
||||
//noinspection deprecation
|
||||
|
||||
@@ -86,7 +86,7 @@ public class Settings {
|
||||
****** Advanced
|
||||
********************/
|
||||
public static final String KEY_SAVE_PARSE_ERROR_BODY = "save_parse_error_body";
|
||||
public static final String KEY_SECURITY = "security";
|
||||
public static final String KEY_SECURITY = "require_unlock";
|
||||
public static final String DEFAULT_SECURITY = "";
|
||||
public static final String KEY_ENABLE_FINGERPRINT = "enable_fingerprint";
|
||||
public static final String KEY_READ_CACHE_SIZE = "read_cache_size";
|
||||
@@ -457,14 +457,6 @@ public class Settings {
|
||||
putBoolean(KEY_SELECT_SITE, value);
|
||||
}
|
||||
|
||||
public static boolean getQuickSearchTip() {
|
||||
return getBoolean(KEY_QUICK_SEARCH_TIP, DEFAULT_QUICK_SEARCH_TIP);
|
||||
}
|
||||
|
||||
public static void putQuickSearchTip(boolean value) {
|
||||
putBoolean(KEY_QUICK_SEARCH_TIP, value);
|
||||
}
|
||||
|
||||
public static int getTheme() {
|
||||
return getIntFromStr(KEY_THEME, DEFAULT_THEME);
|
||||
}
|
||||
@@ -594,14 +586,6 @@ public class Settings {
|
||||
putString(KEY_EXCLUDED_LANGUAGES, value);
|
||||
}
|
||||
|
||||
public static void putEInkMode(boolean value) {
|
||||
putBoolean(KEY_E_INK_MODE, value);
|
||||
}
|
||||
|
||||
public static boolean getEInkMode() {
|
||||
return getBoolean(KEY_E_INK_MODE, DEFAULT_E_INK_MODE);
|
||||
}
|
||||
|
||||
public static boolean getMeteredNetworkWarning() {
|
||||
return getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING);
|
||||
}
|
||||
@@ -1044,22 +1028,18 @@ public class Settings {
|
||||
return getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG);
|
||||
}
|
||||
|
||||
public static String getSecurity() {
|
||||
return getString(KEY_SECURITY, DEFAULT_SECURITY);
|
||||
public static boolean getSecurity() {
|
||||
return getBoolean(KEY_SECURITY, false);
|
||||
}
|
||||
|
||||
public static void putSecurity(String value) {
|
||||
putString(KEY_SECURITY, value);
|
||||
public static void putSecurity(boolean value) {
|
||||
putBoolean(KEY_SECURITY, value);
|
||||
}
|
||||
|
||||
public static boolean getEnableFingerprint() {
|
||||
return getBoolean(KEY_ENABLE_FINGERPRINT, true);
|
||||
}
|
||||
|
||||
public static void putEnableFingerprint(boolean value) {
|
||||
putBoolean(KEY_ENABLE_FINGERPRINT, value);
|
||||
}
|
||||
|
||||
public static int getReadCacheSize() {
|
||||
return getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
+18
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.ehviewer.client.exception.EhException;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import com.hippo.util.ExceptionUtils;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.client.parser;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
@@ -29,7 +29,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationChannelCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -224,8 +223,6 @@ public class DownloadService extends Service implements DownloadManager.Download
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setColorized(true)
|
||||
.addAction(R.drawable.v_pause_x24, getString(R.string.stat_download_action_stop_all), piStopAll)
|
||||
.setShowWhen(false)
|
||||
.setChannelId(CHANNEL_ID);
|
||||
@@ -254,7 +251,6 @@ public class DownloadService extends Service implements DownloadManager.Download
|
||||
mDownloadedBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle(getString(R.string.stat_download_done_title))
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setDeleteIntent(piClear)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
@@ -273,7 +269,6 @@ public class DownloadService extends Service implements DownloadManager.Download
|
||||
.setContentText(getString(R.string.stat_509_alert_title))
|
||||
.setContentText(getString(R.string.stat_509_alert_text))
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)))
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR);
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.gallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.a7zip.ArchiveException;
|
||||
import com.hippo.a7zip.InArchive;
|
||||
import com.hippo.a7zip.PropID;
|
||||
import com.hippo.a7zip.PropType;
|
||||
import com.hippo.a7zip.SeekableInputStream;
|
||||
import com.hippo.unifile.UniRandomAccessFile;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class A7ZipArchive implements Closeable {
|
||||
|
||||
private final InArchive archive;
|
||||
|
||||
private A7ZipArchive(InArchive archive) {
|
||||
this.archive = archive;
|
||||
}
|
||||
|
||||
private static boolean isSupportedFilename(String name) {
|
||||
for (String extension : GalleryProvider2.SUPPORT_IMAGE_EXTENSIONS) {
|
||||
if (name.endsWith(extension)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static A7ZipArchive create(UniRandomAccessFile file) throws ArchiveException {
|
||||
SeekableInputStream store = new UniRandomAccessFileInStream(file);
|
||||
InArchive archive = InArchive.open(store);
|
||||
if ((archive.getArchivePropertyType(PropID.ENCRYPTED) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.ENCRYPTED))
|
||||
|| (archive.getArchivePropertyType(PropID.SOLID) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.SOLID))
|
||||
|| (archive.getArchivePropertyType(PropID.IS_VOLUME) == PropType.BOOL && archive.getArchiveBooleanProperty(PropID.IS_VOLUME))) {
|
||||
throw new ArchiveException("Unsupported archive");
|
||||
}
|
||||
return new A7ZipArchive(archive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
archive.close();
|
||||
}
|
||||
|
||||
List<A7ZipArchiveEntry> getArchiveEntries() {
|
||||
List<A7ZipArchiveEntry> entries = new ArrayList<>();
|
||||
|
||||
for (int i = 0, n = archive.getNumberOfEntries(); i < n; i++) {
|
||||
if (!archive.getEntryBooleanProperty(i, PropID.ENCRYPTED)
|
||||
&& !archive.getEntryBooleanProperty(i, PropID.IS_DIR)
|
||||
&& !archive.getEntryBooleanProperty(i, PropID.IS_VOLUME)
|
||||
&& !archive.getEntryBooleanProperty(i, PropID.SOLID)) {
|
||||
String path = archive.getEntryPath(i);
|
||||
if (isSupportedFilename(path.toLowerCase())) {
|
||||
entries.add(new A7ZipArchiveEntry(archive, i, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static class A7ZipArchiveEntry {
|
||||
|
||||
private final InArchive archive;
|
||||
private final int index;
|
||||
private final String path;
|
||||
|
||||
private A7ZipArchiveEntry(InArchive archive, int index, String path) {
|
||||
this.archive = archive;
|
||||
this.index = index;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
void extract(OutputStream os) throws ArchiveException {
|
||||
archive.extractEntry(index, new OutputStreamSequentialOutStream(os));
|
||||
}
|
||||
}
|
||||
|
||||
private static class UniRandomAccessFileInStream extends SeekableInputStream {
|
||||
|
||||
private final byte[] scratch = new byte[8];
|
||||
private final UniRandomAccessFile file;
|
||||
|
||||
public UniRandomAccessFileInStream(UniRandomAccessFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(long pos) throws IOException {
|
||||
file.seek(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long tell() throws IOException {
|
||||
return file.getFilePointer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return file.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return (file.read(scratch, 0, 1) != -1) ? scratch[0] & 0xff : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NonNull byte[] b) throws IOException {
|
||||
return file.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return file.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class OutputStreamSequentialOutStream extends OutputStream {
|
||||
|
||||
private final OutputStream stream;
|
||||
|
||||
public OutputStreamSequentialOutStream(OutputStream stream) {
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
stream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
stream.write(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
stream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,23 +23,18 @@ import android.os.Process;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.hippo.a7zip.ArchiveException;
|
||||
import com.hippo.UriArchiveAccessor;
|
||||
import com.hippo.ehviewer.GetText;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.glgallery.GalleryPageView;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.unifile.UniFile;
|
||||
import com.hippo.unifile.UniRandomAccessFile;
|
||||
import com.hippo.util.NaturalComparator;
|
||||
import com.hippo.yorozuya.thread.PriorityThread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -47,15 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
|
||||
private static final AtomicInteger sIdGenerator = new AtomicInteger();
|
||||
private static final Comparator<A7ZipArchive.A7ZipArchiveEntry> naturalComparator = new Comparator<A7ZipArchive.A7ZipArchiveEntry>() {
|
||||
private final NaturalComparator comparator = new NaturalComparator();
|
||||
|
||||
@Override
|
||||
public int compare(A7ZipArchive.A7ZipArchiveEntry o1, A7ZipArchive.A7ZipArchiveEntry o2) {
|
||||
return comparator.compare(o1.getPath(), o2.getPath());
|
||||
}
|
||||
};
|
||||
private final UniFile file;
|
||||
private final UriArchiveAccessor archiveAccessor;
|
||||
private final Stack<Integer> requests = new Stack<>();
|
||||
private final AtomicInteger extractingIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);
|
||||
private final LinkedHashMap<Integer, InputStream> streams = new LinkedHashMap<>();
|
||||
@@ -66,7 +53,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
private String error;
|
||||
|
||||
public ArchiveGalleryProvider(Context context, Uri uri) {
|
||||
file = UniFile.fromMediaUri(context, uri);
|
||||
UriArchiveAccessor archiveAccessor1;
|
||||
try {
|
||||
archiveAccessor1 = new UriArchiveAccessor(context, uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
archiveAccessor1 = null;
|
||||
e.printStackTrace();
|
||||
}
|
||||
archiveAccessor = archiveAccessor1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,37 +161,14 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
private class ArchiveTask implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
UniRandomAccessFile uraf = null;
|
||||
try {
|
||||
uraf = file.createRandomAccessFile("r");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (uraf == null) {
|
||||
size = archiveAccessor.open();
|
||||
if (size <= 0) {
|
||||
size = STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_reading_failed);
|
||||
notifyDataChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
A7ZipArchive archive = null;
|
||||
try {
|
||||
archive = A7ZipArchive.create(uraf);
|
||||
} catch (ArchiveException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (archive == null) {
|
||||
size = STATE_ERROR;
|
||||
error = GetText.getString(R.string.error_invalid_archive);
|
||||
notifyDataChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
List<A7ZipArchive.A7ZipArchiveEntry> entries = archive.getArchiveEntries();
|
||||
Collections.sort(entries, naturalComparator);
|
||||
|
||||
// Update size and notify changed
|
||||
size = entries.size();
|
||||
notifyDataChanged();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
@@ -217,13 +188,13 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
}
|
||||
|
||||
// Check index valid
|
||||
if (index < 0 || index >= entries.size()) {
|
||||
if (index < 0 || index >= size) {
|
||||
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
|
||||
notifyPageFailed(index, GetText.getString(R.string.error_out_of_range));
|
||||
continue;
|
||||
}
|
||||
|
||||
Pipe pipe = new Pipe(4 * 1024);
|
||||
Pipe pipe = new Pipe(4096 * 16);
|
||||
|
||||
synchronized (streams) {
|
||||
if (streams.get(index) != null) {
|
||||
@@ -233,13 +204,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
streams.notify();
|
||||
}
|
||||
|
||||
try {
|
||||
entries.get(index).extract(pipe.getOutputStream());
|
||||
} catch (ArchiveException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
extractingIndex.lazySet(GalleryPageView.INVALID_INDEX);
|
||||
}
|
||||
archiveAccessor.extractTargetIndexToOutputStream(index, pipe.getOutputStream());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,4 +247,4 @@ public class ArchiveGalleryProvider extends GalleryProvider2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,13 +107,6 @@ class Pipe {
|
||||
public void write(@NonNull byte[] b, int off, int len) throws IOException {
|
||||
synchronized (Pipe.this) {
|
||||
while (len != 0) {
|
||||
if (outClosed) {
|
||||
throw new IOException("The OutputStream is closed");
|
||||
}
|
||||
if (inClosed) {
|
||||
throw new IOException("The InputStream is closed");
|
||||
}
|
||||
|
||||
if (head == tail && full) {
|
||||
// The buffer is full, wait for InputStream read bytes
|
||||
try {
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.preference;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.EhProxySelector;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.network.InetValidator;
|
||||
import com.hippo.preference.DialogPreference;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
public class ProxyPreference extends DialogPreference implements View.OnClickListener {
|
||||
|
||||
private Spinner mType;
|
||||
private TextInputLayout mIpInputLayout;
|
||||
private EditText mIp;
|
||||
private TextInputLayout mPortInputLayout;
|
||||
private EditText mPort;
|
||||
|
||||
public ProxyPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public ProxyPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public ProxyPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setDialogLayoutResource(R.layout.preference_dialog_proxy);
|
||||
updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort());
|
||||
}
|
||||
|
||||
private String getProxyTypeText(Context context, int type) {
|
||||
String[] array = context.getResources().getStringArray(R.array.proxy_types);
|
||||
return array[MathUtils.clamp(type, 0, array.length - 1)];
|
||||
}
|
||||
|
||||
private void updateSummary(int type, String ip, int port) {
|
||||
if ((type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS)
|
||||
&& (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))) {
|
||||
type = EhProxySelector.TYPE_SYSTEM;
|
||||
}
|
||||
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
Context context = getContext();
|
||||
setSummary(context.getString(R.string.settings_advanced_proxy_summary_1,
|
||||
getProxyTypeText(context, type),
|
||||
ip,
|
||||
port));
|
||||
} else {
|
||||
Context context = getContext();
|
||||
setSummary(context.getString(R.string.settings_advanced_proxy_summary_2,
|
||||
getProxyTypeText(context, type)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SetTextI18n")
|
||||
protected void onDialogCreated(AlertDialog dialog) {
|
||||
super.onDialogCreated(dialog);
|
||||
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this);
|
||||
|
||||
mType = (Spinner) ViewUtils.$$(dialog, R.id.type);
|
||||
mIpInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.ip_input_layout);
|
||||
mIp = (EditText) ViewUtils.$$(dialog, R.id.ip);
|
||||
mPortInputLayout = (TextInputLayout) ViewUtils.$$(dialog, R.id.port_input_layout);
|
||||
mPort = (EditText) ViewUtils.$$(dialog, R.id.port);
|
||||
|
||||
int type = Settings.getProxyType();
|
||||
String[] array = getContext().getResources().getStringArray(R.array.proxy_types);
|
||||
mType.setSelection(MathUtils.clamp(type, 0, array.length));
|
||||
|
||||
mIp.setText(Settings.getProxyIp());
|
||||
|
||||
String portString;
|
||||
int port = Settings.getProxyPort();
|
||||
if (!InetValidator.isValidInetPort(port)) {
|
||||
portString = null;
|
||||
} else {
|
||||
portString = Integer.toString(port);
|
||||
}
|
||||
mPort.setText(portString);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
super.onDialogClosed(positiveResult);
|
||||
mType = null;
|
||||
mIpInputLayout = null;
|
||||
mIp = null;
|
||||
mPortInputLayout = null;
|
||||
mPort = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Dialog dialog = getDialog();
|
||||
Context context = getContext();
|
||||
if (null == dialog || null == context || null == mType ||
|
||||
null == mIpInputLayout || null == mIp ||
|
||||
null == mPortInputLayout || null == mPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
int type = mType.getSelectedItemPosition();
|
||||
|
||||
String ip = mIp.getText().toString().trim();
|
||||
if (ip.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mIpInputLayout.setError(context.getString(R.string.text_is_empty));
|
||||
return;
|
||||
}
|
||||
}
|
||||
mIpInputLayout.setError(null);
|
||||
|
||||
int port;
|
||||
String portString = mPort.getText().toString().trim();
|
||||
if (portString.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mPortInputLayout.setError(context.getString(R.string.text_is_empty));
|
||||
return;
|
||||
} else {
|
||||
port = -1;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
port = Integer.parseInt(portString);
|
||||
} catch (NumberFormatException e) {
|
||||
port = -1;
|
||||
}
|
||||
if (!InetValidator.isValidInetPort(port)) {
|
||||
mPortInputLayout.setError(context.getString(R.string.proxy_invalid_port));
|
||||
return;
|
||||
}
|
||||
}
|
||||
mPortInputLayout.setError(null);
|
||||
|
||||
Settings.putProxyType(type);
|
||||
Settings.putProxyIp(ip);
|
||||
Settings.putProxyPort(port);
|
||||
|
||||
updateSummary(type, ip, port);
|
||||
|
||||
EhApplication.getEhProxySelector(getContext()).updateProxy();
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.preference
|
||||
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import android.widget.EditText
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.EhProxySelector
|
||||
import android.text.TextUtils
|
||||
import com.hippo.network.InetValidator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.AutoCompleteTextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.preference.DialogPreference
|
||||
import com.hippo.yorozuya.MathUtils
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
import java.lang.NumberFormatException
|
||||
|
||||
class ProxyPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : DialogPreference(context, attrs), View.OnClickListener {
|
||||
private var mType: TextInputLayout? = null
|
||||
private var mIpInputLayout: TextInputLayout? = null
|
||||
private var mIp: EditText? = null
|
||||
private var mPortInputLayout: TextInputLayout? = null
|
||||
private var mPort: EditText? = null
|
||||
private val mArray: Array<String>
|
||||
|
||||
init {
|
||||
mArray = context.resources.getStringArray(R.array.proxy_types)
|
||||
dialogLayoutResource = R.layout.preference_dialog_proxy
|
||||
updateSummary(Settings.getProxyType(), Settings.getProxyIp(), Settings.getProxyPort())
|
||||
}
|
||||
|
||||
private fun getProxyTypeText(type: Int): String {
|
||||
return mArray[MathUtils.clamp(type, 0, mArray.size - 1)]
|
||||
}
|
||||
|
||||
private fun updateSummary(type: Int, ip: String?, port: Int) {
|
||||
var type1 = type
|
||||
if ((type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS)
|
||||
&& (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))
|
||||
) {
|
||||
type1 = EhProxySelector.TYPE_SYSTEM
|
||||
}
|
||||
summary = if (type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS) {
|
||||
val context = context
|
||||
context.getString(
|
||||
R.string.settings_advanced_proxy_summary_1,
|
||||
getProxyTypeText(type1),
|
||||
ip,
|
||||
port
|
||||
)
|
||||
} else {
|
||||
val context = context
|
||||
context.getString(
|
||||
R.string.settings_advanced_proxy_summary_2,
|
||||
getProxyTypeText(type1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onDialogCreated(dialog: AlertDialog) {
|
||||
super.onDialogCreated(dialog)
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this)
|
||||
mType = ViewUtils.`$$`(dialog, R.id.type) as TextInputLayout
|
||||
val array = context.resources.getStringArray(R.array.proxy_types)
|
||||
mIpInputLayout = ViewUtils.`$$`(dialog, R.id.ip_input_layout) as TextInputLayout
|
||||
mIp = ViewUtils.`$$`(dialog, R.id.ip) as EditText
|
||||
mPortInputLayout = ViewUtils.`$$`(dialog, R.id.port_input_layout) as TextInputLayout
|
||||
mPort = ViewUtils.`$$`(dialog, R.id.port) as EditText
|
||||
val type = Settings.getProxyType()
|
||||
(mType!!.editText as AutoCompleteTextView).setText(array[MathUtils.clamp(type, 0, array.size)], false)
|
||||
mIp!!.setText(Settings.getProxyIp())
|
||||
val portString: String?
|
||||
val port = Settings.getProxyPort()
|
||||
portString = if (!InetValidator.isValidInetPort(port)) {
|
||||
null
|
||||
} else {
|
||||
port.toString()
|
||||
}
|
||||
mPort!!.setText(portString)
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
super.onDialogClosed(positiveResult)
|
||||
mType = null
|
||||
mIpInputLayout = null
|
||||
mIp = null
|
||||
mPortInputLayout = null
|
||||
mPort = null
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
val dialog = dialog
|
||||
val context: Context = context
|
||||
if (null == dialog || null == mType || null == mIpInputLayout || null == mIp || null == mPortInputLayout || null == mPort) {
|
||||
return
|
||||
}
|
||||
val type = mArray.indexOf(mType!!.editText!!.text.toString())
|
||||
val ip = mIp!!.text.toString().trim { it <= ' ' }
|
||||
if (ip.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mIpInputLayout!!.error = context.getString(R.string.text_is_empty)
|
||||
return
|
||||
}
|
||||
}
|
||||
mIpInputLayout!!.error = null
|
||||
val port: Int
|
||||
val portString = mPort!!.text.toString().trim { it <= ' ' }
|
||||
if (portString.isEmpty()) {
|
||||
if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {
|
||||
mPortInputLayout!!.error = context.getString(R.string.text_is_empty)
|
||||
return
|
||||
} else {
|
||||
port = -1
|
||||
}
|
||||
} else {
|
||||
port = try {
|
||||
portString.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
-1
|
||||
}
|
||||
if (!InetValidator.isValidInetPort(port)) {
|
||||
mPortInputLayout!!.error = context.getString(R.string.proxy_invalid_port)
|
||||
return
|
||||
}
|
||||
}
|
||||
mPortInputLayout!!.error = null
|
||||
Settings.putProxyType(type)
|
||||
Settings.putProxyIp(ip)
|
||||
Settings.putProxyPort(port)
|
||||
updateSummary(type, ip, port)
|
||||
EhApplication.getEhProxySelector(getContext()).updateProxy()
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
|
||||
public class VersionPreference extends Preference {
|
||||
|
||||
public VersionPreference(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public VersionPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public VersionPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
setTitle(R.string.settings_about_version);
|
||||
|
||||
String version;
|
||||
try {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
version = pInfo.versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
version = context.getString(R.string.error_unknown);
|
||||
}
|
||||
setSummary(version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.hippo.Native
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.preference.MessagePreference
|
||||
|
||||
class VersionPreference @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : MessagePreference(context, attrs) {
|
||||
init {
|
||||
setTitle(R.string.settings_about_version)
|
||||
@Suppress("DEPRECATION") val version: String = try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
context.getString(R.string.error_unknown)
|
||||
}
|
||||
summary = version
|
||||
}
|
||||
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
builder.setTitle(R.string.show_library_version)
|
||||
builder.setMessage(Native.getlibarchiveVersion() + "\nlzma(xz-utils) " + Native.getliblzmaVersion() + "\nzlib " + Native.getzlibVersion() + "\nlibjpeg-turbo " + Native.getlibjpeg_turboVersion())
|
||||
}
|
||||
}
|
||||
@@ -26,18 +26,19 @@ import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.material.app.MaterialActivity;
|
||||
import rikka.insets.WindowInsetsHelper;
|
||||
import rikka.layoutinflater.view.LayoutInflaterFactory;
|
||||
|
||||
public abstract class EhActivity extends MaterialActivity {
|
||||
public abstract class EhActivity extends AppCompatActivity {
|
||||
|
||||
private static final String THEME_DEFAULT = "DEFAULT";
|
||||
private static final String THEME_BLACK = "BLACK";
|
||||
@@ -47,10 +48,8 @@ public abstract class EhActivity extends MaterialActivity {
|
||||
}
|
||||
|
||||
public static String getTheme(Context context) {
|
||||
if (isBlackNightTheme()
|
||||
&& isNightMode(context.getResources().getConfiguration()))
|
||||
if (isBlackNightTheme() && isNightMode(context.getResources().getConfiguration()))
|
||||
return THEME_BLACK;
|
||||
|
||||
return THEME_DEFAULT;
|
||||
}
|
||||
|
||||
@@ -58,38 +57,67 @@ public abstract class EhActivity extends MaterialActivity {
|
||||
return (configuration.uiMode & Configuration.UI_MODE_NIGHT_YES) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyUserThemeResource(@NonNull Resources.Theme theme, boolean isDecorView) {
|
||||
theme.applyStyle(getThemeStyleRes(this), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String computeUserThemeKey() {
|
||||
return getTheme(this);
|
||||
}
|
||||
|
||||
@StyleRes
|
||||
public int getThemeStyleRes(Context context) {
|
||||
switch (getTheme(context)) {
|
||||
case THEME_BLACK:
|
||||
return R.style.ThemeOverlay_Black;
|
||||
case THEME_DEFAULT:
|
||||
default:
|
||||
return R.style.ThemeOverlay;
|
||||
if (THEME_BLACK.equals(getTheme(context))) {
|
||||
return R.style.ThemeOverlay_Black;
|
||||
}
|
||||
return R.style.ThemeOverlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
|
||||
if (getParent() == null) {
|
||||
theme.applyStyle(resid, true);
|
||||
} else {
|
||||
try {
|
||||
theme.setTo(getParent().getTheme());
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
theme.applyStyle(resid, false);
|
||||
}
|
||||
theme.applyStyle(getThemeStyleRes(this), true);
|
||||
super.onApplyThemeResource(theme, R.style.ThemeOverlay, first);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNightModeChanged(int mode) {
|
||||
getTheme().applyStyle(getThemeStyleRes(this), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
getLayoutInflater().setFactory2((new LayoutInflaterFactory(getDelegate())).addOnViewCreatedListener(WindowInsetsHelper.getLISTENER()));
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
|
||||
Window window = getWindow();
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
|
||||
window.getDecorView().post(() -> {
|
||||
WindowInsets rootWindowInsets = window.getDecorView().getRootWindowInsets();
|
||||
if (rootWindowInsets != null && rootWindowInsets.getSystemWindowInsetBottom() >= Resources.getSystem().getDisplayMetrics().density * 40) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.setNavigationBarDividerColor(getColor(R.color.navigation_bar_divider));
|
||||
}
|
||||
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(false);
|
||||
}
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
((EhApplication) getApplication()).registerActivity(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
((EhApplication) getApplication()).unregisterActivity(this);
|
||||
}
|
||||
|
||||
@@ -103,26 +131,4 @@ public abstract class EhActivity extends MaterialActivity {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyTranslucentSystemBars() {
|
||||
super.onApplyTranslucentSystemBars();
|
||||
Window window = getWindow();
|
||||
window.setStatusBarColor(Color.TRANSPARENT);
|
||||
|
||||
window.getDecorView().post(() -> {
|
||||
WindowInsets rootWindowInsets = window.getDecorView().getRootWindowInsets();
|
||||
if (rootWindowInsets != null && rootWindowInsets.getSystemWindowInsetBottom() >= Resources.getSystem().getDisplayMetrics().density * 40) {
|
||||
window.setNavigationBarColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.navigationBarColor) & 0x00ffffff | -0x20000000);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(false);
|
||||
}
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ import android.webkit.MimeTypeMap;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -58,6 +57,7 @@ import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -66,6 +66,7 @@ import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.BuildConfig;
|
||||
import com.hippo.ehviewer.R;
|
||||
@@ -105,7 +106,6 @@ import java.io.OutputStream;
|
||||
|
||||
import rikka.core.res.ConfigurationKt;
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
|
||||
public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChangeListener,
|
||||
GalleryView.Listener {
|
||||
@@ -221,16 +221,7 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
private int mLayoutMode;
|
||||
private int mSize;
|
||||
private int mCurrentIndex;
|
||||
private int mSavingPage = -1; ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
result -> {
|
||||
if (result && mSavingPage != -1) {
|
||||
saveImage(mSavingPage);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mSavingPage = -1;
|
||||
});
|
||||
private int mSavingPage = -1;
|
||||
|
||||
private void buildProvider() {
|
||||
if (mGalleryProvider != null) {
|
||||
@@ -247,13 +238,9 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
}
|
||||
} else if (Intent.ACTION_VIEW.equals(mAction)) {
|
||||
if (mUri != null) {
|
||||
// Only support zip now
|
||||
try {
|
||||
grantUriPermission(BuildConfig.APPLICATION_ID, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
} catch (Exception e) {
|
||||
// Some stupid file manager send us a uri and don't allow us to read it
|
||||
// java.lang.SecurityException: UID 10671 does not have permission to
|
||||
// content://com.UCMobile.fileProvider/external_files/BaiduNetdisk/getvoice [user 0]
|
||||
Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mGalleryProvider = new ArchiveGalleryProvider(this, mUri);
|
||||
@@ -302,19 +289,23 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
switch (Settings.getReadTheme()) {
|
||||
case 1:
|
||||
getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_YES, false);
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
break;
|
||||
case 2:
|
||||
getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_NO, false);
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
break;
|
||||
}
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean respectDefaultNightMode() {
|
||||
return Settings.getReadTheme() == 0;
|
||||
}
|
||||
} ActivityResultLauncher<String> requestStoragePermissionLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.RequestPermission(),
|
||||
result -> {
|
||||
if (result && mSavingPage != -1) {
|
||||
saveImage(mSavingPage);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
mSavingPage = -1;
|
||||
});
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"WrongConstant"})
|
||||
@@ -357,7 +348,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
.setStartPosition(Settings.getStartPosition())
|
||||
.setStartPage(startPage)
|
||||
.setBackgroundColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.colorBackground))
|
||||
.setEdgeColor(ResourcesKt.resolveColor(getTheme(), R.attr.colorEdgeEffect) & 0xffffff | 0x33000000)
|
||||
.setPagerInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0)
|
||||
.setScrollInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0)
|
||||
.setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height))
|
||||
@@ -1051,15 +1041,15 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
private final Spinner mScaleMode;
|
||||
private final Spinner mStartPosition;
|
||||
private final Spinner mReadTheme;
|
||||
private final Switch mKeepScreenOn;
|
||||
private final Switch mShowClock;
|
||||
private final Switch mShowProgress;
|
||||
private final Switch mShowBattery;
|
||||
private final Switch mShowPageInterval;
|
||||
private final Switch mVolumePage;
|
||||
private final Switch mReverseVolumePage;
|
||||
private final Switch mReadingFullscreen;
|
||||
private final Switch mCustomScreenLightness;
|
||||
private final MaterialSwitch mKeepScreenOn;
|
||||
private final MaterialSwitch mShowClock;
|
||||
private final MaterialSwitch mShowProgress;
|
||||
private final MaterialSwitch mShowBattery;
|
||||
private final MaterialSwitch mShowPageInterval;
|
||||
private final MaterialSwitch mVolumePage;
|
||||
private final MaterialSwitch mReverseVolumePage;
|
||||
private final MaterialSwitch mReadingFullscreen;
|
||||
private final MaterialSwitch mCustomScreenLightness;
|
||||
private final SeekBar mScreenLightness;
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -1304,4 +1294,6 @@ public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChan
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -22,18 +22,15 @@ import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.verify.domain.DomainVerificationManager;
|
||||
import android.content.pm.verify.domain.DomainVerificationUserState;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
@@ -43,7 +40,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
@@ -51,6 +47,7 @@ import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.hippo.app.EditTextDialogBuilder;
|
||||
import com.hippo.ehviewer.AppConfig;
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
@@ -85,7 +82,6 @@ import com.hippo.unifile.UniFile;
|
||||
import com.hippo.util.BitmapUtils;
|
||||
import com.hippo.util.ClipboardUtil;
|
||||
import com.hippo.widget.DrawerView;
|
||||
import com.hippo.widget.LoadImageView;
|
||||
import com.hippo.yorozuya.IOUtils;
|
||||
import com.hippo.yorozuya.SimpleHandler;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
@@ -96,8 +92,6 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
|
||||
public final class MainActivity extends StageActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
@@ -139,10 +133,6 @@ public final class MainActivity extends StageActivity
|
||||
private NavigationView mNavView;
|
||||
@Nullable
|
||||
private DrawerView mRightDrawer;
|
||||
@Nullable
|
||||
private LoadImageView mAvatar;
|
||||
@Nullable
|
||||
private TextView mDisplayName;
|
||||
private int mNavCheckedItem = 0;
|
||||
|
||||
@Override
|
||||
@@ -153,7 +143,7 @@ public final class MainActivity extends StageActivity
|
||||
@NonNull
|
||||
@Override
|
||||
protected Announcer getLaunchAnnouncer() {
|
||||
if (!TextUtils.isEmpty(Settings.getSecurity())) {
|
||||
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
|
||||
return new Announcer(SecurityScene.class);
|
||||
} else if (Settings.getShowWarning()) {
|
||||
return new Announcer(WarningScene.class);
|
||||
@@ -171,7 +161,7 @@ public final class MainActivity extends StageActivity
|
||||
// Sometimes scene can't show directly
|
||||
private Announcer processAnnouncer(Announcer announcer) {
|
||||
if (0 == getSceneCount()) {
|
||||
if (!TextUtils.isEmpty(Settings.getSecurity())) {
|
||||
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this)) {
|
||||
Bundle newArgs = new Bundle();
|
||||
newArgs.putString(SecurityScene.KEY_TARGET_SCENE, announcer.getClazz().getName());
|
||||
newArgs.putBundle(SecurityScene.KEY_TARGET_ARGS, announcer.getArgs());
|
||||
@@ -320,19 +310,6 @@ public final class MainActivity extends StageActivity
|
||||
mDrawerLayout.setStatusBarBackgroundColor(0);
|
||||
mNavView = (NavigationView) ViewUtils.$$(this, R.id.nav_view);
|
||||
mRightDrawer = (DrawerView) ViewUtils.$$(this, R.id.right_drawer);
|
||||
View headerLayout = mNavView.getHeaderView(0);
|
||||
mAvatar = (LoadImageView) ViewUtils.$$(headerLayout, R.id.avatar);
|
||||
mDisplayName = (TextView) ViewUtils.$$(headerLayout, R.id.display_name);
|
||||
ViewUtils.$$(headerLayout, R.id.night_mode).setOnClickListener(v -> {
|
||||
int theme = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_YES) > 0 ? DayNightDelegate.MODE_NIGHT_NO : DayNightDelegate.MODE_NIGHT_YES;
|
||||
DayNightDelegate.setDefaultNightMode(theme);
|
||||
recreate();
|
||||
if (Settings.getTheme() != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) {
|
||||
Settings.putTheme(theme);
|
||||
}
|
||||
});
|
||||
|
||||
updateProfile();
|
||||
|
||||
if (mNavView != null) {
|
||||
mNavView.setNavigationItemSelectedListener(this);
|
||||
@@ -440,23 +417,21 @@ public final class MainActivity extends StageActivity
|
||||
mDrawerLayout = null;
|
||||
mNavView = null;
|
||||
mRightDrawer = null;
|
||||
mAvatar = null;
|
||||
mDisplayName = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
if (Settings.getSecurity() && SecurityScene.Companion.isAuthenticationSupported(this) && EhApplication.locked) {
|
||||
startScene(new Announcer(SecurityScene.class), true);
|
||||
}
|
||||
super.onResume();
|
||||
|
||||
setNavCheckedItem(mNavCheckedItem);
|
||||
|
||||
checkClipboardUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransactScene() {
|
||||
super.onTransactScene();
|
||||
|
||||
checkClipboardUrl();
|
||||
}
|
||||
|
||||
@@ -546,25 +521,6 @@ public final class MainActivity extends StageActivity
|
||||
}
|
||||
}
|
||||
|
||||
public void updateProfile() {
|
||||
if (null == mAvatar || null == mDisplayName) {
|
||||
return;
|
||||
}
|
||||
|
||||
String avatarUrl = Settings.getAvatar();
|
||||
if (TextUtils.isEmpty(avatarUrl)) {
|
||||
mAvatar.load(R.drawable.default_avatar);
|
||||
} else {
|
||||
mAvatar.load(avatarUrl, avatarUrl);
|
||||
}
|
||||
|
||||
String displayName = Settings.getDisplayName();
|
||||
if (TextUtils.isEmpty(displayName)) {
|
||||
displayName = getString(R.string.default_display_name);
|
||||
}
|
||||
mDisplayName.setText(displayName);
|
||||
}
|
||||
|
||||
public void addAboveSnackView(View view) {
|
||||
if (mStageLayout != null) {
|
||||
mStageLayout.addAboveSnackView(view);
|
||||
|
||||
@@ -21,40 +21,19 @@ import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.EhApplication;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.text.Html;
|
||||
import com.hippo.util.AppHelper;
|
||||
|
||||
public class AboutFragment extends BasePreferenceFragment {
|
||||
|
||||
private static final String KEY_AUTHOR = "author";
|
||||
//private static final String KEY_DONATE = "donate";
|
||||
private static final String KEY_CHECK_FOR_UPDATES = "check_for_updates";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.about_settings);
|
||||
|
||||
Preference author = findPreference(KEY_AUTHOR);
|
||||
//Preference donate = findPreference(KEY_DONATE);
|
||||
Preference checkForUpdate = findPreference(KEY_CHECK_FOR_UPDATES);
|
||||
|
||||
author.setSummary(Html.fromHtml(getString(R.string.settings_about_author_summary).replace('$', '@')));
|
||||
|
||||
author.setOnPreferenceClickListener(this);
|
||||
//donate.setOnPreferenceClickListener(this);
|
||||
checkForUpdate.setOnPreferenceClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
String key = preference.getKey();
|
||||
if (KEY_AUTHOR.equals(key)) {
|
||||
AppHelper.sendEmail(requireActivity(), EhApplication.getDeveloperEmail(),
|
||||
"About EhViewer", null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -27,6 +28,8 @@ import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
@@ -54,8 +57,6 @@ import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import rikka.material.app.LocaleDelegate;
|
||||
|
||||
public class AdvancedFragment extends BasePreferenceFragment {
|
||||
|
||||
private static final String KEY_DUMP_LOGCAT = "dump_logcat";
|
||||
@@ -291,9 +292,9 @@ public class AdvancedFragment extends BasePreferenceFragment {
|
||||
String key = preference.getKey();
|
||||
if (KEY_APP_LANGUAGE.equals(key)) {
|
||||
if ("system".equals(newValue)) {
|
||||
LocaleDelegate.setDefaultLocale(LocaleDelegate.getSystemLocale());
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.wrap(Resources.getSystem().getConfiguration().getLocales()));
|
||||
} else {
|
||||
LocaleDelegate.setDefaultLocale(Locale.forLanguageTag((String) newValue));
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags((String) newValue));
|
||||
}
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.hippo.ehviewer.ui.SettingsActivity;
|
||||
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
|
||||
public class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
@@ -53,11 +67,4 @@ public class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
public void showTip(CharSequence message, int length) {
|
||||
((SettingsActivity) requireActivity()).showTip(message, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
|
||||
RecyclerView recyclerView = super.onCreateRecyclerView(inflater, parent, savedInstanceState);
|
||||
RecyclerViewKt.fixEdgeEffect(recyclerView, false, true);
|
||||
return recyclerView;
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,13 @@ import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.client.EhTagDatabase;
|
||||
|
||||
import rikka.material.app.DayNightDelegate;
|
||||
|
||||
public class EhFragment extends BasePreferenceFragment {
|
||||
|
||||
@Override
|
||||
@@ -62,7 +61,7 @@ public class EhFragment extends BasePreferenceFragment {
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
String key = preference.getKey();
|
||||
if (Settings.KEY_THEME.equals(key)) {
|
||||
DayNightDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
|
||||
AppCompatDelegate.setDefaultNightMode(Integer.parseInt((String) newValue));
|
||||
requireActivity().recreate();
|
||||
return true;
|
||||
} else if (Settings.KEY_GALLERY_SITE.equals(key)) {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.app.Dialog;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
|
||||
/**
|
||||
* Created by Mo10 on 2018/2/10.
|
||||
*/
|
||||
|
||||
public class PrivacyFragment extends BasePreferenceFragment {
|
||||
private static final String KEY_PATTERN_PROTECTION = "pattern_protection";
|
||||
|
||||
@Override
|
||||
public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.privacy_settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
Preference patternProtection = findPreference(KEY_PATTERN_PROTECTION);
|
||||
patternProtection.setSummary(TextUtils.isEmpty(Settings.getSecurity()) ?
|
||||
R.string.settings_privacy_pattern_protection_not_set :
|
||||
R.string.settings_privacy_pattern_protection_set);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.settings_privacy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.ui.scene.SecurityScene.Companion.isAuthenticationSupported
|
||||
|
||||
/**
|
||||
* Created by Mo10 on 2018/2/10.
|
||||
*/
|
||||
class PrivacyFragment : BasePreferenceFragment() {
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.privacy_settings)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (!isAuthenticationSupported(requireContext())) {
|
||||
Settings.putSecurity(false)
|
||||
findPreference<SwitchPreferenceCompat>("require_unlock")?.isEnabled = false
|
||||
findPreference<SwitchPreferenceCompat>("require_unlock")?.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFragmentTitle(): Int {
|
||||
return R.string.settings_privacy
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.biometric.BiometricManager;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.widget.lockpattern.LockPatternUtils;
|
||||
import com.hippo.widget.lockpattern.LockPatternView;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
public class SetSecurityFragment extends BaseFragment implements View.OnClickListener {
|
||||
|
||||
@Nullable
|
||||
private LockPatternView mPatternView;
|
||||
@Nullable
|
||||
private View mCancel;
|
||||
@Nullable
|
||||
private View mSet;
|
||||
@Nullable
|
||||
private CheckBox mFingerprint;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.activity_set_security, container, false);
|
||||
|
||||
mPatternView = (LockPatternView) ViewUtils.$$(view, R.id.pattern_view);
|
||||
mCancel = ViewUtils.$$(view, R.id.cancel);
|
||||
mSet = ViewUtils.$$(view, R.id.set);
|
||||
mFingerprint = (CheckBox) ViewUtils.$$(view, R.id.fingerprint_checkbox);
|
||||
|
||||
String pattern = Settings.getSecurity();
|
||||
if (!TextUtils.isEmpty(pattern)) {
|
||||
mPatternView.setPattern(LockPatternView.DisplayMode.Correct,
|
||||
LockPatternUtils.stringToPattern(pattern));
|
||||
}
|
||||
|
||||
if (BiometricManager.from(requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
mFingerprint.setVisibility(View.VISIBLE);
|
||||
mFingerprint.setChecked(Settings.getEnableFingerprint());
|
||||
}
|
||||
|
||||
mCancel.setOnClickListener(this);
|
||||
mSet.setOnClickListener(this);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
mPatternView = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == mCancel) {
|
||||
requireActivity().onBackPressed();
|
||||
} else if (v == mSet) {
|
||||
if (null != mPatternView && null != mFingerprint) {
|
||||
String security;
|
||||
if (mPatternView.getCellSize() <= 1) {
|
||||
security = "";
|
||||
} else {
|
||||
security = mPatternView.getPatternString();
|
||||
}
|
||||
Settings.putSecurity(security);
|
||||
Settings.putEnableFingerprint(mFingerprint.getVisibility() == View.VISIBLE &&
|
||||
mFingerprint.isChecked() && !security.isEmpty());
|
||||
}
|
||||
requireActivity().onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFragmentTitle() {
|
||||
return R.string.set_pattern_protection;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
@@ -58,13 +58,6 @@ public abstract class BaseScene extends SceneFragment {
|
||||
private SparseArray<Parcelable> drawerViewState;
|
||||
private boolean needWhiteStatusBar = needWhiteStatusBar();
|
||||
|
||||
public void updateAvatar() {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof MainActivity) {
|
||||
((MainActivity) activity).updateProfile();
|
||||
}
|
||||
}
|
||||
|
||||
public void addAboveSnackView(View view) {
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity instanceof MainActivity) {
|
||||
|
||||
@@ -30,7 +30,6 @@ import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -114,7 +113,6 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public class DownloadsScene extends ToolbarScene
|
||||
@@ -378,12 +376,10 @@ public class DownloadsScene extends ToolbarScene
|
||||
mRecyclerView.scrollToPosition(mInitPosition);
|
||||
mInitPosition = -1;
|
||||
}
|
||||
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 4, 4, 4, 4, TypedValue.COMPLEX_UNIT_DIP);
|
||||
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
|
||||
|
||||
mFastScroller.attachToRecyclerView(mRecyclerView);
|
||||
HandlerDrawable handlerDrawable = new HandlerDrawable();
|
||||
handlerDrawable.setColor(com.google.android.material.R.color.material_dynamic_neutral100);
|
||||
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
|
||||
mFastScroller.setHandlerDrawable(handlerDrawable);
|
||||
mFastScroller.setOnDragHandlerListener(this);
|
||||
|
||||
@@ -468,7 +464,6 @@ public class DownloadsScene extends ToolbarScene
|
||||
TapTarget.forBounds(bounds,
|
||||
getString(R.string.guide_download_labels_title),
|
||||
getString(R.string.guide_download_labels_text))
|
||||
.outerCircleColor(R.color.colorPrimary)
|
||||
.transparentTarget(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
@@ -1345,7 +1340,6 @@ public class DownloadsScene extends ToolbarScene
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, list.get(index));
|
||||
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
|
||||
startScene(announcer);
|
||||
} else if (start == v) {
|
||||
Intent intent = new Intent(activity, DownloadService.class);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.ui.scene;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.transition.TransitionInflater;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.scene.TransitionHelper;
|
||||
|
||||
class EnterGalleryDetailTransaction implements TransitionHelper {
|
||||
|
||||
private final View mThumb;
|
||||
|
||||
public EnterGalleryDetailTransaction(View thumb) {
|
||||
mThumb = thumb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTransition(Context context, FragmentTransaction transaction,
|
||||
Fragment exit, Fragment enter) {
|
||||
if (mThumb == null || !(enter instanceof GalleryDetailScene)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String transitionName = ViewCompat.getTransitionName(mThumb);
|
||||
if (transitionName != null) {
|
||||
exit.setSharedElementReturnTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
|
||||
exit.setExitTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
|
||||
enter.setSharedElementEnterTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
|
||||
enter.setEnterTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
|
||||
transaction.addSharedElement(mThumb, transitionName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,8 @@ public class FavoritesScene extends BaseScene implements
|
||||
updateSearchBar();
|
||||
mSearchBarMover = new SearchBarMover(this, mSearchBar, mRecyclerView);
|
||||
|
||||
mActionFabDrawable = new AddDeleteDrawable(context, resources.getColor(R.color.primary_drawable_dark));
|
||||
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
|
||||
mActionFabDrawable = new AddDeleteDrawable(context, colorID);
|
||||
mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable);
|
||||
mFabLayout.setExpanded(false, false);
|
||||
mFabLayout.setAutoCancel(true);
|
||||
@@ -334,7 +335,6 @@ public class FavoritesScene extends BaseScene implements
|
||||
TapTarget.forBounds(bounds,
|
||||
getString(R.string.guide_collections_title),
|
||||
getString(R.string.guide_collections_text))
|
||||
.outerCircleColor(R.color.colorPrimary)
|
||||
.transparentTarget(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
@@ -544,10 +544,6 @@ public class FavoritesScene extends BaseScene implements
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi);
|
||||
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
View thumb;
|
||||
if (null != (thumb = view.findViewById(R.id.thumb))) {
|
||||
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
|
||||
}
|
||||
startScene(announcer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
@@ -94,7 +93,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
|
||||
public final class GalleryCommentsScene extends ToolbarScene
|
||||
implements View.OnClickListener, SwipeRefreshLayout.OnRefreshListener {
|
||||
@@ -176,7 +174,6 @@ public final class GalleryCommentsScene extends ToolbarScene
|
||||
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.scene_gallery_comments, container, false);
|
||||
mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view);
|
||||
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
|
||||
TextView tip = (TextView) ViewUtils.$$(view, R.id.tip);
|
||||
mEditPanel = ViewUtils.$$(view, R.id.edit_panel);
|
||||
mSendImage = (ImageView) ViewUtils.$$(mEditPanel, R.id.send);
|
||||
@@ -223,7 +220,6 @@ public final class GalleryCommentsScene extends ToolbarScene
|
||||
if (itemAnimator instanceof DefaultItemAnimator) {
|
||||
((DefaultItemAnimator) itemAnimator).setSupportsChangeAnimations(false);
|
||||
}
|
||||
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 0, 0, 0, 80, TypedValue.COMPLEX_UNIT_DIP);
|
||||
|
||||
mSendImage.setOnClickListener(this);
|
||||
mFab.setOnClickListener(this);
|
||||
|
||||
@@ -36,15 +36,14 @@ import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.RatingBar;
|
||||
@@ -59,12 +58,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.transition.TransitionInflater;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
@@ -105,7 +100,6 @@ import com.hippo.ehviewer.ui.annotation.WholeLifeCircle;
|
||||
import com.hippo.ehviewer.widget.GalleryRatingBar;
|
||||
import com.hippo.scene.Announcer;
|
||||
import com.hippo.scene.SceneFragment;
|
||||
import com.hippo.scene.TransitionHelper;
|
||||
import com.hippo.text.Html;
|
||||
import com.hippo.text.URLImageGetter;
|
||||
import com.hippo.util.AppHelper;
|
||||
@@ -137,7 +131,7 @@ import java.util.List;
|
||||
import okhttp3.HttpUrl;
|
||||
import rikka.core.res.ResourcesKt;
|
||||
|
||||
public class GalleryDetailScene extends BaseScene implements View.OnClickListener,
|
||||
public class GalleryDetailScene extends ToolbarScene implements View.OnClickListener,
|
||||
com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener,
|
||||
View.OnLongClickListener {
|
||||
|
||||
@@ -178,8 +172,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
@Nullable
|
||||
private TextView mCategory;
|
||||
@Nullable
|
||||
private ImageView mOtherActions;
|
||||
@Nullable
|
||||
private ViewGroup mActionGroup;
|
||||
@Nullable
|
||||
private TextView mDownload;
|
||||
@@ -250,8 +242,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
private View mProgress;
|
||||
@Nullable
|
||||
private ViewTransition mViewTransition2;
|
||||
@Nullable
|
||||
private PopupMenu mPopupMenu;
|
||||
@WholeLifeCircle
|
||||
private int mDownloadState;
|
||||
@Nullable
|
||||
@@ -499,10 +489,62 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
outState.putInt(KEY_REQUEST_ID, mRequestId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMenuResId() {
|
||||
return R.menu.scene_gallery_detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setNavigationIcon(R.drawable.v_arrow_left_dark_x24);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNavigationClick() {
|
||||
onBackPressed();
|
||||
super.onNavigationClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_open_in_other_app) {
|
||||
String url = getGalleryDetailUrl(false);
|
||||
Activity activity = getMainActivity();
|
||||
if (null != url && null != activity) {
|
||||
UrlOpener.openUrl(activity, url, false);
|
||||
}
|
||||
} else if (itemId == R.id.action_refresh) {
|
||||
if (mState != STATE_REFRESH && mState != STATE_REFRESH_HEADER) {
|
||||
adjustViewVisibility(STATE_REFRESH, true);
|
||||
request();
|
||||
}
|
||||
} else if (itemId == R.id.action_add_tag) {
|
||||
if (mGalleryDetail == null) {
|
||||
return false;
|
||||
}
|
||||
if (mGalleryDetail.apiUid < 0) {
|
||||
showTip(R.string.sign_in_first, LENGTH_LONG);
|
||||
return false;
|
||||
}
|
||||
EditTextDialogBuilder builder = new EditTextDialogBuilder(requireContext(), "", getString(R.string.action_add_tag_tip));
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
AlertDialog dialog = builder.setTitle(R.string.action_add_tag)
|
||||
.show();
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
.setOnClickListener(v -> {
|
||||
voteTag(builder.getText().trim(), 1);
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
public View onCreateViewWithToolbar(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
// Get download state
|
||||
long gid = getGid();
|
||||
if (gid != -1) {
|
||||
@@ -541,14 +583,12 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
mTitle = (TextView) ViewUtils.$$(mHeader, R.id.title);
|
||||
mUploader = (TextView) ViewUtils.$$(mHeader, R.id.uploader);
|
||||
mCategory = (TextView) ViewUtils.$$(mHeader, R.id.category);
|
||||
mOtherActions = (ImageView) ViewUtils.$$(mHeader, R.id.other_actions);
|
||||
mActionGroup = (ViewGroup) ViewUtils.$$(mHeader, R.id.action_card);
|
||||
mDownload = (TextView) ViewUtils.$$(mActionGroup, R.id.download);
|
||||
mRead = (TextView) ViewUtils.$$(mActionGroup, R.id.read);
|
||||
|
||||
mUploader.setOnClickListener(this);
|
||||
mCategory.setOnClickListener(this);
|
||||
mOtherActions.setOnClickListener(this);
|
||||
mDownload.setOnClickListener(this);
|
||||
mDownload.setOnLongClickListener(this);
|
||||
mRead.setOnClickListener(this);
|
||||
@@ -558,7 +598,7 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
mBelowHeader = mainView.findViewById(R.id.below_header);
|
||||
View belowHeader = mBelowHeader;
|
||||
|
||||
mInfo = ViewUtils.$$(belowHeader, R.id.info);
|
||||
mInfo = ViewUtils.$$(mHeader, R.id.info);
|
||||
mLanguage = (TextView) ViewUtils.$$(mInfo, R.id.language);
|
||||
mPages = (TextView) ViewUtils.$$(mInfo, R.id.pages);
|
||||
mSize = (TextView) ViewUtils.$$(mInfo, R.id.size);
|
||||
@@ -650,7 +690,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
mTitle = null;
|
||||
mUploader = null;
|
||||
mCategory = null;
|
||||
mOtherActions = null;
|
||||
mActionGroup = null;
|
||||
mDownload = null;
|
||||
mRead = null;
|
||||
@@ -690,8 +729,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
mProgress = null;
|
||||
|
||||
mViewTransition2 = null;
|
||||
|
||||
mPopupMenu = null;
|
||||
}
|
||||
|
||||
private boolean prepareData() {
|
||||
@@ -1084,51 +1121,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePopMenu() {
|
||||
if (mPopupMenu != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
AssertUtils.assertNotNull(context);
|
||||
PopupMenu popup = new PopupMenu(context, mOtherActions, Gravity.TOP);
|
||||
mPopupMenu = popup;
|
||||
popup.getMenuInflater().inflate(R.menu.scene_gallery_detail, popup.getMenu());
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_open_in_other_app) {
|
||||
String url = getGalleryDetailUrl(false);
|
||||
Activity activity = getMainActivity();
|
||||
if (null != url && null != activity) {
|
||||
UrlOpener.openUrl(activity, url, false);
|
||||
}
|
||||
} else if (itemId == R.id.action_refresh) {
|
||||
if (mState != STATE_REFRESH && mState != STATE_REFRESH_HEADER) {
|
||||
adjustViewVisibility(STATE_REFRESH, true);
|
||||
request();
|
||||
}
|
||||
} else if (itemId == R.id.action_add_tag) {
|
||||
if (mGalleryDetail == null) {
|
||||
return false;
|
||||
}
|
||||
if (mGalleryDetail.apiUid < 0) {
|
||||
showTip(R.string.sign_in_first, LENGTH_LONG);
|
||||
return false;
|
||||
}
|
||||
EditTextDialogBuilder builder = new EditTextDialogBuilder(context, "", getString(R.string.action_add_tag_tip));
|
||||
builder.setPositiveButton(android.R.string.ok, null);
|
||||
AlertDialog dialog = builder.setTitle(R.string.action_add_tag)
|
||||
.show();
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
.setOnClickListener(v -> {
|
||||
voteTag(builder.getText().trim(), 1);
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void showSimilarGalleryList() {
|
||||
GalleryDetail gd = mGalleryDetail;
|
||||
if (null == gd) {
|
||||
@@ -1203,11 +1195,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
if (request()) {
|
||||
adjustViewVisibility(STATE_REFRESH, true);
|
||||
}
|
||||
} else if (mOtherActions == v) {
|
||||
ensurePopMenu();
|
||||
if (mPopupMenu != null) {
|
||||
mPopupMenu.show();
|
||||
}
|
||||
} else if (mUploader == v) {
|
||||
String uploader = getUploader();
|
||||
if (TextUtils.isEmpty(uploader)) {
|
||||
@@ -1552,17 +1539,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (mViewTransition != null && mThumb != null &&
|
||||
mViewTransition.getShownViewIndex() == 0 && mThumb.isShown()) {
|
||||
int[] location = new int[2];
|
||||
mThumb.getLocationInWindow(location);
|
||||
// Only show transaction when thumb can be seen
|
||||
if (location[1] + mThumb.getHeight() > 0) {
|
||||
setTransitionName();
|
||||
finish(new ExitTransaction(mThumb));
|
||||
return;
|
||||
}
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
@@ -1724,38 +1700,6 @@ public class GalleryDetailScene extends BaseScene implements View.OnClickListene
|
||||
private @interface State {
|
||||
}
|
||||
|
||||
private static class ExitTransaction implements TransitionHelper {
|
||||
|
||||
private final View mThumb;
|
||||
|
||||
public ExitTransaction(View thumb) {
|
||||
mThumb = thumb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTransition(Context context,
|
||||
FragmentTransaction transaction, Fragment exit, Fragment enter) {
|
||||
if (!(enter instanceof GalleryListScene) && !(enter instanceof DownloadsScene) &&
|
||||
!(enter instanceof FavoritesScene) && !(enter instanceof HistoryScene)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String transitionName = ViewCompat.getTransitionName(mThumb);
|
||||
if (transitionName != null) {
|
||||
exit.setSharedElementReturnTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
|
||||
exit.setExitTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
|
||||
enter.setSharedElementEnterTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_move));
|
||||
enter.setEnterTransition(
|
||||
TransitionInflater.from(context).inflateTransition(R.transition.trans_fade));
|
||||
transaction.addSharedElement(mThumb, transitionName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class GetGalleryDetailListener extends EhCallback<GalleryDetailScene, GalleryDetail> {
|
||||
|
||||
public GetGalleryDetailListener(Context context, int stageId, String sceneTag) {
|
||||
|
||||
@@ -44,7 +44,6 @@ import com.hippo.yorozuya.ViewUtils;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import rikka.core.res.ResourcesKt;
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
|
||||
public final class GalleryInfoScene extends ToolbarScene {
|
||||
|
||||
@@ -174,7 +173,6 @@ public final class GalleryInfoScene extends ToolbarScene {
|
||||
mRecyclerView.addItemDecoration(decoration);
|
||||
mRecyclerView.setClipToPadding(false);
|
||||
mRecyclerView.setHasFixedSize(true);
|
||||
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
|
||||
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableItemViewHolder;
|
||||
import com.hippo.app.CheckBoxDialogBuilder;
|
||||
import com.hippo.app.EditTextDialogBuilder;
|
||||
import com.hippo.drawable.AddDeleteDrawable;
|
||||
import com.hippo.drawable.DrawerArrowDrawable;
|
||||
@@ -614,7 +613,8 @@ public final class GalleryListScene extends BaseScene
|
||||
mFabLayout.setOnExpandListener(this);
|
||||
addAboveSnackView(mFabLayout);
|
||||
|
||||
mActionFabDrawable = new AddDeleteDrawable(context, resources.getColor(R.color.primary_drawable_dark));
|
||||
int colorID = ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnSurface);
|
||||
mActionFabDrawable = new AddDeleteDrawable(context, colorID);
|
||||
mFabLayout.getPrimaryFab().setImageDrawable(mActionFabDrawable);
|
||||
|
||||
mSearchFab.setOnClickListener(this);
|
||||
@@ -658,7 +658,6 @@ public final class GalleryListScene extends BaseScene
|
||||
TapTarget.forBounds(bounds,
|
||||
getString(R.string.guide_quick_search_title),
|
||||
getString(R.string.guide_quick_search_text))
|
||||
.outerCircleColor(R.color.colorPrimary)
|
||||
.transparentTarget(true),
|
||||
new TapTargetView.Listener() {
|
||||
@Override
|
||||
@@ -709,15 +708,10 @@ public final class GalleryListScene extends BaseScene
|
||||
if (null == context) {
|
||||
return;
|
||||
}
|
||||
final CheckBoxDialogBuilder builder = new CheckBoxDialogBuilder(
|
||||
context, getString(R.string.add_quick_search_tip), getString(R.string.get_it), false);
|
||||
final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
|
||||
builder.setMessage(R.string.add_quick_search_tip);
|
||||
builder.setTitle(R.string.readme);
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
if (builder.isChecked()) {
|
||||
Settings.putQuickSearchTip(false);
|
||||
}
|
||||
showAddQuickSearchDialog(adapter, recyclerView, tip);
|
||||
}).show();
|
||||
builder.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -823,11 +817,9 @@ public final class GalleryListScene extends BaseScene
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.action_add) {
|
||||
if (Settings.getQuickSearchTip()) {
|
||||
showQuickSearchTipDialog(qsDrawerAdapter, recyclerView, tip);
|
||||
} else {
|
||||
showAddQuickSearchDialog(qsDrawerAdapter, recyclerView, tip);
|
||||
}
|
||||
showAddQuickSearchDialog(qsDrawerAdapter, recyclerView, tip);
|
||||
} else if (id == R.id.action_help) {
|
||||
showQuickSearchTipDialog(qsDrawerAdapter, recyclerView, tip);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -843,22 +835,6 @@ public final class GalleryListScene extends BaseScene
|
||||
return view;
|
||||
}
|
||||
|
||||
private boolean checkDoubleClickExit() {
|
||||
if (getStackIndex() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
if (time - mPressBackTime > BACK_PRESSED_INTERVAL) {
|
||||
// It is the last scene
|
||||
mPressBackTime = time;
|
||||
showTip(R.string.press_twice_exit, LENGTH_SHORT);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (null != mFabLayout && mFabLayout.isExpanded()) {
|
||||
@@ -866,29 +842,21 @@ public final class GalleryListScene extends BaseScene
|
||||
return;
|
||||
}
|
||||
|
||||
boolean handle;
|
||||
switch (mState) {
|
||||
default:
|
||||
case STATE_NORMAL:
|
||||
handle = checkDoubleClickExit();
|
||||
if (getStackIndex() == 0)
|
||||
requireActivity().moveTaskToBack(false);
|
||||
else
|
||||
finish();
|
||||
break;
|
||||
case STATE_SIMPLE_SEARCH:
|
||||
setState(STATE_NORMAL);
|
||||
handle = true;
|
||||
break;
|
||||
case STATE_SEARCH:
|
||||
setState(STATE_NORMAL);
|
||||
handle = true;
|
||||
break;
|
||||
case STATE_SEARCH_SHOW_LIST:
|
||||
setState(STATE_SEARCH);
|
||||
handle = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!handle) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onItemClick(View view, int position) {
|
||||
@@ -905,10 +873,6 @@ public final class GalleryListScene extends BaseScene
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi);
|
||||
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
View thumb;
|
||||
if (null != (thumb = view.findViewById(R.id.thumb))) {
|
||||
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
|
||||
}
|
||||
startScene(announcer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -86,7 +85,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import rikka.recyclerview.RecyclerViewKt;
|
||||
import rikka.core.res.ResourcesKt;
|
||||
|
||||
public class HistoryScene extends ToolbarScene {
|
||||
|
||||
@@ -232,12 +231,10 @@ public class HistoryScene extends ToolbarScene {
|
||||
mRecyclerView.addItemDecoration(decoration);
|
||||
guardManager.attachRecyclerView(mRecyclerView);
|
||||
swipeManager.attachRecyclerView(mRecyclerView);
|
||||
RecyclerViewKt.fixEdgeEffect(mRecyclerView, false, true);
|
||||
RecyclerViewKt.addEdgeSpacing(mRecyclerView, 4, 4, 4, 4, TypedValue.COMPLEX_UNIT_DIP);
|
||||
|
||||
mFastScroller.attachToRecyclerView(mRecyclerView);
|
||||
HandlerDrawable handlerDrawable = new HandlerDrawable();
|
||||
handlerDrawable.setColor(com.google.android.material.R.color.material_dynamic_neutral100);
|
||||
handlerDrawable.setColor(ResourcesKt.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary));
|
||||
mFastScroller.setHandlerDrawable(handlerDrawable);
|
||||
|
||||
updateLazyList();
|
||||
@@ -347,10 +344,6 @@ public class HistoryScene extends ToolbarScene {
|
||||
args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO);
|
||||
args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, mLazyList.get(position));
|
||||
Announcer announcer = new Announcer(GalleryDetailScene.class).setArgs(args);
|
||||
View thumb;
|
||||
if (null != (thumb = view.findViewById(R.id.thumb))) {
|
||||
announcer.setTranHelper(new EnterGalleryDetailTransaction(thumb));
|
||||
}
|
||||
startScene(announcer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.ui.scene;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.biometric.BiometricManager;
|
||||
import androidx.biometric.BiometricPrompt;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.Settings;
|
||||
import com.hippo.ehviewer.ui.MainActivity;
|
||||
import com.hippo.widget.lockpattern.LockPatternUtils;
|
||||
import com.hippo.widget.lockpattern.LockPatternView;
|
||||
import com.hippo.yorozuya.ObjectUtils;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class SecurityScene extends SolidScene implements
|
||||
LockPatternView.OnPatternListener {
|
||||
|
||||
private static final int MAX_RETRY_TIMES = 5;
|
||||
private static final String KEY_RETRY_TIMES = "retry_times";
|
||||
|
||||
@Nullable
|
||||
private LockPatternView mPatternView;
|
||||
|
||||
private SensorManager mSensorManager;
|
||||
private Sensor mAccelerometer;
|
||||
private BiometricPrompt.PromptInfo promptInfo;
|
||||
private BiometricPrompt biometricPrompt;
|
||||
private boolean canAuthenticate = false;
|
||||
|
||||
private int mRetryTimes;
|
||||
|
||||
@Override
|
||||
public boolean needShowLeftDrawer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Context context = requireContext();
|
||||
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
if (null != mSensorManager) {
|
||||
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||
}
|
||||
|
||||
if (null == savedInstanceState) {
|
||||
mRetryTimes = MAX_RETRY_TIMES;
|
||||
} else {
|
||||
mRetryTimes = savedInstanceState.getInt(KEY_RETRY_TIMES);
|
||||
}
|
||||
|
||||
canAuthenticate = Settings.getEnableFingerprint() &&
|
||||
BiometricManager.from(requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS;
|
||||
biometricPrompt = new BiometricPrompt(this, Executors.newSingleThreadExecutor(), new BiometricPrompt.AuthenticationCallback() {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||
super.onAuthenticationError(errorCode, errString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||
startSceneForCheckStep(CHECK_STEP_SECURITY, getArguments());
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed();
|
||||
}
|
||||
});
|
||||
promptInfo = new BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getString(R.string.app_name))
|
||||
.setNegativeButtonText(getString(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
mSensorManager = null;
|
||||
mAccelerometer = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (canAuthenticate) {
|
||||
startBiometricPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
private void startBiometricPrompt() {
|
||||
biometricPrompt.authenticate(promptInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(KEY_RETRY_TIMES, mRetryTimes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.scene_security, container, false);
|
||||
if (canAuthenticate) {
|
||||
view.setOnClickListener(v -> startBiometricPrompt());
|
||||
}
|
||||
|
||||
mPatternView = (LockPatternView) ViewUtils.$$(view, R.id.pattern_view);
|
||||
mPatternView.setOnPatternListener(this);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
mPatternView = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPatternStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPatternCleared() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPatternDetected(List<LockPatternView.Cell> pattern) {
|
||||
MainActivity activity = getMainActivity();
|
||||
if (null == activity || null == mPatternView) {
|
||||
return;
|
||||
}
|
||||
|
||||
String enteredPatter = LockPatternUtils.patternToString(pattern);
|
||||
String targetPatter = Settings.getSecurity();
|
||||
|
||||
if (ObjectUtils.equal(enteredPatter, targetPatter)) {
|
||||
startSceneForCheckStep(CHECK_STEP_SECURITY, getArguments());
|
||||
finish();
|
||||
} else {
|
||||
mPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
|
||||
mRetryTimes--;
|
||||
if (mRetryTimes <= 0) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.ui.scene
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.biometric.auth.AuthPromptCallback
|
||||
import androidx.biometric.auth.startClass2BiometricOrCredentialAuthentication
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.hippo.ehviewer.EhApplication
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class SecurityScene : SolidScene() {
|
||||
override fun needShowLeftDrawer(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (isAuthenticationSupported(requireContext()))
|
||||
startAuthentication(getString(R.string.settings_privacy_require_unlock))
|
||||
else
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
private fun startAuthentication(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
confirmationRequired: Boolean = true,
|
||||
) {
|
||||
startClass2BiometricOrCredentialAuthentication(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
confirmationRequired = confirmationRequired,
|
||||
executor = ContextCompat.getMainExecutor(requireContext()),
|
||||
callback = AuthenticationCallback()
|
||||
)
|
||||
}
|
||||
|
||||
inner class AuthenticationCallback : AuthPromptCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
activity: FragmentActivity?,
|
||||
result: BiometricPrompt.AuthenticationResult
|
||||
) {
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(
|
||||
activity: FragmentActivity?,
|
||||
errorCode: Int,
|
||||
errString: CharSequence
|
||||
) {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun isAuthenticationSupported(context: Context): Boolean {
|
||||
val authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||
return BiometricManager.from(context)
|
||||
.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSuccess() {
|
||||
EhApplication.locked = false
|
||||
if (stackIndex == 0)
|
||||
startSceneForCheckStep(CHECK_STEP_SECURITY, arguments)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -357,7 +357,6 @@ public final class SignInScene extends SolidScene implements EditText.OnEditorAc
|
||||
|
||||
public void onGetProfileEnd() {
|
||||
mSigningIn = false;
|
||||
updateAvatar();
|
||||
redirectTo();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableLayout;
|
||||
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.EhConfig;
|
||||
import com.hippo.widget.CheckTextView;
|
||||
import com.hippo.yorozuya.NumberUtils;
|
||||
|
||||
public class CategoryTable extends TableLayout implements View.OnLongClickListener {
|
||||
|
||||
private static final String STATE_KEY_SUPER = "super";
|
||||
private static final String STATE_KEY_CATEGORY = "category";
|
||||
|
||||
private CheckTextView mDoujinshi;
|
||||
private CheckTextView mManga;
|
||||
private CheckTextView mArtistCG;
|
||||
private CheckTextView mGameCG;
|
||||
private CheckTextView mWestern;
|
||||
private CheckTextView mNonH;
|
||||
private CheckTextView mImageSets;
|
||||
private CheckTextView mCosplay;
|
||||
private CheckTextView mAsianPorn;
|
||||
private CheckTextView mMisc;
|
||||
|
||||
private CheckTextView[] mOptions;
|
||||
|
||||
public CategoryTable(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CategoryTable(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void init() {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
inflater.inflate(R.layout.widget_category_table, this);
|
||||
|
||||
ViewGroup row0 = (ViewGroup) getChildAt(0);
|
||||
mDoujinshi = (CheckTextView) row0.getChildAt(0);
|
||||
mManga = (CheckTextView) row0.getChildAt(1);
|
||||
|
||||
ViewGroup row1 = (ViewGroup) getChildAt(1);
|
||||
mArtistCG = (CheckTextView) row1.getChildAt(0);
|
||||
mGameCG = (CheckTextView) row1.getChildAt(1);
|
||||
|
||||
ViewGroup row2 = (ViewGroup) getChildAt(2);
|
||||
mWestern = (CheckTextView) row2.getChildAt(0);
|
||||
mNonH = (CheckTextView) row2.getChildAt(1);
|
||||
|
||||
ViewGroup row3 = (ViewGroup) getChildAt(3);
|
||||
mImageSets = (CheckTextView) row3.getChildAt(0);
|
||||
mCosplay = (CheckTextView) row3.getChildAt(1);
|
||||
|
||||
ViewGroup row4 = (ViewGroup) getChildAt(4);
|
||||
mAsianPorn = (CheckTextView) row4.getChildAt(0);
|
||||
mMisc = (CheckTextView) row4.getChildAt(1);
|
||||
|
||||
mOptions = new CheckTextView[]{
|
||||
mDoujinshi, mManga, mArtistCG, mGameCG, mWestern,
|
||||
mNonH, mImageSets, mCosplay, mAsianPorn, mMisc
|
||||
};
|
||||
|
||||
for (CheckTextView option : mOptions) {
|
||||
option.setOnLongClickListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (v instanceof CheckTextView) {
|
||||
boolean checked = ((CheckTextView) v).isChecked();
|
||||
for (CheckTextView option : mOptions) {
|
||||
if (option != v) {
|
||||
option.setChecked(!checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category according to button.
|
||||
*
|
||||
* @return the category of this view
|
||||
*/
|
||||
public int getCategory() {
|
||||
int category = 0;
|
||||
if (!mDoujinshi.isChecked()) category |= EhConfig.DOUJINSHI;
|
||||
if (!mManga.isChecked()) category |= EhConfig.MANGA;
|
||||
if (!mArtistCG.isChecked()) category |= EhConfig.ARTIST_CG;
|
||||
if (!mGameCG.isChecked()) category |= EhConfig.GAME_CG;
|
||||
if (!mWestern.isChecked()) category |= EhConfig.WESTERN;
|
||||
if (!mNonH.isChecked()) category |= EhConfig.NON_H;
|
||||
if (!mImageSets.isChecked()) category |= EhConfig.IMAGE_SET;
|
||||
if (!mCosplay.isChecked()) category |= EhConfig.COSPLAY;
|
||||
if (!mAsianPorn.isChecked()) category |= EhConfig.ASIAN_PORN;
|
||||
if (!mMisc.isChecked()) category |= EhConfig.MISC;
|
||||
return category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set each button checked or not according to category.
|
||||
*
|
||||
* @param category target category
|
||||
*/
|
||||
public void setCategory(int category) {
|
||||
mDoujinshi.setChecked(!NumberUtils.int2boolean(category & EhConfig.DOUJINSHI));
|
||||
mManga.setChecked(!NumberUtils.int2boolean(category & EhConfig.MANGA));
|
||||
mArtistCG.setChecked(!NumberUtils.int2boolean(category & EhConfig.ARTIST_CG));
|
||||
mGameCG.setChecked(!NumberUtils.int2boolean(category & EhConfig.GAME_CG));
|
||||
mWestern.setChecked(!NumberUtils.int2boolean(category & EhConfig.WESTERN));
|
||||
mNonH.setChecked(!NumberUtils.int2boolean(category & EhConfig.NON_H));
|
||||
mImageSets.setChecked(!NumberUtils.int2boolean(category & EhConfig.IMAGE_SET));
|
||||
mCosplay.setChecked(!NumberUtils.int2boolean(category & EhConfig.COSPLAY));
|
||||
mAsianPorn.setChecked(!NumberUtils.int2boolean(category & EhConfig.ASIAN_PORN));
|
||||
mMisc.setChecked(!NumberUtils.int2boolean(category & EhConfig.MISC));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Bundle state = new Bundle();
|
||||
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
|
||||
state.putInt(STATE_KEY_CATEGORY, getCategory());
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
if (state instanceof Bundle) {
|
||||
final Bundle savedState = (Bundle) state;
|
||||
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
|
||||
setCategory(savedState.getInt(STATE_KEY_CATEGORY));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.widget;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
@@ -28,6 +28,7 @@ public class FixedThumb extends LoadImageView {
|
||||
|
||||
private float minAspect;
|
||||
private float maxAspect;
|
||||
private boolean alwaysCutAndScale;
|
||||
|
||||
public FixedThumb(Context context) {
|
||||
super(context);
|
||||
@@ -48,6 +49,7 @@ public class FixedThumb extends LoadImageView {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FixedThumb, defStyleAttr, defStyleRes);
|
||||
minAspect = a.getFloat(R.styleable.FixedThumb_minAspect, 0.0f);
|
||||
maxAspect = a.getFloat(R.styleable.FixedThumb_maxAspect, 0.0f);
|
||||
alwaysCutAndScale = a.getBoolean(R.styleable.FixedThumb_alwaysCutAndScale, false);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@@ -58,6 +60,10 @@ public class FixedThumb extends LoadImageView {
|
||||
|
||||
@Override
|
||||
public void onPreSetImageDrawable(Drawable drawable, boolean isTarget) {
|
||||
if (alwaysCutAndScale) {
|
||||
setScaleType(ScaleType.CENTER_CROP);
|
||||
return;
|
||||
}
|
||||
if (isTarget && drawable != null) {
|
||||
int width = drawable.getIntrinsicWidth();
|
||||
int height = drawable.getIntrinsicHeight();
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.hippo.ehviewer.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -60,9 +61,9 @@ public class GalleryGuideView extends ViewGroup implements View.OnClickListener
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
mBgColor = ResourcesKt.resolveColor(context.getTheme(), R.attr.guideBackgroundColor);
|
||||
mBgColor = Color.GRAY;
|
||||
mPaint = new Paint();
|
||||
mPaint.setColor(ResourcesKt.resolveColor(context.getTheme(), R.attr.guideTitleColor));
|
||||
mPaint.setColor(Color.WHITE);
|
||||
mPaint.setStyle(Paint.Style.STROKE);
|
||||
mPaint.setStrokeWidth(context.getResources().getDimension(R.dimen.gallery_guide_divider_width));
|
||||
setOnClickListener(this);
|
||||
|
||||
@@ -141,7 +141,7 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
|
||||
mImagePath = temp.getPath();
|
||||
mPreview.setImageBitmap(bitmap);
|
||||
mPreview.setVisibility(VISIBLE);
|
||||
((View) mPreview.getParent()).setVisibility(VISIBLE);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
@@ -166,7 +166,7 @@ public final class ImageSearchLayout extends LinearLayout implements View.OnClic
|
||||
}
|
||||
mImagePath = imagePath;
|
||||
mPreview.setImageBitmap(bitmap);
|
||||
mPreview.setVisibility(VISIBLE);
|
||||
((View) mPreview.getParent()).setVisibility(VISIBLE);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Ignore
|
||||
} finally {
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -278,8 +279,10 @@ public class SearchBar extends MaterialCardView implements View.OnClickListener,
|
||||
if (v == mTitleTextView) {
|
||||
mHelper.onClickTitle();
|
||||
} else if (v == mMenuButton) {
|
||||
v.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
|
||||
mHelper.onClickLeftIcon();
|
||||
} else if (v == mActionButton) {
|
||||
v.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
|
||||
mHelper.onClickRightIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* 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.hippo.ehviewer.widget;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView;
|
||||
import com.hippo.easyrecyclerview.MarginItemDecoration;
|
||||
import com.hippo.easyrecyclerview.SimpleHolder;
|
||||
import com.hippo.ehviewer.R;
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder;
|
||||
import com.hippo.ehviewer.client.exception.EhException;
|
||||
import com.hippo.widget.RadioGridGroup;
|
||||
import com.hippo.yorozuya.ViewUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
public class SearchLayout extends EasyRecyclerView implements CompoundButton.OnCheckedChangeListener,
|
||||
View.OnClickListener, ImageSearchLayout.Helper, TabLayout.OnTabSelectedListener {
|
||||
|
||||
public static final int SEARCH_MODE_NORMAL = 0;
|
||||
public static final int SEARCH_MODE_IMAGE = 1;
|
||||
private static final String STATE_KEY_SUPER = "super";
|
||||
private static final String STATE_KEY_SEARCH_MODE = "search_mode";
|
||||
private static final String STATE_KEY_ENABLE_ADVANCE = "enable_advance";
|
||||
private static final int ITEM_TYPE_NORMAL = 0;
|
||||
private static final int ITEM_TYPE_NORMAL_ADVANCE = 1;
|
||||
private static final int ITEM_TYPE_IMAGE = 2;
|
||||
private static final int ITEM_TYPE_ACTION = 3;
|
||||
private static final int[] SEARCH_ITEM_COUNT_ARRAY = {
|
||||
3, 2
|
||||
};
|
||||
private static final int[][] SEARCH_ITEM_TYPE = {
|
||||
{ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION}, // SEARCH_MODE_NORMAL
|
||||
{ITEM_TYPE_IMAGE, ITEM_TYPE_ACTION}, // SEARCH_MODE_IMAGE
|
||||
};
|
||||
private LayoutInflater mInflater;
|
||||
@SearchMode
|
||||
private int mSearchMode = SEARCH_MODE_NORMAL;
|
||||
private boolean mEnableAdvance = false;
|
||||
private View mNormalView;
|
||||
private CategoryTable mCategoryTable;
|
||||
private RadioGridGroup mNormalSearchMode;
|
||||
private ImageView mNormalSearchModeHelp;
|
||||
private Switch mEnableAdvanceSwitch;
|
||||
private View mAdvanceView;
|
||||
private AdvanceSearchTable mTableAdvanceSearch;
|
||||
private ImageSearchLayout mImageView;
|
||||
private View mActionView;
|
||||
private TabLayout mAction;
|
||||
private LinearLayoutManager mLayoutManager;
|
||||
private SearchAdapter mAdapter;
|
||||
private Helper mHelper;
|
||||
|
||||
public SearchLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public SearchLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context);
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private void init(Context context) {
|
||||
Resources resources = context.getResources();
|
||||
mInflater = LayoutInflater.from(context);
|
||||
|
||||
mLayoutManager = new SearchLayoutManager(context);
|
||||
mAdapter = new SearchAdapter();
|
||||
mAdapter.setHasStableIds(true);
|
||||
setLayoutManager(mLayoutManager);
|
||||
setAdapter(mAdapter);
|
||||
setHasFixedSize(true);
|
||||
setClipToPadding(false);
|
||||
((DefaultItemAnimator) getItemAnimator()).setSupportsChangeAnimations(false);
|
||||
int interval = resources.getDimensionPixelOffset(R.dimen.search_layout_interval);
|
||||
int paddingH = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_h);
|
||||
int paddingV = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_v);
|
||||
MarginItemDecoration decoration = new MarginItemDecoration(
|
||||
interval, paddingH, paddingV, paddingH, paddingV);
|
||||
addItemDecoration(decoration);
|
||||
decoration.applyPaddings(this);
|
||||
|
||||
// Create normal view
|
||||
View normalView = mInflater.inflate(R.layout.search_normal, null);
|
||||
mNormalView = normalView;
|
||||
mCategoryTable = normalView.findViewById(R.id.search_category_table);
|
||||
mNormalSearchMode = normalView.findViewById(R.id.normal_search_mode);
|
||||
mNormalSearchModeHelp = normalView.findViewById(R.id.normal_search_mode_help);
|
||||
mEnableAdvanceSwitch = normalView.findViewById(R.id.search_enable_advance);
|
||||
mNormalSearchModeHelp.setOnClickListener(this);
|
||||
mEnableAdvanceSwitch.setOnCheckedChangeListener(this);
|
||||
mEnableAdvanceSwitch.setSwitchPadding(resources.getDimensionPixelSize(R.dimen.switch_padding));
|
||||
|
||||
// Create advance view
|
||||
mAdvanceView = mInflater.inflate(R.layout.search_advance, null);
|
||||
mTableAdvanceSearch = mAdvanceView.findViewById(R.id.search_advance_search_table);
|
||||
|
||||
// Create image view
|
||||
mImageView = (ImageSearchLayout) mInflater.inflate(R.layout.search_image, null);
|
||||
mImageView.setHelper(this);
|
||||
|
||||
// Create action view
|
||||
mActionView = mInflater.inflate(R.layout.search_action, null);
|
||||
mActionView.setLayoutParams(new RecyclerView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
mAction = mActionView.findViewById(R.id.action);
|
||||
mAction.addOnTabSelectedListener(this);
|
||||
}
|
||||
|
||||
public void setHelper(Helper helper) {
|
||||
mHelper = helper;
|
||||
}
|
||||
|
||||
public void scrollSearchContainerToTop() {
|
||||
mLayoutManager.scrollToPositionWithOffset(0, 0);
|
||||
}
|
||||
|
||||
public void setImageUri(Uri imageUri) {
|
||||
mImageView.setImageUri(imageUri);
|
||||
}
|
||||
|
||||
public void setNormalSearchMode(int id) {
|
||||
mNormalSearchMode.check(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectImage() {
|
||||
if (mHelper != null) {
|
||||
mHelper.onSelectImage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchSaveInstanceState(@NonNull SparseArray<Parcelable> container) {
|
||||
super.dispatchSaveInstanceState(container);
|
||||
|
||||
mNormalView.saveHierarchyState(container);
|
||||
mAdvanceView.saveHierarchyState(container);
|
||||
mImageView.saveHierarchyState(container);
|
||||
mActionView.saveHierarchyState(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> container) {
|
||||
super.dispatchRestoreInstanceState(container);
|
||||
|
||||
mNormalView.restoreHierarchyState(container);
|
||||
mAdvanceView.restoreHierarchyState(container);
|
||||
mImageView.restoreHierarchyState(container);
|
||||
mActionView.restoreHierarchyState(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Bundle state = new Bundle();
|
||||
state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState());
|
||||
state.putInt(STATE_KEY_SEARCH_MODE, mSearchMode);
|
||||
state.putBoolean(STATE_KEY_ENABLE_ADVANCE, mEnableAdvance);
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
if (state instanceof Bundle) {
|
||||
final Bundle savedState = (Bundle) state;
|
||||
super.onRestoreInstanceState(savedState.getParcelable(STATE_KEY_SUPER));
|
||||
mSearchMode = savedState.getInt(STATE_KEY_SEARCH_MODE);
|
||||
mEnableAdvance = savedState.getBoolean(STATE_KEY_ENABLE_ADVANCE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
if (buttonView == mEnableAdvanceSwitch) {
|
||||
post(() -> {
|
||||
mEnableAdvance = isChecked;
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL) {
|
||||
if (mEnableAdvance) {
|
||||
mAdapter.notifyItemInserted(1);
|
||||
} else {
|
||||
mAdapter.notifyItemRemoved(1);
|
||||
}
|
||||
|
||||
if (mHelper != null) {
|
||||
mHelper.onChangeSearchMode();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NonConstantResourceId")
|
||||
public void formatListUrlBuilder(ListUrlBuilder urlBuilder, String query) throws EhException {
|
||||
urlBuilder.reset();
|
||||
|
||||
switch (mSearchMode) {
|
||||
case SEARCH_MODE_NORMAL:
|
||||
int nsMode = mNormalSearchMode.getCheckedRadioButtonId();
|
||||
if (nsMode == R.id.search_subscription_search) {
|
||||
urlBuilder.setMode(ListUrlBuilder.MODE_SUBSCRIPTION);
|
||||
} else if (nsMode == R.id.search_specify_uploader) {
|
||||
urlBuilder.setMode(ListUrlBuilder.MODE_UPLOADER);
|
||||
} else if (nsMode == R.id.search_specify_tag) {
|
||||
urlBuilder.setMode(ListUrlBuilder.MODE_TAG);
|
||||
} else {
|
||||
urlBuilder.setMode(ListUrlBuilder.MODE_NORMAL);
|
||||
}
|
||||
urlBuilder.setKeyword(query);
|
||||
urlBuilder.setCategory(mCategoryTable.getCategory());
|
||||
if (mEnableAdvance) {
|
||||
urlBuilder.setAdvanceSearch(mTableAdvanceSearch.getAdvanceSearch());
|
||||
urlBuilder.setMinRating(mTableAdvanceSearch.getMinRating());
|
||||
urlBuilder.setPageFrom(mTableAdvanceSearch.getPageFrom());
|
||||
urlBuilder.setPageTo(mTableAdvanceSearch.getPageTo());
|
||||
}
|
||||
break;
|
||||
case SEARCH_MODE_IMAGE:
|
||||
urlBuilder.setMode(ListUrlBuilder.MODE_IMAGE_SEARCH);
|
||||
mImageView.formatListUrlBuilder(urlBuilder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mNormalSearchModeHelp == v) {
|
||||
new MaterialAlertDialogBuilder(getContext())
|
||||
.setMessage(R.string.search_tip)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
post(() -> setSearchMode(tab.getPosition()));
|
||||
}
|
||||
|
||||
public void setSearchMode(@SearchMode int mode) {
|
||||
int oldItemCount = mAdapter.getItemCount();
|
||||
|
||||
mSearchMode = mode;
|
||||
|
||||
int newItemCount = mAdapter.getItemCount();
|
||||
|
||||
mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1);
|
||||
mAdapter.notifyItemRangeInserted(0, newItemCount - 1);
|
||||
|
||||
if (mHelper != null) {
|
||||
mHelper.onChangeSearchMode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
|
||||
}
|
||||
|
||||
@IntDef({SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
private @interface SearchMode {
|
||||
}
|
||||
|
||||
public interface Helper {
|
||||
void onChangeSearchMode();
|
||||
|
||||
void onSelectImage();
|
||||
}
|
||||
|
||||
static class SearchLayoutManager extends LinearLayoutManager {
|
||||
|
||||
public SearchLayoutManager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
try {
|
||||
super.onLayoutChildren(recycler, state);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchAdapter extends EasyRecyclerView.Adapter<SimpleHolder> {
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = SEARCH_ITEM_COUNT_ARRAY[mSearchMode];
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && !mEnableAdvance) {
|
||||
count--;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
int type = SEARCH_ITEM_TYPE[mSearchMode][position];
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
|
||||
type = ITEM_TYPE_ACTION;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view;
|
||||
|
||||
if (viewType == ITEM_TYPE_ACTION) {
|
||||
ViewUtils.removeFromParent(mActionView);
|
||||
view = mActionView;
|
||||
} else {
|
||||
view = mInflater.inflate(R.layout.search_category, parent, false);
|
||||
TextView title = view.findViewById(R.id.category_title);
|
||||
FrameLayout content = view.findViewById(R.id.category_content);
|
||||
switch (viewType) {
|
||||
case ITEM_TYPE_NORMAL:
|
||||
title.setText(R.string.search_normal);
|
||||
ViewUtils.removeFromParent(mNormalView);
|
||||
content.addView(mNormalView);
|
||||
break;
|
||||
case ITEM_TYPE_NORMAL_ADVANCE:
|
||||
title.setText(R.string.search_advance);
|
||||
ViewUtils.removeFromParent(mAdvanceView);
|
||||
content.addView(mAdvanceView);
|
||||
break;
|
||||
case ITEM_TYPE_IMAGE:
|
||||
title.setText(R.string.search_image);
|
||||
ViewUtils.removeFromParent(mImageView);
|
||||
content.addView(mImageView);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new SimpleHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SimpleHolder holder, int position) {
|
||||
if (holder.getItemViewType() == ITEM_TYPE_ACTION) {
|
||||
mAction.selectTab(mAction.getTabAt(mSearchMode));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
int type = SEARCH_ITEM_TYPE[mSearchMode][position];
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
|
||||
type = ITEM_TYPE_ACTION;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* Copyright 2022 Tarsin Norbin
|
||||
*
|
||||
* This file is part of EhViewer
|
||||
*
|
||||
* EhViewer is free software: you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* EhViewer is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with EhViewer.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.hippo.ehviewer.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.hippo.easyrecyclerview.EasyRecyclerView
|
||||
import com.hippo.easyrecyclerview.MarginItemDecoration
|
||||
import com.hippo.easyrecyclerview.SimpleHolder
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhConfig
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.widget.RadioGridGroup
|
||||
import com.hippo.yorozuya.NumberUtils
|
||||
import com.hippo.yorozuya.ViewUtils
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
class SearchLayout @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : EasyRecyclerView(context, attrs), CompoundButton.OnCheckedChangeListener, View.OnClickListener,
|
||||
ImageSearchLayout.Helper, OnTabSelectedListener {
|
||||
|
||||
@SearchMode
|
||||
private var mSearchMode = SEARCH_MODE_NORMAL
|
||||
private var mEnableAdvance = false
|
||||
private var mCategoryGroup: ChipGroup
|
||||
private var mNormalView: View
|
||||
private var mNormalSearchMode: RadioGridGroup
|
||||
private var mNormalSearchModeHelp: ImageView
|
||||
private var mEnableAdvanceSwitch: CheckBox
|
||||
private var mAdvanceView: View
|
||||
private var mTableAdvanceSearch: AdvanceSearchTable
|
||||
private var mImageView: ImageSearchLayout
|
||||
private var mActionView: View
|
||||
private var mAction: TabLayout
|
||||
private var mAdapter: SearchAdapter
|
||||
private var mHelper: Helper? = null
|
||||
private val mSharePref: SharedPreferences
|
||||
private val mInflater: LayoutInflater
|
||||
|
||||
init {
|
||||
mSharePref = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
mInflater = LayoutInflater.from(context)
|
||||
val resources = context.resources
|
||||
layoutManager = SearchLayoutManager(context)
|
||||
mAdapter = SearchAdapter()
|
||||
mAdapter.setHasStableIds(true)
|
||||
adapter = mAdapter
|
||||
setHasFixedSize(true)
|
||||
clipToPadding = false
|
||||
(itemAnimator as DefaultItemAnimator?)?.supportsChangeAnimations = false
|
||||
val interval = resources.getDimensionPixelOffset(R.dimen.search_layout_interval)
|
||||
val paddingH = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_h)
|
||||
val paddingV = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_v)
|
||||
val decoration = MarginItemDecoration(
|
||||
interval, paddingH, paddingV, paddingH, paddingV
|
||||
)
|
||||
addItemDecoration(decoration)
|
||||
decoration.applyPaddings(this)
|
||||
// Create normal view
|
||||
mNormalView = mInflater.inflate(R.layout.search_normal, null)
|
||||
val mCategoryStored = mSharePref.getInt(SEARCH_CATEGORY_PREF, EhConfig.ALL_CATEGORY)
|
||||
mCategoryGroup = mNormalView.findViewById(R.id.search_category_chipgroup)
|
||||
for (mPair in mCategoryTable) {
|
||||
val mChip = IdentifiedChip(context)
|
||||
mChip.isCheckable = true
|
||||
mChip.setText(mPair.second)
|
||||
mChip.idt = mPair.first
|
||||
mChip.isChecked = NumberUtils.int2boolean(mPair.first and mCategoryStored)
|
||||
mChip.setOnLongClickListener {
|
||||
mCategoryGroup.clearCheck()
|
||||
mChip.isChecked = true
|
||||
true
|
||||
}
|
||||
mCategoryGroup.addView(mChip)
|
||||
}
|
||||
mCategoryGroup.setOnCheckedStateChangeListener { _, checkedIds ->
|
||||
var mCategory = 0
|
||||
for (index in checkedIds) {
|
||||
mCategory = mCategory or findViewById<IdentifiedChip>(index).idt
|
||||
}
|
||||
mSharePref.edit { putInt(SEARCH_CATEGORY_PREF, mCategory) }
|
||||
}
|
||||
mNormalSearchMode = mNormalView.findViewById(R.id.normal_search_mode)
|
||||
mNormalSearchModeHelp = mNormalView.findViewById(R.id.normal_search_mode_help)
|
||||
mEnableAdvanceSwitch = mNormalView.findViewById(R.id.search_enable_advance)
|
||||
mNormalSearchModeHelp.setOnClickListener(this)
|
||||
mEnableAdvanceSwitch.setOnCheckedChangeListener(this)
|
||||
// Create advance view
|
||||
mAdvanceView = mInflater.inflate(R.layout.search_advance, null)
|
||||
mTableAdvanceSearch = mAdvanceView.findViewById(R.id.search_advance_search_table)
|
||||
// Create image view
|
||||
mImageView = mInflater.inflate(R.layout.search_image, null) as ImageSearchLayout
|
||||
mImageView.setHelper(this)
|
||||
// Create action view
|
||||
mActionView = mInflater.inflate(R.layout.search_action, null)
|
||||
mActionView.layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
mAction = mActionView.findViewById(R.id.action)
|
||||
mAction.addOnTabSelectedListener(this)
|
||||
}
|
||||
|
||||
|
||||
fun setHelper(helper: Helper?) {
|
||||
mHelper = helper
|
||||
}
|
||||
|
||||
fun scrollSearchContainerToTop() {
|
||||
(layoutManager as LinearLayoutManager).scrollToPositionWithOffset(0, 0)
|
||||
}
|
||||
|
||||
fun setImageUri(imageUri: Uri?) {
|
||||
mImageView.setImageUri(imageUri)
|
||||
}
|
||||
|
||||
fun setNormalSearchMode(id: Int) {
|
||||
mNormalSearchMode.check(id)
|
||||
}
|
||||
|
||||
override fun onSelectImage() {
|
||||
if (mHelper != null) {
|
||||
mHelper!!.onSelectImage()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
||||
if (buttonView === mEnableAdvanceSwitch) {
|
||||
post {
|
||||
mEnableAdvance = isChecked
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL) {
|
||||
if (mEnableAdvance) {
|
||||
mAdapter.notifyItemInserted(1)
|
||||
} else {
|
||||
mAdapter.notifyItemRemoved(1)
|
||||
}
|
||||
if (mHelper != null) {
|
||||
mHelper!!.onChangeSearchMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NonConstantResourceId")
|
||||
@Throws(EhException::class)
|
||||
fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) {
|
||||
urlBuilder.reset()
|
||||
when (mSearchMode) {
|
||||
SEARCH_MODE_NORMAL -> {
|
||||
when (mNormalSearchMode.checkedRadioButtonId) {
|
||||
R.id.search_subscription_search -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION
|
||||
}
|
||||
R.id.search_specify_uploader -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER
|
||||
}
|
||||
R.id.search_specify_tag -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_TAG
|
||||
}
|
||||
else -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_NORMAL
|
||||
}
|
||||
}
|
||||
urlBuilder.keyword = query
|
||||
urlBuilder.category = PreferenceManager.getDefaultSharedPreferences(context).getInt(
|
||||
SEARCH_CATEGORY_PREF, 0
|
||||
)
|
||||
if (mEnableAdvance) {
|
||||
urlBuilder.advanceSearch = mTableAdvanceSearch.advanceSearch
|
||||
urlBuilder.minRating = mTableAdvanceSearch.minRating
|
||||
urlBuilder.pageFrom = mTableAdvanceSearch.pageFrom
|
||||
urlBuilder.pageTo = mTableAdvanceSearch.pageTo
|
||||
}
|
||||
}
|
||||
SEARCH_MODE_IMAGE -> {
|
||||
urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH
|
||||
mImageView.formatListUrlBuilder(urlBuilder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
if (mNormalSearchModeHelp === v) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.search_tip)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
post { setSearchMode(tab.position) }
|
||||
}
|
||||
|
||||
fun setSearchMode(@SearchMode mode: Int) {
|
||||
val oldItemCount = mAdapter.itemCount
|
||||
mSearchMode = mode
|
||||
val newItemCount = mAdapter.itemCount
|
||||
mAdapter.notifyItemRangeRemoved(0, oldItemCount - 1)
|
||||
mAdapter.notifyItemRangeInserted(0, newItemCount - 1)
|
||||
if (mHelper != null) {
|
||||
mHelper!!.onChangeSearchMode()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {}
|
||||
|
||||
@IntDef(SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE)
|
||||
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||
private annotation class SearchMode
|
||||
interface Helper {
|
||||
fun onChangeSearchMode()
|
||||
fun onSelectImage()
|
||||
}
|
||||
|
||||
internal class SearchLayoutManager(context: Context?) : LinearLayoutManager(context) {
|
||||
override fun onLayoutChildren(recycler: Recycler, state: State) {
|
||||
try {
|
||||
super.onLayoutChildren(recycler, state)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SearchAdapter : Adapter<SimpleHolder>() {
|
||||
override fun getItemCount(): Int {
|
||||
var count = SEARCH_ITEM_COUNT_ARRAY[mSearchMode]
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && !mEnableAdvance) {
|
||||
count--
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
var type = SEARCH_ITEM_TYPE[mSearchMode][position]
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
|
||||
type = ITEM_TYPE_ACTION
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder {
|
||||
val view: View?
|
||||
if (viewType == ITEM_TYPE_ACTION) {
|
||||
ViewUtils.removeFromParent(mActionView)
|
||||
view = mActionView
|
||||
} else {
|
||||
view = mInflater.inflate(R.layout.search_category, parent, false)
|
||||
val title = view.findViewById<TextView>(R.id.category_title)
|
||||
val content = view.findViewById<FrameLayout>(R.id.category_content)
|
||||
when (viewType) {
|
||||
ITEM_TYPE_NORMAL -> {
|
||||
title.setText(R.string.search_normal)
|
||||
ViewUtils.removeFromParent(mNormalView)
|
||||
content.addView(mNormalView)
|
||||
}
|
||||
ITEM_TYPE_NORMAL_ADVANCE -> {
|
||||
title.setText(R.string.search_advance)
|
||||
ViewUtils.removeFromParent(mAdvanceView)
|
||||
content.addView(mAdvanceView)
|
||||
}
|
||||
ITEM_TYPE_IMAGE -> {
|
||||
title.setText(R.string.search_image)
|
||||
ViewUtils.removeFromParent(mImageView)
|
||||
content.addView(mImageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
return SimpleHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SimpleHolder, position: Int) {
|
||||
if (holder.itemViewType == ITEM_TYPE_ACTION) {
|
||||
mAction.selectTab(mAction.getTabAt(mSearchMode))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
var type = SEARCH_ITEM_TYPE[mSearchMode][position]
|
||||
if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {
|
||||
type = ITEM_TYPE_ACTION
|
||||
}
|
||||
return type.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
inner class IdentifiedChip @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : Chip(context, attrs, R.attr.filterChipStyle) {
|
||||
var idt = 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SEARCH_MODE_NORMAL = 0
|
||||
const val SEARCH_MODE_IMAGE = 1
|
||||
const val SEARCH_CATEGORY_PREF = "search_pref"
|
||||
private const val ITEM_TYPE_NORMAL = 0
|
||||
private const val ITEM_TYPE_NORMAL_ADVANCE = 1
|
||||
private const val ITEM_TYPE_IMAGE = 2
|
||||
private const val ITEM_TYPE_ACTION = 3
|
||||
private val SEARCH_ITEM_COUNT_ARRAY = intArrayOf(
|
||||
3, 2
|
||||
)
|
||||
private val mCategoryTable: ArrayList<Pair<Int, Int>> = arrayListOf(
|
||||
Pair(EhConfig.DOUJINSHI, R.string.doujinshi),
|
||||
Pair(EhConfig.MANGA, R.string.manga),
|
||||
Pair(EhConfig.ARTIST_CG, R.string.artist_cg),
|
||||
Pair(EhConfig.GAME_CG, R.string.game_cg),
|
||||
Pair(EhConfig.WESTERN, R.string.western),
|
||||
Pair(EhConfig.NON_H, R.string.non_h),
|
||||
Pair(EhConfig.IMAGE_SET, R.string.image_set),
|
||||
Pair(EhConfig.COSPLAY, R.string.cosplay),
|
||||
Pair(EhConfig.ASIAN_PORN, R.string.asian_porn),
|
||||
Pair(EhConfig.MISC, R.string.misc)
|
||||
)
|
||||
private val SEARCH_ITEM_TYPE = arrayOf(
|
||||
intArrayOf(ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION), intArrayOf(
|
||||
ITEM_TYPE_IMAGE, ITEM_TYPE_ACTION
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
class DownUpDetector {
|
||||
private final DownUpListener mListener;
|
||||
private boolean mStillDown;
|
||||
|
||||
public DownUpDetector(DownUpListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
private void setState(boolean down, MotionEvent e) {
|
||||
if (down == mStillDown) return;
|
||||
mStillDown = down;
|
||||
if (down) {
|
||||
mListener.onDown(e);
|
||||
} else {
|
||||
mListener.onUp(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void onTouchEvent(MotionEvent ev) {
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
setState(true, ev);
|
||||
break;
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
mListener.onPointerDown(ev);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
mListener.onPointerUp(ev);
|
||||
setState(false, ev);
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
setState(false, ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDown() {
|
||||
return mStillDown;
|
||||
}
|
||||
|
||||
public interface DownUpListener {
|
||||
void onDown(MotionEvent e);
|
||||
|
||||
void onUp(MotionEvent e);
|
||||
|
||||
void onPointerDown(MotionEvent e);
|
||||
|
||||
void onPointerUp(MotionEvent e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.SensorManager;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import com.hippo.glview.anim.Animation;
|
||||
|
||||
abstract class Fling extends Animation {
|
||||
|
||||
private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
|
||||
private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
|
||||
private static final float START_TENSION = 0.5f;
|
||||
private static final float END_TENSION = 1.0f;
|
||||
private static final float P1 = START_TENSION * INFLEXION;
|
||||
private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
|
||||
private static final float FLING_FRICTION = ViewConfiguration.getScrollFriction();
|
||||
|
||||
private static final int NB_SAMPLES = 100;
|
||||
private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
|
||||
private static final Interpolator FLING_INTERPOLATOR = new Interpolator() {
|
||||
@Override
|
||||
public float getInterpolation(float input) {
|
||||
final int index = (int) (NB_SAMPLES * input);
|
||||
float distanceCoef = 1.f;
|
||||
float velocityCoef;
|
||||
if (index < NB_SAMPLES) {
|
||||
final float t_inf = (float) index / NB_SAMPLES;
|
||||
final float t_sup = (float) (index + 1) / NB_SAMPLES;
|
||||
final float d_inf = SPLINE_POSITION[index];
|
||||
final float d_sup = SPLINE_POSITION[index + 1];
|
||||
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
|
||||
distanceCoef = d_inf + (input - t_inf) * velocityCoef;
|
||||
}
|
||||
return distanceCoef;
|
||||
}
|
||||
};
|
||||
private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
|
||||
|
||||
static {
|
||||
float x_min = 0.0f;
|
||||
float y_min = 0.0f;
|
||||
for (int i = 0; i < NB_SAMPLES; i++) {
|
||||
final float alpha = (float) i / NB_SAMPLES;
|
||||
|
||||
float x_max = 1.0f;
|
||||
float x, tx, coef;
|
||||
while (true) {
|
||||
x = x_min + (x_max - x_min) / 2.0f;
|
||||
coef = 3.0f * x * (1.0f - x);
|
||||
tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
|
||||
if (Math.abs(tx - alpha) < 1E-5) break;
|
||||
if (tx > alpha) x_max = x;
|
||||
else x_min = x;
|
||||
}
|
||||
SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
|
||||
|
||||
float y_max = 1.0f;
|
||||
float y, dy;
|
||||
while (true) {
|
||||
y = y_min + (y_max - y_min) / 2.0f;
|
||||
coef = 3.0f * y * (1.0f - y);
|
||||
dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
|
||||
if (Math.abs(dy - alpha) < 1E-5) break;
|
||||
if (dy > alpha) y_max = y;
|
||||
else y_min = y;
|
||||
}
|
||||
SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
|
||||
}
|
||||
SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
|
||||
}
|
||||
|
||||
private final float mPhysicalCoeff;
|
||||
|
||||
public Fling(Context context) {
|
||||
final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
|
||||
mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
|
||||
* 39.37f // inch/meter
|
||||
* ppi
|
||||
* 0.84f; // look and feel tuning
|
||||
setInterpolator(FLING_INTERPOLATOR);
|
||||
}
|
||||
|
||||
private double getSplineDeceleration(int velocity) {
|
||||
return Math.log(INFLEXION * Math.abs(velocity) / (FLING_FRICTION * mPhysicalCoeff));
|
||||
}
|
||||
|
||||
/* Returns the duration, expressed in milliseconds */
|
||||
protected int getSplineFlingDuration(int velocity) {
|
||||
final double l = getSplineDeceleration(velocity);
|
||||
final double decelMinusOne = DECELERATION_RATE - 1.0;
|
||||
return (int) (1000.0 * Math.exp(l / decelMinusOne));
|
||||
}
|
||||
|
||||
protected double getSplineFlingDistance(int velocity) {
|
||||
final double l = getSplineDeceleration(velocity);
|
||||
final double decelMinusOne = DECELERATION_RATE - 1.0;
|
||||
return FLING_FRICTION * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies mDuration to the duration it takes to get from start to newFinal using the
|
||||
* spline interpolation. The previous duration was needed to get to oldFinal.
|
||||
**/
|
||||
protected int adjustDuration(int start, int oldFinal, int newFinal, int duration) {
|
||||
final int oldDistance = oldFinal - start;
|
||||
final int newDistance = newFinal - start;
|
||||
final float x = Math.abs((float) newDistance / oldDistance);
|
||||
final int index = (int) (NB_SAMPLES * x);
|
||||
if (index < NB_SAMPLES) {
|
||||
final float x_inf = (float) index / NB_SAMPLES;
|
||||
final float x_sup = (float) (index + 1) / NB_SAMPLES;
|
||||
final float t_inf = SPLINE_TIME[index];
|
||||
final float t_sup = SPLINE_TIME[index + 1];
|
||||
final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
|
||||
duration *= timeCoef;
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import com.hippo.glview.glrenderer.BasicTexture;
|
||||
import com.hippo.glview.glrenderer.Texture;
|
||||
import com.hippo.glview.image.GLImageMovableTextView;
|
||||
import com.hippo.glview.image.ImageMovableTextTexture;
|
||||
import com.hippo.glview.image.ImageTexture;
|
||||
import com.hippo.glview.view.Gravity;
|
||||
import com.hippo.glview.widget.GLFrameLayout;
|
||||
import com.hippo.glview.widget.GLLinearLayout;
|
||||
import com.hippo.glview.widget.GLProgressView;
|
||||
import com.hippo.glview.widget.GLTextureView;
|
||||
|
||||
public class GalleryPageView extends GLFrameLayout {
|
||||
|
||||
public static final int INVALID_INDEX = -1;
|
||||
|
||||
public static final float PROGRESS_GONE = -1.0f;
|
||||
public static final float PROGRESS_INDETERMINATE = -2.0f;
|
||||
|
||||
private final ImageView mImage;
|
||||
private final GLLinearLayout mInfo;
|
||||
private final GLImageMovableTextView mPage;
|
||||
private final GLTextureView mError;
|
||||
private final GLProgressView mProgress;
|
||||
|
||||
private final int mMinHeight;
|
||||
|
||||
private int mIndex = INVALID_INDEX;
|
||||
|
||||
public GalleryPageView(ImageMovableTextTexture pageTextTexture,
|
||||
int progressColor, int progressBgColor, int progressSize,
|
||||
int minHeight, int infoInterval) {
|
||||
// Add image
|
||||
mImage = new ImageView();
|
||||
GravityLayoutParams glp = new GravityLayoutParams(LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
addComponent(mImage, glp);
|
||||
|
||||
// Add other panel
|
||||
mInfo = new GLLinearLayout();
|
||||
mInfo.setOrientation(GLLinearLayout.VERTICAL);
|
||||
mInfo.setInterval(infoInterval);
|
||||
glp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
glp.gravity = Gravity.CENTER;
|
||||
addComponent(mInfo, glp);
|
||||
|
||||
// Add page
|
||||
mPage = new GLImageMovableTextView();
|
||||
mPage.setTextTexture(pageTextTexture);
|
||||
GLLinearLayout.LayoutParams lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
lp.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
mInfo.addComponent(mPage, lp);
|
||||
|
||||
// Add error
|
||||
mError = new GLTextureView();
|
||||
lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
lp.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
mInfo.addComponent(mError, lp);
|
||||
|
||||
// Add progress
|
||||
mProgress = new GLProgressView();
|
||||
mProgress.setBgColor(progressBgColor);
|
||||
mProgress.setColor(progressColor);
|
||||
mProgress.setMinimumWidth(progressSize);
|
||||
mProgress.setMinimumHeight(progressSize);
|
||||
lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT);
|
||||
lp.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
mInfo.addComponent(mProgress, lp);
|
||||
|
||||
mMinHeight = minHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSuggestedMinimumHeight() {
|
||||
// The height of the actual image may be smaller than mPageMinHeight.
|
||||
// Set min height as 0 when the image is visible.
|
||||
// For PageLayoutManager, min height is useless.
|
||||
if (mImage.getVisibility() == VISIBLE) {
|
||||
return 0;
|
||||
} else {
|
||||
return mMinHeight;
|
||||
}
|
||||
}
|
||||
|
||||
int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
void setIndex(int index) {
|
||||
mIndex = index;
|
||||
}
|
||||
|
||||
public void showImage() {
|
||||
mImage.setVisibility(VISIBLE);
|
||||
mInfo.setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void showInfo() {
|
||||
// For image valid rect
|
||||
mImage.setVisibility(INVISIBLE);
|
||||
mInfo.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private void unbindImage() {
|
||||
ImageTexture texture = mImage.getImageTexture();
|
||||
if (texture != null) {
|
||||
mImage.setImageTexture(null);
|
||||
texture.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
public void setImage(ImageTexture imageTexture) {
|
||||
unbindImage();
|
||||
if (imageTexture != null) {
|
||||
mImage.setImageTexture(imageTexture);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPage(int page) {
|
||||
mPage.setVisibility(VISIBLE);
|
||||
mPage.setText(Integer.toString(page));
|
||||
}
|
||||
|
||||
public void hidePage() {
|
||||
mPage.setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
if (progress == PROGRESS_GONE) {
|
||||
mProgress.setVisibility(GONE);
|
||||
} else if (progress == PROGRESS_INDETERMINATE) {
|
||||
mProgress.setVisibility(VISIBLE);
|
||||
mProgress.setIndeterminate(true);
|
||||
} else {
|
||||
mProgress.setVisibility(VISIBLE);
|
||||
mProgress.setIndeterminate(false);
|
||||
mProgress.setProgress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void unbindError() {
|
||||
Texture texture = mError.getTexture();
|
||||
if (texture != null) {
|
||||
mError.setTexture(null);
|
||||
if (texture instanceof BasicTexture) {
|
||||
((BasicTexture) texture).recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setError(String error, GalleryView galleryView) {
|
||||
unbindError();
|
||||
if (error == null) {
|
||||
mError.setVisibility(GONE);
|
||||
} else {
|
||||
mError.setVisibility(VISIBLE);
|
||||
galleryView.bindErrorView(mError, error);
|
||||
}
|
||||
}
|
||||
|
||||
ImageView getImageView() {
|
||||
return mImage;
|
||||
}
|
||||
|
||||
boolean isLoaded() {
|
||||
return mImage.getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
boolean isError() {
|
||||
return mError.getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
boolean isUnderInfo(float x, float y) {
|
||||
return mInfo.bounds().contains((int) x, (int) y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.hippo.glview.glrenderer.GLCanvas;
|
||||
import com.hippo.glview.image.ImageWrapper;
|
||||
import com.hippo.glview.view.GLRoot;
|
||||
import com.hippo.image.Image;
|
||||
import com.hippo.yorozuya.ConcurrentPool;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
import com.hippo.yorozuya.OSUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
public abstract class GalleryProvider {
|
||||
|
||||
public static final int STATE_WAIT = -1;
|
||||
public static final int STATE_ERROR = -2;
|
||||
|
||||
private final ConcurrentPool<NotifyTask> mNotifyTaskPool = new ConcurrentPool<>(5);
|
||||
private final ImageCache mImageCache = new ImageCache();
|
||||
private volatile Listener mListener;
|
||||
private volatile GLRoot mGLRoot;
|
||||
private boolean mStarted = false;
|
||||
|
||||
@UiThread
|
||||
public void start() {
|
||||
OSUtils.checkMainLoop();
|
||||
|
||||
if (mStarted) {
|
||||
throw new IllegalStateException("Can't start it twice");
|
||||
}
|
||||
mStarted = true;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
public void stop() {
|
||||
OSUtils.checkMainLoop();
|
||||
mImageCache.evictAll();
|
||||
}
|
||||
|
||||
public void setGLRoot(GLRoot glRoot) {
|
||||
mGLRoot = glRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link #STATE_WAIT} for wait,
|
||||
* {@link #STATE_ERROR} for error, {@link #getError()} to get error message,
|
||||
* 0 for empty
|
||||
*/
|
||||
public abstract int size();
|
||||
|
||||
public final void request(int index) {
|
||||
ImageWrapper imageWrapper = mImageCache.get(index);
|
||||
if (imageWrapper != null) {
|
||||
notifyPageSucceed(index, imageWrapper);
|
||||
} else {
|
||||
onRequest(index);
|
||||
}
|
||||
}
|
||||
|
||||
public final void forceRequest(int index) {
|
||||
onForceRequest(index);
|
||||
}
|
||||
|
||||
public void removeCache(int index) {
|
||||
mImageCache.remove(index);
|
||||
}
|
||||
|
||||
protected abstract void onRequest(int index);
|
||||
|
||||
protected abstract void onForceRequest(int index);
|
||||
|
||||
public final void cancelRequest(int index) {
|
||||
onCancelRequest(index);
|
||||
}
|
||||
|
||||
protected abstract void onCancelRequest(int index);
|
||||
|
||||
public abstract String getError();
|
||||
|
||||
public void setListener(Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void notifyDataChanged() {
|
||||
notify(NotifyTask.TYPE_DATA_CHANGED, -1, 0.0f, null, null);
|
||||
}
|
||||
|
||||
public void notifyDataChanged(int index) {
|
||||
notify(NotifyTask.TYPE_DATA_CHANGED, index, 0.0f, null, null);
|
||||
}
|
||||
|
||||
public void notifyPageWait(int index) {
|
||||
notify(NotifyTask.TYPE_WAIT, index, 0.0f, null, null);
|
||||
}
|
||||
|
||||
public void notifyPagePercent(int index, float percent) {
|
||||
notify(NotifyTask.TYPE_PERCENT, index, percent, null, null);
|
||||
}
|
||||
|
||||
public void notifyPageSucceed(int index, Image image) {
|
||||
ImageWrapper imageWrapper = new ImageWrapper(image);
|
||||
mImageCache.add(index, imageWrapper);
|
||||
notifyPageSucceed(index, imageWrapper);
|
||||
}
|
||||
|
||||
public void notifyPageSucceed(int index, ImageWrapper image) {
|
||||
notify(NotifyTask.TYPE_SUCCEED, index, 0.0f, image, null);
|
||||
}
|
||||
|
||||
public void notifyPageFailed(int index, String error) {
|
||||
notify(NotifyTask.TYPE_FAILED, index, 0.0f, null, error);
|
||||
}
|
||||
|
||||
private void notify(@NotifyTask.Type int type, int index, float percent, ImageWrapper image, String error) {
|
||||
Listener listener = mListener;
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GLRoot glRoot = mGLRoot;
|
||||
if (glRoot == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
NotifyTask task = mNotifyTaskPool.pop();
|
||||
if (task == null) {
|
||||
task = new NotifyTask(listener, mNotifyTaskPool);
|
||||
}
|
||||
task.setData(type, index, percent, image, error);
|
||||
glRoot.addOnGLIdleListener(task);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
|
||||
void onDataChanged();
|
||||
|
||||
void onPageWait(int index);
|
||||
|
||||
void onPagePercent(int index, float percent);
|
||||
|
||||
void onPageSucceed(int index, ImageWrapper image);
|
||||
|
||||
void onPageFailed(int index, String error);
|
||||
|
||||
void onDataChanged(int index);
|
||||
}
|
||||
|
||||
private static class NotifyTask implements GLRoot.OnGLIdleListener {
|
||||
|
||||
public static final int TYPE_DATA_CHANGED = 0;
|
||||
public static final int TYPE_WAIT = 1;
|
||||
public static final int TYPE_PERCENT = 2;
|
||||
public static final int TYPE_SUCCEED = 3;
|
||||
public static final int TYPE_FAILED = 4;
|
||||
private final Listener mListener;
|
||||
private final ConcurrentPool<NotifyTask> mPool;
|
||||
@Type
|
||||
private int mType;
|
||||
private int mIndex;
|
||||
private float mPercent;
|
||||
private ImageWrapper mImage;
|
||||
private String mError;
|
||||
|
||||
public NotifyTask(Listener listener, ConcurrentPool<NotifyTask> pool) {
|
||||
mListener = listener;
|
||||
mPool = pool;
|
||||
}
|
||||
|
||||
public void setData(@Type int type, int index, float percent, ImageWrapper image, String error) {
|
||||
mType = type;
|
||||
mIndex = index;
|
||||
mPercent = percent;
|
||||
mImage = image;
|
||||
mError = error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {
|
||||
switch (mType) {
|
||||
case TYPE_DATA_CHANGED:
|
||||
if (mIndex < 0) {
|
||||
mListener.onDataChanged();
|
||||
} else {
|
||||
mListener.onDataChanged(mIndex);
|
||||
}
|
||||
break;
|
||||
case TYPE_WAIT:
|
||||
mListener.onPageWait(mIndex);
|
||||
break;
|
||||
case TYPE_PERCENT:
|
||||
mListener.onPagePercent(mIndex, mPercent);
|
||||
break;
|
||||
case TYPE_SUCCEED:
|
||||
mListener.onPageSucceed(mIndex, mImage);
|
||||
break;
|
||||
case TYPE_FAILED:
|
||||
mListener.onPageFailed(mIndex, mError);
|
||||
break;
|
||||
}
|
||||
|
||||
// Clean data
|
||||
mImage = null;
|
||||
mError = null;
|
||||
// Push back
|
||||
mPool.push(this);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@IntDef({TYPE_DATA_CHANGED, NotifyTask.TYPE_WAIT, TYPE_PERCENT, TYPE_SUCCEED, TYPE_FAILED})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface Type {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ImageCache extends LruCache<Integer, ImageWrapper> {
|
||||
|
||||
private static final long MAX_CACHE_SIZE = 128 * 1024 * 1024;
|
||||
private static final long MIN_CACHE_SIZE = 32 * 1024 * 1024;
|
||||
|
||||
public ImageCache() {
|
||||
super((int) MathUtils.clamp(OSUtils.getTotalMemory() / 16, MIN_CACHE_SIZE, MAX_CACHE_SIZE));
|
||||
}
|
||||
|
||||
public void add(Integer key, ImageWrapper value) {
|
||||
if (value.getFormat() != Image.FORMAT_GIF && value.obtain()) {
|
||||
put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int sizeOf(Integer key, ImageWrapper value) {
|
||||
int size = value.getWidth() * value.getHeight() * 4;
|
||||
if (value.getFormat() == Image.FORMAT_GIF) {
|
||||
size *= 5;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entryRemoved(boolean evicted, Integer key, ImageWrapper oldValue, ImageWrapper newValue) {
|
||||
if (oldValue != null) {
|
||||
oldValue.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
|
||||
// This class aggregates three gesture detectors: GestureDetector,
|
||||
// ScaleGestureDetector, and DownUpDetector.
|
||||
class GestureRecognizer {
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = "GestureRecognizer";
|
||||
private final GestureDetectorCompat mGestureDetector;
|
||||
private final ScaleGestureDetector mScaleDetector;
|
||||
private final DownUpDetector mDownUpDetector;
|
||||
private final Listener mListener;
|
||||
|
||||
public GestureRecognizer(Context context, Listener listener) {
|
||||
mListener = listener;
|
||||
MyGestureListener gestureListener = new MyGestureListener();
|
||||
mGestureDetector = new GestureDetectorCompat(context, gestureListener,
|
||||
null /* ignoreMultitouch */);
|
||||
mGestureDetector.setOnDoubleTapListener(gestureListener);
|
||||
mScaleDetector = new ScaleGestureDetector(
|
||||
context, new MyScaleListener());
|
||||
mDownUpDetector = new DownUpDetector(new MyDownUpListener());
|
||||
}
|
||||
|
||||
public void onTouchEvent(MotionEvent event) {
|
||||
mScaleDetector.onTouchEvent(event);
|
||||
mGestureDetector.onTouchEvent(event);
|
||||
mDownUpDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public boolean isDown() {
|
||||
return mDownUpDetector.isDown();
|
||||
}
|
||||
|
||||
public void cancelScale() {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
MotionEvent cancelEvent = MotionEvent.obtain(
|
||||
now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
|
||||
mScaleDetector.onTouchEvent(cancelEvent);
|
||||
cancelEvent.recycle();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
boolean onSingleTapUp(float x, float y);
|
||||
|
||||
boolean onSingleTapConfirmed(float x, float y);
|
||||
|
||||
boolean onDoubleTap(float x, float y);
|
||||
|
||||
boolean onDoubleTapConfirmed(float x, float y);
|
||||
|
||||
void onLongPress(float x, float y);
|
||||
|
||||
boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y);
|
||||
|
||||
/**
|
||||
* @param velocityX Finger from top to bottom is positive
|
||||
* @param velocityY Finger from left to right is positive
|
||||
*/
|
||||
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
|
||||
|
||||
boolean onScaleBegin(float focusX, float focusY);
|
||||
|
||||
boolean onScale(float focusX, float focusY, float scale);
|
||||
|
||||
void onScaleEnd();
|
||||
|
||||
void onDown(float x, float y);
|
||||
|
||||
void onUp();
|
||||
|
||||
void onPointerDown(float x, float y);
|
||||
|
||||
void onPointerUp();
|
||||
}
|
||||
|
||||
private class MyGestureListener
|
||||
extends GestureDetector.SimpleOnGestureListener {
|
||||
@Override
|
||||
public boolean onSingleTapUp(MotionEvent e) {
|
||||
return mListener.onSingleTapUp(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
return mListener.onSingleTapConfirmed(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTapEvent(MotionEvent e) {
|
||||
if (e.getAction() == MotionEvent.ACTION_UP) {
|
||||
return mListener.onDoubleTapConfirmed(e.getX(), e.getY());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
return mListener.onDoubleTap(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
mListener.onLongPress(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(
|
||||
MotionEvent e1, MotionEvent e2, float dx, float dy) {
|
||||
return mListener.onScroll(
|
||||
dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY(), e2.getX(), e2.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
|
||||
float velocityY) {
|
||||
return mListener.onFling(e1, e2, velocityX, velocityY);
|
||||
}
|
||||
}
|
||||
|
||||
private class MyScaleListener
|
||||
extends ScaleGestureDetector.SimpleOnScaleGestureListener {
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
return mListener.onScaleBegin(
|
||||
detector.getFocusX(), detector.getFocusY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
return mListener.onScale(detector.getFocusX(),
|
||||
detector.getFocusY(), detector.getScaleFactor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
mListener.onScaleEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private class MyDownUpListener implements DownUpDetector.DownUpListener {
|
||||
@Override
|
||||
public void onDown(MotionEvent e) {
|
||||
mListener.onDown(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUp(MotionEvent e) {
|
||||
mListener.onUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointerDown(MotionEvent e) {
|
||||
mListener.onPointerDown(e.getX(), e.getY());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointerUp(MotionEvent e) {
|
||||
mListener.onPointerUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.hippo.glview.anim.AlphaAnimation;
|
||||
import com.hippo.glview.glrenderer.GLCanvas;
|
||||
import com.hippo.glview.glrenderer.Texture;
|
||||
import com.hippo.glview.image.ImageTexture;
|
||||
import com.hippo.glview.view.GLView;
|
||||
import com.hippo.yorozuya.AnimationUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
class ImageView extends GLView implements ImageTexture.Callback {
|
||||
|
||||
public static final int SCALE_ORIGIN = 0;
|
||||
public static final int SCALE_FIT_WIDTH = 1;
|
||||
public static final int SCALE_FIT_HEIGHT = 2;
|
||||
public static final int SCALE_FIT = 3;
|
||||
public static final int SCALE_FIXED = 4;
|
||||
public static final int START_POSITION_TOP_LEFT = 0;
|
||||
public static final int START_POSITION_TOP_RIGHT = 1;
|
||||
public static final int START_POSITION_BOTTOM_LEFT = 2;
|
||||
public static final int START_POSITION_BOTTOM_RIGHT = 3;
|
||||
public static final int START_POSITION_CENTER = 4;
|
||||
// TODO adjust scale max and min according to image size and screen size
|
||||
private static final float SCALE_MIN = 1 / 10.0f;
|
||||
private static final float SCALE_MAX = 10.0f;
|
||||
private static final long ALPHA_ANIMATION_DURING = 300L;
|
||||
private final RectF mDst = new RectF();
|
||||
private final RectF mSrcActual = new RectF();
|
||||
private final RectF mDstActual = new RectF();
|
||||
private final Rect mValidRect = new Rect();
|
||||
private final AlphaAnimation mAlphaAnimation;
|
||||
private ImageTexture mImageTexture;
|
||||
private int mTextureWidth;
|
||||
private int mTextureHeight;
|
||||
private int mScaleMode = SCALE_FIT;
|
||||
private int mStartPosition = START_POSITION_TOP_RIGHT;
|
||||
private float mScaleValue = 1.0f;
|
||||
private float mScale = 1.0f;
|
||||
private boolean mScaleOffsetDirty = true;
|
||||
private boolean mPositionInRootDirty = true;
|
||||
|
||||
public ImageView() {
|
||||
mAlphaAnimation = new AlphaAnimation(0.0f, 1.0f);
|
||||
mAlphaAnimation.setDuration(ALPHA_ANIMATION_DURING);
|
||||
mAlphaAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
|
||||
}
|
||||
|
||||
public static int sanitizeScaleMode(int scaleMode) {
|
||||
if (scaleMode != SCALE_ORIGIN &&
|
||||
scaleMode != SCALE_FIT_WIDTH &&
|
||||
scaleMode != SCALE_FIT_HEIGHT &&
|
||||
scaleMode != SCALE_FIT &&
|
||||
scaleMode != SCALE_FIXED) {
|
||||
return SCALE_FIT;
|
||||
} else {
|
||||
return scaleMode;
|
||||
}
|
||||
}
|
||||
|
||||
public static int sanitizeStartPosition(int startPosition) {
|
||||
if (startPosition != START_POSITION_TOP_LEFT &&
|
||||
startPosition != START_POSITION_TOP_RIGHT &&
|
||||
startPosition != START_POSITION_BOTTOM_LEFT &&
|
||||
startPosition != START_POSITION_BOTTOM_RIGHT &&
|
||||
startPosition != START_POSITION_CENTER) {
|
||||
return START_POSITION_TOP_RIGHT;
|
||||
} else {
|
||||
return startPosition;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSuggestedMinimumWidth() {
|
||||
return Math.max(super.getSuggestedMinimumWidth(),
|
||||
mImageTexture == null ? 0 : mTextureWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getSuggestedMinimumHeight() {
|
||||
return Math.max(super.getSuggestedMinimumHeight(),
|
||||
mImageTexture == null ? 0 : mTextureHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||
if (mImageTexture == null) {
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
} else {
|
||||
float ratio = (float) mTextureWidth / mTextureHeight;
|
||||
int widthSize = MeasureSpec.getSize(widthSpec);
|
||||
int heightSize = MeasureSpec.getSize(heightSpec);
|
||||
int widthMode = MeasureSpec.getMode(widthSpec);
|
||||
int heightMode = MeasureSpec.getMode(heightSpec);
|
||||
int measureWidth = -1;
|
||||
int measureHeight = -1;
|
||||
|
||||
if (widthMode == MeasureSpec.EXACTLY) {
|
||||
measureWidth = widthSize;
|
||||
if (heightMode == MeasureSpec.EXACTLY) {
|
||||
measureHeight = heightSize;
|
||||
} else {
|
||||
measureHeight = (int) (widthSize / ratio);
|
||||
if (heightMode == MeasureSpec.AT_MOST) {
|
||||
measureHeight = Math.min(measureHeight, heightSize);
|
||||
}
|
||||
}
|
||||
} else if (heightMode == MeasureSpec.EXACTLY) {
|
||||
measureHeight = heightSize;
|
||||
measureWidth = (int) (heightSize * ratio);
|
||||
if (widthMode == MeasureSpec.AT_MOST) {
|
||||
measureWidth = Math.min(measureWidth, widthSize);
|
||||
}
|
||||
}
|
||||
|
||||
if (measureWidth == -1 || measureHeight == -1) {
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
} else {
|
||||
setMeasuredSize(measureWidth, measureHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int newW, int newH, int oldW, int oldH) {
|
||||
mScaleOffsetDirty = true;
|
||||
mPositionInRootDirty = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) {
|
||||
mPositionInRootDirty = true;
|
||||
|
||||
if (mImageTexture != null) {
|
||||
getValidRect(mValidRect);
|
||||
if (!mValidRect.isEmpty()) {
|
||||
mImageTexture.start();
|
||||
} else {
|
||||
mImageTexture.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void getScaleDefault(float[] scaleDefault) {
|
||||
if (mImageTexture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
scaleDefault[0] = 1.0f;
|
||||
scaleDefault[1] = (float) getWidth() / mTextureWidth;
|
||||
scaleDefault[2] = (float) getHeight() / mTextureHeight;
|
||||
scaleDefault[3] = Math.max(scaleDefault[1], scaleDefault[2]) * 2;
|
||||
|
||||
scaleDefault[0] = MathUtils.clamp(scaleDefault[0], SCALE_MIN, SCALE_MAX);
|
||||
scaleDefault[1] = MathUtils.clamp(scaleDefault[1], SCALE_MIN, SCALE_MAX);
|
||||
scaleDefault[2] = MathUtils.clamp(scaleDefault[2], SCALE_MIN, SCALE_MAX);
|
||||
scaleDefault[3] = MathUtils.clamp(scaleDefault[3], SCALE_MIN, SCALE_MAX);
|
||||
|
||||
Arrays.sort(scaleDefault);
|
||||
}
|
||||
|
||||
public ImageTexture getImageTexture() {
|
||||
return mImageTexture;
|
||||
}
|
||||
|
||||
public void setImageTexture(ImageTexture imageTexture) {
|
||||
// Remove callback
|
||||
if (mImageTexture != null) {
|
||||
mImageTexture.setCallback(null);
|
||||
mImageTexture.stop();
|
||||
}
|
||||
|
||||
int oldTextureWidth = mTextureWidth;
|
||||
int oldTextureHeight = mTextureHeight;
|
||||
|
||||
mImageTexture = imageTexture;
|
||||
|
||||
if (imageTexture != null) {
|
||||
imageTexture.setCallback(this);
|
||||
mTextureWidth = imageTexture.getWidth();
|
||||
mTextureHeight = imageTexture.getHeight();
|
||||
// Avoid zero and negative
|
||||
if (mTextureWidth <= 0) {
|
||||
mTextureWidth = 1;
|
||||
}
|
||||
if (mTextureHeight <= 0) {
|
||||
mTextureHeight = 1;
|
||||
}
|
||||
|
||||
// Start alpha animation, do not show animation for image has no valid rect
|
||||
getValidRect(mValidRect);
|
||||
if (!mValidRect.isEmpty()) {
|
||||
startAnimation(mAlphaAnimation, true);
|
||||
mImageTexture.start();
|
||||
}
|
||||
} else {
|
||||
mTextureWidth = 1;
|
||||
mTextureHeight = 1;
|
||||
}
|
||||
|
||||
mScaleOffsetDirty = true;
|
||||
mPositionInRootDirty = true;
|
||||
|
||||
if (oldTextureWidth != mTextureWidth || oldTextureHeight != mTextureHeight) {
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return mImageTexture != null;
|
||||
}
|
||||
|
||||
public boolean canFlingVertically() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return mDst.top < 0.0f || mDst.bottom > getHeight();
|
||||
}
|
||||
|
||||
public boolean canFlingHorizontally() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return mDst.left < 0.0f || mDst.right > getWidth();
|
||||
}
|
||||
|
||||
public boolean canFling() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return mDst.left < 0.0f || mDst.top < 0.0f || mDst.right > getWidth() || mDst.bottom > getHeight();
|
||||
}
|
||||
|
||||
public int getMaxDx() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return Math.max(0, -(int) mDst.left);
|
||||
}
|
||||
|
||||
public int getMinDx() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return Math.min(0, getWidth() - (int) mDst.right);
|
||||
}
|
||||
|
||||
public int getMaxDy() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return Math.max(0, -(int) mDst.top);
|
||||
}
|
||||
|
||||
public int getMinDy() {
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
if (mScaleOffsetDirty) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return Math.min(0, getHeight() - (int) mDst.bottom);
|
||||
}
|
||||
|
||||
public float getScale() {
|
||||
return mScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* If target is shorter then screen, make it in screen center. If target is
|
||||
* longer then parent, make sure target fill parent over
|
||||
*/
|
||||
private void adjustPosition() {
|
||||
RectF dst = mDst;
|
||||
int screenWidth = getWidth();
|
||||
int screenHeight = getHeight();
|
||||
float targetWidth = dst.width();
|
||||
float targetHeight = dst.height();
|
||||
|
||||
if (targetWidth > screenWidth) {
|
||||
float fixXOffset = dst.left;
|
||||
if (fixXOffset > 0) {
|
||||
dst.left -= fixXOffset;
|
||||
dst.right -= fixXOffset;
|
||||
} else if ((fixXOffset = screenWidth - dst.right) > 0) {
|
||||
dst.left += fixXOffset;
|
||||
dst.right += fixXOffset;
|
||||
}
|
||||
} else {
|
||||
float left = (screenWidth - targetWidth) / 2;
|
||||
dst.offsetTo(left, dst.top);
|
||||
}
|
||||
if (targetHeight > screenHeight) {
|
||||
float fixYOffset = dst.top;
|
||||
if (fixYOffset > 0) {
|
||||
dst.top -= fixYOffset;
|
||||
dst.bottom -= fixYOffset;
|
||||
} else if ((fixYOffset = screenHeight - dst.bottom) > 0) {
|
||||
dst.top += fixYOffset;
|
||||
dst.bottom += fixYOffset;
|
||||
}
|
||||
} else {
|
||||
float top = (screenHeight - targetHeight) / 2;
|
||||
dst.offsetTo(dst.left, top);
|
||||
}
|
||||
}
|
||||
|
||||
public void setScaleOffset(int scaleMode, int startPosition, float scaleValue) {
|
||||
mScaleMode = scaleMode;
|
||||
mStartPosition = startPosition;
|
||||
mScaleValue = scaleValue;
|
||||
|
||||
int screenWidth = getWidth();
|
||||
int screenHeight = getHeight();
|
||||
|
||||
if (mImageTexture == null || screenWidth == 0 || screenHeight == 0) {
|
||||
mScaleOffsetDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int textureWidth = mTextureWidth;
|
||||
int textureHeight = mTextureHeight;
|
||||
|
||||
// Set scale
|
||||
float targetWidth;
|
||||
float targetHeight;
|
||||
switch (scaleMode) {
|
||||
case SCALE_ORIGIN:
|
||||
mScale = 1.0f;
|
||||
targetWidth = textureWidth;
|
||||
targetHeight = textureHeight;
|
||||
break;
|
||||
case SCALE_FIT_WIDTH:
|
||||
mScale = (float) screenWidth / textureWidth;
|
||||
targetWidth = screenWidth;
|
||||
targetHeight = textureHeight * mScale;
|
||||
break;
|
||||
case SCALE_FIT_HEIGHT:
|
||||
mScale = (float) screenHeight / textureHeight;
|
||||
targetWidth = textureWidth * mScale;
|
||||
targetHeight = screenHeight;
|
||||
break;
|
||||
case SCALE_FIT:
|
||||
float scaleX = (float) screenWidth / textureWidth;
|
||||
float scaleY = (float) screenHeight / textureHeight;
|
||||
if (scaleX < scaleY) {
|
||||
mScale = scaleX;
|
||||
targetWidth = screenWidth;
|
||||
targetHeight = textureHeight * scaleX;
|
||||
} else {
|
||||
mScale = scaleY;
|
||||
targetWidth = textureWidth * scaleY;
|
||||
targetHeight = screenHeight;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SCALE_FIXED:
|
||||
default:
|
||||
mScale = scaleValue;
|
||||
targetWidth = textureWidth * scaleValue;
|
||||
targetHeight = textureHeight * scaleValue;
|
||||
break;
|
||||
}
|
||||
|
||||
// adjust scale, not too big, not too small
|
||||
if (mScale < SCALE_MIN) {
|
||||
mScale = SCALE_MIN;
|
||||
targetWidth = textureWidth * SCALE_MIN;
|
||||
targetHeight = textureHeight * SCALE_MIN;
|
||||
} else if (mScale > SCALE_MAX) {
|
||||
mScale = SCALE_MAX;
|
||||
targetWidth = textureWidth * SCALE_MAX;
|
||||
targetHeight = textureHeight * SCALE_MAX;
|
||||
}
|
||||
|
||||
// Set mDst.left and mDst.right
|
||||
RectF dst = mDst;
|
||||
switch (startPosition) {
|
||||
case START_POSITION_TOP_LEFT:
|
||||
dst.left = 0;
|
||||
dst.top = 0;
|
||||
break;
|
||||
case START_POSITION_TOP_RIGHT:
|
||||
dst.left = screenWidth - targetWidth;
|
||||
dst.top = 0;
|
||||
break;
|
||||
case START_POSITION_BOTTOM_LEFT:
|
||||
dst.left = 0;
|
||||
dst.top = screenHeight - targetHeight;
|
||||
break;
|
||||
case START_POSITION_BOTTOM_RIGHT:
|
||||
dst.left = screenWidth - targetWidth;
|
||||
dst.top = screenHeight - targetHeight;
|
||||
break;
|
||||
case START_POSITION_CENTER:
|
||||
default:
|
||||
dst.left = (screenWidth - targetWidth) / 2;
|
||||
dst.top = (screenHeight - targetHeight) / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Set mDst.right and mDst.bottom
|
||||
dst.right = dst.left + targetWidth;
|
||||
dst.bottom = dst.top + targetHeight;
|
||||
|
||||
// adjust position
|
||||
adjustPosition();
|
||||
|
||||
mScaleOffsetDirty = false;
|
||||
mPositionInRootDirty = true;
|
||||
}
|
||||
|
||||
public void scroll(int dx, int dy, int[] remain) {
|
||||
// Only work after layout
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
if (mScaleOffsetDirty) {
|
||||
remain[0] = dx;
|
||||
remain[1] = dy;
|
||||
return;
|
||||
}
|
||||
|
||||
RectF dst = mDst;
|
||||
int screenWidth = getWidth();
|
||||
int screenHeight = getHeight();
|
||||
float targetWidth = dst.width();
|
||||
float targetHeight = dst.height();
|
||||
|
||||
if (targetWidth > screenWidth) {
|
||||
dst.left -= dx;
|
||||
dst.right -= dx;
|
||||
|
||||
float fixXOffset = dst.left;
|
||||
if (fixXOffset > 0) {
|
||||
dst.left -= fixXOffset;
|
||||
dst.right -= fixXOffset;
|
||||
remain[0] = -(int) fixXOffset;
|
||||
} else if ((fixXOffset = screenWidth - dst.right) > 0) {
|
||||
dst.left += fixXOffset;
|
||||
dst.right += fixXOffset;
|
||||
remain[0] = (int) fixXOffset;
|
||||
} else {
|
||||
remain[0] = 0;
|
||||
}
|
||||
} else {
|
||||
remain[0] = dx;
|
||||
}
|
||||
if (targetHeight > screenHeight) {
|
||||
dst.top -= dy;
|
||||
dst.bottom -= dy;
|
||||
|
||||
float fixYOffset = dst.top;
|
||||
if (fixYOffset > 0) {
|
||||
dst.top -= fixYOffset;
|
||||
dst.bottom -= fixYOffset;
|
||||
remain[1] = -(int) fixYOffset;
|
||||
} else if ((fixYOffset = screenHeight - dst.bottom) > 0) {
|
||||
dst.top += fixYOffset;
|
||||
dst.bottom += fixYOffset;
|
||||
remain[1] = (int) fixYOffset;
|
||||
} else {
|
||||
remain[1] = 0;
|
||||
}
|
||||
} else {
|
||||
remain[1] = dy;
|
||||
}
|
||||
|
||||
if (dx != remain[0] || dy != remain[1]) {
|
||||
mPositionInRootDirty = true;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public void scale(float focusX, float focusY, float scale) {
|
||||
// Only work after layout
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
if (mScaleOffsetDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((mScale == SCALE_MAX && scale >= 1.0f) || (mScale == SCALE_MIN && scale < 1.0f)) {
|
||||
return;
|
||||
}
|
||||
|
||||
float newScale = mScale * scale;
|
||||
newScale = MathUtils.clamp(newScale, SCALE_MIN, SCALE_MAX);
|
||||
mScale = newScale;
|
||||
RectF dst = mDst;
|
||||
float left = (focusX - ((focusX - dst.left) * scale));
|
||||
float top = (focusY - ((focusY - dst.top) * scale));
|
||||
dst.set(left, top,
|
||||
(left + (mImageTexture.getWidth() * newScale)),
|
||||
(top + (mImageTexture.getHeight() * newScale)));
|
||||
|
||||
// adjust position
|
||||
adjustPosition();
|
||||
|
||||
mPositionInRootDirty = true;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void applyPositionInRoot() {
|
||||
int width = mImageTexture.getWidth();
|
||||
int height = mImageTexture.getHeight();
|
||||
RectF dst = mDst;
|
||||
RectF dstActual = mDstActual;
|
||||
RectF srcActual = mSrcActual;
|
||||
|
||||
dstActual.set(dst);
|
||||
getValidRect(mValidRect);
|
||||
if (dstActual.intersect(mValidRect.left, mValidRect.top, mValidRect.right, mValidRect.bottom)) {
|
||||
srcActual.left = MathUtils.lerp(0, width,
|
||||
MathUtils.delerp(dst.left, dst.right, dstActual.left));
|
||||
srcActual.right = MathUtils.lerp(0, width,
|
||||
MathUtils.delerp(dst.left, dst.right, dstActual.right));
|
||||
srcActual.top = MathUtils.lerp(0, height,
|
||||
MathUtils.delerp(dst.top, dst.bottom, dstActual.top));
|
||||
srcActual.bottom = MathUtils.lerp(0, height,
|
||||
MathUtils.delerp(dst.top, dst.bottom, dstActual.bottom));
|
||||
} else {
|
||||
// Can't be seen, set src and dst empty
|
||||
srcActual.setEmpty();
|
||||
dstActual.setEmpty();
|
||||
}
|
||||
|
||||
mPositionInRootDirty = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRender(GLCanvas canvas) {
|
||||
Texture texture = mImageTexture;
|
||||
if (texture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mScaleOffsetDirty) {
|
||||
setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
|
||||
if (mPositionInRootDirty) {
|
||||
applyPositionInRoot();
|
||||
}
|
||||
|
||||
if (!mSrcActual.isEmpty()) {
|
||||
texture.draw(canvas, mSrcActual, mDstActual);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateImageTexture(ImageTexture who) {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,963 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.Log;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.glview.anim.Animation;
|
||||
import com.hippo.glview.view.GLView;
|
||||
import com.hippo.glview.widget.GLProgressView;
|
||||
import com.hippo.glview.widget.GLTextureView;
|
||||
import com.hippo.yorozuya.AnimationUtils;
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
class PagerLayoutManager extends GalleryView.LayoutManager {
|
||||
|
||||
public static final int MODE_LEFT_TO_RIGHT = 0;
|
||||
public static final int MODE_RIGHT_TO_LEFT = 1;
|
||||
private static final String TAG = PagerLayoutManager.class.getSimpleName();
|
||||
private static final Interpolator SMOOTH_SCROLLER_INTERPOLATOR = new Interpolator() {
|
||||
@Override
|
||||
public float getInterpolation(float t) {
|
||||
t -= 1.0f;
|
||||
return t * t * t * t * t + 1.0f;
|
||||
}
|
||||
};
|
||||
private final int[] mScrollRemain = new int[2];
|
||||
private final float[] mScaleDefault = new float[4];
|
||||
private final SmoothScroller mSmoothScroller;
|
||||
private final PageFling mPageFling;
|
||||
private final SmoothScaler mSmoothScaler;
|
||||
private final Rect mTempRect = new Rect();
|
||||
private GalleryView.Adapter mAdapter;
|
||||
private GLProgressView mProgress;
|
||||
private String mErrorStr;
|
||||
private GLTextureView mErrorView;
|
||||
private GalleryPageView mPrevious;
|
||||
private GalleryPageView mCurrent;
|
||||
private GalleryPageView mNext;
|
||||
@Mode
|
||||
private int mMode = MODE_RIGHT_TO_LEFT;
|
||||
private int mScaleMode;
|
||||
private int mStartPosition;
|
||||
private float mScaleValue;
|
||||
private int mOffset;
|
||||
private int mDeltaX;
|
||||
private int mDeltaY;
|
||||
private boolean mCanScrollBetweenPages = false;
|
||||
private boolean mStopAnimationFinger;
|
||||
private int mInterval;
|
||||
// Current index
|
||||
private int mIndex;
|
||||
|
||||
public PagerLayoutManager(Context context, @NonNull GalleryView galleryView,
|
||||
int scaleMode, int startPoint, float scaleValue, int interval) {
|
||||
super(galleryView);
|
||||
|
||||
mScaleMode = scaleMode;
|
||||
mStartPosition = startPoint;
|
||||
mScaleValue = scaleValue;
|
||||
|
||||
mInterval = interval;
|
||||
mSmoothScroller = new SmoothScroller();
|
||||
mPageFling = new PageFling(context);
|
||||
mSmoothScaler = new SmoothScaler();
|
||||
}
|
||||
|
||||
public void setInterval(int interval) {
|
||||
if (mInterval == interval) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mAdapter != null) {
|
||||
int index = getInternalCurrentIndex();
|
||||
GalleryView.Adapter adapter = onDetach();
|
||||
mInterval = interval;
|
||||
onAttach(adapter);
|
||||
setCurrentIndex(index);
|
||||
mGalleryView.requestFill();
|
||||
} else {
|
||||
mInterval = interval;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetParameters() {
|
||||
mOffset = 0;
|
||||
mDeltaX = 0;
|
||||
mDeltaY = 0;
|
||||
mCanScrollBetweenPages = false;
|
||||
mStopAnimationFinger = false;
|
||||
}
|
||||
|
||||
private boolean cancelAllAnimations() {
|
||||
boolean running = mSmoothScroller.isRunning() ||
|
||||
mPageFling.isRunning() ||
|
||||
mSmoothScaler.isRunning();
|
||||
mSmoothScroller.cancel();
|
||||
mPageFling.cancel();
|
||||
mSmoothScaler.cancel();
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setMode(@Mode int mode) {
|
||||
if (mMode == mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMode = mode;
|
||||
if (mAdapter != null) {
|
||||
// It is attached, refill
|
||||
// Cancel all animations
|
||||
cancelAllAnimations();
|
||||
// Remove all view
|
||||
removeProgress();
|
||||
removeErrorView();
|
||||
removeAllPages();
|
||||
// Reset parameters
|
||||
resetParameters();
|
||||
// Request fill
|
||||
mGalleryView.requestFill();
|
||||
}
|
||||
}
|
||||
|
||||
public void setScaleMode(int scaleMode) {
|
||||
if (mScaleMode == scaleMode) {
|
||||
return;
|
||||
}
|
||||
mScaleMode = scaleMode;
|
||||
|
||||
if (mCurrent != null) {
|
||||
mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
if (mPrevious != null) {
|
||||
mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
if (mNext != null) {
|
||||
mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void setStartPosition(int startPosition) {
|
||||
if (mStartPosition == startPosition) {
|
||||
return;
|
||||
}
|
||||
mStartPosition = startPosition;
|
||||
|
||||
if (mCurrent != null) {
|
||||
mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
if (mPrevious != null) {
|
||||
mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
if (mNext != null) {
|
||||
mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(GalleryView.Adapter adapter) {
|
||||
AssertUtils.assertNull("The PagerLayoutManager is attached", mAdapter);
|
||||
AssertUtils.assertNotNull("The adapter is null", adapter);
|
||||
mAdapter = adapter;
|
||||
// Reset parameters
|
||||
resetParameters();
|
||||
}
|
||||
|
||||
private void removeProgress() {
|
||||
if (mProgress != null) {
|
||||
mGalleryView.removeComponent(mProgress);
|
||||
mGalleryView.releaseProgress(mProgress);
|
||||
mProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void removeErrorView() {
|
||||
if (mErrorView != null) {
|
||||
mGalleryView.removeComponent(mErrorView);
|
||||
mGalleryView.releaseErrorView(mErrorView);
|
||||
mErrorView = null;
|
||||
mErrorStr = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void removePage(@NonNull GalleryPageView page) {
|
||||
mGalleryView.removeComponent(page);
|
||||
mAdapter.unbind(page);
|
||||
mGalleryView.releasePage(page);
|
||||
}
|
||||
|
||||
private void removeAllPages() {
|
||||
// Remove gallery view
|
||||
if (mPrevious != null) {
|
||||
removePage(mPrevious);
|
||||
mPrevious = null;
|
||||
}
|
||||
if (mCurrent != null) {
|
||||
removePage(mCurrent);
|
||||
mCurrent = null;
|
||||
}
|
||||
if (mNext != null) {
|
||||
removePage(mNext);
|
||||
mNext = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GalleryView.Adapter onDetach() {
|
||||
AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter);
|
||||
|
||||
// Cancel all animations
|
||||
cancelAllAnimations();
|
||||
|
||||
// Remove all view
|
||||
removeProgress();
|
||||
removeErrorView();
|
||||
removeAllPages();
|
||||
|
||||
// Clear iterator
|
||||
GalleryView.Adapter adapter = mAdapter;
|
||||
mAdapter = null;
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private GalleryPageView getLeftPage() {
|
||||
switch (mMode) {
|
||||
case MODE_LEFT_TO_RIGHT:
|
||||
return mPrevious;
|
||||
default:
|
||||
case MODE_RIGHT_TO_LEFT:
|
||||
return mNext;
|
||||
}
|
||||
}
|
||||
|
||||
private GalleryPageView getRightPage() {
|
||||
switch (mMode) {
|
||||
case MODE_LEFT_TO_RIGHT:
|
||||
return mNext;
|
||||
default:
|
||||
case MODE_RIGHT_TO_LEFT:
|
||||
return mPrevious;
|
||||
}
|
||||
}
|
||||
|
||||
private GalleryPageView obtainPage() {
|
||||
GalleryPageView page = mGalleryView.obtainPage();
|
||||
page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
return page;
|
||||
}
|
||||
|
||||
private void layoutPage(GalleryPageView page, int widthSpec, int heightSpec,
|
||||
int left, int top, int right, int bottom) {
|
||||
Rect rect = mTempRect;
|
||||
page.getValidRect(rect);
|
||||
boolean oldValid = !rect.isEmpty();
|
||||
page.measure(widthSpec, heightSpec);
|
||||
page.layout(left, top, right, bottom);
|
||||
page.getValidRect(rect);
|
||||
boolean newValid = !rect.isEmpty();
|
||||
if (!oldValid && newValid) {
|
||||
page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFill() {
|
||||
GalleryView.Adapter adapter = mAdapter;
|
||||
GalleryView galleryView = mGalleryView;
|
||||
AssertUtils.assertNotNull("The PagerLayoutManager is not attached", adapter);
|
||||
|
||||
int width = galleryView.getWidth();
|
||||
int height = galleryView.getHeight();
|
||||
int size = adapter.size();
|
||||
String errorStr = adapter.getError();
|
||||
|
||||
if (size == GalleryProvider.STATE_WAIT) { // Wait here, show progress bar
|
||||
// Remove error view and all pages
|
||||
removeErrorView();
|
||||
removeAllPages();
|
||||
|
||||
// Ensure progress
|
||||
if (mProgress == null) {
|
||||
mProgress = galleryView.obtainProgress();
|
||||
galleryView.addComponent(mProgress);
|
||||
}
|
||||
|
||||
// Place progress center
|
||||
placeCenter(mProgress);
|
||||
} else if (size <= GalleryProvider.STATE_ERROR || size == 0) { // Get error or empty, show error text
|
||||
// Ensure error is not null
|
||||
if (0 == size) {
|
||||
errorStr = galleryView.getEmptyStr();
|
||||
} else if (null == errorStr) {
|
||||
errorStr = galleryView.getDefaultErrorStr();
|
||||
}
|
||||
|
||||
// Remove progress and all pages
|
||||
removeProgress();
|
||||
removeAllPages();
|
||||
|
||||
// Ensure error view
|
||||
if (mErrorView == null) {
|
||||
mErrorView = galleryView.obtainErrorView();
|
||||
galleryView.addComponent(mErrorView);
|
||||
}
|
||||
|
||||
// Update error string
|
||||
if (!errorStr.equals(mErrorStr)) {
|
||||
mErrorStr = errorStr;
|
||||
galleryView.bindErrorView(mErrorView, errorStr);
|
||||
}
|
||||
|
||||
// Place error view center
|
||||
placeCenter(mErrorView);
|
||||
} else {
|
||||
// Remove progress and error view
|
||||
removeProgress();
|
||||
removeErrorView();
|
||||
|
||||
// Ensure index in range
|
||||
int index = mIndex;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
mIndex = index;
|
||||
removeAllPages();
|
||||
Log.e(TAG, "index < 0, index = " + index);
|
||||
} else if (index >= size) {
|
||||
index = size - 1;
|
||||
mIndex = index;
|
||||
removeAllPages();
|
||||
Log.e(TAG, "index >= size, index = " + index + ", size = " + size);
|
||||
}
|
||||
|
||||
// Ensure pages
|
||||
if (mCurrent == null) {
|
||||
mCurrent = obtainPage();
|
||||
galleryView.addComponent(mCurrent);
|
||||
adapter.bind(mCurrent, index);
|
||||
}
|
||||
if (mPrevious == null && index > 0) {
|
||||
mPrevious = obtainPage();
|
||||
galleryView.addComponent(mPrevious);
|
||||
adapter.bind(mPrevious, index - 1);
|
||||
} else if (mPrevious != null && index == 0) {
|
||||
removePage(mPrevious);
|
||||
}
|
||||
if (mNext == null && index < size - 1) {
|
||||
mNext = obtainPage();
|
||||
galleryView.addComponent(mNext);
|
||||
adapter.bind(mNext, index + 1);
|
||||
} else if (mNext != null && index == size - 1) {
|
||||
removePage(mNext);
|
||||
}
|
||||
|
||||
GalleryPageView leftPage = getLeftPage();
|
||||
GalleryPageView rightPage = getRightPage();
|
||||
|
||||
// Fix offset
|
||||
final int min = rightPage == null ? 0 : -width - mInterval + 1;
|
||||
final int max = leftPage == null ? 0 : width + mInterval - 1;
|
||||
mOffset = MathUtils.clamp(mOffset, min, max);
|
||||
|
||||
// Measure and layout pages
|
||||
final int offset = mOffset;
|
||||
final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(width, GLView.MeasureSpec.EXACTLY);
|
||||
final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.EXACTLY);
|
||||
if (mCurrent != null) {
|
||||
layoutPage(mCurrent, widthSpec, heightSpec,
|
||||
offset, 0, width + offset, height);
|
||||
}
|
||||
if (leftPage != null) {
|
||||
layoutPage(leftPage, widthSpec, heightSpec,
|
||||
-mInterval - width + offset, 0, -mInterval + offset, height);
|
||||
}
|
||||
if (rightPage != null) {
|
||||
layoutPage(rightPage, widthSpec, heightSpec,
|
||||
width + mInterval + offset, 0, width + mInterval + width + offset, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDown() {
|
||||
mDeltaX = 0;
|
||||
mDeltaY = 0;
|
||||
mStopAnimationFinger = cancelAllAnimations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUp() {
|
||||
if (mCurrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll
|
||||
if (mOffset != 0) {
|
||||
int width = mGalleryView.getWidth();
|
||||
int dx;
|
||||
if (mOffset >= mInterval && getLeftPage() != null) {
|
||||
dx = mOffset - width - mInterval;
|
||||
} else if (mOffset <= -mInterval && getRightPage() != null) {
|
||||
dx = mOffset + width + mInterval;
|
||||
} else {
|
||||
dx = mOffset;
|
||||
}
|
||||
final float pageDelta = 7 * (float) Math.abs(mOffset) / (width + mInterval);
|
||||
int duration = (int) ((pageDelta + 1) * 100);
|
||||
mSmoothScroller.startSmoothScroll(dx, 0, duration);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleTapConfirmed(float x, float y) {
|
||||
if (mCurrent == null || !mCurrent.getImageView().isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] scales = mScaleDefault;
|
||||
ImageView image = mCurrent.getImageView();
|
||||
image.getScaleDefault(scales);
|
||||
float scale = image.getScale();
|
||||
float endScale = scales[0];
|
||||
for (float value : scales) {
|
||||
if (scale < value - 0.01f) {
|
||||
endScale = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mSmoothScaler.startSmoothScaler(x, y, scale, endScale, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(float x, float y) {
|
||||
}
|
||||
|
||||
private void pagePrevious() {
|
||||
if (mIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
mIndex--;
|
||||
|
||||
if (mNext != null) {
|
||||
removePage(mNext);
|
||||
}
|
||||
mNext = mCurrent;
|
||||
mCurrent = mPrevious;
|
||||
mPrevious = null;
|
||||
|
||||
if (mIndex > 0) {
|
||||
mPrevious = obtainPage();
|
||||
mGalleryView.addComponent(mPrevious);
|
||||
mAdapter.bind(mPrevious, mIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void pageNext() {
|
||||
GalleryView.Adapter adapter = mAdapter;
|
||||
int size = adapter.size();
|
||||
if (mIndex >= size - 1) {
|
||||
return;
|
||||
}
|
||||
mIndex++;
|
||||
|
||||
if (mPrevious != null) {
|
||||
removePage(mPrevious);
|
||||
}
|
||||
mPrevious = mCurrent;
|
||||
mCurrent = mNext;
|
||||
mNext = null;
|
||||
|
||||
if (mIndex < size - 1) {
|
||||
mNext = obtainPage();
|
||||
mGalleryView.addComponent(mNext);
|
||||
adapter.bind(mNext, mIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void pageLeft() {
|
||||
switch (mMode) {
|
||||
case MODE_LEFT_TO_RIGHT:
|
||||
pagePrevious();
|
||||
break;
|
||||
default:
|
||||
case MODE_RIGHT_TO_LEFT:
|
||||
pageNext();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void pageRight() {
|
||||
switch (mMode) {
|
||||
case MODE_LEFT_TO_RIGHT:
|
||||
pageNext();
|
||||
break;
|
||||
default:
|
||||
case MODE_RIGHT_TO_LEFT:
|
||||
pagePrevious();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private int scrollBetweenPages(int dx) {
|
||||
GalleryPageView leftPage = getLeftPage();
|
||||
GalleryPageView rightPage = getRightPage();
|
||||
int width = mGalleryView.getWidth();
|
||||
|
||||
int remain;
|
||||
if (dx < 0) { // Try to show left
|
||||
int limit;
|
||||
if (leftPage == null) {
|
||||
limit = 0;
|
||||
} else {
|
||||
limit = width + mInterval;
|
||||
}
|
||||
|
||||
if (dx > mOffset - limit) {
|
||||
remain = 0;
|
||||
mOffset -= dx;
|
||||
} else {
|
||||
// Go to left page if left page not null
|
||||
if (leftPage != null) {
|
||||
pageLeft();
|
||||
}
|
||||
remain = dx + limit - mOffset;
|
||||
mOffset = 0;
|
||||
}
|
||||
} else { // Try to show right
|
||||
int limit;
|
||||
if (rightPage == null) {
|
||||
limit = 0;
|
||||
} else {
|
||||
limit = -width - mInterval;
|
||||
}
|
||||
|
||||
if (dx < mOffset - limit) {
|
||||
remain = 0;
|
||||
mOffset -= dx;
|
||||
} else {
|
||||
// Go to right page if right page not null
|
||||
if (rightPage != null) {
|
||||
pageRight();
|
||||
}
|
||||
remain = dx + limit - mOffset;
|
||||
mOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return remain;
|
||||
}
|
||||
|
||||
public void scrollInternal(float dx, float dy, float x, float y) {
|
||||
if (mCurrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean needFill = false;
|
||||
boolean canImageScroll = true;
|
||||
int remainX = (int) dx;
|
||||
int remainY = (int) dy;
|
||||
|
||||
if (mGalleryView.isFirstScroll()) {
|
||||
mCanScrollBetweenPages = Math.abs(dx) > Math.abs(dy) * 1.5;
|
||||
}
|
||||
|
||||
while (remainX != 0 || remainY != 0) {
|
||||
if (mOffset == 0 && canImageScroll) {
|
||||
ImageView image = mCurrent.getImageView();
|
||||
image.scroll(remainX, remainY, mScrollRemain);
|
||||
remainX = mScrollRemain[0];
|
||||
remainY = mScrollRemain[1];
|
||||
canImageScroll = false;
|
||||
mDeltaX = 0;
|
||||
mDeltaY = 0;
|
||||
} else if (remainX == 0 ||
|
||||
(getLeftPage() == null && mOffset == 0 && remainX < 0) ||
|
||||
(getRightPage() == null && mOffset == 0 && remainX > 0)) {
|
||||
// On edge
|
||||
remainX = 0;
|
||||
remainY = 0;
|
||||
} else if (mCanScrollBetweenPages) {
|
||||
remainX = scrollBetweenPages(remainX);
|
||||
canImageScroll = true;
|
||||
needFill = true;
|
||||
mDeltaX = 0;
|
||||
mDeltaY = 0;
|
||||
} else {
|
||||
remainX = 0;
|
||||
remainY = 0;
|
||||
mDeltaX = 0;
|
||||
mDeltaY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (needFill) {
|
||||
mGalleryView.requestFill();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) {
|
||||
scrollInternal(dx, dy, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFling(float velocityX, float velocityY) {
|
||||
if (mCurrent == null || mOffset != 0 || !mCurrent.getImageView().isLoaded() ||
|
||||
!mCurrent.getImageView().canFling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageView image = mCurrent.getImageView();
|
||||
mPageFling.startFling((int) velocityX, image.getMinDx(), image.getMaxDx(),
|
||||
(int) velocityY, image.getMinDy(), image.getMaxDy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canScale() {
|
||||
return mCurrent != null && mOffset == 0 && mCurrent.getImageView().isLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScale(float focusX, float focusY, float scale) {
|
||||
if (mCurrent == null || !mCurrent.getImageView().isLoaded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrent.getImageView().scale(focusX, focusY, scale);
|
||||
mScaleValue = mCurrent.getImageView().getScale();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUpdateAnimation(long time) {
|
||||
boolean invalidate = mSmoothScroller.calculate(time);
|
||||
invalidate |= mPageFling.calculate(time);
|
||||
invalidate |= mSmoothScaler.calculate(time);
|
||||
return invalidate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChanged() {
|
||||
AssertUtils.assertNotNull("The PagerLayoutManager is not attached", mAdapter);
|
||||
|
||||
// Cancel all animations
|
||||
cancelAllAnimations();
|
||||
// Remove all views
|
||||
removeProgress();
|
||||
removeErrorView();
|
||||
removeAllPages();
|
||||
// Reset parameters
|
||||
resetParameters();
|
||||
mGalleryView.requestFill();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageLeft() {
|
||||
int size = mAdapter.size();
|
||||
if (size <= 0 || mCurrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMode == MODE_LEFT_TO_RIGHT) {
|
||||
if (mIndex != 0) {
|
||||
setCurrentIndex(mIndex - 1);
|
||||
}
|
||||
} else {
|
||||
if (mIndex < size - 1) {
|
||||
setCurrentIndex(mIndex + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageRight() {
|
||||
int size = mAdapter.size();
|
||||
if (size <= 0 || mCurrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mMode == MODE_LEFT_TO_RIGHT) {
|
||||
if (mIndex < size - 1) {
|
||||
setCurrentIndex(mIndex + 1);
|
||||
}
|
||||
} else {
|
||||
if (mIndex != 0) {
|
||||
setCurrentIndex(mIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTapOrPressEnable() {
|
||||
return !mStopAnimationFinger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GalleryPageView findPageByIndex(int index) {
|
||||
if (mCurrent != null && mCurrent.getIndex() == index) {
|
||||
return mCurrent;
|
||||
}
|
||||
if (mPrevious != null && mPrevious.getIndex() == index) {
|
||||
return mPrevious;
|
||||
}
|
||||
if (mNext != null && mNext.getIndex() == index) {
|
||||
return mNext;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentIndex() {
|
||||
if (mCurrent != null) {
|
||||
return mCurrent.getIndex();
|
||||
} else {
|
||||
return GalleryPageView.INVALID_INDEX;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentIndex(int index) {
|
||||
int size = mAdapter.size();
|
||||
if (size <= 0) {
|
||||
// Can't get size now, assume size is MAX
|
||||
size = Integer.MAX_VALUE;
|
||||
}
|
||||
if (index == mIndex || index < 0 || index >= size) {
|
||||
return;
|
||||
}
|
||||
if (mCurrent == null) {
|
||||
mIndex = index;
|
||||
} else if (index == mIndex - 1) {
|
||||
// Cancel all animations
|
||||
cancelAllAnimations();
|
||||
// Reset parameters
|
||||
resetParameters();
|
||||
// Go to previous
|
||||
pagePrevious();
|
||||
// Request fill
|
||||
mGalleryView.requestFill();
|
||||
} else if (index == mIndex + 1) {
|
||||
// Cancel all animations
|
||||
cancelAllAnimations();
|
||||
// Reset parameters
|
||||
resetParameters();
|
||||
// Go to next
|
||||
pageNext();
|
||||
// Request fill
|
||||
mGalleryView.requestFill();
|
||||
} else {
|
||||
mIndex = index;
|
||||
// It is attached, refill
|
||||
// Cancel all animations
|
||||
cancelAllAnimations();
|
||||
// Remove all view
|
||||
removeProgress();
|
||||
removeErrorView();
|
||||
removeAllPages();
|
||||
// Reset parameters
|
||||
resetParameters();
|
||||
// Request fill
|
||||
mGalleryView.requestFill();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexUnder(float x, float y) {
|
||||
if (mCurrent == null) {
|
||||
return GalleryPageView.INVALID_INDEX;
|
||||
} else {
|
||||
int intX = (int) x;
|
||||
int intY = (int) y;
|
||||
if (mCurrent.bounds().contains(intX, intY)) {
|
||||
return mCurrent.getIndex();
|
||||
} else if (mPrevious != null && mPrevious.bounds().contains(intX, intY)) {
|
||||
return mPrevious.getIndex();
|
||||
} else if (mNext != null && mNext.bounds().contains(intX, intY)) {
|
||||
return mNext.getIndex();
|
||||
} else {
|
||||
return GalleryPageView.INVALID_INDEX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
int getInternalCurrentIndex() {
|
||||
int currentIndex = getCurrentIndex();
|
||||
if (currentIndex == GalleryPageView.INVALID_INDEX) {
|
||||
currentIndex = mIndex;
|
||||
}
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
@IntDef({MODE_LEFT_TO_RIGHT, MODE_RIGHT_TO_LEFT})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
private @interface Mode {
|
||||
}
|
||||
|
||||
private class SmoothScroller extends Animation {
|
||||
|
||||
private int mDx;
|
||||
private int mDy;
|
||||
private int mLastX;
|
||||
private int mLastY;
|
||||
|
||||
public SmoothScroller() {
|
||||
setInterpolator(SMOOTH_SCROLLER_INTERPOLATOR);
|
||||
}
|
||||
|
||||
public void startSmoothScroll(int dx, int dy, int duration) {
|
||||
mDx = dx;
|
||||
mDy = dy;
|
||||
mLastX = 0;
|
||||
mLastY = 0;
|
||||
setDuration(duration);
|
||||
start();
|
||||
mGalleryView.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCalculate(float progress) {
|
||||
int x = (int) (mDx * progress);
|
||||
int y = (int) (mDy * progress);
|
||||
int offsetX = x - mLastX;
|
||||
while (offsetX != 0) {
|
||||
int oldOffsetX = offsetX;
|
||||
offsetX = scrollBetweenPages(offsetX);
|
||||
// Avoid loop infinitely
|
||||
if (offsetX == oldOffsetX) {
|
||||
break;
|
||||
} else {
|
||||
mGalleryView.requestFill();
|
||||
}
|
||||
}
|
||||
mLastX = x;
|
||||
mLastY = y;
|
||||
}
|
||||
}
|
||||
|
||||
private class PageFling extends Fling {
|
||||
|
||||
private final int[] mTemp = new int[2];
|
||||
private int mVelocityX;
|
||||
private int mVelocityY;
|
||||
private int mDx;
|
||||
private int mDy;
|
||||
private int mLastX;
|
||||
private int mLastY;
|
||||
|
||||
public PageFling(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void startFling(int velocityX, int minX, int maxX,
|
||||
int velocityY, int minY, int maxY) {
|
||||
mVelocityX = velocityX;
|
||||
mVelocityY = velocityY;
|
||||
mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX));
|
||||
mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY));
|
||||
mLastX = 0;
|
||||
mLastY = 0;
|
||||
int durationX = getSplineFlingDuration(velocityX);
|
||||
int durationY = getSplineFlingDuration(velocityY);
|
||||
|
||||
if (mDx < minX) {
|
||||
durationX = adjustDuration(0, mDx, minX, durationX);
|
||||
mDx = minX;
|
||||
}
|
||||
if (mDx > maxX) {
|
||||
durationX = adjustDuration(0, mDx, maxX, durationX);
|
||||
mDx = maxX;
|
||||
}
|
||||
if (mDy < minY) {
|
||||
durationY = adjustDuration(0, mDy, minY, durationY);
|
||||
mDy = minY;
|
||||
}
|
||||
if (mDy > maxY) {
|
||||
durationY = adjustDuration(0, mDy, maxY, durationY);
|
||||
mDy = maxY;
|
||||
}
|
||||
|
||||
if (mDx == 0 && mDy == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDuration(Math.max(durationX, durationY));
|
||||
start();
|
||||
mGalleryView.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCalculate(float progress) {
|
||||
int x = (int) (mDx * progress);
|
||||
int y = (int) (mDy * progress);
|
||||
int offsetX = x - mLastX;
|
||||
int offsetY = y - mLastY;
|
||||
if (mCurrent != null && (offsetX != 0 || offsetY != 0)) {
|
||||
mCurrent.getImageView().scroll(-offsetX, -offsetY, mTemp);
|
||||
}
|
||||
mLastX = x;
|
||||
mLastY = y;
|
||||
}
|
||||
}
|
||||
|
||||
private class SmoothScaler extends Animation {
|
||||
|
||||
private float mFocusX;
|
||||
private float mFocusY;
|
||||
private float mStartScale;
|
||||
private float mEndScale;
|
||||
private float mLastScale;
|
||||
|
||||
public SmoothScaler() {
|
||||
setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);
|
||||
}
|
||||
|
||||
public void startSmoothScaler(float focusX, float focusY,
|
||||
float startScale, float endScale, int duration) {
|
||||
mFocusX = focusX;
|
||||
mFocusY = focusY;
|
||||
mStartScale = startScale;
|
||||
mEndScale = endScale;
|
||||
mLastScale = startScale;
|
||||
setDuration(duration);
|
||||
start();
|
||||
mGalleryView.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCalculate(float progress) {
|
||||
if (mCurrent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float scale = MathUtils.lerp(mStartScale, mEndScale, progress);
|
||||
mCurrent.getImageView().scale(mFocusX, mFocusY, scale / mLastScale);
|
||||
mLastScale = scale;
|
||||
mScaleValue = scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* 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.hippo.glgallery;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hippo.glview.image.ImageTexture;
|
||||
import com.hippo.glview.image.ImageWrapper;
|
||||
import com.hippo.glview.view.GLRootView;
|
||||
|
||||
public class SimpleAdapter extends GalleryView.Adapter implements GalleryProvider.Listener {
|
||||
|
||||
private final GalleryProvider mProvider;
|
||||
private final ImageTexture.Uploader mUploader;
|
||||
private boolean mShowIndex = true;
|
||||
|
||||
public SimpleAdapter(@NonNull GLRootView glRootView, @NonNull GalleryProvider provider) {
|
||||
mProvider = provider;
|
||||
mUploader = new ImageTexture.Uploader(glRootView);
|
||||
}
|
||||
|
||||
public void clearUploader() {
|
||||
mUploader.clear();
|
||||
}
|
||||
|
||||
public void setShowIndex(boolean show) {
|
||||
mShowIndex = show;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(GalleryPageView view, int index) {
|
||||
mProvider.request(index);
|
||||
view.showInfo();
|
||||
view.setImage(null);
|
||||
if (mShowIndex) {
|
||||
view.setPage(index + 1);
|
||||
} else {
|
||||
view.hidePage();
|
||||
}
|
||||
view.setProgress(GalleryPageView.PROGRESS_INDETERMINATE);
|
||||
view.setError(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnbind(GalleryPageView view, int index) {
|
||||
mProvider.cancelRequest(index);
|
||||
view.setImage(null);
|
||||
view.setError(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getError() {
|
||||
return mProvider.getError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return mProvider.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChanged() {
|
||||
mGalleryView.onDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageWait(int index) {
|
||||
GalleryPageView page = findPageByIndex(index);
|
||||
if (page != null) {
|
||||
page.showInfo();
|
||||
page.setImage(null);
|
||||
if (mShowIndex) {
|
||||
page.setPage(index + 1);
|
||||
} else {
|
||||
page.hidePage();
|
||||
}
|
||||
page.setProgress(GalleryPageView.PROGRESS_INDETERMINATE);
|
||||
page.setError(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPagePercent(int index, float percent) {
|
||||
GalleryPageView page = findPageByIndex(index);
|
||||
if (page != null) {
|
||||
page.showInfo();
|
||||
page.setImage(null);
|
||||
if (mShowIndex) {
|
||||
page.setPage(index + 1);
|
||||
} else {
|
||||
page.hidePage();
|
||||
}
|
||||
page.setProgress(percent);
|
||||
page.setError(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSucceed(int index, ImageWrapper image) {
|
||||
GalleryPageView page = findPageByIndex(index);
|
||||
if (page != null) {
|
||||
if (image.obtain()) {
|
||||
ImageTexture imageTexture = new ImageTexture(image);
|
||||
mUploader.addTexture(imageTexture);
|
||||
page.showImage();
|
||||
page.setImage(imageTexture);
|
||||
if (mShowIndex) {
|
||||
page.setPage(index + 1);
|
||||
} else {
|
||||
page.hidePage();
|
||||
}
|
||||
page.setProgress(GalleryPageView.PROGRESS_GONE);
|
||||
page.setError(null, null);
|
||||
} else {
|
||||
// The image is recycled, request again.
|
||||
// TODO request loop ?
|
||||
mProvider.request(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFailed(int index, String error) {
|
||||
GalleryPageView page = findPageByIndex(index);
|
||||
if (page != null) {
|
||||
page.showInfo();
|
||||
page.setImage(null);
|
||||
if (mShowIndex) {
|
||||
page.setPage(index + 1);
|
||||
} else {
|
||||
page.hidePage();
|
||||
}
|
||||
page.setProgress(GalleryPageView.PROGRESS_GONE);
|
||||
page.setError(error, mGalleryView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChanged(int index) {
|
||||
GalleryPageView page = findPageByIndex(index);
|
||||
if (page != null) {
|
||||
mProvider.request(index);
|
||||
}
|
||||
}
|
||||
|
||||
private GalleryPageView findPageByIndex(int index) {
|
||||
return mGalleryView != null ? mGalleryView.findPageByIndex(index) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.anim;
|
||||
|
||||
import com.hippo.glview.glrenderer.GLCanvas;
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
public class AlphaAnimation extends CanvasAnimation {
|
||||
private final float mStartAlpha;
|
||||
private final float mEndAlpha;
|
||||
private float mCurrentAlpha;
|
||||
|
||||
public AlphaAnimation(float from, float to) {
|
||||
mStartAlpha = from;
|
||||
mEndAlpha = to;
|
||||
mCurrentAlpha = from;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(GLCanvas canvas) {
|
||||
canvas.multiplyAlpha(mCurrentAlpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCanvasSaveFlags() {
|
||||
return GLCanvas.SAVE_FLAG_ALPHA;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCalculate(float progress) {
|
||||
mCurrentAlpha = MathUtils.clamp(mStartAlpha +
|
||||
(mEndAlpha - mStartAlpha) * progress, 0f, 1f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.anim;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
// Animation calculates a value according to the current input time.
|
||||
//
|
||||
// 1. First we need to use setDuration(int) to set the duration of the
|
||||
// animation. The duration is in milliseconds.
|
||||
// 2. Then we should call start(). The actual start time is the first value
|
||||
// passed to calculate(long).
|
||||
// 3. Each time we want to get an animation value, we call
|
||||
// calculate(long currentTimeMillis) to ask the Animation to calculate it.
|
||||
// The parameter passed to calculate(long) should be nonnegative.
|
||||
// 4. Use get() to get that value.
|
||||
//
|
||||
// In step 3, onCalculate(float progress) is called so subclasses can calculate
|
||||
// the value according to progress (progress is a value in [0,1]).
|
||||
//
|
||||
// Before onCalculate(float) is called, There is an optional interpolator which
|
||||
// can change the progress value. The interpolator can be set by
|
||||
// setInterpolator(Interpolator). If the interpolator is used, the value passed
|
||||
// to onCalculate may be (for example, the overshoot effect).
|
||||
//
|
||||
// The isActive() method returns true after the animation start() is called and
|
||||
// before calculate is passed a value which reaches the duration of the
|
||||
// animation.
|
||||
//
|
||||
// The start() method can be called again to restart the Animation.
|
||||
//
|
||||
abstract public class Animation {
|
||||
/**
|
||||
* When the animation reaches the end and <code>repeatCount</code> is INFINITE
|
||||
* or a positive value, the animation restarts from the beginning.
|
||||
*/
|
||||
public static final int RESTART = 1;
|
||||
/**
|
||||
* When the animation reaches the end and <code>repeatCount</code> is INFINITE
|
||||
* or a positive value, the animation reverses direction on every iteration.
|
||||
*/
|
||||
public static final int REVERSE = 2;
|
||||
/**
|
||||
* This value used used with the {@link #setRepeatCount(int)} property to repeat
|
||||
* the animation indefinitely.
|
||||
*/
|
||||
public static final int INFINITE = -1;
|
||||
private static final long ANIMATION_START = -1;
|
||||
private static final long NO_ANIMATION = -2;
|
||||
private long mStartTime = NO_ANIMATION;
|
||||
private long mDuration;
|
||||
private Interpolator mInterpolator;
|
||||
private int mRepeatCount;
|
||||
private int mRepeatMode;
|
||||
|
||||
private int mRunnedCount;
|
||||
private long mLastFrameTime;
|
||||
|
||||
public void setInterpolator(Interpolator interpolator) {
|
||||
mInterpolator = interpolator;
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
mDuration = duration;
|
||||
}
|
||||
|
||||
public void setRepeatCount(int repeatCount) {
|
||||
mRepeatCount = repeatCount;
|
||||
}
|
||||
|
||||
public void setRepeatMode(int repeatMode) {
|
||||
mRepeatMode = repeatMode;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return mStartTime > 0 || mStartTime == ANIMATION_START;
|
||||
}
|
||||
|
||||
public long getLastFrameTime() {
|
||||
return mLastFrameTime;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (mStartTime == NO_ANIMATION) {
|
||||
mStartTime = ANIMATION_START;
|
||||
mRunnedCount = 0;
|
||||
mLastFrameTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void startAt(long time) {
|
||||
start();
|
||||
setStartTime(time);
|
||||
}
|
||||
|
||||
public void startNow() {
|
||||
start();
|
||||
setStartTime(SystemClock.uptimeMillis());
|
||||
}
|
||||
|
||||
public void setStartTime(long time) {
|
||||
mStartTime = time;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
mStartTime = NO_ANIMATION;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
mStartTime = ANIMATION_START;
|
||||
mRunnedCount = 0;
|
||||
mLastFrameTime = 0;
|
||||
}
|
||||
|
||||
// TODO mRepeatMode
|
||||
public boolean calculate(long currentTimeMillis) {
|
||||
if (mStartTime == NO_ANIMATION) {
|
||||
return false;
|
||||
}
|
||||
if (mStartTime == ANIMATION_START) {
|
||||
mStartTime = currentTimeMillis;
|
||||
}
|
||||
mLastFrameTime = currentTimeMillis;
|
||||
|
||||
long elapse = currentTimeMillis - mStartTime;
|
||||
float x = MathUtils.clamp(mDuration == 0.0f ? 1.0f : (float) elapse / mDuration, 0.0f, 1.0f); // Avoid NaN
|
||||
Interpolator i = mInterpolator;
|
||||
onCalculate(i != null ? i.getInterpolation(x) : x);
|
||||
|
||||
// It is ok to call cancel() in onCalculate()
|
||||
if (mStartTime != NO_ANIMATION && elapse >= mDuration) {
|
||||
mRunnedCount++;
|
||||
if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) {
|
||||
onFinish();
|
||||
mStartTime = NO_ANIMATION;
|
||||
} else {
|
||||
mStartTime += elapse;
|
||||
}
|
||||
}
|
||||
|
||||
return mStartTime != NO_ANIMATION;
|
||||
}
|
||||
|
||||
abstract protected void onCalculate(float progress);
|
||||
|
||||
protected void onFinish() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.anim;
|
||||
|
||||
import com.hippo.glview.glrenderer.GLCanvas;
|
||||
|
||||
public abstract class CanvasAnimation extends Animation {
|
||||
|
||||
public abstract int getCanvasSaveFlags();
|
||||
|
||||
public abstract void apply(GLCanvas canvas);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.anim;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
public class FloatAnimation extends Animation {
|
||||
|
||||
private float mFrom;
|
||||
private float mTo;
|
||||
private float mCurrent;
|
||||
|
||||
public void setRange(float from, float to) {
|
||||
mFrom = from;
|
||||
mTo = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCalculate(float progress) {
|
||||
mCurrent = MathUtils.lerp(mFrom, mTo, progress);
|
||||
}
|
||||
|
||||
public float get() {
|
||||
return mCurrent;
|
||||
}
|
||||
}
|
||||
+9
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 Hippo Seven
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,18 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.a7zip;
|
||||
package com.hippo.glview.annotation;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Keep
|
||||
public class PasswordException extends ArchiveException {
|
||||
|
||||
public PasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public PasswordException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
|
||||
public @interface RenderThread {
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.glrenderer;
|
||||
|
||||
import android.graphics.RectF;
|
||||
import android.util.Log;
|
||||
|
||||
import com.hippo.yorozuya.MathUtils;
|
||||
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
// BasicTexture is a Texture corresponds to a real GL texture.
|
||||
// The state of a BasicTexture indicates whether its data is loaded to GL memory.
|
||||
// If a BasicTexture is loaded into GL memory, it has a GL texture id.
|
||||
public abstract class BasicTexture implements Texture {
|
||||
|
||||
protected static final int UNSPECIFIED = -1;
|
||||
protected static final int STATE_UNLOADED = 0;
|
||||
protected static final int STATE_LOADED = 1;
|
||||
protected static final int STATE_ERROR = -1;
|
||||
private static final String TAG = "BasicTexture";
|
||||
// Log a warning if a texture is larger along a dimension
|
||||
private static final int MAX_TEXTURE_SIZE = 4096;
|
||||
private final static WeakHashMap<BasicTexture, Object> sAllTextures
|
||||
= new WeakHashMap<>();
|
||||
private static final ThreadLocal<Class> sInFinalizer = new ThreadLocal<>();
|
||||
protected int mId = -1;
|
||||
protected int mState;
|
||||
protected int mWidth = UNSPECIFIED;
|
||||
protected int mHeight = UNSPECIFIED;
|
||||
protected int mTextureWidth;
|
||||
protected int mTextureHeight;
|
||||
protected GLCanvas mCanvasRef = null;
|
||||
private boolean mHasBorder;
|
||||
|
||||
protected BasicTexture(GLCanvas canvas, int id, int state) {
|
||||
setAssociatedCanvas(canvas);
|
||||
mId = id;
|
||||
mState = state;
|
||||
synchronized (sAllTextures) {
|
||||
sAllTextures.put(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected BasicTexture() {
|
||||
this(null, 0, STATE_UNLOADED);
|
||||
}
|
||||
|
||||
// This is for deciding if we can call Bitmap's recycle().
|
||||
// We cannot call Bitmap's recycle() in finalizer because at that point
|
||||
// the finalizer of Bitmap may already be called so recycle() will crash.
|
||||
public static boolean inFinalizer() {
|
||||
return sInFinalizer.get() != null;
|
||||
}
|
||||
|
||||
public static void yieldAllTextures() {
|
||||
synchronized (sAllTextures) {
|
||||
for (BasicTexture t : sAllTextures.keySet()) {
|
||||
t.yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidateAllTextures() {
|
||||
synchronized (sAllTextures) {
|
||||
for (BasicTexture t : sAllTextures.keySet()) {
|
||||
t.mState = STATE_UNLOADED;
|
||||
t.setAssociatedCanvas(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void setAssociatedCanvas(GLCanvas canvas) {
|
||||
mCanvasRef = canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content size of this texture. In OpenGL, the actual texture
|
||||
* size must be of power of 2, the size of the content may be smaller.
|
||||
*/
|
||||
public void setSize(int width, int height) {
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
mTextureWidth = width > 0 ? MathUtils.nextPowerOf2(width) : 0;
|
||||
mTextureHeight = height > 0 ? MathUtils.nextPowerOf2(height) : 0;
|
||||
if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {
|
||||
Log.w(TAG, String.format("texture is too large: %d x %d",
|
||||
mTextureWidth, mTextureHeight), new Exception());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFlippedVertically() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
// Returns the width rounded to the next power of 2.
|
||||
public int getTextureWidth() {
|
||||
return mTextureWidth;
|
||||
}
|
||||
|
||||
// Returns the height rounded to the next power of 2.
|
||||
public int getTextureHeight() {
|
||||
return mTextureHeight;
|
||||
}
|
||||
|
||||
// Returns true if the texture has one pixel transparent border around the
|
||||
// actual content. This is used to avoid jigged edges.
|
||||
//
|
||||
// The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap
|
||||
// mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially
|
||||
// covered by the texture will use the color of the edge texel. If we add
|
||||
// the transparent border, the color of the edge texel will be mixed with
|
||||
// appropriate amount of transparent.
|
||||
//
|
||||
// Currently our background is black, so we can draw the thumbnails without
|
||||
// enabling blending.
|
||||
public boolean hasBorder() {
|
||||
return mHasBorder;
|
||||
}
|
||||
|
||||
protected void setBorder(boolean hasBorder) {
|
||||
mHasBorder = hasBorder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y) {
|
||||
canvas.drawTexture(this, x, y, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
|
||||
canvas.drawTexture(this, x, y, w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, RectF source, RectF target) {
|
||||
canvas.drawTexture(this, source, target);
|
||||
}
|
||||
|
||||
// onBind is called before GLCanvas binds this texture.
|
||||
// It should make sure the data is uploaded to GL memory.
|
||||
abstract protected boolean onBind(GLCanvas canvas);
|
||||
|
||||
// Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).
|
||||
abstract protected int getTarget();
|
||||
|
||||
public boolean isLoaded() {
|
||||
return mState == STATE_LOADED;
|
||||
}
|
||||
|
||||
// recycle() is called when the texture will never be used again,
|
||||
// so it can free all resources.
|
||||
public void recycle() {
|
||||
freeResource();
|
||||
}
|
||||
|
||||
// yield() is called when the texture will not be used temporarily,
|
||||
// so it can free some resources.
|
||||
// The default implementation unloads the texture from GL memory, so
|
||||
// the subclass should make sure it can reload the texture to GL memory
|
||||
// later, or it will have to override this method.
|
||||
public void yield() {
|
||||
freeResource();
|
||||
}
|
||||
|
||||
private void freeResource() {
|
||||
GLCanvas canvas = mCanvasRef;
|
||||
if (canvas != null && mId != -1) {
|
||||
canvas.unloadTexture(this);
|
||||
mId = -1; // Don't free it again.
|
||||
}
|
||||
mState = STATE_UNLOADED;
|
||||
setAssociatedCanvas(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
try {
|
||||
sInFinalizer.set(BasicTexture.class);
|
||||
recycle();
|
||||
sInFinalizer.set(null);
|
||||
} finally {
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.glrenderer;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import com.hippo.yorozuya.AssertUtils;
|
||||
|
||||
// BitmapTexture is a texture whose content is specified by a fixed Bitmap.
|
||||
//
|
||||
// The texture does not own the Bitmap. The user should make sure the Bitmap
|
||||
// is valid during the texture's lifetime. When the texture is recycled, it
|
||||
// does not free the Bitmap.
|
||||
public class BitmapTexture extends UploadedTexture {
|
||||
protected Bitmap mContentBitmap;
|
||||
|
||||
public BitmapTexture(Bitmap bitmap) {
|
||||
this(bitmap, false);
|
||||
}
|
||||
|
||||
public BitmapTexture(Bitmap bitmap, boolean hasBorder) {
|
||||
super(hasBorder);
|
||||
AssertUtils.assertTrue(bitmap != null && !bitmap.isRecycled());
|
||||
mContentBitmap = bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFreeBitmap(Bitmap bitmap) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap onGetBitmap() {
|
||||
return mContentBitmap;
|
||||
}
|
||||
|
||||
public Bitmap getBitmap() {
|
||||
return mContentBitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.glrenderer;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.Canvas;
|
||||
|
||||
// CanvasTexture is a texture whose content is the drawing on a Canvas.
|
||||
// The subclasses should override onDraw() to draw on the bitmap.
|
||||
// By default CanvasTexture is not opaque.
|
||||
abstract class CanvasTexture extends UploadedTexture {
|
||||
private final Config mConfig;
|
||||
|
||||
public CanvasTexture(int width, int height) {
|
||||
mConfig = Config.ARGB_8888;
|
||||
setSize(width, height);
|
||||
setOpaque(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap onGetBitmap() {
|
||||
Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
onDraw(canvas, bitmap);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFreeBitmap(Bitmap bitmap) {
|
||||
if (!inFinalizer()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected void onDraw(Canvas canvas, Bitmap backing);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (C) 2010 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.hippo.glview.glrenderer;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
import com.hippo.yorozuya.ColorUtils;
|
||||
|
||||
// ColorTexture is a texture which fills the rectangle with the specified color.
|
||||
public class ColorTexture implements Texture {
|
||||
|
||||
private final int mColor;
|
||||
private int mWidth = 0;
|
||||
private int mHeight = 0;
|
||||
|
||||
public ColorTexture(int color) {
|
||||
mColor = color;
|
||||
}
|
||||
|
||||
private boolean isValidSize() {
|
||||
return mWidth > 0 && mHeight > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y) {
|
||||
if (isValidSize()) {
|
||||
draw(canvas, x, y, mWidth, mHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
|
||||
canvas.fillRect(x, y, w, h, mColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, RectF source, RectF target) {
|
||||
canvas.fillRect(target.left, target.top, target.width(), target.height(), mColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpaque() {
|
||||
return ColorUtils.isOpaque(mColor);
|
||||
}
|
||||
|
||||
public void setSize(int width, int height) {
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2012 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.hippo.glview.glrenderer;
|
||||
|
||||
// ExtTexture is a texture whose content comes from a external texture.
|
||||
// Before drawing, setSize() should be called.
|
||||
public class ExtTexture extends BasicTexture {
|
||||
|
||||
private final int mTarget;
|
||||
|
||||
public ExtTexture(GLCanvas canvas, int target) {
|
||||
GLId glId = canvas.getGLId();
|
||||
mId = glId.generateTexture();
|
||||
mTarget = target;
|
||||
}
|
||||
|
||||
private void uploadToCanvas(GLCanvas canvas) {
|
||||
canvas.setTextureParameters(this);
|
||||
setAssociatedCanvas(canvas);
|
||||
mState = STATE_LOADED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onBind(GLCanvas canvas) {
|
||||
if (!isLoaded()) {
|
||||
uploadToCanvas(canvas);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTarget() {
|
||||
return mTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpaque() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void yield() {
|
||||
// we cannot free the texture because we have no backup.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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.hippo.glview.glrenderer;
|
||||
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
// FadeInTexture is a texture which begins with a color, then gradually animates
|
||||
// into a given texture.
|
||||
public class FadeInTexture extends FadeTexture implements Texture {
|
||||
|
||||
private final int mColor;
|
||||
private final TiledTexture mTexture;
|
||||
|
||||
public FadeInTexture(int color, TiledTexture texture) {
|
||||
super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
|
||||
mColor = color;
|
||||
mTexture = texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
|
||||
if (isAnimating()) {
|
||||
mTexture.drawMixed(canvas, mColor, getRatio(), x, y, w, h);
|
||||
} else {
|
||||
mTexture.draw(canvas, x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, RectF source, RectF target) {
|
||||
if (isAnimating()) {
|
||||
mTexture.drawMixed(canvas, mColor, getRatio(), source, target);
|
||||
} else {
|
||||
mTexture.draw(canvas, source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2011 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.hippo.glview.glrenderer;
|
||||
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
// FadeOutTexture is a texture which begins with a given texture, then gradually animates
|
||||
// into fading out totally.
|
||||
public class FadeOutTexture extends FadeTexture {
|
||||
|
||||
private final BasicTexture mTexture;
|
||||
|
||||
public FadeOutTexture(BasicTexture texture) {
|
||||
super(texture.getWidth(), texture.getHeight(), texture.isOpaque());
|
||||
mTexture = texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, int x, int y, int w, int h) {
|
||||
if (isAnimating()) {
|
||||
canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
|
||||
canvas.setAlpha(getRatio());
|
||||
mTexture.draw(canvas, x, y, w, h);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(GLCanvas canvas, RectF source, RectF target) {
|
||||
if (isAnimating()) {
|
||||
canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
|
||||
canvas.setAlpha(getRatio());
|
||||
mTexture.draw(canvas, source, target);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 444 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user