Comparar commits
624 Commits
v4.0.2a-beta
...
dev
| Autor | SHA1 | Data | |
|---|---|---|---|
| dffd572afc | |||
| b7f3defd19 | |||
| e11a718d3b | |||
| a47cede6c5 | |||
| b1a8b7a0d5 | |||
| aca3b6c08a | |||
| 69dba8d5f1 | |||
| eda498c65f | |||
| d80e7e2edc | |||
| f6c818fbb5 | |||
| d59aeef3a9 | |||
| dbd7e6c2e6 | |||
| d75675e006 | |||
| 7a256707a7 | |||
| 674ebb88f9 | |||
| d6a1450bef | |||
| 99c09a9d63 | |||
| f322c570c0 | |||
| 984aa133ec | |||
| 008e61b5a8 | |||
| 6d47d7232f | |||
| e9d01dc104 | |||
| a7748ceee2 | |||
| 6e940b0a15 | |||
| cabea7e097 | |||
| 4d400f995f | |||
| bd98619d4f | |||
| 40cda1317a | |||
| e26330a5bd | |||
| dbc186db9b | |||
| d7faeaa2fc | |||
| ae6726b290 | |||
| f05312e915 | |||
| 58d8cb6a36 | |||
| 57d5298bec | |||
| bd8c439161 | |||
| f90ab177d5 | |||
| 1685a13df3 | |||
| 87ae1eb8fe | |||
| 8f230e3550 | |||
| 3e8f3b2702 | |||
| 8bcb3668c0 | |||
| b8b2bd090f | |||
| 14f08a8fef | |||
| d3ac7187bb | |||
| 2f8feead71 | |||
| ee6314f521 | |||
| caa0de84ce | |||
| 97a64401e8 | |||
| 3833fdb449 | |||
| 9c3607aa3d | |||
| 3fe2761552 | |||
| 8763f35668 | |||
| 1198aeeb4d | |||
| 6308677438 | |||
| e0ace14029 | |||
| 9ea98e13ad | |||
| a6edd3ca29 | |||
| 8132b34bbf | |||
| 486078a7d1 | |||
| 7486ebe3c4 | |||
| b2794b9d11 | |||
| f98f45225c | |||
| 2c4db0c54b | |||
| d3ead42f8e | |||
| 71a6c93551 | |||
| 737c02d6e8 | |||
| f2d2c8ed5f | |||
| 0ab302775c | |||
| fd5c26cc52 | |||
| 19b6a5bfc5 | |||
| 45df40f580 | |||
| a1978c73b8 | |||
| 6e76e7d430 | |||
| 9b34a553ed | |||
| c9323cc7fd | |||
| 2bca40901f | |||
| b81d9a0ed8 | |||
| 79d619f82b | |||
| c684472f6e | |||
| 7f4cab1e2e | |||
| c4e5553785 | |||
| 000ecbdc25 | |||
| f1467a9a96 | |||
| 941f54d615 | |||
| 77465c83dd | |||
| d861a9a502 | |||
| c05cc7c9be | |||
| ac3f43a76f | |||
| 84627b3fae | |||
| de4fdc86e0 | |||
| c4921bbf20 | |||
| e2d46bdae2 | |||
| ba3edc00e8 | |||
| 965c5f565f | |||
| 4a21d3f4f9 | |||
| 135cf2e572 | |||
| 65c2c9c461 | |||
| 0e211ebf85 | |||
| 359a252f24 | |||
| 970ffbaca8 | |||
| b82d304d7f | |||
| 17e2640248 | |||
| 9cf0a7e11e | |||
| 12c2ada750 | |||
| ff3d94635a | |||
| 8f38b91dc1 | |||
| 4eb292f40f | |||
| dcd042b9d5 | |||
| 04e0d5650f | |||
| b93413f9a3 | |||
| 416dc4594d | |||
| c19dbe09bb | |||
| 425392456c | |||
| 29836bd98a | |||
| f73f82030f | |||
| 600034b6fa | |||
| ac107d6704 | |||
| cb52aa0065 | |||
| db52a94d8c | |||
| 076b74e867 | |||
| f6b60894f6 | |||
| 24385c4334 | |||
| 22960c9bd6 | |||
| 930880b339 | |||
| a434c0af68 | |||
| c95f1f86e9 | |||
| 68a4475ec7 | |||
| da4985d4de | |||
| 8b44ce12fa | |||
| 6084c9b478 | |||
| a24eb45ae4 | |||
| a60ae614d9 | |||
| 46b1269730 | |||
| ee52e00c83 | |||
| 5368d76218 | |||
| 35855a1c02 | |||
| c1083f6aab | |||
| 3d745cbe6e | |||
| 0185b5c1ba | |||
| 25ff01ed79 | |||
| 6aaee4ce48 | |||
| ae15c9c816 | |||
| 09679571d7 | |||
| 34327136d9 | |||
| 290e77e696 | |||
| edbd95418c | |||
| 27e01483b1 | |||
| 9b56d92922 | |||
| 7318a818c4 | |||
| e06d530528 | |||
| 057b4296d7 | |||
| f00bb77851 | |||
| cb19ce2d0a | |||
| 8d390e1d6d | |||
| 2e55ceba0c | |||
| 95dddf1992 | |||
| f56631708e | |||
| 5e4ec63c32 | |||
| c3ec66d3a2 | |||
| f48b71b390 | |||
| 39683c704e | |||
| 0a4d81f7e2 | |||
| de2d0b2ca4 | |||
| 8da11b4f08 | |||
| 2a4b636a53 | |||
| c5328c4e3d | |||
| 4f67fd8e94 | |||
| 9731250d27 | |||
| 7354e354db | |||
| 171715f40c | |||
| 6ea15553de | |||
| 8b82ac5e51 | |||
| 47341ce927 | |||
| c83a7d0058 | |||
| 734574616d | |||
| cb98ee783b | |||
| 3a95aea82f | |||
| 79b8253b21 | |||
| 1eeddaf502 | |||
| aa9a7123b2 | |||
| 6f914e9e17 | |||
| fd7cc30470 | |||
| 5059a3d01b | |||
| bfc6c3dadc | |||
| 63f2c5f798 | |||
| cc75ba1bc7 | |||
| 1eb2407543 | |||
| 006eb5e191 | |||
| 9a9a06fe7b | |||
| 23dc83fb6a | |||
| d66f5e4c17 | |||
| 6df7cdf331 | |||
| 7a0c79d11e | |||
| 4e3193bfc8 | |||
| 5dfc948fd3 | |||
| 80ac1928c1 | |||
| 441b189fad | |||
| dc188c54e3 | |||
| 3597f7f812 | |||
| 2abf75b669 | |||
| f2aa6d6e5c | |||
| 32d36f3687 | |||
| 8169294c80 | |||
| 7aaf6d1771 | |||
| 34312bb988 | |||
| 94b69fd328 | |||
| fd8cfb7031 | |||
| fc93858918 | |||
| c0ce7e74bd | |||
| 13c6594e0c | |||
| bf4c90b121 | |||
| 9f755aeed7 | |||
| e707e338ef | |||
| 7bba86d963 | |||
| 577efb76a4 | |||
| 1c96b62eb6 | |||
| 72ee377a35 | |||
| 88549bf156 | |||
| ce0e02585c | |||
| 99e4773e45 | |||
| 159053841a | |||
| 5d55e480c9 | |||
| 367f2a09d7 | |||
| a3f3fbd401 | |||
| d3867d29bd | |||
| c04cff510b | |||
| d7017789f6 | |||
| 3c51870486 | |||
| f3a9a9a46d | |||
| 741d389da4 | |||
| b8058ad345 | |||
| 0672b87ec7 | |||
| ab7273106f | |||
| b3cec67313 | |||
| 36860cc848 | |||
| 0c467d5bfe | |||
| aacec74aba | |||
| 8cb4b455cf | |||
| 5fa6b529ca | |||
| c352c331ad | |||
| c190066db2 | |||
| 06e80ad541 | |||
| 61b57cd992 | |||
| a015d810ea | |||
| 3cb576d358 | |||
| f761383fc4 | |||
| b0c1bcc028 | |||
| 1f025debd7 | |||
| c67a1108cd | |||
| 3bd08d00f3 | |||
| 38d1973a93 | |||
| 6bbc0805de | |||
| e157d45d39 | |||
| 7cec3bd6e4 | |||
| a71a8c3493 | |||
| f1da3c4147 | |||
| 42471026b3 | |||
| 6749ca39b8 | |||
| 6f36410e87 | |||
| 3615018816 | |||
| 030b839aa6 | |||
| b3f991e598 | |||
| 4f839e0866 | |||
| 05efb4eb72 | |||
| 2563e81f7a | |||
| 5c2cf07e20 | |||
| 7f965b0829 | |||
| 5c8fd41c6b | |||
| b6b2a25dbe | |||
| 9a2ed38440 | |||
| 4be31553ad | |||
| 7661ea35ee | |||
| 748397f1f0 | |||
| c65cccb25c | |||
| 2da5c4194c | |||
| 8a6ad81027 | |||
| 51f783cea4 | |||
| 74073178bf | |||
| f0c3b743d4 | |||
| 74a75d4adb | |||
| 5628433718 | |||
| 919043cad9 | |||
| 0b94eda458 | |||
| 57a25eb9dc | |||
| 965ccee8b7 | |||
| 5fd401c2c0 | |||
| 161f4100b3 | |||
| 875cd45c7b | |||
| 0ac2337ff8 | |||
| 838270b4b0 | |||
| 3fab58955c | |||
| dbf0457d79 | |||
| 5dff2db5df | |||
| d5102b5e54 | |||
| 7f07edcdf7 | |||
| b33c4caf67 | |||
| 681a76df50 | |||
| e00c82655a | |||
| 732d309888 | |||
| 3b75765d92 | |||
| b0169e73d2 | |||
| 71d6da0eee | |||
| 1b0b256ce8 | |||
| dcc67fbdb6 | |||
| 2619210f8c | |||
| 83790bec70 | |||
| 23e97306dd | |||
| 47103ba3d0 | |||
| 8061d8726a | |||
| 1896fa6151 | |||
| 4eaf01e6cc | |||
| 3c9cd73bf0 | |||
| 98f0daceaa | |||
| 367c62bd39 | |||
| 04c9f75a90 | |||
| dd18526ddf | |||
| 85d92db738 | |||
| b68ad65abc | |||
| a0ade8acc9 | |||
| 676ba822af | |||
| 1e385ceb9a | |||
| 9f2f9d74eb | |||
| 68f5c4fb45 | |||
| a08d793320 | |||
| d8fc799586 | |||
| f3b0e46801 | |||
| d5e1e06d84 | |||
| 119245a5fa | |||
| ff5810c89a | |||
| 2aa03a87a6 | |||
| d6fbfeaf29 | |||
| 88f07e3ced | |||
| c301f3963a | |||
| 0a67f9e92a | |||
| c9579b9d82 | |||
| f39631bd23 | |||
| f963b4de7f | |||
| 6be3cab470 | |||
| b619a12ae3 | |||
| 58c9e820ed | |||
| 33eb739824 | |||
| 81cef51479 | |||
| 839616a4e4 | |||
| e71e09c2e8 | |||
| 25a80a86a5 | |||
| 59c720d7d8 | |||
| 7e67770617 | |||
| c4e244a82b | |||
| 29a20a7e58 | |||
| a738308a50 | |||
| 5081ee2ea6 | |||
| 29d2a5f3e5 | |||
| c39a835dc2 | |||
| 4ba7c7c5a3 | |||
| 08eedbe121 | |||
| 4a32f0dfba | |||
| f101ea34ce | |||
| 9f036410d2 | |||
| 86834fca60 | |||
| 3b13999b03 | |||
| cc78b4196f | |||
| b8b610347f | |||
| 24a99deb52 | |||
| 0f9a69ba17 | |||
| 71fcd174d7 | |||
| 399037d49b | |||
| 240c9a5a37 | |||
| 4a3362e8f1 | |||
| 7331345348 | |||
| 73e8f7c314 | |||
| aced4a3cc7 | |||
| 69deb5b5a2 | |||
| 3a76439a65 | |||
| fa7b04adee | |||
| a708050c5b | |||
| 645b98cd50 | |||
| 19103e9b2c | |||
| dce29954e1 | |||
| f061a35472 | |||
| 800d037035 | |||
| e35b368d50 | |||
| aa21657875 | |||
| f314b64e40 | |||
| 6c9d23488b | |||
| 41cb2c4d27 | |||
| 969cab81e7 | |||
| b98dd272c0 | |||
| 67b506c5f8 | |||
| e9af39a20b | |||
| 5950c66567 | |||
| 56c9934145 | |||
| 1d5b904c64 | |||
| 7d9f382333 | |||
| c60b4389a0 | |||
| 6b80df1d27 | |||
| b60f555553 | |||
| 2127863465 | |||
| d47a86d9b5 | |||
| a03444f4d0 | |||
| 86b8eaee12 | |||
| d8b8d2c047 | |||
| 9dc9634299 | |||
| e13e2dd006 | |||
| 4c7bb196dd | |||
| a2f2fbc82b | |||
| 80d765b61c | |||
| b478c1ea98 | |||
| 7e5dbbc811 | |||
| 2eec8be4ce | |||
| 0c10efec00 | |||
| 2cbe2e80e7 | |||
| e7029a7b64 | |||
| 1e0770057e | |||
| 810483ec74 | |||
| a0b2197d8f | |||
| a5a20eebbd | |||
| 6e8da9f6d3 | |||
| c74b84d070 | |||
| e9203f20b3 | |||
| 4a38511218 | |||
| 3517435956 | |||
| 8bb38d1a90 | |||
| 111d594c6b | |||
| 6c2a557135 | |||
| 46fbc56604 | |||
| 0beec60b7f | |||
| 1de67105d9 | |||
| eacadcebba | |||
| 9f8dff8c5d | |||
| b400ef0647 | |||
| 025714bd97 | |||
| 5e131a0a6c | |||
| 9677135c28 | |||
| a0268a9dfa | |||
| 28498d7d92 | |||
| 2f3655045c | |||
| 459c5f8ff0 | |||
| e335a2b936 | |||
| 051a453e7b | |||
| df903551c0 | |||
| 99a23c2eef | |||
| fa1994c8b2 | |||
| f5fcc2e62b | |||
| cb27bf8afa | |||
| 51fc8d7f85 | |||
| 13d85c0f90 | |||
| 15fcb2ed62 | |||
| e6174e82d2 | |||
| f8c2d0096d | |||
| db734dfa7d | |||
| e40f1d44a5 | |||
| e7188c5985 | |||
| 8738af72f0 | |||
| b02f726a32 | |||
| 4868cf0cc6 | |||
| fa77fe228a | |||
| e65e062652 | |||
| 27513bd94e | |||
| d39bf65880 | |||
| 8e32668305 | |||
| ce3923d336 | |||
| 38bce6a9a0 | |||
| 0c88fbaec8 | |||
| 5c1b765616 | |||
| 79d32127f4 | |||
| e31f533bbb | |||
| 89515e4420 | |||
| 35e57fa1a6 | |||
| 7789fc7480 | |||
| 0d12624cbb | |||
| 837dd64c2c | |||
| d3ab62dce6 | |||
| 4fb1a50f03 | |||
| 2b59ea1906 | |||
| 419e36ffd1 | |||
| 37afaf7035 | |||
| b552dc116f | |||
| 128ebfab14 | |||
| 9ebd876f27 | |||
| 03fe3cdee5 | |||
| e042830e17 | |||
| 4711fa696a | |||
| f4078cce33 | |||
| 9d92a97fe2 | |||
| c4c50467dd | |||
| 792cb0d4d6 | |||
| 981d263331 | |||
| 15c5703a3b | |||
| a6a1baf41b | |||
| 38d44b8c2f | |||
| 56bd6e07fc | |||
| 6125e385df | |||
| 532860245d | |||
| 68a9b1de7e | |||
| 97e2e8d79a | |||
| be3a59c74c | |||
| 17a63733fe | |||
| 3f5c08bb5a | |||
| 6f30103cd9 | |||
| 2d347074a6 | |||
| bd8d2ee0b8 | |||
| c1baab8c9c | |||
| 4e5eac4d5b | |||
| cc6d7c7aa9 | |||
| 4822996da1 | |||
| 546dbe4f8c | |||
| 013011ef09 | |||
| 344b662619 | |||
| 93f8b971e7 | |||
| b135e27fd9 | |||
| 38f3a9c9df | |||
| 80d019798d | |||
| 1727d0739e | |||
| 7ab948ce3f | |||
| 8c29cb4450 | |||
| af4fa8ed2a | |||
| ab5a118947 | |||
| c8dec7b305 | |||
| c6d0a1a788 | |||
| 3239bcefe3 | |||
| 2790664866 | |||
| bad6872f69 | |||
| d9e888e8a9 | |||
| 5dafd6f815 | |||
| addaa3b2b3 | |||
| 2551b3dc27 | |||
| aedf76e3ae | |||
| 625fbb1aa9 | |||
| c75ca89775 | |||
| 763524555b | |||
| a4f0c010d1 | |||
| 1d6a445d33 | |||
| 7defcff9b1 | |||
| 5944cdc5df | |||
| 5e6a654170 | |||
| 450ba6b0fd | |||
| c87c57661f | |||
| 4699b583f0 | |||
| 9ff1614a0f | |||
| 58ca7fa303 | |||
| 1f1ed20a7e | |||
| 8b3da70d92 | |||
| f2f6f2761c | |||
| ecdf533188 | |||
| 0a4f650869 | |||
| 32f4a457bb | |||
| 0116481022 | |||
| dfb0febbe7 | |||
| b87eb5e90e | |||
| 666294834a | |||
| 3870e8c156 | |||
| 86d83b887a | |||
| 4cc65d0d77 | |||
| 095810671d | |||
| 5fb00c08c2 | |||
| 88e5a0eabb | |||
| 698693586c | |||
| f1cc80eb28 | |||
| 11d94564de | |||
| a21e2f6a7c | |||
| 971b0cd022 | |||
| d60fe82b4a | |||
| 0c57e14f05 | |||
| fa3c784722 | |||
| a4878914e2 | |||
| db20a4eeac | |||
| 10668a019b | |||
| 313f9fb105 | |||
| e7dacc9c10 | |||
| 9173e8270a | |||
| ab134a8927 | |||
| 71471f0718 | |||
| 87ee80fc8b | |||
| 29d55ec890 | |||
| 9eedc19b11 | |||
| 675df18a7d | |||
| 382cfdbc65 | |||
| ddfc5d9334 | |||
| 5edbff4f39 | |||
| 86aefa5e54 | |||
| 1e647c8e78 | |||
| 4f1a1f3aa9 | |||
| cbfacffff7 | |||
| ac48ddfbce | |||
| 489a814f54 | |||
| 43950d4f71 | |||
| 76178eddc6 | |||
| cec457fc37 | |||
| 76ab43743d | |||
| 9ffeecc584 | |||
| 0e212539e9 | |||
| 6407f1101a | |||
| 03ac2f8b42 | |||
| 35c585b3f4 | |||
| 04c4d202b2 | |||
| 6269325a44 | |||
| bd308dead7 | |||
| 920113b49a | |||
| 7f8253b470 | |||
| 17afd700d8 | |||
| 7e23135824 | |||
| 8314676918 | |||
| c2b436ecfe | |||
| a897ae4d3e | |||
| 9853804fd8 | |||
| 42de0b3ae7 | |||
| 1eed3ca948 | |||
| 8be2b62601 | |||
| a201f88906 | |||
| a292c6c776 | |||
| b4714a17c8 | |||
| eff7e3800d | |||
| bdb36be4e4 | |||
| 3c9f63b0a6 | |||
| 61d569bb7d | |||
| 376ac564b8 | |||
| 785449fad6 | |||
| c9e2026526 | |||
| ced119f311 | |||
| 7b8088f3d4 | |||
| 762fe5b55b | |||
| fd781f4a63 | |||
| dd919513c1 |
@@ -1,22 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
*.csproj merge=union
|
||||
*.vbproj merge=union
|
||||
*.fsproj merge=union
|
||||
*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
@@ -1,165 +1,54 @@
|
||||
#################
|
||||
## Eclipse
|
||||
#################
|
||||
|
||||
*.pydevproject
|
||||
.project
|
||||
.metadata
|
||||
bin/
|
||||
tmp/
|
||||
gen/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.classpath
|
||||
.settings/
|
||||
.loadpath
|
||||
proguard/
|
||||
# Android Studio
|
||||
*.jks
|
||||
.DS_Store
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/build
|
||||
|
||||
# External tool builders
|
||||
.externalToolBuilders/
|
||||
|
||||
# Locally stored "Eclipse launch configurations"
|
||||
*.launch
|
||||
|
||||
# CDT-specific
|
||||
.cproject
|
||||
|
||||
# PDT-specific
|
||||
.buildpath
|
||||
|
||||
|
||||
#################
|
||||
## Visual Studio
|
||||
#################
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
*_i.c
|
||||
*_p.c
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.vspscc
|
||||
.builds
|
||||
*.dotCover
|
||||
|
||||
## TODO: If you have NuGet Package Restore enabled, uncomment this
|
||||
#packages/
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish
|
||||
|
||||
# Others
|
||||
[Bb]in
|
||||
[Oo]bj
|
||||
sql
|
||||
TestResults
|
||||
*.Cache
|
||||
ClientBin
|
||||
stylecop.*
|
||||
~$*
|
||||
*.dbmdl
|
||||
Generated_Code #added for RIA/Silverlight projects
|
||||
|
||||
# Backup & report files from converting an old project file to a newer
|
||||
# Visual Studio version. Backup files are not needed, because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
|
||||
|
||||
|
||||
############
|
||||
## Windows
|
||||
############
|
||||
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
gradle.properties
|
||||
.directory
|
||||
|
||||
# Intellij project files
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
#############
|
||||
## Python
|
||||
#############
|
||||
|
||||
*.py[co]
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
# Gradle
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
.gradle
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
|
||||
#Translations
|
||||
*.mo
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
# Mac crap
|
||||
# https://gist.github.com/AltNico/c581f370b3f88715876b
|
||||
*.apk
|
||||
*.ap_
|
||||
*.dex
|
||||
*.class
|
||||
build.xml
|
||||
.DS_Store
|
||||
gen/
|
||||
.gradle/
|
||||
proguard/
|
||||
out
|
||||
.settings/
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Source:
|
||||
# https://raw.githubusercontent.com/github/gitignore/master/Android.gitignore
|
||||
# https://gitlab.com/fdroid/fdroidclient/raw/master/.gitignore
|
||||
|
||||
*.iml
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
language: android
|
||||
sudo: false
|
||||
android:
|
||||
components:
|
||||
- build-tools-19.1.0
|
||||
- android-19
|
||||
- tools
|
||||
- build-tools-23.0.3
|
||||
- build-tools-22.0.1
|
||||
- android-23
|
||||
- android-22
|
||||
- extra-android-support
|
||||
- extra-android-m2repository
|
||||
licenses:
|
||||
- 'android-sdk-license-.+'
|
||||
- '.*intel.+'
|
||||
before_install:
|
||||
- chmod +x gradlew
|
||||
- git submodule update --init --recursive
|
||||
install:
|
||||
- ./setup-ant.sh
|
||||
- ./gradlew
|
||||
script:
|
||||
- ant debug && lint -w --exitcode --disable MissingTranslation .
|
||||
- ./gradlew assembleDebug --stacktrace
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2014 A.C.R. Development
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="acr.browser.lightning"
|
||||
android:versionCode="66"
|
||||
android:versionName="3.2.0.1a" >
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS" />
|
||||
<uses-permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="14"
|
||||
android:targetSdkVersion="20" />
|
||||
|
||||
<application
|
||||
android:name="acr.browser.lightning.BrowserApp"
|
||||
android:allowBackup="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name" >
|
||||
<activity
|
||||
android:name="acr.browser.lightning.MainActivity"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/LightTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.APP_BROWSER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="about" />
|
||||
<data android:scheme="javascript" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
For these schemes where any of these particular MIME types
|
||||
have been supplied, we are a good candidate.
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="inline" />
|
||||
<data android:mimeType="text/html" />
|
||||
<data android:mimeType="text/plain" />
|
||||
<data android:mimeType="application/xhtml+xml" />
|
||||
<data android:mimeType="application/vnd.wap.xhtml+xml" />
|
||||
</intent-filter>
|
||||
<!-- For viewing saved web archives. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="file" />
|
||||
<data android:mimeType="application/x-webarchive-xml" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.WEB_SEARCH" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.SettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SETTINGS" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.GeneralSettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings_general"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GENERAL_SETTINGS" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.DisplaySettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings_display"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DISPLAY_SETTINGS" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.PrivacySettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings_privacy"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PRIVACY_SETTINGS" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.AdvancedSettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings_advanced"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ADVANCED_SETTINGS" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.AboutSettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings_about"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ABOUT_SETTINGS" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.IncognitoActivity"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/DarkTheme"
|
||||
android:windowSoftInputMode="stateHidden" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.INCOGNITO" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="acr.browser.lightning.LicenseActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/licenses"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LICENSE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="acr.browser.lightning.BookmarkActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/bookmark_settings"
|
||||
android:theme="@style/DefaultTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOKMARK" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id="Lightning-Browser" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="java-gradle" name="Java-Gradle">
|
||||
<configuration>
|
||||
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
||||
<option name="BUILDABLE" value="false" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -4,10 +4,17 @@
|
||||
####Download
|
||||
* [Download APK from here](https://github.com/anthonycr/Lightning-Browser/releases)
|
||||
|
||||
* [Download from Google Play](https://play.google.com/store/apps/details?id=acr.browser.barebones)
|
||||
* [Download from F-Droid](https://f-droid.org/repository/browse/?fdfilter=lightning&fdid=acr.browser.lightning)
|
||||
|
||||
####Master Branch: [](https://travis-ci.org/anthonycr/Lightning-Browser)
|
||||
####Dev Branch: [](https://travis-ci.org/anthonycr/Lightning-Browser)
|
||||
* [Download Free from Google Play](https://play.google.com/store/apps/details?id=acr.browser.barebones)
|
||||
|
||||
* [Download Paid from Google Play](https://play.google.com/store/apps/details?id=acr.browser.lightning)
|
||||
|
||||
####Master Branch
|
||||
* [](https://travis-ci.org/anthonycr/Lightning-Browser)
|
||||
|
||||
####Dev Branch
|
||||
* [](https://travis-ci.org/anthonycr/Lightning-Browser)
|
||||
|
||||
####Features
|
||||
* Bookmarks
|
||||
@@ -18,15 +25,13 @@
|
||||
|
||||
* Incognito mode
|
||||
|
||||
* Flash support (prior to 4.4)
|
||||
|
||||
* Follows Google design guidelines
|
||||
|
||||
* Unique utilization of navigation drawer for tabs
|
||||
|
||||
* Google search suggestions
|
||||
|
||||
* Orbot Proxy support
|
||||
* Orbot Proxy support and I2P support
|
||||
|
||||
####Permissions
|
||||
|
||||
@@ -38,16 +43,23 @@
|
||||
|
||||
* ````ACCESS_FINE_LOCATION````: For sites like Google Maps, it is disabled by default in settings and displays a pop-up asking if a site may use your location when it is enabled
|
||||
|
||||
* ````READ_HISTORY_BOOKMARKS````: To synchronize history and bookmarks between the stock browser and Lightning
|
||||
|
||||
* ````WRITE_HISTORY_BOOKMARKS````: To synchronize history and bookmarks between the stock browser and Lightning
|
||||
|
||||
* ````ACCESS_NETWORK_STATE````: Required for the WebView to function by some OEM versions of WebKit
|
||||
|
||||
####The Code
|
||||
* Please contribute code back if you can. The code isn't perfect.
|
||||
* Please add translations/translation fixes as you see need
|
||||
|
||||
####Contributing
|
||||
* [The Trello Board](https://trello.com/b/Gwjx8MC3/lightning-browser)
|
||||
* Contributions are always welcome
|
||||
* If you want a feature and can code, feel free to fork and add the change yourself and make a pull request
|
||||
* PLEASE use the ````dev```` branch when contributing as the ````master```` branch is supposed to be for stable builds. I will not reject your pull request if you make it on master, but it will annoy me and make my life harder.
|
||||
* Code Style
|
||||
* Hungarian Notation
|
||||
* Prefix member variables with 'm'
|
||||
* Prefix static member variables with 's'
|
||||
* Use 4 spaces instead of a tab (\t)
|
||||
|
||||
####Setting Up the Project
|
||||
Due to the inclusion of the netcipher library for Orbot proxy support, importing the project will show you some errors. To fix this, first run the following git command in your project folder (NOTE: You need the git command installed to use this):
|
||||
````
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/build
|
||||
*.apk
|
||||
manifest-merger-release-report.txt
|
||||
@@ -0,0 +1,107 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.3"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 23
|
||||
versionName "4.3.3"
|
||||
generatedDensities = []
|
||||
}
|
||||
|
||||
aaptOptions {
|
||||
additionalParameters "--no-version-vectors"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
lightningPlus.setRoot('src/LightningPlus')
|
||||
lightningLite.setRoot('src/LightningLite')
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
proguardFiles 'proguard-project.txt'
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles 'proguard-project.txt'
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
lightningPlus {
|
||||
buildConfigField "boolean", "FULL_VERSION", "true"
|
||||
applicationId "acr.browser.lightning"
|
||||
versionCode 88
|
||||
}
|
||||
|
||||
lightningLite {
|
||||
buildConfigField "boolean", "FULL_VERSION", "false"
|
||||
applicationId "acr.browser.barebones"
|
||||
versionCode 90
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '.readme'
|
||||
}
|
||||
}
|
||||
|
||||
dexcount {
|
||||
includeClasses = false
|
||||
includeFieldCount = false
|
||||
printAsTree = true
|
||||
orderByMethodCount = true
|
||||
verbose = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// support libraries
|
||||
compile 'com.android.support:palette-v7:23.4.0'
|
||||
compile 'com.android.support:appcompat-v7:23.4.0'
|
||||
compile 'com.android.support:design:23.4.0'
|
||||
compile 'com.android.support:recyclerview-v7:23.4.0'
|
||||
compile 'com.android.support:support-v4:23.4.0'
|
||||
|
||||
// html parsing for reading mode
|
||||
compile 'org.jsoup:jsoup:1.9.2'
|
||||
|
||||
// event bus
|
||||
compile 'com.squareup:otto:1.3.8'
|
||||
|
||||
// dependency injection
|
||||
compile 'com.google.dagger:dagger:2.0.2'
|
||||
apt 'com.google.dagger:dagger-compiler:2.0.2'
|
||||
provided 'javax.annotation:jsr250-api:1.0'
|
||||
|
||||
// view binding
|
||||
compile 'com.jakewharton:butterknife:7.0.1'
|
||||
|
||||
// permissions
|
||||
compile 'com.anthonycr.grant:permissions:1.1.2'
|
||||
|
||||
// proxy support
|
||||
compile 'net.i2p.android:client:0.8'
|
||||
|
||||
// Use the following code to update the libnetcipher submodule
|
||||
// git submodule foreach git reset --hard
|
||||
// git submodule update --remote
|
||||
compile project(':libnetcipher')
|
||||
|
||||
// memory leak analysis
|
||||
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
|
||||
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
|
||||
}
|
||||
@@ -34,12 +34,44 @@
|
||||
-keep public class * extends android.app.backup.BackupAgentHelper
|
||||
-keep public class * extends android.preference.Preference
|
||||
-keep public class com.android.vending.licensing.ILicensingService
|
||||
-keep public class acr.browser.lightning.reading.*
|
||||
-keep class org.lucasr.twowayview.** { *; }
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class ** {
|
||||
@com.squareup.otto.Subscribe public *;
|
||||
@com.squareup.otto.Produce public *;
|
||||
}
|
||||
|
||||
-assumenosideeffects class android.util.Log {
|
||||
public static *** d(...);
|
||||
public static *** v(...);
|
||||
public static *** w(...);
|
||||
public static *** i(...);
|
||||
}
|
||||
|
||||
-keep class butterknife.** { *; }
|
||||
-dontwarn butterknife.internal.**
|
||||
-keep class **$$ViewBinder { *; }
|
||||
|
||||
-keepclasseswithmembernames class * {
|
||||
@butterknife.* <fields>;
|
||||
}
|
||||
|
||||
-keepclasseswithmembernames class * {
|
||||
@butterknife.* <methods>;
|
||||
}
|
||||
|
||||
# this will fix a force close in ReadingActivity
|
||||
-keep public class org.jsoup.** {
|
||||
public *;
|
||||
}
|
||||
|
||||
# Without this rule, openFileChooser does not get called on KitKat
|
||||
-keep class acr.browser.lightning.LightningView$LightningChromeClient {
|
||||
void openFileChooser(android.webkit.ValueCallback);
|
||||
void openFileChooser(android.webkit.ValueCallback, java.lang.String);
|
||||
void openFileChooser(android.webkit.ValueCallback, java.lang.String, java.lang.String);
|
||||
-keep class acr.browser.lightning.view.LightningView$LightningChromeClient {
|
||||
void openFileChooser(android.webkit.ValueCallback);
|
||||
void openFileChooser(android.webkit.ValueCallback, java.lang.String);
|
||||
void openFileChooser(android.webkit.ValueCallback, java.lang.String, java.lang.String);
|
||||
}
|
||||
|
||||
-keepclasseswithmembernames class * {
|
||||
@@ -66,3 +98,16 @@
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
|
||||
# The support library contains references to newer platform versions.
|
||||
# Don't warn about those in case this app is linking against an older
|
||||
# platform version. We know about them, and they are safe.
|
||||
-dontwarn android.support.**
|
||||
|
||||
# The I2P Java API bundled inside the I2P Android client library contains
|
||||
# references to javax.naming classes that Android doesn't have. But those
|
||||
# classes are never used on Android, and it is safe to ignore the warnings.
|
||||
-dontwarn net.i2p.crypto.CertUtil
|
||||
-dontwarn org.apache.http.conn.ssl.DefaultHostnameVerifier
|
||||
|
||||
-dontwarn org.apache.http.HttpHost
|
||||
@@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright 2014 A.C.R. Development -->
|
||||
<manifest package="acr.browser.lightning"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
|
||||
<uses-permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"/>
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.location"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false"/>
|
||||
|
||||
<application
|
||||
android:name=".app.BrowserApp"
|
||||
android:allowBackup="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name">
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.LightTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.APP_BROWSER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="file"/>
|
||||
<data android:mimeType="text/html"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="application/xhtml+xml"/>
|
||||
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="about"/>
|
||||
<data android:scheme="javascript"/>
|
||||
</intent-filter>
|
||||
<!--
|
||||
For these schemes where any of these particular MIME types
|
||||
have been supplied, we are a good candidate.
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="inline"/>
|
||||
<data android:mimeType="text/html"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="application/xhtml+xml"/>
|
||||
<data android:mimeType="application/vnd.wap.xhtml+xml"/>
|
||||
</intent-filter>
|
||||
<!-- For viewing saved web archives. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:scheme="file"/>
|
||||
<data android:mimeType="application/x-webarchive-xml"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.WEB_SEARCH"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.SettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/settings"
|
||||
android:theme="@style/Theme.SettingsTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SETTINGS"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.IncognitoActivity"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.DarkTheme"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.INCOGNITO"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.ReadingActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/reading_mode"
|
||||
android:theme="@style/Theme.SettingsTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.READING"/>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
Depois Largura: | Altura: | Tamanho: 89 KiB |
|
Antes Largura: | Altura: | Tamanho: 3.4 KiB Depois Largura: | Altura: | Tamanho: 3.4 KiB |
|
Antes Largura: | Altura: | Tamanho: 12 KiB Depois Largura: | Altura: | Tamanho: 12 KiB |
|
Antes Largura: | Altura: | Tamanho: 20 KiB Depois Largura: | Altura: | Tamanho: 20 KiB |
|
Depois Largura: | Altura: | Tamanho: 23 KiB |
|
Antes Largura: | Altura: | Tamanho: 21 KiB Depois Largura: | Altura: | Tamanho: 21 KiB |
|
Antes Largura: | Altura: | Tamanho: 14 KiB Depois Largura: | Altura: | Tamanho: 14 KiB |
|
Antes Largura: | Altura: | Tamanho: 44 KiB Depois Largura: | Altura: | Tamanho: 44 KiB |
|
Antes Largura: | Altura: | Tamanho: 18 KiB Depois Largura: | Altura: | Tamanho: 18 KiB |
@@ -0,0 +1,125 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
|
||||
/**
|
||||
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
|
||||
* to be used with AppCompat.
|
||||
* <p/>
|
||||
* This technique can be used with an {@link android.app.Activity} class, not just
|
||||
* {@link android.preference.PreferenceActivity}.
|
||||
*/
|
||||
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
||||
|
||||
private AppCompatDelegate mDelegate;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
ActionBar getSupportActionBar() {
|
||||
return getDelegate().getSupportActionBar();
|
||||
}
|
||||
|
||||
void setSupportActionBar(@Nullable Toolbar toolbar) {
|
||||
getDelegate().setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(@LayoutRes int layoutResID) {
|
||||
getDelegate().setContentView(layoutResID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view) {
|
||||
getDelegate().setContentView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().setContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().addContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
getDelegate().onPostResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTitleChanged(CharSequence title, int color) {
|
||||
super.onTitleChanged(title, color);
|
||||
getDelegate().setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
getDelegate().onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (isFinishing()) {
|
||||
overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_out_to_right);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
getDelegate().onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
getDelegate().onDestroy();
|
||||
}
|
||||
|
||||
public void invalidateOptionsMenu() {
|
||||
getDelegate().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
if (mDelegate == null) {
|
||||
mDelegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
return mDelegate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.Menu;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.CookieSyncManager;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.react.Action;
|
||||
import acr.browser.lightning.react.Observable;
|
||||
import acr.browser.lightning.react.Subscriber;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class IncognitoActivity extends BrowserActivity {
|
||||
|
||||
@Override
|
||||
public Observable<Void> updateCookiePreference() {
|
||||
return Observable.create(new Action<Void>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Subscriber<Void> subscriber) {
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
CookieSyncManager.createInstance(IncognitoActivity.this);
|
||||
}
|
||||
cookieManager.setAcceptCookie(mPreferences.getIncognitoCookiesEnabled());
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.incognito, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
// handleNewIntent(intent);
|
||||
super.onNewIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// saveOpenTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateHistory(@Nullable String title, @NonNull String url) {
|
||||
// addItemToHistory(title, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIncognito() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeActivity() {
|
||||
closeDrawers(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
closeBrowser();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.Menu;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.CookieSyncManager;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.react.Action;
|
||||
import acr.browser.lightning.react.Observable;
|
||||
import acr.browser.lightning.react.Subscriber;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class MainActivity extends BrowserActivity {
|
||||
|
||||
@Override
|
||||
public Observable<Void> updateCookiePreference() {
|
||||
return Observable.create(new Action<Void>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Subscriber<Void> subscriber) {
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
CookieSyncManager.createInstance(MainActivity.this);
|
||||
}
|
||||
cookieManager.setAcceptCookie(mPreferences.getCookiesEnabled());
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.main, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if (isPanicTrigger(intent)) {
|
||||
panicClean();
|
||||
} else {
|
||||
handleNewIntent(intent);
|
||||
super.onNewIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
saveOpenTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateHistory(@Nullable String title, @NonNull String url) {
|
||||
addItemToHistory(title, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIncognito() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeActivity() {
|
||||
closeDrawers(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
performExitCleanUp();
|
||||
moveTaskToBack(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener;
|
||||
import android.widget.TextView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.react.Action;
|
||||
import acr.browser.lightning.react.Observable;
|
||||
import acr.browser.lightning.react.OnSubscribe;
|
||||
import acr.browser.lightning.react.Subscriber;
|
||||
import acr.browser.lightning.react.Schedulers;
|
||||
import acr.browser.lightning.react.Subscription;
|
||||
import acr.browser.lightning.reading.HtmlFetcher;
|
||||
import acr.browser.lightning.reading.JResult;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
public class ReadingActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = ReadingActivity.class.getSimpleName();
|
||||
|
||||
@Bind(R.id.textViewTitle)
|
||||
TextView mTitle;
|
||||
|
||||
@Bind(R.id.textViewBody)
|
||||
TextView mBody;
|
||||
|
||||
@Inject PreferenceManager mPreferences;
|
||||
|
||||
private boolean mInvert;
|
||||
private String mUrl = null;
|
||||
private int mTextSize;
|
||||
private ProgressDialog mProgressDialog;
|
||||
private Subscription mPageLoaderSubscription;
|
||||
|
||||
private static final float XXLARGE = 30.0f;
|
||||
private static final float XLARGE = 26.0f;
|
||||
private static final float LARGE = 22.0f;
|
||||
private static final float MEDIUM = 18.0f;
|
||||
private static final float SMALL = 14.0f;
|
||||
private static final float XSMALL = 10.0f;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
|
||||
overridePendingTransition(R.anim.slide_in_from_right, R.anim.fade_out_scale);
|
||||
mInvert = mPreferences.getInvertColors();
|
||||
final int color;
|
||||
if (mInvert) {
|
||||
setTheme(R.style.Theme_SettingsTheme_Dark);
|
||||
color = ThemeUtils.getPrimaryColorDark(this);
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(color));
|
||||
} else {
|
||||
setTheme(R.style.Theme_SettingsTheme);
|
||||
color = ThemeUtils.getPrimaryColor(this);
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(color));
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.reading_view);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
mTextSize = mPreferences.getReadingTextSize();
|
||||
mBody.setTextSize(getTextSize(mTextSize));
|
||||
mTitle.setText(getString(R.string.untitled));
|
||||
mBody.setText(getString(R.string.loading));
|
||||
|
||||
mTitle.setVisibility(View.INVISIBLE);
|
||||
mBody.setVisibility(View.INVISIBLE);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (!loadPage(intent)) {
|
||||
setText(getString(R.string.untitled), getString(R.string.loading_failed));
|
||||
}
|
||||
}
|
||||
|
||||
private static float getTextSize(int size) {
|
||||
switch (size) {
|
||||
case 0:
|
||||
return XSMALL;
|
||||
case 1:
|
||||
return SMALL;
|
||||
case 2:
|
||||
return MEDIUM;
|
||||
case 3:
|
||||
return LARGE;
|
||||
case 4:
|
||||
return XLARGE;
|
||||
case 5:
|
||||
return XXLARGE;
|
||||
default:
|
||||
return MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.reading, menu);
|
||||
MenuItem invert = menu.findItem(R.id.invert_item);
|
||||
MenuItem textSize = menu.findItem(R.id.text_size_item);
|
||||
int iconColor = mInvert ? ThemeUtils.getIconDarkThemeColor(this) : ThemeUtils.getIconLightThemeColor(this);
|
||||
if (invert != null && invert.getIcon() != null)
|
||||
invert.getIcon().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
|
||||
if (textSize != null && textSize.getIcon() != null)
|
||||
textSize.getIcon().setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
private boolean loadPage(Intent intent) {
|
||||
if (intent == null) {
|
||||
return false;
|
||||
}
|
||||
mUrl = intent.getStringExtra(Constants.LOAD_READING_URL);
|
||||
if (mUrl == null) {
|
||||
return false;
|
||||
}
|
||||
if (getSupportActionBar() != null)
|
||||
getSupportActionBar().setTitle(Utils.getDomainName(mUrl));
|
||||
mPageLoaderSubscription = loadPage(mUrl).subscribeOn(Schedulers.worker())
|
||||
.observeOn(Schedulers.main())
|
||||
.subscribe(new OnSubscribe<ReaderInfo>() {
|
||||
@Override
|
||||
public void onStart() {
|
||||
mProgressDialog = new ProgressDialog(ReadingActivity.this);
|
||||
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
mProgressDialog.setCancelable(false);
|
||||
mProgressDialog.setIndeterminate(true);
|
||||
mProgressDialog.setMessage(getString(R.string.loading));
|
||||
mProgressDialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(@Nullable ReaderInfo item) {
|
||||
if (item == null || item.getTitle().isEmpty() || item.getBody().isEmpty()) {
|
||||
setText(getString(R.string.untitled), getString(R.string.loading_failed));
|
||||
} else {
|
||||
setText(item.getTitle(), item.getBody());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable throwable) {
|
||||
setText(getString(R.string.untitled), getString(R.string.loading_failed));
|
||||
if (mProgressDialog != null && mProgressDialog.isShowing()) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (mProgressDialog != null && mProgressDialog.isShowing()) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Observable<ReaderInfo> loadPage(@NonNull final String url) {
|
||||
return Observable.create(new Action<ReaderInfo>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Subscriber<ReaderInfo> subscriber) {
|
||||
HtmlFetcher fetcher = new HtmlFetcher();
|
||||
try {
|
||||
JResult result = fetcher.fetchAndExtract(url, 2500, true);
|
||||
subscriber.onNext(new ReaderInfo(result.getTitle(), result.getText()));
|
||||
} catch (Exception e) {
|
||||
subscriber.onError(new Throwable("Encountered exception"));
|
||||
Log.e(TAG, "Error parsing page", e);
|
||||
} catch (OutOfMemoryError e) {
|
||||
System.gc();
|
||||
subscriber.onError(new Throwable("Out of memory"));
|
||||
Log.e(TAG, "Out of memory", e);
|
||||
}
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ReaderInfo {
|
||||
@NonNull private final String mTitleText;
|
||||
@NonNull private final String mBodyText;
|
||||
|
||||
public ReaderInfo(@NonNull String title, @NonNull String body) {
|
||||
mTitleText = title;
|
||||
mBodyText = body;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getTitle() {
|
||||
return mTitleText;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getBody() {
|
||||
return mBodyText;
|
||||
}
|
||||
}
|
||||
|
||||
private void setText(String title, String body) {
|
||||
if (mTitle == null || mBody == null)
|
||||
return;
|
||||
if (mTitle.getVisibility() == View.INVISIBLE) {
|
||||
mTitle.setAlpha(0.0f);
|
||||
mTitle.setVisibility(View.VISIBLE);
|
||||
mTitle.setText(title);
|
||||
ObjectAnimator animator = ObjectAnimator.ofFloat(mTitle, "alpha", 1.0f);
|
||||
animator.setDuration(300);
|
||||
animator.start();
|
||||
} else {
|
||||
mTitle.setText(title);
|
||||
}
|
||||
|
||||
if (mBody.getVisibility() == View.INVISIBLE) {
|
||||
mBody.setAlpha(0.0f);
|
||||
mBody.setVisibility(View.VISIBLE);
|
||||
mBody.setText(body);
|
||||
ObjectAnimator animator = ObjectAnimator.ofFloat(mBody, "alpha", 1.0f);
|
||||
animator.setDuration(300);
|
||||
animator.start();
|
||||
} else {
|
||||
mBody.setText(body);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mPageLoaderSubscription.unsubscribe();
|
||||
|
||||
if (mProgressDialog != null && mProgressDialog.isShowing()) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (isFinishing()) {
|
||||
overridePendingTransition(R.anim.fade_in_scale, R.anim.slide_out_to_right);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.invert_item:
|
||||
mPreferences.setInvertColors(!mInvert);
|
||||
Intent read = new Intent(this, ReadingActivity.class);
|
||||
read.putExtra(Constants.LOAD_READING_URL, mUrl);
|
||||
startActivity(read);
|
||||
finish();
|
||||
break;
|
||||
case R.id.text_size_item:
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
LayoutInflater inflater = this.getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.seek_layout, null);
|
||||
final SeekBar bar = (SeekBar) view.findViewById(R.id.text_size_seekbar);
|
||||
bar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar view, int size, boolean user) {
|
||||
mBody.setTextSize(getTextSize(size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar arg0) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar arg0) {
|
||||
}
|
||||
|
||||
});
|
||||
bar.setMax(5);
|
||||
bar.setProgress(mTextSize);
|
||||
builder.setView(view);
|
||||
builder.setTitle(R.string.size);
|
||||
builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface arg0, int arg1) {
|
||||
mTextSize = bar.getProgress();
|
||||
mBody.setTextSize(getTextSize(mTextSize));
|
||||
mPreferences.setReadingTextSize(bar.getProgress());
|
||||
}
|
||||
|
||||
});
|
||||
builder.show();
|
||||
break;
|
||||
default:
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.anthonycr.grant.PermissionsManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
|
||||
public class SettingsActivity extends ThemableSettingsActivity {
|
||||
|
||||
private static final List<String> mFragments = new ArrayList<>(7);
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// this is a workaround for the Toolbar in PreferenceActitivty
|
||||
ViewGroup root = (ViewGroup) findViewById(android.R.id.content);
|
||||
LinearLayout content = (LinearLayout) root.getChildAt(0);
|
||||
LinearLayout toolbarContainer = (LinearLayout) View.inflate(this, R.layout.toolbar_settings, null);
|
||||
|
||||
root.removeAllViews();
|
||||
toolbarContainer.addView(content);
|
||||
root.addView(toolbarContainer);
|
||||
|
||||
// now we can set the Toolbar using AppCompatPreferenceActivity
|
||||
Toolbar toolbar = (Toolbar) toolbarContainer.findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuildHeaders(List<Header> target) {
|
||||
loadHeadersFromResource(R.xml.preferences_headers, target);
|
||||
mFragments.clear();
|
||||
Iterator<Header> headerIterator = target.iterator();
|
||||
while (headerIterator.hasNext()) {
|
||||
Header header = headerIterator.next();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
// Workaround for bug in the AppCompat support library
|
||||
header.iconRes = R.drawable.empty;
|
||||
}
|
||||
|
||||
if (header.titleRes == R.string.debug_title) {
|
||||
if (BrowserApp.isRelease()) {
|
||||
headerIterator.remove();
|
||||
} else {
|
||||
mFragments.add(header.fragment);
|
||||
}
|
||||
} else {
|
||||
mFragments.add(header.fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isValidFragment(String fragmentName) {
|
||||
return mFragments.contains(fragmentName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
PermissionsManager.getInstance().notifyPermissionsChange(permissions, grantResults);
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.BookmarkPage;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.constant.HistoryPage;
|
||||
import acr.browser.lightning.constant.StartPage;
|
||||
import acr.browser.lightning.database.BookmarkManager;
|
||||
import acr.browser.lightning.database.HistoryDatabase;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.react.Action;
|
||||
import acr.browser.lightning.react.Observable;
|
||||
import acr.browser.lightning.react.OnSubscribe;
|
||||
import acr.browser.lightning.react.Schedulers;
|
||||
import acr.browser.lightning.react.Subscriber;
|
||||
import acr.browser.lightning.utils.FileUtils;
|
||||
import acr.browser.lightning.utils.UrlUtils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
/**
|
||||
* A manager singleton that holds all the {@link LightningView}
|
||||
* and tracks the current tab. It handles creation, deletion,
|
||||
* restoration, state saving, and switching of tabs.
|
||||
*/
|
||||
public class TabsManager {
|
||||
|
||||
private static final String TAG = TabsManager.class.getSimpleName();
|
||||
private static final String BUNDLE_KEY = "WEBVIEW_";
|
||||
private static final String URL_KEY = "URL_KEY";
|
||||
private static final String BUNDLE_STORAGE = "SAVED_TABS.parcel";
|
||||
|
||||
private final List<LightningView> mTabList = new ArrayList<>(1);
|
||||
@Nullable private LightningView mCurrentTab;
|
||||
@Nullable private TabNumberChangedListener mTabNumberListener;
|
||||
|
||||
private boolean mIsInitialized = false;
|
||||
private final List<Runnable> mPostInitializationWorkList = new ArrayList<>();
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
@Inject BookmarkManager mBookmarkManager;
|
||||
@Inject HistoryDatabase mHistoryManager;
|
||||
@Inject Bus mEventBus;
|
||||
@Inject Application mApp;
|
||||
|
||||
public TabsManager() {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
}
|
||||
|
||||
// TODO remove and make presenter call new tab methods so it always knows
|
||||
@Deprecated
|
||||
public interface TabNumberChangedListener {
|
||||
void tabNumberChanged(int newNumber);
|
||||
}
|
||||
|
||||
public void setTabNumberChangedListener(@Nullable TabNumberChangedListener listener) {
|
||||
mTabNumberListener = listener;
|
||||
}
|
||||
|
||||
public void cancelPendingWork() {
|
||||
mPostInitializationWorkList.clear();
|
||||
}
|
||||
|
||||
public void doAfterInitialization(@NonNull Runnable runnable) {
|
||||
if (mIsInitialized) {
|
||||
runnable.run();
|
||||
} else {
|
||||
mPostInitializationWorkList.add(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishInitialization() {
|
||||
mIsInitialized = true;
|
||||
for (Runnable runnable : mPostInitializationWorkList) {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores old tabs that were open before the browser
|
||||
* was closed. Handles the intent used to open the browser.
|
||||
*
|
||||
* @param activity the activity needed to create tabs.
|
||||
* @param intent the intent that started the browser activity.
|
||||
* @param incognito whether or not we are in incognito mode.
|
||||
*/
|
||||
public synchronized Observable<Void> initializeTabs(@NonNull final Activity activity,
|
||||
@Nullable final Intent intent,
|
||||
final boolean incognito) {
|
||||
return Observable.create(new Action<Void>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull final Subscriber<Void> subscriber) {
|
||||
|
||||
// Make sure we start with a clean tab list
|
||||
shutdown();
|
||||
|
||||
// If incognito, only create one tab, do not handle intent
|
||||
// in order to protect user privacy
|
||||
if (incognito) {
|
||||
newTab(activity, null, true);
|
||||
subscriber.onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = null;
|
||||
if (intent != null) {
|
||||
url = intent.getDataString();
|
||||
}
|
||||
Log.d(TAG, "URL from intent: " + url);
|
||||
mCurrentTab = null;
|
||||
if (mPreferenceManager.getRestoreLostTabsEnabled()) {
|
||||
restoreLostTabs(url, activity, subscriber);
|
||||
} else {
|
||||
newTab(activity, null, false);
|
||||
finishInitialization();
|
||||
subscriber.onComplete();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void restoreLostTabs(@Nullable final String url, @NonNull final Activity activity,
|
||||
@NonNull final Subscriber subscriber) {
|
||||
|
||||
restoreState().subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.main()).subscribe(new OnSubscribe<Bundle>() {
|
||||
@Override
|
||||
public void onNext(Bundle item) {
|
||||
LightningView tab = newTab(activity, "", false);
|
||||
String url = item.getString(URL_KEY);
|
||||
if (url != null && tab.getWebView() != null) {
|
||||
if (UrlUtils.isBookmarkUrl(url)) {
|
||||
new BookmarkPage(tab, activity, mBookmarkManager).load();
|
||||
} else if (UrlUtils.isStartPageUrl(url)) {
|
||||
new StartPage(tab, mApp).load();
|
||||
} else if (UrlUtils.isHistoryUrl(url)) {
|
||||
new HistoryPage(tab, mApp, mHistoryManager).load();
|
||||
}
|
||||
} else if (tab.getWebView() != null) {
|
||||
tab.getWebView().restoreState(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (url != null) {
|
||||
if (url.startsWith(Constants.FILE)) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setCancelable(true)
|
||||
.setTitle(R.string.title_warning)
|
||||
.setMessage(R.string.message_blocked_local)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.action_open, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
newTab(activity, url, false);
|
||||
}
|
||||
}).show();
|
||||
} else {
|
||||
newTab(activity, url, false);
|
||||
}
|
||||
}
|
||||
if (mTabList.size() == 0) {
|
||||
newTab(activity, null, false);
|
||||
}
|
||||
finishInitialization();
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used to resume all the tabs in the browser.
|
||||
* This is necessary because we cannot pause the
|
||||
* WebView when the app is open currently due to a
|
||||
* bug in the WebView, where calling onResume doesn't
|
||||
* consistently resume it.
|
||||
*
|
||||
* @param context the context needed to initialize
|
||||
* the LightningView preferences.
|
||||
*/
|
||||
public void resumeAll(@NonNull Context context) {
|
||||
LightningView current = getCurrentTab();
|
||||
if (current != null) {
|
||||
current.resumeTimers();
|
||||
}
|
||||
for (LightningView tab : mTabList) {
|
||||
if (tab != null) {
|
||||
tab.onResume();
|
||||
tab.initializePreferences(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used to pause all the tabs in the browser.
|
||||
* This is necessary because we cannot pause the
|
||||
* WebView when the app is open currently due to a
|
||||
* bug in the WebView, where calling onResume doesn't
|
||||
* consistently resume it.
|
||||
*/
|
||||
public void pauseAll() {
|
||||
LightningView current = getCurrentTab();
|
||||
if (current != null) {
|
||||
current.pauseTimers();
|
||||
}
|
||||
for (LightningView tab : mTabList) {
|
||||
if (tab != null) {
|
||||
tab.onPause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the tab at the given position in tabs list, or
|
||||
* null if position is not in tabs list range.
|
||||
*
|
||||
* @param position the index in tabs list
|
||||
* @return the corespondent {@link LightningView},
|
||||
* or null if the index is invalid
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized LightningView getTabAtPosition(final int position) {
|
||||
if (position < 0 || position >= mTabList.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mTabList.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees memory for each tab in the
|
||||
* manager. Note: this will only work
|
||||
* on API < KITKAT as on KITKAT onward
|
||||
* the WebViews manage their own
|
||||
* memory correctly.
|
||||
*/
|
||||
public synchronized void freeMemory() {
|
||||
for (LightningView tab : mTabList) {
|
||||
//noinspection deprecation
|
||||
tab.freeMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the manager. This destroys
|
||||
* all tabs and clears the references
|
||||
* to those tabs. Current tab is also
|
||||
* released for garbage collection.
|
||||
*/
|
||||
public synchronized void shutdown() {
|
||||
for (LightningView tab : mTabList) {
|
||||
tab.onDestroy();
|
||||
}
|
||||
mTabList.clear();
|
||||
mIsInitialized = false;
|
||||
mCurrentTab = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards network connection status to the WebViews.
|
||||
*
|
||||
* @param isConnected whether there is a network
|
||||
* connection or not.
|
||||
*/
|
||||
public synchronized void notifyConnectionStatus(final boolean isConnected) {
|
||||
for (LightningView tab : mTabList) {
|
||||
final WebView webView = tab.getWebView();
|
||||
if (webView != null) {
|
||||
webView.setNetworkAvailable(isConnected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current number of tabs in the manager.
|
||||
*
|
||||
* @return the number of tabs in the list.
|
||||
*/
|
||||
public synchronized int size() {
|
||||
return mTabList.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* The index of the last tab in the manager.
|
||||
*
|
||||
* @return the last tab in the list or -1 if there are no tabs.
|
||||
*/
|
||||
public synchronized int last() {
|
||||
return mTabList.size() - 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The last tab in the tab manager.
|
||||
*
|
||||
* @return the last tab, or null if
|
||||
* there are no tabs.
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized LightningView lastTab() {
|
||||
if (last() < 0) {
|
||||
return null;
|
||||
}
|
||||
return mTabList.get(last());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new tab. The tab is
|
||||
* automatically added to the tabs list.
|
||||
*
|
||||
* @param activity the activity needed to create the tab.
|
||||
* @param url the URL to initialize the tab with.
|
||||
* @param isIncognito whether the tab is an incognito
|
||||
* tab or not.
|
||||
* @return a valid initialized tab.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized LightningView newTab(@NonNull final Activity activity,
|
||||
@Nullable final String url,
|
||||
final boolean isIncognito) {
|
||||
Log.d(TAG, "New tab");
|
||||
final LightningView tab = new LightningView(activity, url, isIncognito);
|
||||
mTabList.add(tab);
|
||||
if (mTabNumberListener != null) {
|
||||
mTabNumberListener.tabNumberChanged(size());
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tab from the list and destroys the tab.
|
||||
* If the tab removed is the current tab, the reference
|
||||
* to the current tab will be nullified.
|
||||
*
|
||||
* @param position The position of the tab to remove.
|
||||
*/
|
||||
private synchronized void removeTab(final int position) {
|
||||
if (position >= mTabList.size()) {
|
||||
return;
|
||||
}
|
||||
final LightningView tab = mTabList.remove(position);
|
||||
if (mCurrentTab == tab) {
|
||||
mCurrentTab = null;
|
||||
}
|
||||
tab.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tab from the manager. If the tab
|
||||
* being deleted is the current tab, this method
|
||||
* will switch the current tab to a new valid tab.
|
||||
*
|
||||
* @param position the position of the tab to delete.
|
||||
* @return returns true if the current tab
|
||||
* was deleted, false otherwise.
|
||||
*/
|
||||
public synchronized boolean deleteTab(int position) {
|
||||
Log.d(TAG, "Delete tab: " + position);
|
||||
final LightningView currentTab = getCurrentTab();
|
||||
int current = positionOf(currentTab);
|
||||
|
||||
if (current == position) {
|
||||
if (size() == 1) {
|
||||
mCurrentTab = null;
|
||||
} else if (current < size() - 1) {
|
||||
// There is another tab after this one
|
||||
switchToTab(current + 1);
|
||||
} else {
|
||||
switchToTab(current - 1);
|
||||
}
|
||||
}
|
||||
|
||||
removeTab(position);
|
||||
if (mTabNumberListener != null) {
|
||||
mTabNumberListener.tabNumberChanged(size());
|
||||
}
|
||||
return current == position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the position of the given tab.
|
||||
*
|
||||
* @param tab the tab to look for.
|
||||
* @return the position of the tab or -1
|
||||
* if the tab is not in the list.
|
||||
*/
|
||||
public synchronized int positionOf(final LightningView tab) {
|
||||
return mTabList.indexOf(tab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the state of the current WebViews,
|
||||
* to a bundle which is then stored in persistent
|
||||
* storage and can be unparceled.
|
||||
*/
|
||||
public void saveState() {
|
||||
Bundle outState = new Bundle(ClassLoader.getSystemClassLoader());
|
||||
Log.d(Constants.TAG, "Saving tab state");
|
||||
for (int n = 0; n < mTabList.size(); n++) {
|
||||
LightningView tab = mTabList.get(n);
|
||||
Bundle state = new Bundle(ClassLoader.getSystemClassLoader());
|
||||
if (tab.getWebView() != null && !UrlUtils.isSpecialUrl(tab.getUrl())) {
|
||||
tab.getWebView().saveState(state);
|
||||
outState.putBundle(BUNDLE_KEY + n, state);
|
||||
} else if (tab.getWebView() != null) {
|
||||
state.putString(URL_KEY, tab.getUrl());
|
||||
outState.putBundle(BUNDLE_KEY + n, state);
|
||||
}
|
||||
}
|
||||
FileUtils.writeBundleToStorage(mApp, outState, BUNDLE_STORAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to clear the saved
|
||||
* state if you do not wish it to be
|
||||
* restored when the browser next starts.
|
||||
*/
|
||||
public void clearSavedState() {
|
||||
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the previously saved tabs from the
|
||||
* bundle stored in peristent file storage.
|
||||
* It will create new tabs for each tab saved
|
||||
* and will delete the saved instance file when
|
||||
* restoration is complete.
|
||||
*/
|
||||
private Observable<Bundle> restoreState() {
|
||||
return Observable.create(new Action<Bundle>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Subscriber<Bundle> subscriber) {
|
||||
Bundle savedState = FileUtils.readBundleFromStorage(mApp, BUNDLE_STORAGE);
|
||||
if (savedState != null) {
|
||||
Log.d(Constants.TAG, "Restoring previous WebView state now");
|
||||
for (String key : savedState.keySet()) {
|
||||
if (key.startsWith(BUNDLE_KEY)) {
|
||||
subscriber.onNext(savedState.getBundle(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
FileUtils.deleteBundleInStorage(mApp, BUNDLE_STORAGE);
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link WebView} associated to the current tab,
|
||||
* or null if there is no current tab.
|
||||
*
|
||||
* @return a {@link WebView} or null if there is no current tab.
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized WebView getCurrentWebView() {
|
||||
return mCurrentTab != null ? mCurrentTab.getWebView() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the current tab.
|
||||
*
|
||||
* @return Return the index of the current tab, or -1 if the
|
||||
* current tab is null.
|
||||
*/
|
||||
public int indexOfCurrentTab() {
|
||||
return mTabList.indexOf(mCurrentTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the tab.
|
||||
*
|
||||
* @return Return the index of the tab, or -1 if the tab isn't in the list.
|
||||
*/
|
||||
public int indexOfTab(LightningView tab) {
|
||||
return mTabList.indexOf(tab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current {@link LightningView} or null if
|
||||
* no current tab has been set.
|
||||
*
|
||||
* @return a {@link LightningView} or null if there
|
||||
* is no current tab.
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized LightningView getCurrentTab() {
|
||||
return mCurrentTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the current tab to the one at the given position.
|
||||
* It returns the selected tab that has been switced to.
|
||||
*
|
||||
* @return the selected tab or null if position is out of tabs range.
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized LightningView switchToTab(final int position) {
|
||||
Log.d(TAG, "switch to tab: " + position);
|
||||
if (position < 0 || position >= mTabList.size()) {
|
||||
Log.e(TAG, "Returning a null LightningView requested for position: " + position);
|
||||
return null;
|
||||
} else {
|
||||
final LightningView tab = mTabList.get(position);
|
||||
if (tab != null) {
|
||||
mCurrentTab = tab;
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
|
||||
public abstract class ThemableBrowserActivity extends AppCompatActivity {
|
||||
|
||||
@Inject PreferenceManager mPreferences;
|
||||
|
||||
private int mTheme;
|
||||
private boolean mShowTabsInDrawer;
|
||||
private boolean mShouldRunOnResumeActions = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
mTheme = mPreferences.getUseTheme();
|
||||
mShowTabsInDrawer = mPreferences.getShowTabsInDrawer(!isTablet());
|
||||
|
||||
// set the theme
|
||||
if (mTheme == 1) {
|
||||
setTheme(R.style.Theme_DarkTheme);
|
||||
} else if (mTheme == 2) {
|
||||
setTheme(R.style.Theme_BlackTheme);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus && mShouldRunOnResumeActions) {
|
||||
mShouldRunOnResumeActions = false;
|
||||
onWindowVisibleToUserAfterResume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the activity is resumed
|
||||
* and the UI becomes visible to the user.
|
||||
* Called by onWindowFocusChanged only if
|
||||
* onResume has been called.
|
||||
*/
|
||||
public void onWindowVisibleToUserAfterResume() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
mShouldRunOnResumeActions = true;
|
||||
int theme = mPreferences.getUseTheme();
|
||||
boolean drawerTabs = mPreferences.getShowTabsInDrawer(!isTablet());
|
||||
if (theme != mTheme || mShowTabsInDrawer != drawerTabs) {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isTablet() {
|
||||
return (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_XLARGE;
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
finish();
|
||||
startActivity(new Intent(this, getClass()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package acr.browser.lightning.activity;
|
||||
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
|
||||
public abstract class ThemableSettingsActivity extends AppCompatPreferenceActivity {
|
||||
|
||||
private int mTheme;
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
mTheme = mPreferenceManager.getUseTheme();
|
||||
|
||||
// set the theme
|
||||
if (mTheme == 0) {
|
||||
setTheme(R.style.Theme_SettingsTheme);
|
||||
this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColor(this)));
|
||||
} else if (mTheme == 1) {
|
||||
setTheme(R.style.Theme_SettingsTheme_Dark);
|
||||
this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColorDark(this)));
|
||||
} else if (mTheme == 2) {
|
||||
setTheme(R.style.Theme_SettingsTheme_Black);
|
||||
this.getWindow().setBackgroundDrawable(new ColorDrawable(ThemeUtils.getPrimaryColorDark(this)));
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mPreferenceManager.getUseTheme() != mTheme) {
|
||||
restart();
|
||||
}
|
||||
}
|
||||
|
||||
private void restart() {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package acr.browser.lightning.app;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import acr.browser.lightning.activity.BrowserActivity;
|
||||
import acr.browser.lightning.activity.ReadingActivity;
|
||||
import acr.browser.lightning.activity.TabsManager;
|
||||
import acr.browser.lightning.activity.ThemableBrowserActivity;
|
||||
import acr.browser.lightning.activity.ThemableSettingsActivity;
|
||||
import acr.browser.lightning.browser.BrowserPresenter;
|
||||
import acr.browser.lightning.constant.StartPage;
|
||||
import acr.browser.lightning.dialog.LightningDialogBuilder;
|
||||
import acr.browser.lightning.download.LightningDownloadListener;
|
||||
import acr.browser.lightning.fragment.BookmarkSettingsFragment;
|
||||
import acr.browser.lightning.fragment.BookmarksFragment;
|
||||
import acr.browser.lightning.fragment.DebugSettingsFragment;
|
||||
import acr.browser.lightning.fragment.LightningPreferenceFragment;
|
||||
import acr.browser.lightning.fragment.PrivacySettingsFragment;
|
||||
import acr.browser.lightning.fragment.TabsFragment;
|
||||
import acr.browser.lightning.search.SuggestionsAdapter;
|
||||
import acr.browser.lightning.utils.AdBlock;
|
||||
import acr.browser.lightning.utils.ProxyUtils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
import acr.browser.lightning.view.LightningWebClient;
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {AppModule.class})
|
||||
public interface AppComponent {
|
||||
|
||||
void inject(BrowserActivity activity);
|
||||
|
||||
void inject(BookmarksFragment fragment);
|
||||
|
||||
void inject(BookmarkSettingsFragment fragment);
|
||||
|
||||
void inject(SuggestionsAdapter adapter);
|
||||
|
||||
void inject(LightningDialogBuilder builder);
|
||||
|
||||
void inject(TabsFragment fragment);
|
||||
|
||||
void inject(LightningView lightningView);
|
||||
|
||||
void inject(ThemableBrowserActivity activity);
|
||||
|
||||
void inject(LightningPreferenceFragment fragment);
|
||||
|
||||
void inject(BrowserApp app);
|
||||
|
||||
void inject(ProxyUtils proxyUtils);
|
||||
|
||||
void inject(ReadingActivity activity);
|
||||
|
||||
void inject(LightningWebClient webClient);
|
||||
|
||||
void inject(ThemableSettingsActivity activity);
|
||||
|
||||
void inject(AdBlock adBlock);
|
||||
|
||||
void inject(LightningDownloadListener listener);
|
||||
|
||||
void inject(PrivacySettingsFragment fragment);
|
||||
|
||||
void inject(StartPage startPage);
|
||||
|
||||
void inject(BrowserPresenter presenter);
|
||||
|
||||
void inject(TabsManager manager);
|
||||
|
||||
void inject(DebugSettingsFragment fragment);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package acr.browser.lightning.app;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import net.i2p.android.ui.I2PAndroidHelper;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
public class AppModule {
|
||||
private final BrowserApp mApp;
|
||||
@NonNull private final Bus mBus;
|
||||
|
||||
public AppModule(BrowserApp app) {
|
||||
this.mApp = app;
|
||||
this.mBus = new Bus();
|
||||
}
|
||||
|
||||
@Provides
|
||||
public Application provideApplication() {
|
||||
return mApp;
|
||||
}
|
||||
|
||||
@Provides
|
||||
public Context provideContext() {
|
||||
return mApp.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Provides
|
||||
public Bus provideBus() {
|
||||
return mBus;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Provides
|
||||
@Singleton
|
||||
public I2PAndroidHelper provideI2PAndroidHelper() {
|
||||
return new I2PAndroidHelper(mApp.getApplicationContext());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package acr.browser.lightning.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.BuildConfig;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.MemoryLeakUtils;
|
||||
|
||||
public class BrowserApp extends Application {
|
||||
|
||||
private static final String TAG = BrowserApp.class.getSimpleName();
|
||||
|
||||
private static AppComponent mAppComponent;
|
||||
private static final Executor mIOThread = Executors.newSingleThreadExecutor();
|
||||
private static final Executor mTaskThread = Executors.newCachedThreadPool();
|
||||
|
||||
@Inject Bus mBus;
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
mAppComponent = DaggerAppComponent.builder().appModule(new AppModule(this)).build();
|
||||
mAppComponent.inject(this);
|
||||
|
||||
if (mPreferenceManager.getUseLeakCanary() && !isRelease()) {
|
||||
LeakCanary.install(this);
|
||||
}
|
||||
if (!isRelease() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(new MemoryLeakUtils.LifecycleAdapter() {
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
Log.d(TAG, "Cleaning up after the Android framework");
|
||||
MemoryLeakUtils.clearNextServedView(BrowserApp.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static BrowserApp get(@NonNull Context context) {
|
||||
return (BrowserApp) context.getApplicationContext();
|
||||
}
|
||||
|
||||
public static AppComponent getAppComponent() {
|
||||
return mAppComponent;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Executor getIOThread() {
|
||||
return mIOThread;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Executor getTaskThread() {
|
||||
return mTaskThread;
|
||||
}
|
||||
|
||||
public static Bus getBus(@NonNull Context context) {
|
||||
return get(context).mBus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this is a release build.
|
||||
*
|
||||
* @return true if this is a release build, false otherwise.
|
||||
*/
|
||||
public static boolean isRelease() {
|
||||
return !BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.toLowerCase().equals("release");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package acr.browser.lightning.async;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
/**
|
||||
* Created 9/27/2015 Anthony Restaino
|
||||
*/
|
||||
public class AsyncExecutor implements Executor {
|
||||
|
||||
private static final String TAG = AsyncExecutor.class.getSimpleName();
|
||||
private static final AsyncExecutor INSTANCE = new AsyncExecutor();
|
||||
private final Queue<Runnable> mQueue = new ArrayDeque<>(1);
|
||||
private final ExecutorService mExecutor = Executors.newFixedThreadPool(4);
|
||||
|
||||
private AsyncExecutor() {}
|
||||
|
||||
@NonNull
|
||||
public static AsyncExecutor getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public synchronized void notifyThreadFinish() {
|
||||
if (mQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Runnable runnable = mQueue.remove();
|
||||
execute(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
mExecutor.shutdownNow();
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull Runnable command) {
|
||||
try {
|
||||
mExecutor.execute(command);
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
mQueue.add(command);
|
||||
Log.d(TAG, "Thread was enqueued");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package acr.browser.lightning.async;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
public class ImageDownloadTask extends AsyncTask<Void, Void, Bitmap> {
|
||||
|
||||
private static final String TAG = ImageDownloadTask.class.getSimpleName();
|
||||
@NonNull private final WeakReference<ImageView> mFaviconImage;
|
||||
@NonNull private final WeakReference<Context> mContextReference;
|
||||
@NonNull private final HistoryItem mWeb;
|
||||
private final String mUrl;
|
||||
@NonNull private final Bitmap mDefaultBitmap;
|
||||
|
||||
public ImageDownloadTask(@NonNull ImageView bmImage,
|
||||
@NonNull HistoryItem web,
|
||||
@NonNull Bitmap defaultBitmap,
|
||||
@NonNull Context context) {
|
||||
// Set a tag on the ImageView so we know if the view
|
||||
// has gone out of scope and should not be used
|
||||
bmImage.setTag(web.getUrl().hashCode());
|
||||
this.mFaviconImage = new WeakReference<>(bmImage);
|
||||
this.mWeb = web;
|
||||
this.mUrl = web.getUrl();
|
||||
this.mDefaultBitmap = defaultBitmap;
|
||||
this.mContextReference = new WeakReference<>(context.getApplicationContext());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
Bitmap mIcon = null;
|
||||
// unique path for each url that is bookmarked.
|
||||
if (mUrl == null) {
|
||||
return mDefaultBitmap;
|
||||
}
|
||||
Context context = mContextReference.get();
|
||||
if (context == null) {
|
||||
return mDefaultBitmap;
|
||||
}
|
||||
File cache = context.getCacheDir();
|
||||
final Uri uri = Uri.parse(mUrl);
|
||||
if (uri.getHost() == null || uri.getScheme() == null) {
|
||||
return mDefaultBitmap;
|
||||
}
|
||||
final String hash = String.valueOf(uri.getHost().hashCode());
|
||||
final File image = new File(cache, hash + ".png");
|
||||
final String urlDisplay = uri.getScheme() + "://" + uri.getHost() + "/favicon.ico";
|
||||
// checks to see if the image exists
|
||||
if (!image.exists()) {
|
||||
FileOutputStream fos = null;
|
||||
InputStream in = null;
|
||||
try {
|
||||
// if not, download it...
|
||||
final URL urlDownload = new URL(urlDisplay);
|
||||
final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setConnectTimeout(1000);
|
||||
connection.setReadTimeout(1000);
|
||||
connection.connect();
|
||||
in = connection.getInputStream();
|
||||
|
||||
if (in != null) {
|
||||
mIcon = BitmapFactory.decodeStream(in);
|
||||
}
|
||||
// ...and cache it
|
||||
if (mIcon != null) {
|
||||
fos = new FileOutputStream(image);
|
||||
mIcon.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.flush();
|
||||
Log.d(Constants.TAG, "Downloaded: " + urlDisplay);
|
||||
}
|
||||
|
||||
} catch (Exception ignored) {
|
||||
Log.d(TAG, "Could not download: " + urlDisplay);
|
||||
} finally {
|
||||
Utils.close(in);
|
||||
Utils.close(fos);
|
||||
}
|
||||
} else {
|
||||
// if it exists, retrieve it from the cache
|
||||
mIcon = BitmapFactory.decodeFile(image.getPath());
|
||||
}
|
||||
if (mIcon == null) {
|
||||
InputStream in = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
// if not, download it...
|
||||
final URL urlDownload = new URL("https://www.google.com/s2/favicons?domain_url=" + uri.toString());
|
||||
final HttpURLConnection connection = (HttpURLConnection) urlDownload.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.setConnectTimeout(1000);
|
||||
connection.setReadTimeout(1000);
|
||||
connection.connect();
|
||||
in = connection.getInputStream();
|
||||
|
||||
if (in != null) {
|
||||
mIcon = BitmapFactory.decodeStream(in);
|
||||
}
|
||||
// ...and cache it
|
||||
if (mIcon != null) {
|
||||
fos = new FileOutputStream(image);
|
||||
mIcon.compress(Bitmap.CompressFormat.PNG, 100, fos);
|
||||
fos.flush();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Could not download Google favicon");
|
||||
} finally {
|
||||
Utils.close(in);
|
||||
Utils.close(fos);
|
||||
}
|
||||
}
|
||||
if (mIcon == null) {
|
||||
return mDefaultBitmap;
|
||||
} else {
|
||||
return mIcon;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
super.onPostExecute(bitmap);
|
||||
AsyncExecutor.getInstance().notifyThreadFinish();
|
||||
final Bitmap fav = Utils.padFavicon(bitmap);
|
||||
final ImageView view = mFaviconImage.get();
|
||||
if (view != null && view.getTag().equals(mWeb.getUrl().hashCode())) {
|
||||
Context context = view.getContext();
|
||||
if (context instanceof Activity) {
|
||||
((Activity) context).runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
view.setImageBitmap(fav);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
view.setImageBitmap(fav);
|
||||
}
|
||||
}
|
||||
mWeb.setBitmap(fav);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package acr.browser.lightning.browser;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.activity.TabsManager;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.controller.UIController;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.react.OnSubscribe;
|
||||
import acr.browser.lightning.utils.UrlUtils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
/**
|
||||
* Presenter in charge of keeping track of
|
||||
* the current tab and setting the current tab
|
||||
* of the
|
||||
*/
|
||||
public class BrowserPresenter {
|
||||
|
||||
private static final String TAG = BrowserPresenter.class.getSimpleName();
|
||||
|
||||
@NonNull private final TabsManager mTabsModel;
|
||||
@Inject PreferenceManager mPreferences;
|
||||
@Inject Bus mEventBus;
|
||||
|
||||
@NonNull private final BrowserView mView;
|
||||
@Nullable private LightningView mCurrentTab;
|
||||
|
||||
private final boolean mIsIncognito;
|
||||
private boolean mShouldClose;
|
||||
|
||||
public BrowserPresenter(@NonNull BrowserView view, boolean isIncognito) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
mTabsModel = ((UIController) view).getTabModel();
|
||||
mView = view;
|
||||
mIsIncognito = isIncognito;
|
||||
mTabsModel.setTabNumberChangedListener(new TabsManager.TabNumberChangedListener() {
|
||||
@Override
|
||||
public void tabNumberChanged(int newNumber) {
|
||||
mView.updateTabNumber(newNumber);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the tab manager with the new intent
|
||||
* that is handed in by the BrowserActivity.
|
||||
*
|
||||
* @param intent the intent to handle, may be null.
|
||||
*/
|
||||
public void setupTabs(@Nullable Intent intent) {
|
||||
mTabsModel.initializeTabs((Activity) mView, intent, mIsIncognito)
|
||||
.subscribe(new OnSubscribe<Void>() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
// At this point we always have at least a tab in the tab manager
|
||||
tabChanged(mTabsModel.last());
|
||||
mView.updateTabNumber(mTabsModel.size());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the presenter that a change occurred to
|
||||
* the current tab. Currently doesn't do anything
|
||||
* other than tell the view to notify the adapter
|
||||
* about the change.
|
||||
*
|
||||
* @param tab the tab that changed, may be null.
|
||||
*/
|
||||
public void tabChangeOccurred(@Nullable LightningView tab) {
|
||||
mView.notifyTabViewChanged(mTabsModel.indexOfTab(tab));
|
||||
}
|
||||
|
||||
private void onTabChanged(@Nullable LightningView newTab) {
|
||||
Log.d(TAG, "On tab changed");
|
||||
if (newTab == null) {
|
||||
mView.removeTabView();
|
||||
if (mCurrentTab != null) {
|
||||
mCurrentTab.pauseTimers();
|
||||
mCurrentTab.onDestroy();
|
||||
}
|
||||
} else {
|
||||
if (newTab.getWebView() == null) {
|
||||
mView.removeTabView();
|
||||
if (mCurrentTab != null) {
|
||||
mCurrentTab.pauseTimers();
|
||||
mCurrentTab.onDestroy();
|
||||
}
|
||||
} else {
|
||||
if (mCurrentTab != null) {
|
||||
// TODO: Restore this when Google fixes the bug where the WebView is
|
||||
// blank after calling onPause followed by onResume.
|
||||
// mCurrentTab.onPause();
|
||||
mCurrentTab.setForegroundTab(false);
|
||||
}
|
||||
|
||||
newTab.resumeTimers();
|
||||
newTab.onResume();
|
||||
newTab.setForegroundTab(true);
|
||||
|
||||
mView.updateProgress(newTab.getProgress());
|
||||
mView.setBackButtonEnabled(newTab.canGoBack());
|
||||
mView.setForwardButtonEnabled(newTab.canGoForward());
|
||||
mView.updateUrl(newTab.getUrl(), true);
|
||||
mView.setTabView(newTab.getWebView());
|
||||
int index = mTabsModel.indexOfTab(newTab);
|
||||
if (index >= 0) {
|
||||
mView.notifyTabViewChanged(mTabsModel.indexOfTab(newTab));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentTab = newTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all tabs but the current tab.
|
||||
*/
|
||||
public void closeAllOtherTabs() {
|
||||
|
||||
while (mTabsModel.last() != mTabsModel.indexOfCurrentTab()) {
|
||||
deleteTab(mTabsModel.last());
|
||||
}
|
||||
|
||||
while (0 != mTabsModel.indexOfCurrentTab()) {
|
||||
deleteTab(0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the tab at the specified position.
|
||||
*
|
||||
* @param position the position at which to
|
||||
* delete the tab.
|
||||
*/
|
||||
public void deleteTab(int position) {
|
||||
Log.d(TAG, "delete Tab");
|
||||
final LightningView tabToDelete = mTabsModel.getTabAtPosition(position);
|
||||
|
||||
if (tabToDelete == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!UrlUtils.isSpecialUrl(tabToDelete.getUrl()) && !mIsIncognito) {
|
||||
mPreferences.setSavedUrl(tabToDelete.getUrl());
|
||||
}
|
||||
|
||||
final boolean isShown = tabToDelete.isShown();
|
||||
boolean shouldClose = mShouldClose && isShown && Boolean.TRUE.equals(tabToDelete.getTag());
|
||||
final LightningView currentTab = mTabsModel.getCurrentTab();
|
||||
if (mTabsModel.size() == 1 && currentTab != null &&
|
||||
(UrlUtils.isSpecialUrl(currentTab.getUrl()) ||
|
||||
currentTab.getUrl().equals(mPreferences.getHomepage()))) {
|
||||
mView.closeActivity();
|
||||
return;
|
||||
} else {
|
||||
if (isShown) {
|
||||
mView.removeTabView();
|
||||
}
|
||||
boolean currentDeleted = mTabsModel.deleteTab(position);
|
||||
if (currentDeleted) {
|
||||
tabChanged(mTabsModel.indexOfCurrentTab());
|
||||
}
|
||||
}
|
||||
|
||||
final LightningView afterTab = mTabsModel.getCurrentTab();
|
||||
mView.notifyTabViewRemoved(position);
|
||||
|
||||
if (afterTab == null) {
|
||||
mView.closeBrowser();
|
||||
return;
|
||||
} else if (afterTab != currentTab) {
|
||||
//TODO remove this?
|
||||
// switchTabs(currentTab, afterTab);
|
||||
// if (currentTab != null) {
|
||||
// currentTab.pauseTimers();
|
||||
// }
|
||||
mView.notifyTabViewChanged(mTabsModel.indexOfCurrentTab());
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
mShouldClose = false;
|
||||
mView.closeActivity();
|
||||
}
|
||||
|
||||
mView.updateTabNumber(mTabsModel.size());
|
||||
|
||||
Log.d(TAG, "deleted tab");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new intent from the the main
|
||||
* BrowserActivity.
|
||||
*
|
||||
* @param intent the intent to handle,
|
||||
* may be null.
|
||||
*/
|
||||
public void onNewIntent(@Nullable final Intent intent) {
|
||||
mTabsModel.doAfterInitialization(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final String url;
|
||||
if (intent != null) {
|
||||
url = intent.getDataString();
|
||||
} else {
|
||||
url = null;
|
||||
}
|
||||
int num = 0;
|
||||
if (intent != null && intent.getExtras() != null) {
|
||||
num = intent.getExtras().getInt(Constants.INTENT_ORIGIN);
|
||||
}
|
||||
|
||||
if (num == 1) {
|
||||
loadUrlInCurrentView(url);
|
||||
} else if (url != null) {
|
||||
if (url.startsWith(Constants.FILE)) {
|
||||
mView.showBlockedLocalFileDialog(new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
newTab(url, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
newTab(url, true);
|
||||
}
|
||||
mShouldClose = true;
|
||||
LightningView tab = mTabsModel.lastTab();
|
||||
if (tab != null) {
|
||||
tab.setTag(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a URL in the current tab.
|
||||
*
|
||||
* @param url the URL to load, must
|
||||
* not be null.
|
||||
*/
|
||||
public void loadUrlInCurrentView(@NonNull final String url) {
|
||||
final LightningView currentTab = mTabsModel.getCurrentTab();
|
||||
if (currentTab == null) {
|
||||
// This is a problem, probably an assert will be better than a return
|
||||
return;
|
||||
}
|
||||
|
||||
currentTab.loadUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the presenter that it should
|
||||
* shut down. This should be called when
|
||||
* the BrowserActivity is destroyed so that
|
||||
* we don't leak any memory.
|
||||
*/
|
||||
public void shutdown() {
|
||||
onTabChanged(null);
|
||||
mTabsModel.setTabNumberChangedListener(null);
|
||||
mTabsModel.cancelPendingWork();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the presenter that we wish
|
||||
* to switch to a different tab at the
|
||||
* specified position. If the position
|
||||
* is not in the model, this method will
|
||||
* do nothing.
|
||||
*
|
||||
* @param position the position of the
|
||||
* tab to switch to.
|
||||
*/
|
||||
public synchronized void tabChanged(int position) {
|
||||
Log.d(TAG, "tabChanged: " + position);
|
||||
if (position < 0 || position >= mTabsModel.size()) {
|
||||
return;
|
||||
}
|
||||
LightningView tab = mTabsModel.switchToTab(position);
|
||||
onTabChanged(tab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new tab with the specified URL. You
|
||||
* can choose to show the tab or load it in the
|
||||
* background.
|
||||
*
|
||||
* @param url the URL to load, may be null if you
|
||||
* don't wish to load anything.
|
||||
* @param show whether or not to switch to this
|
||||
* tab after opening it.
|
||||
* @return true if we successfully created the tab,
|
||||
* false if we have hit max tabs.
|
||||
*/
|
||||
public synchronized boolean newTab(@Nullable String url, boolean show) {
|
||||
// Limit number of tabs for limited version of app
|
||||
if (!Constants.FULL_VERSION && mTabsModel.size() >= 10) {
|
||||
mView.showSnackbar(R.string.max_tabs);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.d(TAG, "New tab, show: " + show);
|
||||
|
||||
LightningView startingTab = mTabsModel.newTab((Activity) mView, url, mIsIncognito);
|
||||
if (mTabsModel.size() == 1) {
|
||||
startingTab.resumeTimers();
|
||||
}
|
||||
|
||||
mView.notifyTabViewAdded();
|
||||
|
||||
if (show) {
|
||||
LightningView tab = mTabsModel.switchToTab(mTabsModel.last());
|
||||
onTabChanged(tab);
|
||||
}
|
||||
|
||||
mView.updateTabNumber(mTabsModel.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package acr.browser.lightning.browser;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.view.View;
|
||||
|
||||
public interface BrowserView {
|
||||
|
||||
void setTabView(@NonNull View view);
|
||||
|
||||
void removeTabView();
|
||||
|
||||
void updateUrl(String url, boolean shortUrl);
|
||||
|
||||
void updateProgress(int progress);
|
||||
|
||||
void updateTabNumber(int number);
|
||||
|
||||
void closeBrowser();
|
||||
|
||||
void closeActivity();
|
||||
|
||||
void showBlockedLocalFileDialog(DialogInterface.OnClickListener listener);
|
||||
|
||||
void showSnackbar(@StringRes int resource);
|
||||
|
||||
void setForwardButtonEnabled(boolean enabled);
|
||||
|
||||
void setBackButtonEnabled(boolean enabled);
|
||||
|
||||
void notifyTabViewRemoved(int position);
|
||||
|
||||
void notifyTabViewAdded();
|
||||
|
||||
void notifyTabViewChanged(int position);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package acr.browser.lightning.browser;
|
||||
|
||||
public interface TabsView {
|
||||
|
||||
void tabAdded();
|
||||
|
||||
void tabRemoved(int position);
|
||||
|
||||
void tabChanged(int position);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package acr.browser.lightning.bus;
|
||||
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
|
||||
public final class BookmarkEvents {
|
||||
|
||||
private BookmarkEvents() {
|
||||
// No instances
|
||||
}
|
||||
|
||||
/**
|
||||
* The user ask to delete the selected bookmark
|
||||
*/
|
||||
public static class Deleted {
|
||||
public final HistoryItem item;
|
||||
|
||||
public Deleted(final HistoryItem item) {
|
||||
this.item = item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user ask to add/del a bookmark to the currently displayed page
|
||||
*/
|
||||
public static class ToggleBookmarkForCurrentPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sended by the {@link acr.browser.lightning.fragment.BookmarksFragment} when it wants to close
|
||||
* itself (generally in reply to a {@link acr.browser.lightning.bus.BrowserEvents.UserPressedBack}
|
||||
* event.
|
||||
*/
|
||||
public static class CloseBookmarks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sended when a bookmark is edited
|
||||
*/
|
||||
public static class BookmarkChanged {
|
||||
|
||||
public final HistoryItem oldBookmark;
|
||||
public final HistoryItem newBookmark;
|
||||
|
||||
public BookmarkChanged(final HistoryItem oldItem, final HistoryItem newItem) {
|
||||
oldBookmark = oldItem;
|
||||
newBookmark = newItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package acr.browser.lightning.bus;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
|
||||
public final class BrowserEvents {
|
||||
|
||||
private BrowserEvents() {
|
||||
// No instances
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link acr.browser.lightning.activity.BrowserActivity} signal a new bookmark was added
|
||||
* (mainly to the {@link acr.browser.lightning.fragment.BookmarksFragment}).
|
||||
*/
|
||||
public static class BookmarkAdded {
|
||||
public final String title, url;
|
||||
|
||||
public BookmarkAdded(final String title, final String url) {
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the current page has a new url. This is generally used to update the
|
||||
* {@link acr.browser.lightning.fragment.BookmarksFragment} interface.
|
||||
*/
|
||||
public static class CurrentPageUrl {
|
||||
public final String url;
|
||||
|
||||
public CurrentPageUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the BookmarksFragment and TabsFragment that the user pressed the back button
|
||||
*/
|
||||
public static class UserPressedBack {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Notify the Browser to display a SnackBar in the main activity
|
||||
*/
|
||||
public static class ShowSnackBarMessage {
|
||||
@Nullable public final String message;
|
||||
@StringRes
|
||||
public final int stringRes;
|
||||
|
||||
public ShowSnackBarMessage(@Nullable final String message) {
|
||||
this.message = message;
|
||||
this.stringRes = -1;
|
||||
}
|
||||
|
||||
public ShowSnackBarMessage(@StringRes final int stringRes) {
|
||||
this.message = null;
|
||||
this.stringRes = stringRes;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class OpenHistoryInCurrentTab {
|
||||
}
|
||||
|
||||
/**
|
||||
* The user want to open the given url in the current tab
|
||||
*/
|
||||
public final static class OpenUrlInCurrentTab {
|
||||
public final String url;
|
||||
|
||||
public OpenUrlInCurrentTab(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user ask to open the given url as new tab
|
||||
*/
|
||||
public final static class OpenUrlInNewTab {
|
||||
public final String url;
|
||||
|
||||
public OpenUrlInNewTab(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package acr.browser.lightning.bus;
|
||||
|
||||
/**
|
||||
* Collections of navigation events, like go back or go forward
|
||||
*
|
||||
* @author Stefano Pacifici
|
||||
* @date 2015/09/15
|
||||
*/
|
||||
public class NavigationEvents {
|
||||
private NavigationEvents() {
|
||||
// No instances please
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired by {@link acr.browser.lightning.fragment.TabsFragment} when the user presses back
|
||||
* button.
|
||||
*/
|
||||
public static class GoBack {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired by {@link acr.browser.lightning.fragment.TabsFragment} when the user presses forward
|
||||
* button.
|
||||
*/
|
||||
public static class GoForward {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired by {@link acr.browser.lightning.fragment.TabsFragment} when the user presses the home
|
||||
* button.
|
||||
*/
|
||||
public static class GoHome {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package acr.browser.lightning.bus;
|
||||
|
||||
/**
|
||||
* A collection of events been sent by {@link acr.browser.lightning.fragment.TabsFragment}
|
||||
*
|
||||
* @author Stefano Pacifici
|
||||
* @date 2015/09/14
|
||||
*/
|
||||
public final class TabEvents {
|
||||
|
||||
private TabEvents() {
|
||||
// No instances
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user click on the
|
||||
* tab exit button
|
||||
*/
|
||||
public static class CloseTab {
|
||||
public final int position;
|
||||
|
||||
public CloseTab(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user click on the
|
||||
* tab itself.
|
||||
*/
|
||||
public static class ShowTab {
|
||||
public final int position;
|
||||
|
||||
public ShowTab(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user long press on the
|
||||
* tab itself.
|
||||
*/
|
||||
public static class ShowCloseDialog {
|
||||
public final int position;
|
||||
|
||||
public ShowCloseDialog(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user want to create a
|
||||
* new tab.
|
||||
*/
|
||||
public static class NewTab {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sended by {@link acr.browser.lightning.fragment.TabsFragment} when the user long presses on
|
||||
* new tab button.
|
||||
*/
|
||||
public static class NewTabLongPress {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.constant;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.database.BookmarkManager;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
public final class BookmarkPage extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
/**
|
||||
* The bookmark page standard suffix
|
||||
*/
|
||||
public static final String FILENAME = "bookmarks.html";
|
||||
|
||||
private static final String HEADING_1 = "<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>\n" +
|
||||
"<head>\n" +
|
||||
"<meta content=en-us http-equiv=Content-Language />\n" +
|
||||
"<meta content='text/html; charset=utf-8' http-equiv=Content-Type />\n" +
|
||||
"<meta name=viewport content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>\n" +
|
||||
"<title>";
|
||||
|
||||
private static final String HEADING_2 = "</title>\n" +
|
||||
"</head>\n" +
|
||||
"<style>body{background:#e1e1e1;max-width:100%;min-height:100%}#content{width:100%;max-width:800px;margin:0 auto;text-align:center}.box{vertical-align:middle;text-align:center;position:relative;display:inline-block;height:45px;width:150px;margin:10px;background-color:#fff;box-shadow:0 3px 6px rgba(0,0,0,0.25);font-family:Arial;color:#444;font-size:12px;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}.box-content{height:25px;width:100%;vertical-align:middle;text-align:center;display:table-cell}p.ellipses{" +
|
||||
"width:130px;font-size: small;font-family: Arial, Helvetica, 'sans-serif';white-space:nowrap;overflow:hidden;text-align:left;vertical-align:middle;margin:auto;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis}.box a{width:100%;height:100%;position:absolute;left:0;top:0}img{vertical-align:middle;margin-right:10px;width:20px;height:20px;}.margin{margin:10px}</style>\n" +
|
||||
"<body><div id=content>";
|
||||
|
||||
private static final String PART1 = "<div class=box><a href='";
|
||||
|
||||
private static final String PART2 = "'></a>\n" +
|
||||
"<div class=margin>\n" +
|
||||
"<div class=box-content>\n" +
|
||||
"<p class=ellipses>\n" +
|
||||
"<img src='";
|
||||
|
||||
private static final String PART3 = "https://www.google.com/s2/favicons?domain=";
|
||||
|
||||
private static final String PART4 = "' />";
|
||||
|
||||
private static final String PART5 = "</p></div></div></div>";
|
||||
|
||||
private static final String END = "</div></body></html>";
|
||||
|
||||
private File mFilesDir;
|
||||
private File mCacheDir;
|
||||
|
||||
private final Application mApp;
|
||||
private final BookmarkManager mManager;
|
||||
@NonNull private final WeakReference<LightningView> mTabReference;
|
||||
private final Bitmap mFolderIcon;
|
||||
@NonNull private final String mTitle;
|
||||
|
||||
public BookmarkPage(LightningView tab, @NonNull Activity activity, BookmarkManager manager) {
|
||||
mApp = BrowserApp.get(activity);
|
||||
final Bitmap folderIcon = ThemeUtils.getThemedBitmap(activity, R.drawable.ic_folder, false);
|
||||
mTitle = mApp.getString(R.string.action_bookmarks);
|
||||
mManager = manager;
|
||||
mTabReference = new WeakReference<>(tab);
|
||||
mFolderIcon = folderIcon;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mCacheDir = mApp.getCacheDir();
|
||||
mFilesDir = mApp.getFilesDir();
|
||||
cacheDefaultFolderIcon();
|
||||
buildBookmarkPage(null, mManager);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
LightningView tab = mTabReference.get();
|
||||
if (tab != null) {
|
||||
File bookmarkWebPage = new File(mFilesDir, FILENAME);
|
||||
tab.loadUrl(Constants.FILE + bookmarkWebPage);
|
||||
}
|
||||
}
|
||||
|
||||
private void cacheDefaultFolderIcon() {
|
||||
FileOutputStream outputStream = null;
|
||||
File image = new File(mCacheDir, "folder.png");
|
||||
try {
|
||||
outputStream = new FileOutputStream(image);
|
||||
mFolderIcon.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||
mFolderIcon.recycle();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.close(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private void buildBookmarkPage(@Nullable final String folder, @NonNull final BookmarkManager manager) {
|
||||
final List<HistoryItem> list = manager.getBookmarksCopyFromFolder(folder, true);
|
||||
final File bookmarkWebPage;
|
||||
if (folder == null || folder.isEmpty()) {
|
||||
bookmarkWebPage = new File(mFilesDir, FILENAME);
|
||||
} else {
|
||||
bookmarkWebPage = new File(mFilesDir, folder + '-' + FILENAME);
|
||||
}
|
||||
final StringBuilder bookmarkBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2);
|
||||
|
||||
final String folderIconPath = Constants.FILE + mCacheDir + "/folder.png";
|
||||
for (int n = 0, size = list.size(); n < size; n++) {
|
||||
final HistoryItem item = list.get(n);
|
||||
bookmarkBuilder.append(PART1);
|
||||
if (item.isFolder()) {
|
||||
final File folderPage = new File(mFilesDir, item.getTitle() + '-' + FILENAME);
|
||||
bookmarkBuilder.append(Constants.FILE).append(folderPage);
|
||||
bookmarkBuilder.append(PART2);
|
||||
bookmarkBuilder.append(folderIconPath);
|
||||
buildBookmarkPage(item.getTitle(), manager);
|
||||
} else {
|
||||
bookmarkBuilder.append(item.getUrl());
|
||||
bookmarkBuilder.append(PART2).append(PART3);
|
||||
bookmarkBuilder.append(item.getUrl());
|
||||
}
|
||||
bookmarkBuilder.append(PART4);
|
||||
bookmarkBuilder.append(item.getTitle());
|
||||
bookmarkBuilder.append(PART5);
|
||||
}
|
||||
bookmarkBuilder.append(END);
|
||||
FileWriter bookWriter = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
bookWriter = new FileWriter(bookmarkWebPage, false);
|
||||
bookWriter.write(bookmarkBuilder.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.close(bookWriter);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
executeOnExecutor(BrowserApp.getIOThread());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.constant;
|
||||
|
||||
import acr.browser.lightning.BuildConfig;
|
||||
|
||||
public final class Constants {
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
public static final boolean FULL_VERSION = BuildConfig.FULL_VERSION;
|
||||
|
||||
public static final String DESKTOP_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36";
|
||||
public static final String MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; U; Android 4.4; en-us; Nexus 4 Build/JOP24G) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30";
|
||||
public static final String YAHOO_SEARCH = "https://search.yahoo.com/search?p=";
|
||||
public static final String GOOGLE_SEARCH = "https://www.google.com/search?client=lightning&ie=UTF-8&oe=UTF-8&q=";
|
||||
public static final String BING_SEARCH = "https://www.bing.com/search?q=";
|
||||
public static final String DUCK_SEARCH = "https://duckduckgo.com/?t=lightning&q=";
|
||||
public static final String DUCK_LITE_SEARCH = "https://duckduckgo.com/lite/?t=lightning&q=";
|
||||
public static final String STARTPAGE_MOBILE_SEARCH = "https://startpage.com/do/m/mobilesearch?language=english&query=";
|
||||
public static final String STARTPAGE_SEARCH = "https://startpage.com/do/search?language=english&query=";
|
||||
public static final String ASK_SEARCH = "http://www.ask.com/web?qsrc=0&o=0&l=dir&qo=lightningBrowser&q=";
|
||||
public static final String HOMEPAGE = "about:home";
|
||||
public static final String BAIDU_SEARCH = "https://www.baidu.com/s?wd=";
|
||||
public static final String YANDEX_SEARCH = "https://yandex.ru/yandsearch?lr=21411&text=";
|
||||
public static final String JAVASCRIPT_INVERT_PAGE = "javascript:(function(){var e='img {-webkit-filter: invert(100%);'+'-moz-filter: invert(100%);'+'-o-filter: invert(100%);'+'-ms-filter: invert(100%); }',t=document.getElementsByTagName('head')[0],n=document.createElement('style');if(!window.counter){window.counter=1}else{window.counter++;if(window.counter%2==0){var e='html {-webkit-filter: invert(0%); -moz-filter: invert(0%); -o-filter: invert(0%); -ms-filter: invert(0%); }'}}n.type='text/css';if(n.styleSheet){n.styleSheet.cssText=e}else{n.appendChild(document.createTextNode(e))}t.appendChild(n)})();";
|
||||
public static final String JAVASCRIPT_TEXT_REFLOW = "javascript:document.getElementsByTagName('body')[0].style.width=window.innerWidth+'px';";
|
||||
public static final String JAVASCRIPT_THEME_COLOR = "(function () {\n" +
|
||||
" \"use strict\";\n" +
|
||||
" var metas, i, tag;\n" +
|
||||
" metas = document.getElementsByTagName('meta');\n" +
|
||||
" if (metas !== null) {\n" +
|
||||
" for (i = 0; i < metas.length; i++) {\n" +
|
||||
" tag = metas[i].getAttribute('name');\n" +
|
||||
" if (tag !== null && tag.toLowerCase() === 'theme-color') {\n" +
|
||||
" return metas[i].getAttribute('content');\n" +
|
||||
" }\n" +
|
||||
" console.log(tag);\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
'\n' +
|
||||
" return '';\n" +
|
||||
"}());";
|
||||
|
||||
public static final String LOAD_READING_URL = "ReadingUrl";
|
||||
|
||||
public static final String SEPARATOR = "\\|\\$\\|SEPARATOR\\|\\$\\|";
|
||||
public static final String HTTP = "http://";
|
||||
public static final String HTTPS = "https://";
|
||||
public static final String FILE = "file://";
|
||||
public static final String FOLDER = "folder://";
|
||||
public static final String TAG = "Lightning";
|
||||
|
||||
// These should match the order of @array/proxy_choices_array
|
||||
public static final int NO_PROXY = 0;
|
||||
public static final int PROXY_ORBOT = 1;
|
||||
public static final int PROXY_I2P = 2;
|
||||
public static final int PROXY_MANUAL = 3;
|
||||
|
||||
public static final String DEFAULT_ENCODING = "UTF-8";
|
||||
|
||||
public static final String[] TEXT_ENCODINGS = {"ISO-8859-1", "UTF-8", "GBK", "Big5", "ISO-2022-JP", "SHIFT_JS", "EUC-JP", "EUC-KR"};
|
||||
|
||||
public static final String INTENT_ORIGIN = "URL_INTENT_ORIGIN";
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.constant;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.database.HistoryDatabase;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
public class HistoryPage extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
public static final String FILENAME = "history.html";
|
||||
|
||||
private static final String HEADING_1 = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><meta content=\"en-us\" http-equiv=\"Content-Language\" /><meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"><title>";
|
||||
|
||||
private static final String HEADING_2 = "</title></head><style>body { background: #e1e1e1;}.box { vertical-align:middle;position:relative; display: block; margin: 10px;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px; background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );font-family: Arial;color: #444;font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}.box a { width: 100%; height: 100%; position: absolute; left: 0; top: 0;}.black {color: black;font-size: 15px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}.font {color: gray;font-size: 10px;font-family: Arial; white-space: nowrap; overflow: hidden;margin:auto; text-overflow: ellipsis; -o-text-overflow: ellipsis; -ms-text-overflow: ellipsis;}</style><body><div id=\"content\">";
|
||||
|
||||
private static final String PART1 = "<div class=\"box\"><a href=\"";
|
||||
|
||||
private static final String PART2 = "\"></a><p class=\"black\">";
|
||||
|
||||
private static final String PART3 = "</p><p class=\"font\">";
|
||||
|
||||
private static final String PART4 = "</p></div></div>";
|
||||
|
||||
private static final String END = "</div></body></html>";
|
||||
|
||||
@NonNull private final WeakReference<LightningView> mTabReference;
|
||||
@NonNull private final Application mApp;
|
||||
@NonNull private final String mTitle;
|
||||
private final HistoryDatabase mHistoryDatabase;
|
||||
|
||||
@Nullable private String mHistoryUrl = null;
|
||||
|
||||
public HistoryPage(LightningView tab, @NonNull Application app, HistoryDatabase database) {
|
||||
mTabReference = new WeakReference<>(tab);
|
||||
mApp = app;
|
||||
mTitle = app.getString(R.string.action_history);
|
||||
mHistoryDatabase = database;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mHistoryUrl = getHistoryPage();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
LightningView tab = mTabReference.get();
|
||||
if (tab != null && mHistoryUrl != null) {
|
||||
tab.loadUrl(mHistoryUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String getHistoryPage() {
|
||||
StringBuilder historyBuilder = new StringBuilder(HEADING_1 + mTitle + HEADING_2);
|
||||
List<HistoryItem> historyList = mHistoryDatabase.getLastHundredItems();
|
||||
Iterator<HistoryItem> it = historyList.iterator();
|
||||
HistoryItem helper;
|
||||
while (it.hasNext()) {
|
||||
helper = it.next();
|
||||
historyBuilder.append(PART1);
|
||||
historyBuilder.append(helper.getUrl());
|
||||
historyBuilder.append(PART2);
|
||||
historyBuilder.append(helper.getTitle());
|
||||
historyBuilder.append(PART3);
|
||||
historyBuilder.append(helper.getUrl());
|
||||
historyBuilder.append(PART4);
|
||||
}
|
||||
|
||||
historyBuilder.append(END);
|
||||
File historyWebPage = new File(mApp.getFilesDir(), FILENAME);
|
||||
FileWriter historyWriter = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
historyWriter = new FileWriter(historyWebPage, false);
|
||||
historyWriter.write(historyBuilder.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.close(historyWriter);
|
||||
}
|
||||
return Constants.FILE + historyWebPage;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
executeOnExecutor(BrowserApp.getIOThread());
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to immediately delete the history
|
||||
* page on the current thread. This will clear the
|
||||
* cached history page that was stored on file.
|
||||
*
|
||||
* @param application the application object needed to get the file.
|
||||
*/
|
||||
public static void deleteHistoryPage(@NonNull Application application) {
|
||||
File historyWebPage = new File(application.getFilesDir(), FILENAME);
|
||||
if (historyWebPage.exists()) {
|
||||
historyWebPage.delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.constant;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
public class StartPage extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
public static final String FILENAME = "homepage.html";
|
||||
|
||||
private static final String HEAD_1 = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\">"
|
||||
+ "<head>"
|
||||
+ "<meta content=\"en-us\" http-equiv=\"Content-Language\" />"
|
||||
+ "<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\" />"
|
||||
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">"
|
||||
+ "<title>";
|
||||
|
||||
private static final String HEAD_2 = "</title>"
|
||||
+ "</head>"
|
||||
+ "<style>body{background:#f2f2f2;text-align:center;margin:0px;}#search_input{height:35px; "
|
||||
+ "width:100%;outline:none;border:none;font-size: 16px;background-color:transparent;}"
|
||||
+ "span { display: block; overflow: hidden; padding-left:5px;vertical-align:middle;}"
|
||||
+ ".search_bar{display:table;vertical-align:middle;width:90%;height:35px;max-width:500px;margin:0 auto;background-color:#fff;box-shadow: 0px 2px 3px rgba( 0, 0, 0, 0.25 );"
|
||||
+ "font-family: Arial;color: #444;-moz-border-radius: 2px;-webkit-border-radius: 2px;border-radius: 2px;}"
|
||||
+ "#search_submit{outline:none;height:37px;float:right;color:#404040;font-size:16px;font-weight:bold;border:none;"
|
||||
+ "background-color:transparent;}.outer { display: table; position: absolute; height: 100%; width: 100%;}"
|
||||
+ ".middle { display: table-cell; vertical-align: middle;}.inner { margin-left: auto; margin-right: auto; "
|
||||
+ "margin-bottom:10%; width: 100%;}img.smaller{width:50%;max-width:300px;}"
|
||||
+ ".box { vertical-align:middle;position:relative; display: block; margin: 10px;padding-left:10px;padding-right:10px;padding-top:5px;padding-bottom:5px;"
|
||||
+ " background-color:#fff;box-shadow: 0px 3px rgba( 0, 0, 0, 0.1 );font-family: Arial;color: #444;"
|
||||
+ "font-size: 12px;-moz-border-radius: 2px;-webkit-border-radius: 2px;"
|
||||
+ "border-radius: 2px;}</style><body> <div class=\"outer\"><div class=\"middle\"><div class=\"inner\"><img class=\"smaller\" src=\"";
|
||||
|
||||
private static final String MIDDLE = "\" ></br></br><form onsubmit=\"return search()\" class=\"search_bar\" autocomplete=\"off\">"
|
||||
+ "<input type=\"submit\" id=\"search_submit\" value=\"Search\" ><span><input class=\"search\" type=\"text\" value=\"\" id=\"search_input\" >"
|
||||
+ "</span></form></br></br></div></div></div><script type=\"text/javascript\">function search(){if(document.getElementById(\"search_input\").value != \"\"){window.location.href = \"";
|
||||
|
||||
private static final String END = "\" + document.getElementById(\"search_input\").value;document.getElementById(\"search_input\").value = \"\";}return false;}</script></body></html>";
|
||||
|
||||
@NonNull private final String mTitle;
|
||||
@NonNull private final Application mApp;
|
||||
@NonNull private final WeakReference<LightningView> mTabReference;
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
private String mStartpageUrl;
|
||||
|
||||
public StartPage(LightningView tab, @NonNull Application app) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
mTitle = app.getString(R.string.home);
|
||||
mApp = app;
|
||||
mTabReference = new WeakReference<>(tab);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
mStartpageUrl = getHomepage();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void aVoid) {
|
||||
super.onPostExecute(aVoid);
|
||||
LightningView tab = mTabReference.get();
|
||||
if (tab != null) {
|
||||
tab.loadUrl(mStartpageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method builds the homepage and returns the local URL to be loaded
|
||||
* when it finishes building.
|
||||
*
|
||||
* @return the URL to load
|
||||
*/
|
||||
@NonNull
|
||||
private String getHomepage() {
|
||||
StringBuilder homepageBuilder = new StringBuilder(HEAD_1 + mTitle + HEAD_2);
|
||||
String icon;
|
||||
String searchUrl;
|
||||
switch (mPreferenceManager.getSearchChoice()) {
|
||||
case 0:
|
||||
// CUSTOM SEARCH
|
||||
icon = "file:///android_asset/lightning.png";
|
||||
searchUrl = mPreferenceManager.getSearchUrl();
|
||||
break;
|
||||
case 1:
|
||||
// GOOGLE_SEARCH;
|
||||
icon = "file:///android_asset/google.png";
|
||||
// "https://www.google.com/images/srpr/logo11w.png";
|
||||
searchUrl = Constants.GOOGLE_SEARCH;
|
||||
break;
|
||||
case 2:
|
||||
// ANDROID SEARCH;
|
||||
icon = "file:///android_asset/ask.png";
|
||||
searchUrl = Constants.ASK_SEARCH;
|
||||
break;
|
||||
case 3:
|
||||
// BING_SEARCH;
|
||||
icon = "file:///android_asset/bing.png";
|
||||
// "http://upload.wikimedia.org/wikipedia/commons/thumb/b/b1/Bing_logo_%282013%29.svg/500px-Bing_logo_%282013%29.svg.png";
|
||||
searchUrl = Constants.BING_SEARCH;
|
||||
break;
|
||||
case 4:
|
||||
// YAHOO_SEARCH;
|
||||
icon = "file:///android_asset/yahoo.png";
|
||||
// "http://upload.wikimedia.org/wikipedia/commons/thumb/2/24/Yahoo%21_logo.svg/799px-Yahoo%21_logo.svg.png";
|
||||
searchUrl = Constants.YAHOO_SEARCH;
|
||||
break;
|
||||
case 5:
|
||||
// STARTPAGE_SEARCH;
|
||||
icon = "file:///android_asset/png";
|
||||
// "https://com/graphics/startp_logo.gif";
|
||||
searchUrl = Constants.STARTPAGE_SEARCH;
|
||||
break;
|
||||
case 6:
|
||||
// STARTPAGE_MOBILE
|
||||
icon = "file:///android_asset/png";
|
||||
// "https://com/graphics/startp_logo.gif";
|
||||
searchUrl = Constants.STARTPAGE_MOBILE_SEARCH;
|
||||
break;
|
||||
case 7:
|
||||
// DUCK_SEARCH;
|
||||
icon = "file:///android_asset/duckduckgo.png";
|
||||
// "https://duckduckgo.com/assets/logo_homepage.normal.v101.png";
|
||||
searchUrl = Constants.DUCK_SEARCH;
|
||||
break;
|
||||
case 8:
|
||||
// DUCK_LITE_SEARCH;
|
||||
icon = "file:///android_asset/duckduckgo.png";
|
||||
// "https://duckduckgo.com/assets/logo_homepage.normal.v101.png";
|
||||
searchUrl = Constants.DUCK_LITE_SEARCH;
|
||||
break;
|
||||
case 9:
|
||||
// BAIDU_SEARCH;
|
||||
icon = "file:///android_asset/baidu.png";
|
||||
// "http://www.baidu.com/img/bdlogo.gif";
|
||||
searchUrl = Constants.BAIDU_SEARCH;
|
||||
break;
|
||||
case 10:
|
||||
// YANDEX_SEARCH;
|
||||
icon = "file:///android_asset/yandex.png";
|
||||
// "http://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Yandex.svg/600px-Yandex.svg.png";
|
||||
searchUrl = Constants.YANDEX_SEARCH;
|
||||
break;
|
||||
default:
|
||||
// DEFAULT GOOGLE_SEARCH;
|
||||
icon = "file:///android_asset/google.png";
|
||||
searchUrl = Constants.GOOGLE_SEARCH;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
homepageBuilder.append(icon);
|
||||
homepageBuilder.append(MIDDLE);
|
||||
homepageBuilder.append(searchUrl);
|
||||
homepageBuilder.append(END);
|
||||
|
||||
File homepage = new File(mApp.getFilesDir(), FILENAME);
|
||||
FileWriter hWriter = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
hWriter = new FileWriter(homepage, false);
|
||||
hWriter.write(homepageBuilder.toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.close(hWriter);
|
||||
}
|
||||
|
||||
return Constants.FILE + homepage;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
executeOnExecutor(BrowserApp.getIOThread());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.controller;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient.CustomViewCallback;
|
||||
|
||||
import acr.browser.lightning.activity.TabsManager;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
public interface UIController {
|
||||
|
||||
void changeToolbarBackground(@NonNull Bitmap favicon, @Nullable Drawable drawable);
|
||||
|
||||
@ColorInt
|
||||
int getUiColor();
|
||||
|
||||
boolean getUseDarkTheme();
|
||||
|
||||
void updateUrl(@Nullable String title, boolean shortUrl);
|
||||
|
||||
void updateProgress(int n);
|
||||
|
||||
void updateHistory(@Nullable String title, @NonNull String url);
|
||||
|
||||
void openFileChooser(ValueCallback<Uri> uploadMsg);
|
||||
|
||||
void onShowCustomView(View view, CustomViewCallback callback);
|
||||
|
||||
void onShowCustomView(View view, CustomViewCallback callback, int requestedOrienation);
|
||||
|
||||
void onHideCustomView();
|
||||
|
||||
void onCreateWindow(Message resultMsg);
|
||||
|
||||
void onCloseWindow(LightningView view);
|
||||
|
||||
void hideActionBar();
|
||||
|
||||
void showActionBar();
|
||||
|
||||
void showFileChooser(ValueCallback<Uri[]> filePathCallback);
|
||||
|
||||
void closeEmptyTab();
|
||||
|
||||
void showCloseDialog(int position);
|
||||
|
||||
void newTabClicked();
|
||||
|
||||
void setForwardButtonEnabled(boolean enabled);
|
||||
|
||||
void setBackButtonEnabled(boolean enabled);
|
||||
|
||||
void tabChanged(LightningView tab);
|
||||
|
||||
TabsManager getTabModel();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package acr.browser.lightning.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.WorkerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import acr.browser.lightning.react.Action;
|
||||
import acr.browser.lightning.react.Observable;
|
||||
import acr.browser.lightning.react.Subscriber;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
public class BookmarkLocalSync {
|
||||
|
||||
private static final String TAG = BookmarkLocalSync.class.getSimpleName();
|
||||
|
||||
private static final String STOCK_BOOKMARKS_CONTENT = "content://browser/bookmarks";
|
||||
private static final String CHROME_BOOKMARKS_CONTENT = "content://com.android.chrome.browser/bookmarks";
|
||||
private static final String CHROME_BETA_BOOKMARKS_CONTENT = "content://com.chrome.beta.browser/bookmarks";
|
||||
private static final String CHROME_DEV_BOOKMARKS_CONTENT = "content://com.chrome.dev.browser/bookmarks";
|
||||
|
||||
private static final String COLUMN_TITLE = "title";
|
||||
private static final String COLUMN_URL = "url";
|
||||
private static final String COLUMN_BOOKMARK = "bookmark";
|
||||
|
||||
@NonNull private final Context mContext;
|
||||
|
||||
public enum Source {
|
||||
STOCK,
|
||||
CHROME_STABLE,
|
||||
CHROME_BETA,
|
||||
CHROME_DEV
|
||||
}
|
||||
|
||||
public BookmarkLocalSync(@NonNull Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public List<HistoryItem> getBookmarksFromContentUri(String contentUri) {
|
||||
List<HistoryItem> list = new ArrayList<>();
|
||||
Cursor cursor = getBrowserCursor(contentUri);
|
||||
try {
|
||||
if (cursor != null) {
|
||||
for (int n = 0; n < cursor.getColumnCount(); n++) {
|
||||
Log.d(TAG, cursor.getColumnName(n));
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
if (cursor.getInt(2) == 1) {
|
||||
String url = cursor.getString(0);
|
||||
String title = cursor.getString(1);
|
||||
if (url.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (title == null || title.isEmpty()) {
|
||||
title = Utils.getDomainName(url);
|
||||
}
|
||||
if (title != null) {
|
||||
list.add(new HistoryItem(url, title));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Utils.close(cursor);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@WorkerThread
|
||||
private Cursor getBrowserCursor(String contentUri) {
|
||||
Cursor cursor;
|
||||
Uri uri = Uri.parse(contentUri);
|
||||
try {
|
||||
cursor = mContext.getContentResolver().query(uri,
|
||||
new String[]{COLUMN_URL, COLUMN_TITLE, COLUMN_BOOKMARK}, null, null, null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Observable<List<Source>> getSupportedBrowsers() {
|
||||
return Observable.create(new Action<List<Source>>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Subscriber<List<Source>> subscriber) {
|
||||
List<Source> sources = new ArrayList<>(1);
|
||||
if (isBrowserSupported(STOCK_BOOKMARKS_CONTENT)) {
|
||||
sources.add(Source.STOCK);
|
||||
}
|
||||
if (isBrowserSupported(CHROME_BOOKMARKS_CONTENT)) {
|
||||
sources.add(Source.CHROME_STABLE);
|
||||
}
|
||||
if (isBrowserSupported(CHROME_BETA_BOOKMARKS_CONTENT)) {
|
||||
sources.add(Source.CHROME_BETA);
|
||||
}
|
||||
if (isBrowserSupported(CHROME_DEV_BOOKMARKS_CONTENT)) {
|
||||
sources.add(Source.CHROME_DEV);
|
||||
}
|
||||
subscriber.onNext(sources);
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isBrowserSupported(String contentUri) {
|
||||
Cursor cursor = getBrowserCursor(contentUri);
|
||||
boolean supported = cursor != null;
|
||||
Utils.close(cursor);
|
||||
return supported;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
public List<HistoryItem> getBookmarksFromStockBrowser() {
|
||||
return getBookmarksFromContentUri(STOCK_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
public List<HistoryItem> getBookmarksFromChrome() {
|
||||
return getBookmarksFromContentUri(CHROME_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
public List<HistoryItem> getBookmarksFromChromeBeta() {
|
||||
return getBookmarksFromContentUri(CHROME_BETA_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@WorkerThread
|
||||
public List<HistoryItem> getBookmarksFromChromeDev() {
|
||||
return getBookmarksFromContentUri(CHROME_DEV_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public boolean isBrowserImportSupported() {
|
||||
Cursor chrome = getChromeCursor();
|
||||
Utils.close(chrome);
|
||||
Cursor dev = getChromeDevCursor();
|
||||
Utils.close(dev);
|
||||
Cursor beta = getChromeBetaCursor();
|
||||
Cursor stock = getStockCursor();
|
||||
Utils.close(stock);
|
||||
return chrome != null || dev != null || beta != null || stock != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@WorkerThread
|
||||
private Cursor getChromeBetaCursor() {
|
||||
return getBrowserCursor(CHROME_BETA_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@WorkerThread
|
||||
private Cursor getChromeDevCursor() {
|
||||
return getBrowserCursor(CHROME_DEV_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@WorkerThread
|
||||
private Cursor getChromeCursor() {
|
||||
return getBrowserCursor(CHROME_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@WorkerThread
|
||||
private Cursor getStockCursor() {
|
||||
return getBrowserCursor(STOCK_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
public void printAllColumns() {
|
||||
printColumns(CHROME_BETA_BOOKMARKS_CONTENT);
|
||||
printColumns(CHROME_BOOKMARKS_CONTENT);
|
||||
printColumns(CHROME_DEV_BOOKMARKS_CONTENT);
|
||||
printColumns(STOCK_BOOKMARKS_CONTENT);
|
||||
}
|
||||
|
||||
public void printColumns(String contentProvider) {
|
||||
Cursor cursor = null;
|
||||
Log.e(TAG, contentProvider);
|
||||
Uri uri = Uri.parse(contentProvider);
|
||||
try {
|
||||
cursor = mContext.getContentResolver().query(uri, null, null, null, null);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error Occurred", e);
|
||||
}
|
||||
if (cursor != null) {
|
||||
for (int n = 0; n < cursor.getColumnCount(); n++) {
|
||||
Log.d(TAG, cursor.getColumnName(n));
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
package acr.browser.lightning.database;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
@Singleton
|
||||
public class BookmarkManager {
|
||||
|
||||
private static final String TAG = BookmarkManager.class.getSimpleName();
|
||||
|
||||
private static final String TITLE = "title";
|
||||
private static final String URL = "url";
|
||||
private static final String FOLDER = "folder";
|
||||
private static final String ORDER = "order";
|
||||
private static final String FILE_BOOKMARKS = "bookmarks.dat";
|
||||
|
||||
@NonNull private final String DEFAULT_BOOKMARK_TITLE;
|
||||
|
||||
private Map<String, HistoryItem> mBookmarksMap;
|
||||
@NonNull private String mCurrentFolder = "";
|
||||
@NonNull private final ExecutorService mExecutor;
|
||||
private File mFilesDir;
|
||||
|
||||
@Inject
|
||||
public BookmarkManager(@NonNull Context context) {
|
||||
mExecutor = Executors.newSingleThreadExecutor();
|
||||
DEFAULT_BOOKMARK_TITLE = context.getString(R.string.untitled);
|
||||
mExecutor.execute(new BookmarkInitializer(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for bookmark using the url
|
||||
*
|
||||
* @param url the lookup url
|
||||
* @return the bookmark as an {@link HistoryItem} or null
|
||||
*/
|
||||
@Nullable
|
||||
public HistoryItem findBookmarkForUrl(final String url) {
|
||||
return mBookmarksMap.get(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the BookmarkManager, it's a one-time operation and will be executed asynchronously.
|
||||
* When done, mReady flag will been set to true.
|
||||
*/
|
||||
private class BookmarkInitializer implements Runnable {
|
||||
private final Context mContext;
|
||||
|
||||
public BookmarkInitializer(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (BookmarkManager.this) {
|
||||
mFilesDir = mContext.getFilesDir();
|
||||
final Map<String, HistoryItem> bookmarks = new HashMap<>();
|
||||
final File bookmarksFile = new File(mFilesDir, FILE_BOOKMARKS);
|
||||
|
||||
BufferedReader bookmarksReader = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
if (bookmarksFile.exists() && bookmarksFile.isFile()) {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
inputStream = new FileInputStream(bookmarksFile);
|
||||
} else {
|
||||
inputStream = mContext.getResources().openRawResource(R.raw.default_bookmarks);
|
||||
}
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
bookmarksReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
String line;
|
||||
while ((line = bookmarksReader.readLine()) != null) {
|
||||
try {
|
||||
JSONObject object = new JSONObject(line);
|
||||
HistoryItem item = new HistoryItem();
|
||||
item.setTitle(object.getString(TITLE));
|
||||
final String url = object.getString(URL);
|
||||
item.setUrl(url);
|
||||
item.setFolder(object.getString(FOLDER));
|
||||
item.setOrder(object.getInt(ORDER));
|
||||
item.setImageId(R.drawable.ic_bookmark);
|
||||
bookmarks.put(url, item);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Can't parse line " + line, e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error reading the bookmarks file", e);
|
||||
} finally {
|
||||
Utils.close(bookmarksReader);
|
||||
Utils.close(inputStream);
|
||||
}
|
||||
mBookmarksMap = bookmarks;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump all the given bookmarks to the bookmark file using a temporary file
|
||||
*/
|
||||
private class BookmarksWriter implements Runnable {
|
||||
|
||||
private final List<HistoryItem> mBookmarks;
|
||||
|
||||
public BookmarksWriter(List<HistoryItem> bookmarks) {
|
||||
mBookmarks = bookmarks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final File tempFile = new File(mFilesDir,
|
||||
String.format(Locale.US, "bm_%d.dat", System.currentTimeMillis()));
|
||||
final File bookmarksFile = new File(mFilesDir, FILE_BOOKMARKS);
|
||||
boolean success = false;
|
||||
BufferedWriter bookmarkWriter = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
bookmarkWriter = new BufferedWriter(new FileWriter(tempFile, false));
|
||||
JSONObject object = new JSONObject();
|
||||
for (HistoryItem item : mBookmarks) {
|
||||
object.put(TITLE, item.getTitle());
|
||||
object.put(URL, item.getUrl());
|
||||
object.put(FOLDER, item.getFolder());
|
||||
object.put(ORDER, item.getOrder());
|
||||
bookmarkWriter.write(object.toString());
|
||||
bookmarkWriter.newLine();
|
||||
}
|
||||
success = true;
|
||||
} catch (@NonNull IOException | JSONException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.close(bookmarkWriter);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
// Overwrite the bookmarks file by renaming the temp file
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
tempFile.renameTo(bookmarksFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
mExecutor.shutdownNow();
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
public boolean isBookmark(String url) {
|
||||
return mBookmarksMap.containsKey(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds the the HistoryItem item to permanent bookmark storage.<br>
|
||||
* This operation is blocking if the manager is still not ready.
|
||||
*
|
||||
* @param item the item to add
|
||||
* @return It returns true if the operation was successful.
|
||||
*/
|
||||
public synchronized boolean addBookmark(@NonNull HistoryItem item) {
|
||||
final String url = item.getUrl();
|
||||
if (mBookmarksMap.containsKey(url)) {
|
||||
return false;
|
||||
}
|
||||
mBookmarksMap.put(url, item);
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds the list of HistoryItems to permanent bookmark storage
|
||||
*
|
||||
* @param list the list of HistoryItems to add to bookmarks
|
||||
*/
|
||||
public synchronized void addBookmarkList(@Nullable List<HistoryItem> list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (HistoryItem item : list) {
|
||||
final String url = item.getUrl();
|
||||
if (!mBookmarksMap.containsKey(url)) {
|
||||
mBookmarksMap.put(url, item);
|
||||
}
|
||||
}
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deletes the bookmark with the given url. It returns
|
||||
* true if the deletion was successful.
|
||||
*
|
||||
* @param deleteItem the bookmark item to delete
|
||||
*/
|
||||
public synchronized boolean deleteBookmark(@Nullable HistoryItem deleteItem) {
|
||||
if (deleteItem == null || deleteItem.isFolder()) {
|
||||
return false;
|
||||
}
|
||||
mBookmarksMap.remove(deleteItem.getUrl());
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* renames a folder and moves all it's contents to that folder
|
||||
*
|
||||
* @param oldName the folder to be renamed
|
||||
* @param newName the new name of the folder
|
||||
*/
|
||||
public synchronized void renameFolder(@NonNull String oldName, @NonNull String newName) {
|
||||
if (newName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (HistoryItem item : mBookmarksMap.values()) {
|
||||
if (item.getFolder().equals(oldName)) {
|
||||
item.setFolder(newName);
|
||||
} else if (item.isFolder() && item.getTitle().equals(oldName)) {
|
||||
item.setTitle(newName);
|
||||
item.setUrl(Constants.FOLDER + newName);
|
||||
}
|
||||
}
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the folder and move all bookmarks to the top level
|
||||
*
|
||||
* @param name the name of the folder to be deleted
|
||||
*/
|
||||
public synchronized void deleteFolder(@NonNull String name) {
|
||||
final Map<String, HistoryItem> bookmarks = new HashMap<>();
|
||||
for (HistoryItem item : mBookmarksMap.values()) {
|
||||
final String url = item.getUrl();
|
||||
if (item.isFolder()) {
|
||||
if (!item.getTitle().equals(name)) {
|
||||
bookmarks.put(url, item);
|
||||
}
|
||||
} else {
|
||||
if (item.getFolder().equals(name)) {
|
||||
item.setFolder("");
|
||||
}
|
||||
bookmarks.put(url, item);
|
||||
}
|
||||
}
|
||||
mBookmarksMap = bookmarks;
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method deletes ALL bookmarks created
|
||||
* by the user. Use this method carefully and
|
||||
* do not use it without explicit user consent.
|
||||
*/
|
||||
public synchronized void deleteAllBookmarks() {
|
||||
mBookmarksMap = new HashMap<>();
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method edits a particular bookmark in the bookmark database
|
||||
*
|
||||
* @param oldItem This is the old item that you wish to edit
|
||||
* @param newItem This is the new item that will overwrite the old item
|
||||
*/
|
||||
public synchronized void editBookmark(@Nullable HistoryItem oldItem, @Nullable HistoryItem newItem) {
|
||||
if (oldItem == null || newItem == null || oldItem.isFolder()) {
|
||||
return;
|
||||
}
|
||||
if (newItem.getUrl().isEmpty()) {
|
||||
deleteBookmark(oldItem);
|
||||
return;
|
||||
}
|
||||
if (newItem.getTitle().isEmpty()) {
|
||||
newItem.setTitle(DEFAULT_BOOKMARK_TITLE);
|
||||
}
|
||||
final String oldUrl = oldItem.getUrl();
|
||||
final String newUrl = newItem.getUrl();
|
||||
if (!oldUrl.equals(newUrl)) {
|
||||
// The url has been changed, remove the old one
|
||||
mBookmarksMap.remove(oldUrl);
|
||||
}
|
||||
mBookmarksMap.put(newUrl, newItem);
|
||||
mExecutor.execute(new BookmarksWriter(new LinkedList<>(mBookmarksMap.values())));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method exports the stored bookmarks to a text file in the device's
|
||||
* external download directory
|
||||
*/
|
||||
public synchronized void exportBookmarks(@NonNull Activity activity) {
|
||||
List<HistoryItem> bookmarkList = getAllBookmarks(true);
|
||||
File bookmarksExport = new File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"BookmarksExport.txt");
|
||||
int counter = 0;
|
||||
while (bookmarksExport.exists()) {
|
||||
counter++;
|
||||
bookmarksExport = new File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||
"BookmarksExport-" + counter + ".txt");
|
||||
}
|
||||
BufferedWriter bookmarkWriter = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
bookmarkWriter = new BufferedWriter(new FileWriter(bookmarksExport,
|
||||
false));
|
||||
JSONObject object = new JSONObject();
|
||||
for (HistoryItem item : bookmarkList) {
|
||||
object.put(TITLE, item.getTitle());
|
||||
object.put(URL, item.getUrl());
|
||||
object.put(FOLDER, item.getFolder());
|
||||
object.put(ORDER, item.getOrder());
|
||||
bookmarkWriter.write(object.toString());
|
||||
bookmarkWriter.newLine();
|
||||
}
|
||||
Utils.showSnackbar(activity, activity.getString(R.string.bookmark_export_path)
|
||||
+ ' ' + bookmarksExport.getPath());
|
||||
} catch (@NonNull IOException | JSONException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.close(bookmarkWriter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a list of ALL stored bookmarks.
|
||||
* This is a disk-bound operation and should not be
|
||||
* done very frequently.
|
||||
*
|
||||
* @param sort force to sort the returned bookmarkList
|
||||
* @return returns a list of bookmarks that can be sorted
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized List<HistoryItem> getAllBookmarks(boolean sort) {
|
||||
final List<HistoryItem> bookmarks = new ArrayList<>(mBookmarksMap.values());
|
||||
if (sort) {
|
||||
Collections.sort(bookmarks, new SortIgnoreCase());
|
||||
}
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a list of bookmarks and folders located in the specified folder.
|
||||
* This method should generally be used by the UI when it needs a list to display to the
|
||||
* user as it returns a subset of all bookmarks and includes folders as well which are
|
||||
* really 'fake' bookmarks.
|
||||
*
|
||||
* @param folder the name of the folder to retrieve bookmarks from
|
||||
* @return a list of bookmarks found in that folder
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized List<HistoryItem> getBookmarksFromFolder(@Nullable String folder, boolean sort) {
|
||||
List<HistoryItem> bookmarks = new ArrayList<>(1);
|
||||
if (folder == null || folder.isEmpty()) {
|
||||
bookmarks.addAll(getFolders(sort));
|
||||
folder = "";
|
||||
}
|
||||
mCurrentFolder = folder;
|
||||
for (HistoryItem item : mBookmarksMap.values()) {
|
||||
if (item.getFolder().equals(folder))
|
||||
bookmarks.add(item);
|
||||
}
|
||||
if (sort) {
|
||||
Collections.sort(bookmarks, new SortIgnoreCase());
|
||||
}
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Different from {@link #getBookmarksFromFolder(String, boolean)} only in
|
||||
* that it doesn't affect the internal state of the bookmark manager which
|
||||
* tracks the current folder used by the bookmark drawer.
|
||||
* <p/>
|
||||
* This method returns a list of bookmarks and folders located in the specified folder.
|
||||
* This method should generally be used by the UI when it needs a list to display to the
|
||||
* user as it returns a subset of all bookmarks and includes folders as well which are
|
||||
* really 'fake' bookmarks.
|
||||
*
|
||||
* @param folder the name of the folder to retrieve bookmarks from
|
||||
* @return a list of bookmarks found in that folder
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized List<HistoryItem> getBookmarksCopyFromFolder(@Nullable String folder, boolean sort) {
|
||||
List<HistoryItem> bookmarks = new ArrayList<>(1);
|
||||
if (folder == null || folder.isEmpty()) {
|
||||
bookmarks.addAll(getFolders(sort));
|
||||
folder = "";
|
||||
}
|
||||
for (HistoryItem item : mBookmarksMap.values()) {
|
||||
if (item.getFolder().equals(folder))
|
||||
bookmarks.add(item);
|
||||
}
|
||||
if (sort) {
|
||||
Collections.sort(bookmarks, new SortIgnoreCase());
|
||||
}
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells you if you are at the root folder or in a subfolder
|
||||
*
|
||||
* @return returns true if you are in the root folder
|
||||
*/
|
||||
public boolean isRootFolder() {
|
||||
return mCurrentFolder.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current folder
|
||||
*
|
||||
* @return the current folder
|
||||
*/
|
||||
@Nullable
|
||||
public String getCurrentFolder() {
|
||||
return mCurrentFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a list of all folders.
|
||||
* Folders cannot be empty as they are generated from
|
||||
* the list of bookmarks that have non-empty folder fields.
|
||||
*
|
||||
* @return a list of all folders
|
||||
*/
|
||||
@NonNull
|
||||
private synchronized List<HistoryItem> getFolders(boolean sort) {
|
||||
final HashMap<String, HistoryItem> folders = new HashMap<>();
|
||||
for (HistoryItem item : mBookmarksMap.values()) {
|
||||
final String folderName = item.getFolder();
|
||||
if (!folderName.isEmpty() && !folders.containsKey(folderName)) {
|
||||
final HistoryItem folder = new HistoryItem();
|
||||
folder.setIsFolder(true);
|
||||
folder.setTitle(folderName);
|
||||
folder.setImageId(R.drawable.ic_folder);
|
||||
folder.setUrl(Constants.FOLDER + folderName);
|
||||
folders.put(folderName, folder);
|
||||
}
|
||||
}
|
||||
final List<HistoryItem> result = new ArrayList<>(folders.values());
|
||||
if (sort) {
|
||||
Collections.sort(result, new SortIgnoreCase());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a list of folder titles that can be used for suggestions in a
|
||||
* simple list adapter
|
||||
*
|
||||
* @return a list of folder title strings
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized List<String> getFolderTitles() {
|
||||
final Set<String> folders = new HashSet<>();
|
||||
for (HistoryItem item : mBookmarksMap.values()) {
|
||||
final String folderName = item.getFolder();
|
||||
if (!folderName.isEmpty()) {
|
||||
folders.add(folderName);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports the bookmarks from a backup file that is located on
|
||||
* external storage
|
||||
*
|
||||
* @param file the file to attempt to import bookmarks from
|
||||
*/
|
||||
public synchronized void importBookmarksFromFile(@Nullable File file, @NonNull Activity activity) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
List<HistoryItem> list = new ArrayList<>();
|
||||
BufferedReader bookmarksReader = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
bookmarksReader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
int number = 0;
|
||||
while ((line = bookmarksReader.readLine()) != null) {
|
||||
JSONObject object = new JSONObject(line);
|
||||
HistoryItem item = new HistoryItem();
|
||||
item.setTitle(object.getString(TITLE));
|
||||
item.setUrl(object.getString(URL));
|
||||
item.setFolder(object.getString(FOLDER));
|
||||
item.setOrder(object.getInt(ORDER));
|
||||
list.add(item);
|
||||
number++;
|
||||
}
|
||||
addBookmarkList(list);
|
||||
String message = activity.getResources().getString(R.string.message_import);
|
||||
Utils.showSnackbar(activity, number + " " + message);
|
||||
} catch (@NonNull IOException | JSONException e) {
|
||||
e.printStackTrace();
|
||||
Utils.createInformativeDialog(activity, R.string.title_error, R.string.import_bookmark_error);
|
||||
} finally {
|
||||
Utils.close(bookmarksReader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class sorts bookmarks alphabetically, with folders coming after bookmarks
|
||||
*/
|
||||
private static class SortIgnoreCase implements Comparator<HistoryItem> {
|
||||
|
||||
public int compare(@Nullable HistoryItem o1, @Nullable HistoryItem o2) {
|
||||
if (o1 == null || o2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (o1.isFolder() == o2.isFolder()) {
|
||||
return o1.getTitle().toLowerCase(Locale.getDefault())
|
||||
.compareTo(o2.getTitle().toLowerCase(Locale.getDefault()));
|
||||
|
||||
} else {
|
||||
return o1.isFolder() ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
|
||||
@Singleton
|
||||
public class HistoryDatabase extends SQLiteOpenHelper {
|
||||
|
||||
// All Static variables
|
||||
// Database Version
|
||||
private static final int DATABASE_VERSION = 2;
|
||||
|
||||
// Database Name
|
||||
private static final String DATABASE_NAME = "historyManager";
|
||||
|
||||
// HistoryItems table name
|
||||
private static final String TABLE_HISTORY = "history";
|
||||
|
||||
// HistoryItems Table Columns names
|
||||
private static final String KEY_ID = "id";
|
||||
private static final String KEY_URL = "url";
|
||||
private static final String KEY_TITLE = "title";
|
||||
private static final String KEY_TIME_VISITED = "time";
|
||||
|
||||
@Nullable private SQLiteDatabase mDatabase;
|
||||
|
||||
@Inject
|
||||
public HistoryDatabase(@NonNull Context context) {
|
||||
super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
BrowserApp.getTaskThread().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (HistoryDatabase.this) {
|
||||
mDatabase = HistoryDatabase.this.getWritableDatabase();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Creating Tables
|
||||
@Override
|
||||
public void onCreate(@NonNull SQLiteDatabase db) {
|
||||
String CREATE_HISTORY_TABLE = "CREATE TABLE " + TABLE_HISTORY + '(' + KEY_ID
|
||||
+ " INTEGER PRIMARY KEY," + KEY_URL + " TEXT," + KEY_TITLE + " TEXT,"
|
||||
+ KEY_TIME_VISITED + " INTEGER" + ')';
|
||||
db.execSQL(CREATE_HISTORY_TABLE);
|
||||
}
|
||||
|
||||
// Upgrading database
|
||||
@Override
|
||||
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
// Drop older table if it exists
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
|
||||
// Create tables again
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public synchronized void deleteHistory() {
|
||||
mDatabase = openIfNecessary();
|
||||
mDatabase.delete(TABLE_HISTORY, null, null);
|
||||
mDatabase.close();
|
||||
mDatabase = this.getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (mDatabase != null) {
|
||||
mDatabase.close();
|
||||
mDatabase = null;
|
||||
}
|
||||
super.close();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private SQLiteDatabase openIfNecessary() {
|
||||
if (mDatabase == null || !mDatabase.isOpen()) {
|
||||
mDatabase = this.getWritableDatabase();
|
||||
}
|
||||
return mDatabase;
|
||||
}
|
||||
|
||||
public synchronized void deleteHistoryItem(@NonNull String url) {
|
||||
mDatabase = openIfNecessary();
|
||||
mDatabase.delete(TABLE_HISTORY, KEY_URL + " = ?", new String[]{url});
|
||||
}
|
||||
|
||||
public synchronized void visitHistoryItem(@NonNull String url, @Nullable String title) {
|
||||
mDatabase = openIfNecessary();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_TITLE, title == null ? "" : title);
|
||||
values.put(KEY_TIME_VISITED, System.currentTimeMillis());
|
||||
Cursor q = mDatabase.query(false, TABLE_HISTORY, new String[]{KEY_URL},
|
||||
KEY_URL + " = ?", new String[]{url}, null, null, null, "1");
|
||||
if (q.getCount() > 0) {
|
||||
mDatabase.update(TABLE_HISTORY, values, KEY_URL + " = ?", new String[]{url});
|
||||
} else {
|
||||
addHistoryItem(new HistoryItem(url, title == null ? "" : title));
|
||||
}
|
||||
q.close();
|
||||
}
|
||||
|
||||
private synchronized void addHistoryItem(@NonNull HistoryItem item) {
|
||||
mDatabase = openIfNecessary();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_URL, item.getUrl());
|
||||
values.put(KEY_TITLE, item.getTitle());
|
||||
values.put(KEY_TIME_VISITED, System.currentTimeMillis());
|
||||
mDatabase.insert(TABLE_HISTORY, null, values);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
synchronized String getHistoryItem(@NonNull String url) {
|
||||
mDatabase = openIfNecessary();
|
||||
Cursor cursor = mDatabase.query(TABLE_HISTORY, new String[]{KEY_ID, KEY_URL, KEY_TITLE},
|
||||
KEY_URL + " = ?", new String[]{url}, null, null, null, null);
|
||||
String m = null;
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
m = cursor.getString(0);
|
||||
|
||||
cursor.close();
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized List<HistoryItem> findItemsContaining(@Nullable String search) {
|
||||
mDatabase = openIfNecessary();
|
||||
List<HistoryItem> itemList = new ArrayList<>(5);
|
||||
if (search == null) {
|
||||
return itemList;
|
||||
}
|
||||
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " WHERE " + KEY_TITLE + " LIKE '%"
|
||||
+ search + "%' OR " + KEY_URL + " LIKE '%" + search + "%' " + "ORDER BY "
|
||||
+ KEY_TIME_VISITED + " DESC LIMIT 5";
|
||||
Cursor cursor = mDatabase.rawQuery(selectQuery, null);
|
||||
|
||||
int n = 0;
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
HistoryItem item = new HistoryItem();
|
||||
item.setUrl(cursor.getString(1));
|
||||
item.setTitle(cursor.getString(2));
|
||||
item.setImageId(R.drawable.ic_history);
|
||||
itemList.add(item);
|
||||
n++;
|
||||
} while (cursor.moveToNext() && n < 5);
|
||||
}
|
||||
cursor.close();
|
||||
return itemList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized List<HistoryItem> getLastHundredItems() {
|
||||
mDatabase = openIfNecessary();
|
||||
List<HistoryItem> itemList = new ArrayList<>(100);
|
||||
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED
|
||||
+ " DESC";
|
||||
|
||||
Cursor cursor = mDatabase.rawQuery(selectQuery, null);
|
||||
int counter = 0;
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
HistoryItem item = new HistoryItem();
|
||||
item.setUrl(cursor.getString(1));
|
||||
item.setTitle(cursor.getString(2));
|
||||
item.setImageId(R.drawable.ic_history);
|
||||
itemList.add(item);
|
||||
counter++;
|
||||
} while (cursor.moveToNext() && counter < 100);
|
||||
}
|
||||
cursor.close();
|
||||
return itemList;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public synchronized List<HistoryItem> getAllHistoryItems() {
|
||||
mDatabase = openIfNecessary();
|
||||
List<HistoryItem> itemList = new ArrayList<>();
|
||||
String selectQuery = "SELECT * FROM " + TABLE_HISTORY + " ORDER BY " + KEY_TIME_VISITED
|
||||
+ " DESC";
|
||||
|
||||
Cursor cursor = mDatabase.rawQuery(selectQuery, null);
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
HistoryItem item = new HistoryItem();
|
||||
item.setUrl(cursor.getString(1));
|
||||
item.setTitle(cursor.getString(2));
|
||||
item.setImageId(R.drawable.ic_history);
|
||||
itemList.add(item);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
cursor.close();
|
||||
return itemList;
|
||||
}
|
||||
|
||||
public synchronized int getHistoryItemsCount() {
|
||||
mDatabase = openIfNecessary();
|
||||
String countQuery = "SELECT * FROM " + TABLE_HISTORY;
|
||||
Cursor cursor = mDatabase.rawQuery(countQuery, null);
|
||||
int n = cursor.getCount();
|
||||
cursor.close();
|
||||
return n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.database;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import acr.browser.lightning.utils.Preconditions;
|
||||
|
||||
public class HistoryItem implements Comparable<HistoryItem> {
|
||||
|
||||
// private variables
|
||||
@NonNull
|
||||
private String mUrl = "";
|
||||
|
||||
@NonNull
|
||||
private String mTitle = "";
|
||||
|
||||
@NonNull
|
||||
private String mFolder = "";
|
||||
|
||||
@Nullable
|
||||
private Bitmap mBitmap = null;
|
||||
|
||||
private int mImageId = 0;
|
||||
private int mOrder = 0;
|
||||
private boolean mIsFolder = false;
|
||||
|
||||
public HistoryItem() {}
|
||||
|
||||
public HistoryItem(@NonNull HistoryItem item) {
|
||||
this.mUrl = item.mUrl;
|
||||
this.mTitle = item.mTitle;
|
||||
this.mFolder = item.mFolder;
|
||||
this.mOrder = item.mOrder;
|
||||
this.mIsFolder = item.mIsFolder;
|
||||
}
|
||||
|
||||
public HistoryItem(@NonNull String url, @NonNull String title) {
|
||||
Preconditions.checkNonNull(url);
|
||||
Preconditions.checkNonNull(title);
|
||||
this.mUrl = url;
|
||||
this.mTitle = title;
|
||||
this.mBitmap = null;
|
||||
}
|
||||
|
||||
public HistoryItem(@NonNull String url, @NonNull String title, int imageId) {
|
||||
Preconditions.checkNonNull(url);
|
||||
Preconditions.checkNonNull(title);
|
||||
this.mUrl = url;
|
||||
this.mTitle = title;
|
||||
this.mBitmap = null;
|
||||
this.mImageId = imageId;
|
||||
}
|
||||
|
||||
public int getImageId() {
|
||||
return this.mImageId;
|
||||
}
|
||||
|
||||
public void setImageId(int id) {
|
||||
this.mImageId = id;
|
||||
}
|
||||
|
||||
public void setBitmap(Bitmap image) {
|
||||
mBitmap = image;
|
||||
}
|
||||
|
||||
public void setFolder(@Nullable String folder) {
|
||||
mFolder = (folder == null) ? "" : folder;
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
mOrder = order;
|
||||
}
|
||||
|
||||
public int getOrder() {
|
||||
return mOrder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getFolder() {
|
||||
return mFolder;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Bitmap getBitmap() {
|
||||
return mBitmap;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getUrl() {
|
||||
return this.mUrl;
|
||||
}
|
||||
|
||||
public void setUrl(@Nullable String url) {
|
||||
this.mUrl = (url == null) ? "" : url;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getTitle() {
|
||||
return this.mTitle;
|
||||
}
|
||||
|
||||
public void setTitle(@Nullable String title) {
|
||||
this.mTitle = (title == null) ? "" : title;
|
||||
}
|
||||
|
||||
public void setIsFolder(boolean isFolder) {
|
||||
mIsFolder = isFolder;
|
||||
}
|
||||
|
||||
public boolean isFolder() {
|
||||
return mIsFolder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull HistoryItem another) {
|
||||
int compare = this.mTitle.compareTo(another.mTitle);
|
||||
if (compare == 0) {
|
||||
return this.mUrl.compareTo(another.mUrl);
|
||||
}
|
||||
return compare;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object object) {
|
||||
|
||||
if (this == object) return true;
|
||||
if (object == null) return false;
|
||||
if (!(object instanceof HistoryItem)) return false;
|
||||
|
||||
HistoryItem that = (HistoryItem) object;
|
||||
|
||||
return mImageId == that.mImageId &&
|
||||
this.mTitle.equals(that.mTitle) && this.mUrl.equals(that.mUrl) &&
|
||||
this.mFolder.equals(that.mFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = mUrl.hashCode();
|
||||
result = 31 * result + mImageId;
|
||||
result = 31 * result + mTitle.hashCode();
|
||||
result = 32 * result + mFolder.hashCode();
|
||||
result = 31 * result + mImageId;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package acr.browser.lightning.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.bus.BookmarkEvents;
|
||||
import acr.browser.lightning.bus.BrowserEvents;
|
||||
import acr.browser.lightning.constant.BookmarkPage;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.database.BookmarkManager;
|
||||
import acr.browser.lightning.database.HistoryDatabase;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
/**
|
||||
* TODO Rename this class it doesn't build dialogs only for bookmarks
|
||||
* <p/>
|
||||
* Created by Stefano Pacifici on 02/09/15, based on Anthony C. Restaino's code.
|
||||
*/
|
||||
public class LightningDialogBuilder {
|
||||
|
||||
@Inject BookmarkManager mBookmarkManager;
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
@Inject HistoryDatabase mHistoryDatabase;
|
||||
@Inject Bus mEventBus;
|
||||
|
||||
@Inject
|
||||
public LightningDialogBuilder() {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the appropriated dialog for the long pressed link. It means that we try to understand
|
||||
* if the link is relative to a bookmark or is just a folder.
|
||||
*
|
||||
* @param context used to show the dialog
|
||||
* @param url the long pressed url
|
||||
*/
|
||||
public void showLongPressedDialogForBookmarkUrl(@NonNull final Context context, @NonNull final String url) {
|
||||
final HistoryItem item;
|
||||
if (url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME)) {
|
||||
// TODO hacky, make a better bookmark mechanism in the future
|
||||
final Uri uri = Uri.parse(url);
|
||||
final String filename = uri.getLastPathSegment();
|
||||
final String folderTitle = filename.substring(0, filename.length() - BookmarkPage.FILENAME.length() - 1);
|
||||
item = new HistoryItem();
|
||||
item.setIsFolder(true);
|
||||
item.setTitle(folderTitle);
|
||||
item.setImageId(R.drawable.ic_folder);
|
||||
item.setUrl(Constants.FOLDER + folderTitle);
|
||||
} else {
|
||||
item = mBookmarkManager.findBookmarkForUrl(url);
|
||||
}
|
||||
if (item != null) {
|
||||
if (item.isFolder()) {
|
||||
showBookmarkFolderLongPressedDialog(context, item);
|
||||
} else {
|
||||
showLongPressedDialogForBookmarkUrl(context, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showLongPressedDialogForBookmarkUrl(@NonNull final Context context, @NonNull final HistoryItem item) {
|
||||
final DialogInterface.OnClickListener dialogClickListener =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(item.getUrl()));
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
if (mBookmarkManager.deleteBookmark(item)) {
|
||||
mEventBus.post(new BookmarkEvents.Deleted(item));
|
||||
}
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEUTRAL:
|
||||
showEditBookmarkDialog(context, item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.action_bookmarks)
|
||||
.setMessage(R.string.dialog_bookmark)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
|
||||
.setNegativeButton(R.string.action_delete, dialogClickListener)
|
||||
.setNeutralButton(R.string.action_edit, dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showEditBookmarkDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
|
||||
final AlertDialog.Builder editBookmarkDialog = new AlertDialog.Builder(context);
|
||||
editBookmarkDialog.setTitle(R.string.title_edit_bookmark);
|
||||
final View dialogLayout = View.inflate(context, R.layout.dialog_edit_bookmark, null);
|
||||
final EditText getTitle = (EditText) dialogLayout.findViewById(R.id.bookmark_title);
|
||||
getTitle.setText(item.getTitle());
|
||||
final EditText getUrl = (EditText) dialogLayout.findViewById(R.id.bookmark_url);
|
||||
getUrl.setText(item.getUrl());
|
||||
final AutoCompleteTextView getFolder =
|
||||
(AutoCompleteTextView) dialogLayout.findViewById(R.id.bookmark_folder);
|
||||
getFolder.setHint(R.string.folder);
|
||||
getFolder.setText(item.getFolder());
|
||||
final List<String> folders = mBookmarkManager.getFolderTitles();
|
||||
final ArrayAdapter<String> suggestionsAdapter = new ArrayAdapter<>(context,
|
||||
android.R.layout.simple_dropdown_item_1line, folders);
|
||||
getFolder.setThreshold(1);
|
||||
getFolder.setAdapter(suggestionsAdapter);
|
||||
editBookmarkDialog.setView(dialogLayout);
|
||||
editBookmarkDialog.setPositiveButton(context.getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
HistoryItem editedItem = new HistoryItem();
|
||||
editedItem.setTitle(getTitle.getText().toString());
|
||||
editedItem.setUrl(getUrl.getText().toString());
|
||||
editedItem.setUrl(getUrl.getText().toString());
|
||||
editedItem.setFolder(getFolder.getText().toString());
|
||||
mBookmarkManager.editBookmark(item, editedItem);
|
||||
mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
|
||||
}
|
||||
});
|
||||
editBookmarkDialog.show();
|
||||
}
|
||||
|
||||
public void showBookmarkFolderLongPressedDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
|
||||
// assert item.isFolder();
|
||||
final DialogInterface.OnClickListener dialogClickListener =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
showRenameFolderDialog(context, item);
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
mBookmarkManager.deleteFolder(item.getTitle());
|
||||
// setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
|
||||
mEventBus.post(new BookmarkEvents.Deleted(item));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.action_folder)
|
||||
.setMessage(R.string.dialog_folder)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.action_rename, dialogClickListener)
|
||||
.setNegativeButton(R.string.action_delete, dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showRenameFolderDialog(@NonNull final Context context, @NonNull final HistoryItem item) {
|
||||
// assert item.isFolder();
|
||||
final AlertDialog.Builder editFolderDialog = new AlertDialog.Builder(context);
|
||||
editFolderDialog.setTitle(R.string.title_rename_folder);
|
||||
final EditText getTitle = new EditText(context);
|
||||
getTitle.setHint(R.string.hint_title);
|
||||
getTitle.setText(item.getTitle());
|
||||
getTitle.setSingleLine();
|
||||
LinearLayout layout = new LinearLayout(context);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
int padding = Utils.dpToPx(10);
|
||||
layout.setPadding(padding, padding, padding, padding);
|
||||
layout.addView(getTitle);
|
||||
editFolderDialog.setView(layout);
|
||||
editFolderDialog.setPositiveButton(context.getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
final String oldTitle = item.getTitle();
|
||||
final String newTitle = getTitle.getText().toString();
|
||||
final HistoryItem editedItem = new HistoryItem();
|
||||
editedItem.setTitle(newTitle);
|
||||
editedItem.setUrl(Constants.FOLDER + newTitle);
|
||||
editedItem.setFolder(item.getFolder());
|
||||
editedItem.setIsFolder(true);
|
||||
mBookmarkManager.renameFolder(oldTitle, newTitle);
|
||||
mEventBus.post(new BookmarkEvents.BookmarkChanged(item, editedItem));
|
||||
}
|
||||
});
|
||||
editFolderDialog.show();
|
||||
}
|
||||
|
||||
public void showLongPressedHistoryLinkDialog(final Context context, @NonNull final String url) {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
mHistoryDatabase.deleteHistoryItem(url);
|
||||
// openHistory();
|
||||
mEventBus.post(new BrowserEvents.OpenHistoryInCurrentTab());
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEUTRAL:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(R.string.action_history)
|
||||
.setMessage(R.string.dialog_history_long_press)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
|
||||
.setNegativeButton(R.string.action_delete, dialogClickListener)
|
||||
.setNeutralButton(R.string.action_open, dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
// TODO There should be a way in which we do not need an activity reference to dowload a file
|
||||
public void showLongPressImageDialog(@NonNull final Activity activity, @NonNull final String url,
|
||||
@NonNull final String userAgent) {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEUTRAL:
|
||||
Utils.downloadFile(activity, mPreferenceManager, url, userAgent, "attachment");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(url.replace(Constants.HTTP, ""))
|
||||
.setCancelable(true)
|
||||
.setMessage(R.string.dialog_image)
|
||||
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
|
||||
.setNegativeButton(R.string.action_open, dialogClickListener)
|
||||
.setNeutralButton(R.string.action_download, dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
public void showLongPressLinkDialog(@NonNull final Context context, final String url) {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInNewTab(url));
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(url));
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEUTRAL:
|
||||
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("label", url);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context); // dialog
|
||||
builder.setTitle(url)
|
||||
.setCancelable(true)
|
||||
.setMessage(R.string.dialog_link)
|
||||
.setPositiveButton(R.string.action_new_tab, dialogClickListener)
|
||||
.setNegativeButton(R.string.action_open, dialogClickListener)
|
||||
.setNeutralButton(R.string.action_copy, dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.download;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import acr.browser.lightning.BuildConfig;
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.activity.MainActivity;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.bus.BrowserEvents;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
|
||||
/**
|
||||
* Handle download requests
|
||||
*/
|
||||
public class DownloadHandler {
|
||||
|
||||
private static final String TAG = DownloadHandler.class.getSimpleName();
|
||||
private static final String COOKIE_REQUEST_HEADER = "Cookie";
|
||||
|
||||
public static final String DEFAULT_DOWNLOAD_PATH =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
.getPath();
|
||||
|
||||
|
||||
/**
|
||||
* Notify the host application a download should be done, or that the data
|
||||
* should be streamed if a streaming viewer is available.
|
||||
*
|
||||
* @param context The context in which the download was requested.
|
||||
* @param url The full url to the content that should be downloaded
|
||||
* @param userAgent User agent of the downloading application.
|
||||
* @param contentDisposition Content-disposition http header, if present.
|
||||
* @param mimetype The mimetype of the content reported by the server
|
||||
*/
|
||||
public static void onDownloadStart(@NonNull Context context, @NonNull PreferenceManager manager, String url, String userAgent,
|
||||
@Nullable String contentDisposition, String mimetype) {
|
||||
// if we're dealing wih A/V content that's not explicitly marked
|
||||
// for download, check if it's streamable.
|
||||
if (contentDisposition == null
|
||||
|| !contentDisposition.regionMatches(true, 0, "attachment", 0, 10)) {
|
||||
// query the package manager to see if there's a registered handler
|
||||
// that matches.
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(url), mimetype);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setComponent(null);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||
intent.setSelector(null);
|
||||
}
|
||||
ResolveInfo info = context.getPackageManager().resolveActivity(intent,
|
||||
PackageManager.MATCH_DEFAULT_ONLY);
|
||||
if (info != null) {
|
||||
// If we resolved to ourselves, we don't want to attempt to
|
||||
// load the url only to try and download it again.
|
||||
if (BuildConfig.APPLICATION_ID.equals(info.activityInfo.packageName)
|
||||
|| MainActivity.class.getName().equals(info.activityInfo.name)) {
|
||||
// someone (other than us) knows how to handle this mime
|
||||
// type with this scheme, don't download.
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
return;
|
||||
} catch (ActivityNotFoundException ex) {
|
||||
// Best behavior is to fall back to a download in this
|
||||
// case
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onDownloadStartNoStream(context, manager, url, userAgent, contentDisposition, mimetype);
|
||||
}
|
||||
|
||||
// This is to work around the fact that java.net.URI throws Exceptions
|
||||
// instead of just encoding URL's properly
|
||||
// Helper method for onDownloadStartNoStream
|
||||
@NonNull
|
||||
private static String encodePath(@NonNull String path) {
|
||||
char[] chars = path.toCharArray();
|
||||
|
||||
boolean needed = false;
|
||||
for (char c : chars) {
|
||||
if (c == '[' || c == ']' || c == '|') {
|
||||
needed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!needed) {
|
||||
return path;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("");
|
||||
for (char c : chars) {
|
||||
if (c == '[' || c == ']' || c == '|') {
|
||||
sb.append('%');
|
||||
sb.append(Integer.toHexString(c));
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the host application a download should be done, even if there is a
|
||||
* streaming viewer available for thise type.
|
||||
*
|
||||
* @param context The context in which the download is requested.
|
||||
* @param url The full url to the content that should be downloaded
|
||||
* @param userAgent User agent of the downloading application.
|
||||
* @param contentDisposition Content-disposition http header, if present.
|
||||
* @param mimetype The mimetype of the content reported by the server
|
||||
*/
|
||||
/* package */
|
||||
private static void onDownloadStartNoStream(@NonNull final Context context, @NonNull PreferenceManager preferences,
|
||||
String url, String userAgent,
|
||||
String contentDisposition, @Nullable String mimetype) {
|
||||
final Bus eventBus = BrowserApp.getBus(context);
|
||||
final String filename = URLUtil.guessFileName(url, contentDisposition, mimetype);
|
||||
|
||||
// Check to see if we have an SDCard
|
||||
String status = Environment.getExternalStorageState();
|
||||
if (!status.equals(Environment.MEDIA_MOUNTED)) {
|
||||
int title;
|
||||
String msg;
|
||||
|
||||
// Check to see if the SDCard is busy, same as the music app
|
||||
if (status.equals(Environment.MEDIA_SHARED)) {
|
||||
msg = context.getString(R.string.download_sdcard_busy_dlg_msg);
|
||||
title = R.string.download_sdcard_busy_dlg_title;
|
||||
} else {
|
||||
msg = context.getString(R.string.download_no_sdcard_dlg_msg);
|
||||
title = R.string.download_no_sdcard_dlg_title;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context).setTitle(title)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert).setMessage(msg)
|
||||
.setPositiveButton(R.string.action_ok, null).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// java.net.URI is a lot stricter than KURL so we have to encode some
|
||||
// extra characters. Fix for b 2538060 and b 1634719
|
||||
WebAddress webAddress;
|
||||
try {
|
||||
webAddress = new WebAddress(url);
|
||||
webAddress.setPath(encodePath(webAddress.getPath()));
|
||||
} catch (Exception e) {
|
||||
// This only happens for very bad urls, we want to catch the
|
||||
// exception here
|
||||
Log.e(TAG, "Exception while trying to parse url '" + url + '\'', e);
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_download));
|
||||
return;
|
||||
}
|
||||
|
||||
String addressString = webAddress.toString();
|
||||
Uri uri = Uri.parse(addressString);
|
||||
final DownloadManager.Request request;
|
||||
try {
|
||||
request = new DownloadManager.Request(uri);
|
||||
} catch (IllegalArgumentException e) {
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.cannot_download));
|
||||
return;
|
||||
}
|
||||
request.setMimeType(mimetype);
|
||||
// set downloaded file destination to /sdcard/Download.
|
||||
// or, should it be set to one of several Environment.DIRECTORY* dirs
|
||||
// depending on mimetype?
|
||||
|
||||
String location = preferences.getDownloadDirectory();
|
||||
Uri downloadFolder;
|
||||
location = addNecessarySlashes(location);
|
||||
downloadFolder = Uri.parse(location);
|
||||
|
||||
File dir = new File(downloadFolder.getPath());
|
||||
if (!dir.isDirectory() && !dir.mkdirs()) {
|
||||
// Cannot make the directory
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isWriteAccessAvailable(downloadFolder)) {
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
|
||||
return;
|
||||
}
|
||||
request.setDestinationUri(Uri.parse(Constants.FILE + location + filename));
|
||||
// let this downloaded file be scanned by MediaScanner - so that it can
|
||||
// show up in Gallery app, for example.
|
||||
request.setVisibleInDownloadsUi(true);
|
||||
request.allowScanningByMediaScanner();
|
||||
request.setDescription(webAddress.getHost());
|
||||
// XXX: Have to use the old url since the cookies were stored using the
|
||||
// old percent-encoded url.
|
||||
String cookies = CookieManager.getInstance().getCookie(url);
|
||||
request.addRequestHeader(COOKIE_REQUEST_HEADER, cookies);
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
|
||||
if (mimetype == null) {
|
||||
Log.d(TAG, "Mimetype is null");
|
||||
if (TextUtils.isEmpty(addressString)) {
|
||||
return;
|
||||
}
|
||||
// We must have long pressed on a link or image to download it. We
|
||||
// are not sure of the mimetype in this case, so do a head request
|
||||
new FetchUrlMimeType(context, request, addressString, cookies, userAgent).start();
|
||||
} else {
|
||||
Log.d(TAG, "Valid mimetype, attempting to download");
|
||||
final DownloadManager manager = (DownloadManager) context
|
||||
.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
try {
|
||||
manager.enqueue(request);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Probably got a bad URL or something
|
||||
Log.e(TAG, "Unable to enqueue request", e);
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.cannot_download));
|
||||
} catch (SecurityException e) {
|
||||
// TODO write a download utility that downloads files rather than rely on the system
|
||||
// because the system can only handle Environment.getExternal... as a path
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.problem_location_download));
|
||||
}
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(
|
||||
context.getString(R.string.download_pending) + ' ' + filename));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final String sFileName = "test";
|
||||
private static final String sFileExtension = ".txt";
|
||||
|
||||
/**
|
||||
* Determine whether there is write access in the given directory. Returns false if a
|
||||
* file cannot be created in the directory or if the directory does not exist.
|
||||
*
|
||||
* @param directory the directory to check for write access
|
||||
* @return returns true if the directory can be written to or is in a directory that can
|
||||
* be written to. false if there is no write access.
|
||||
*/
|
||||
public static boolean isWriteAccessAvailable(@Nullable String directory) {
|
||||
if (directory == null || directory.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String dir = addNecessarySlashes(directory);
|
||||
dir = getFirstRealParentDirectory(dir);
|
||||
File file = new File(dir + sFileName + sFileExtension);
|
||||
for (int n = 0; n < 100; n++) {
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
if (file.createNewFile()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
return true;
|
||||
} catch (IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
file = new File(dir + sFileName + '-' + n + sFileExtension);
|
||||
}
|
||||
}
|
||||
return file.canWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first parent directory of a directory that exists. This is useful
|
||||
* for subdirectories that do not exist but their parents do.
|
||||
*
|
||||
* @param directory the directory to find the first existent parent
|
||||
* @return the first existent parent
|
||||
*/
|
||||
@Nullable
|
||||
private static String getFirstRealParentDirectory(@Nullable String directory) {
|
||||
while (true) {
|
||||
if (directory == null || directory.isEmpty()) {
|
||||
return "/";
|
||||
}
|
||||
directory = addNecessarySlashes(directory);
|
||||
File file = new File(directory);
|
||||
if (!file.isDirectory()) {
|
||||
int indexSlash = directory.lastIndexOf('/');
|
||||
if (indexSlash > 0) {
|
||||
String parent = directory.substring(0, indexSlash);
|
||||
int previousIndex = parent.lastIndexOf('/');
|
||||
if (previousIndex > 0) {
|
||||
directory = parent.substring(0, previousIndex);
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
} else {
|
||||
return directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isWriteAccessAvailable(@NonNull Uri fileUri) {
|
||||
File file = new File(fileUri.getPath());
|
||||
try {
|
||||
if (file.createNewFile()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
file.delete();
|
||||
}
|
||||
return true;
|
||||
} catch (IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String addNecessarySlashes(@Nullable String originalPath) {
|
||||
if (originalPath == null || originalPath.length() == 0) {
|
||||
return "/";
|
||||
}
|
||||
if (originalPath.charAt(originalPath.length() - 1) != '/') {
|
||||
originalPath = originalPath + '/';
|
||||
}
|
||||
if (originalPath.charAt(0) != '/') {
|
||||
originalPath = '/' + originalPath;
|
||||
}
|
||||
return originalPath;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.download;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.bus.BrowserEvents;
|
||||
|
||||
/**
|
||||
* This class is used to pull down the http headers of a given URL so that we
|
||||
* can analyse the mimetype and make any correction needed before we give the
|
||||
* URL to the download manager. This operation is needed when the user
|
||||
* long-clicks on a link or image and we don't know the mimetype. If the user
|
||||
* just clicks on the link, we will do the same steps of correcting the mimetype
|
||||
* down in android.os.webkit.LoadListener rather than handling it here.
|
||||
*/
|
||||
class FetchUrlMimeType extends Thread {
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private final DownloadManager.Request mRequest;
|
||||
|
||||
private final String mUri;
|
||||
|
||||
private final String mCookies;
|
||||
|
||||
private final String mUserAgent;
|
||||
|
||||
public FetchUrlMimeType(Context context, DownloadManager.Request request, String uri,
|
||||
String cookies, String userAgent) {
|
||||
mContext = context;
|
||||
mRequest = request;
|
||||
mUri = uri;
|
||||
mCookies = cookies;
|
||||
mUserAgent = userAgent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// User agent is likely to be null, though the AndroidHttpClient
|
||||
// seems ok with that.
|
||||
final Bus eventBus = BrowserApp.getBus(mContext);
|
||||
String mimeType = null;
|
||||
String contentDisposition = null;
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URL(mUri);
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
if (mCookies != null && !mCookies.isEmpty()) {
|
||||
connection.addRequestProperty("Cookie", mCookies);
|
||||
connection.setRequestProperty("User-Agent", mUserAgent);
|
||||
}
|
||||
connection.connect();
|
||||
// We could get a redirect here, but if we do lets let
|
||||
// the download manager take care of it, and thus trust that
|
||||
// the server sends the right mimetype
|
||||
if (connection.getResponseCode() == 200) {
|
||||
String header = connection.getHeaderField("Content-Type");
|
||||
if (header != null) {
|
||||
mimeType = header;
|
||||
final int semicolonIndex = mimeType.indexOf(';');
|
||||
if (semicolonIndex != -1) {
|
||||
mimeType = mimeType.substring(0, semicolonIndex);
|
||||
}
|
||||
}
|
||||
String contentDispositionHeader = connection.getHeaderField("Content-Disposition");
|
||||
if (contentDispositionHeader != null) {
|
||||
contentDisposition = contentDispositionHeader;
|
||||
}
|
||||
}
|
||||
} catch (@NonNull IllegalArgumentException | IOException ex) {
|
||||
if (connection != null)
|
||||
connection.disconnect();
|
||||
} finally {
|
||||
if (connection != null)
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
String filename = "";
|
||||
if (mimeType != null) {
|
||||
if (mimeType.equalsIgnoreCase("text/plain")
|
||||
|| mimeType.equalsIgnoreCase("application/octet-stream")) {
|
||||
String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
|
||||
MimeTypeMap.getFileExtensionFromUrl(mUri));
|
||||
if (newMimeType != null) {
|
||||
mRequest.setMimeType(newMimeType);
|
||||
}
|
||||
}
|
||||
filename = URLUtil.guessFileName(mUri, contentDisposition, mimeType);
|
||||
mRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
|
||||
}
|
||||
|
||||
// Start the download
|
||||
DownloadManager manager = (DownloadManager) mContext
|
||||
.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
manager.enqueue(mRequest);
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
final String file = filename;
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventBus.post(new BrowserEvents.ShowSnackBarMessage(mContext.getString(R.string.download_pending) + ' ' + file));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.download;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.webkit.DownloadListener;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
|
||||
import com.anthonycr.grant.PermissionsManager;
|
||||
import com.anthonycr.grant.PermissionsResultAction;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class LightningDownloadListener implements DownloadListener {
|
||||
|
||||
private final Activity mActivity;
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
public LightningDownloadListener(Activity context) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
mActivity = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadStart(final String url, final String userAgent,
|
||||
final String contentDisposition, final String mimetype, long contentLength) {
|
||||
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(mActivity,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
new PermissionsResultAction() {
|
||||
@Override
|
||||
public void onGranted() {
|
||||
String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
DownloadHandler.onDownloadStart(mActivity, mPreferenceManager, url, userAgent,
|
||||
contentDisposition, mimetype);
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); // dialog
|
||||
builder.setTitle(fileName)
|
||||
.setMessage(mActivity.getResources().getString(R.string.dialog_download))
|
||||
.setPositiveButton(mActivity.getResources().getString(R.string.action_download),
|
||||
dialogClickListener)
|
||||
.setNegativeButton(mActivity.getResources().getString(R.string.action_cancel),
|
||||
dialogClickListener).show();
|
||||
Log.i(Constants.TAG, "Downloading" + fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDenied(String permission) {
|
||||
//TODO show message
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.download;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static android.util.Patterns.GOOD_IRI_CHAR;
|
||||
|
||||
/**
|
||||
* Web Address Parser
|
||||
* <p/>
|
||||
* This is called WebAddress, rather than URL or URI, because it attempts to
|
||||
* parse the stuff that a user will actually type into a browser address widget.
|
||||
* <p/>
|
||||
* Unlike java.net.uri, this parser will not choke on URIs missing schemes. It
|
||||
* will only throw a ParseException if the input is really hosed.
|
||||
* <p/>
|
||||
* If given an https scheme but no port, fills in port
|
||||
*/
|
||||
class WebAddress {
|
||||
|
||||
private String mScheme;
|
||||
private String mHost;
|
||||
private int mPort;
|
||||
private String mPath;
|
||||
private String mAuthInfo;
|
||||
private static final int MATCH_GROUP_SCHEME = 1;
|
||||
private static final int MATCH_GROUP_AUTHORITY = 2;
|
||||
private static final int MATCH_GROUP_HOST = 3;
|
||||
private static final int MATCH_GROUP_PORT = 4;
|
||||
private static final int MATCH_GROUP_PATH = 5;
|
||||
private static final Pattern sAddressPattern = Pattern.compile(
|
||||
/* scheme */"(?:(http|https|file)://)?" +
|
||||
/* authority */"(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?::[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
|
||||
/* host */"([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" +
|
||||
/* port */"(?::([0-9]*))?" +
|
||||
/* path */"(/?[^#]*)?" +
|
||||
/* anchor */".*", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* Parses given URI-like string.
|
||||
*/
|
||||
public WebAddress(@Nullable String address) throws IllegalArgumentException {
|
||||
|
||||
if (address == null) {
|
||||
throw new IllegalArgumentException("address can't be null");
|
||||
}
|
||||
|
||||
mScheme = "";
|
||||
mHost = "";
|
||||
mPort = -1;
|
||||
mPath = "/";
|
||||
mAuthInfo = "";
|
||||
|
||||
Matcher m = sAddressPattern.matcher(address);
|
||||
String t;
|
||||
if (!m.matches()) {
|
||||
throw new IllegalArgumentException("Parsing of address '" + address + "' failed");
|
||||
}
|
||||
|
||||
t = m.group(MATCH_GROUP_SCHEME);
|
||||
if (t != null) {
|
||||
mScheme = t.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
t = m.group(MATCH_GROUP_AUTHORITY);
|
||||
if (t != null) {
|
||||
mAuthInfo = t;
|
||||
}
|
||||
t = m.group(MATCH_GROUP_HOST);
|
||||
if (t != null) {
|
||||
mHost = t;
|
||||
}
|
||||
t = m.group(MATCH_GROUP_PORT);
|
||||
if (t != null && !t.isEmpty()) {
|
||||
// The ':' character is not returned by the regex.
|
||||
try {
|
||||
mPort = Integer.parseInt(t);
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new RuntimeException("Parsing of port number failed", ex);
|
||||
}
|
||||
}
|
||||
t = m.group(MATCH_GROUP_PATH);
|
||||
if (t != null && !t.isEmpty()) {
|
||||
/*
|
||||
* handle busted myspace frontpage redirect with missing initial "/"
|
||||
*/
|
||||
if (t.charAt(0) == '/') {
|
||||
mPath = t;
|
||||
} else {
|
||||
mPath = '/' + t;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get port from scheme or scheme from port, if necessary and possible
|
||||
*/
|
||||
if (mPort == 443 && mScheme != null && mScheme.isEmpty()) {
|
||||
mScheme = "https";
|
||||
} else if (mPort == -1) {
|
||||
if ("https".equals(mScheme)) {
|
||||
mPort = 443;
|
||||
} else {
|
||||
mPort = 80; // default
|
||||
}
|
||||
}
|
||||
if (mScheme != null && mScheme.isEmpty()) {
|
||||
mScheme = "http";
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
String port = "";
|
||||
if ((mPort != 443 && "https".equals(mScheme)) || (mPort != 80 && "http".equals(mScheme))) {
|
||||
port = ':' + Integer.toString(mPort);
|
||||
}
|
||||
String authInfo = "";
|
||||
if (!mAuthInfo.isEmpty()) {
|
||||
authInfo = mAuthInfo + '@';
|
||||
}
|
||||
|
||||
return mScheme + "://" + authInfo + mHost + port + mPath;
|
||||
}
|
||||
|
||||
public void setScheme(String scheme) {
|
||||
mScheme = scheme;
|
||||
}
|
||||
|
||||
public String getScheme() {
|
||||
return mScheme;
|
||||
}
|
||||
|
||||
public void setHost(@NonNull String host) {
|
||||
mHost = host;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return mHost;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
mPort = port;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return mPort;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
mPath = path;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public void setAuthInfo(String authInfo) {
|
||||
mAuthInfo = authInfo;
|
||||
}
|
||||
|
||||
public String getAuthInfo() {
|
||||
return mAuthInfo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
|
||||
public class AboutSettingsFragment extends PreferenceFragment {
|
||||
|
||||
private Activity mActivity;
|
||||
|
||||
private static final String SETTINGS_VERSION = "pref_version";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preference_about);
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
Preference version = findPreference(SETTINGS_VERSION);
|
||||
version.setSummary(getVersion());
|
||||
}
|
||||
|
||||
private String getVersion() {
|
||||
try {
|
||||
PackageInfo p = mActivity.getPackageManager().getPackageInfo(mActivity.getPackageName(), 0);
|
||||
return p.versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return "1.0";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.Preference;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
|
||||
public class AdvancedSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
|
||||
private static final String SETTINGS_NEWWINDOW = "allow_new_window";
|
||||
private static final String SETTINGS_ENABLECOOKIES = "allow_cookies";
|
||||
private static final String SETTINGS_COOKIESINKOGNITO = "incognito_cookies";
|
||||
private static final String SETTINGS_RESTORETABS = "restore_tabs";
|
||||
private static final String SETTINGS_RENDERINGMODE = "rendering_mode";
|
||||
private static final String SETTINGS_URLCONTENT = "url_contents";
|
||||
private static final String SETTINGS_TEXTENCODING = "text_encoding";
|
||||
|
||||
private Activity mActivity;
|
||||
private CheckBoxPreference cbAllowPopups, cbenablecookies, cbcookiesInkognito, cbrestoreTabs;
|
||||
private Preference renderingmode, urlcontent, textEncoding;
|
||||
private CharSequence[] mUrlOptions;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preference_advanced);
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
initPrefs();
|
||||
}
|
||||
|
||||
private void initPrefs() {
|
||||
|
||||
renderingmode = findPreference(SETTINGS_RENDERINGMODE);
|
||||
textEncoding = findPreference(SETTINGS_TEXTENCODING);
|
||||
urlcontent = findPreference(SETTINGS_URLCONTENT);
|
||||
cbAllowPopups = (CheckBoxPreference) findPreference(SETTINGS_NEWWINDOW);
|
||||
cbenablecookies = (CheckBoxPreference) findPreference(SETTINGS_ENABLECOOKIES);
|
||||
cbcookiesInkognito = (CheckBoxPreference) findPreference(SETTINGS_COOKIESINKOGNITO);
|
||||
cbrestoreTabs = (CheckBoxPreference) findPreference(SETTINGS_RESTORETABS);
|
||||
|
||||
renderingmode.setOnPreferenceClickListener(this);
|
||||
textEncoding.setOnPreferenceClickListener(this);
|
||||
urlcontent.setOnPreferenceClickListener(this);
|
||||
cbAllowPopups.setOnPreferenceChangeListener(this);
|
||||
cbenablecookies.setOnPreferenceChangeListener(this);
|
||||
cbcookiesInkognito.setOnPreferenceChangeListener(this);
|
||||
cbrestoreTabs.setOnPreferenceChangeListener(this);
|
||||
|
||||
switch (mPreferenceManager.getRenderingMode()) {
|
||||
case 0:
|
||||
renderingmode.setSummary(getString(R.string.name_normal));
|
||||
break;
|
||||
case 1:
|
||||
renderingmode.setSummary(getString(R.string.name_inverted));
|
||||
break;
|
||||
case 2:
|
||||
renderingmode.setSummary(getString(R.string.name_grayscale));
|
||||
break;
|
||||
case 3:
|
||||
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
|
||||
break;
|
||||
case 4:
|
||||
renderingmode.setSummary(getString(R.string.name_increase_contrast));
|
||||
break;
|
||||
}
|
||||
|
||||
textEncoding.setSummary(mPreferenceManager.getTextEncoding());
|
||||
|
||||
mUrlOptions = getResources().getStringArray(R.array.url_content_array);
|
||||
int option = mPreferenceManager.getUrlBoxContentChoice();
|
||||
urlcontent.setSummary(mUrlOptions[option]);
|
||||
|
||||
cbAllowPopups.setChecked(mPreferenceManager.getPopupsEnabled());
|
||||
cbenablecookies.setChecked(mPreferenceManager.getCookiesEnabled());
|
||||
cbcookiesInkognito.setChecked(mPreferenceManager.getIncognitoCookiesEnabled());
|
||||
cbrestoreTabs.setChecked(mPreferenceManager.getRestoreLostTabsEnabled());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_RENDERINGMODE:
|
||||
renderPicker();
|
||||
return true;
|
||||
case SETTINGS_URLCONTENT:
|
||||
urlBoxPicker();
|
||||
return true;
|
||||
case SETTINGS_TEXTENCODING:
|
||||
textEncodingPicker();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
// switch preferences
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_NEWWINDOW:
|
||||
mPreferenceManager.setPopupsEnabled((Boolean) newValue);
|
||||
cbAllowPopups.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_ENABLECOOKIES:
|
||||
mPreferenceManager.setCookiesEnabled((Boolean) newValue);
|
||||
cbenablecookies.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_COOKIESINKOGNITO:
|
||||
mPreferenceManager.setIncognitoCookiesEnabled((Boolean) newValue);
|
||||
cbcookiesInkognito.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_RESTORETABS:
|
||||
mPreferenceManager.setRestoreLostTabsEnabled((Boolean) newValue);
|
||||
cbrestoreTabs.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderPicker() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.rendering_mode));
|
||||
CharSequence[] chars = {mActivity.getString(R.string.name_normal),
|
||||
mActivity.getString(R.string.name_inverted),
|
||||
mActivity.getString(R.string.name_grayscale),
|
||||
mActivity.getString(R.string.name_inverted_grayscale),
|
||||
mActivity.getString(R.string.name_increase_contrast)};
|
||||
|
||||
int n = mPreferenceManager.getRenderingMode();
|
||||
|
||||
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setRenderingMode(which);
|
||||
switch (which) {
|
||||
case 0:
|
||||
renderingmode.setSummary(getString(R.string.name_normal));
|
||||
break;
|
||||
case 1:
|
||||
renderingmode.setSummary(getString(R.string.name_inverted));
|
||||
break;
|
||||
case 2:
|
||||
renderingmode.setSummary(getString(R.string.name_grayscale));
|
||||
break;
|
||||
case 3:
|
||||
renderingmode.setSummary(getString(R.string.name_inverted_grayscale));
|
||||
break;
|
||||
case 4:
|
||||
renderingmode.setSummary(getString(R.string.name_increase_contrast));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private void textEncodingPicker() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.text_encoding));
|
||||
final List<String> textEncodingList = Arrays.asList(Constants.TEXT_ENCODINGS);
|
||||
int n = textEncodingList.indexOf(mPreferenceManager.getTextEncoding());
|
||||
|
||||
picker.setSingleChoiceItems(Constants.TEXT_ENCODINGS, n, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setTextEncoding(Constants.TEXT_ENCODINGS[which]);
|
||||
textEncoding.setSummary(Constants.TEXT_ENCODINGS[which]);
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private void urlBoxPicker() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.url_contents));
|
||||
|
||||
int n = mPreferenceManager.getUrlBoxContentChoice();
|
||||
|
||||
picker.setSingleChoiceItems(mUrlOptions, n, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setUrlBoxContentChoice(which);
|
||||
if (which < mUrlOptions.length) {
|
||||
urlcontent.setSummary(mUrlOptions[which]);
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import com.anthonycr.grant.PermissionsManager;
|
||||
import com.anthonycr.grant.PermissionsResultAction;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.database.BookmarkLocalSync;
|
||||
import acr.browser.lightning.database.BookmarkLocalSync.Source;
|
||||
import acr.browser.lightning.database.BookmarkManager;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.react.OnSubscribe;
|
||||
import acr.browser.lightning.react.Schedulers;
|
||||
import acr.browser.lightning.utils.Preconditions;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
public class BookmarkSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener {
|
||||
|
||||
private static final String SETTINGS_EXPORT = "export_bookmark";
|
||||
private static final String SETTINGS_IMPORT = "import_bookmark";
|
||||
private static final String SETTINGS_IMPORT_BROWSER = "import_browser";
|
||||
private static final String SETTINGS_DELETE_BOOKMARKS = "delete_bookmarks";
|
||||
|
||||
@Nullable private Activity mActivity;
|
||||
|
||||
@Inject BookmarkManager mBookmarkManager;
|
||||
private File[] mFileList;
|
||||
private String[] mFileNameList;
|
||||
@Nullable private BookmarkLocalSync mSync;
|
||||
|
||||
private static final String[] REQUIRED_PERMISSIONS = new String[]{
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
};
|
||||
private static final File mPath = new File(Environment.getExternalStorageDirectory().toString());
|
||||
|
||||
private class ImportBookmarksTask extends AsyncTask<Void, Void, Integer> {
|
||||
|
||||
@NonNull private final WeakReference<Activity> mActivityReference;
|
||||
private final Source mSource;
|
||||
|
||||
public ImportBookmarksTask(Activity activity, Source source) {
|
||||
mActivityReference = new WeakReference<>(activity);
|
||||
mSource = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(Void... params) {
|
||||
List<HistoryItem> list;
|
||||
Log.d(Constants.TAG, "Loading bookmarks from: " + mSource.name());
|
||||
switch (mSource) {
|
||||
case STOCK:
|
||||
list = getSync().getBookmarksFromStockBrowser();
|
||||
break;
|
||||
case CHROME_STABLE:
|
||||
list = getSync().getBookmarksFromChrome();
|
||||
break;
|
||||
case CHROME_BETA:
|
||||
list = getSync().getBookmarksFromChromeBeta();
|
||||
break;
|
||||
case CHROME_DEV:
|
||||
list = getSync().getBookmarksFromChromeDev();
|
||||
break;
|
||||
default:
|
||||
list = new ArrayList<>(0);
|
||||
break;
|
||||
}
|
||||
int count = 0;
|
||||
if (!list.isEmpty()) {
|
||||
mBookmarkManager.addBookmarkList(list);
|
||||
count = list.size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer num) {
|
||||
super.onPostExecute(num);
|
||||
Activity activity = mActivityReference.get();
|
||||
if (activity != null) {
|
||||
int number = num;
|
||||
final String message = activity.getResources().getString(R.string.message_import);
|
||||
Utils.showSnackbar(activity, number + " " + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private BookmarkLocalSync getSync() {
|
||||
Preconditions.checkNonNull(mActivity);
|
||||
if (mSync == null) {
|
||||
mSync = new BookmarkLocalSync(mActivity);
|
||||
}
|
||||
|
||||
return mSync;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preference_bookmarks);
|
||||
|
||||
mActivity = getActivity();
|
||||
mSync = new BookmarkLocalSync(mActivity);
|
||||
|
||||
initPrefs();
|
||||
|
||||
PermissionsManager permissionsManager = PermissionsManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
permissionsManager.requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mActivity = null;
|
||||
}
|
||||
|
||||
private void initPrefs() {
|
||||
|
||||
Preference exportPref = findPreference(SETTINGS_EXPORT);
|
||||
Preference importPref = findPreference(SETTINGS_IMPORT);
|
||||
Preference deletePref = findPreference(SETTINGS_DELETE_BOOKMARKS);
|
||||
|
||||
exportPref.setOnPreferenceClickListener(this);
|
||||
importPref.setOnPreferenceClickListener(this);
|
||||
deletePref.setOnPreferenceClickListener(this);
|
||||
|
||||
BrowserApp.getTaskThread().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final boolean isBrowserImportSupported = getSync().isBrowserImportSupported();
|
||||
Schedulers.main().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Preference importStock = findPreference(SETTINGS_IMPORT_BROWSER);
|
||||
importStock.setEnabled(isBrowserImportSupported);
|
||||
importStock.setOnPreferenceClickListener(BookmarkSettingsFragment.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_EXPORT:
|
||||
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
|
||||
new PermissionsResultAction() {
|
||||
@Override
|
||||
public void onGranted() {
|
||||
mBookmarkManager.exportBookmarks(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDenied(String permission) {
|
||||
//TODO Show message
|
||||
}
|
||||
});
|
||||
return true;
|
||||
case SETTINGS_IMPORT:
|
||||
PermissionsManager.getInstance().requestPermissionsIfNecessaryForResult(getActivity(), REQUIRED_PERMISSIONS,
|
||||
new PermissionsResultAction() {
|
||||
@Override
|
||||
public void onGranted() {
|
||||
loadFileList(null);
|
||||
createDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDenied(String permission) {
|
||||
//TODO Show message
|
||||
}
|
||||
});
|
||||
return true;
|
||||
case SETTINGS_IMPORT_BROWSER:
|
||||
getSync().getSupportedBrowsers().subscribeOn(Schedulers.worker())
|
||||
.observeOn(Schedulers.main()).subscribe(new OnSubscribe<List<Source>>() {
|
||||
@Override
|
||||
public void onNext(@Nullable List<Source> items) {
|
||||
Activity activity = getActivity();
|
||||
if (items == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
List<String> titles = buildTitleList(activity, items);
|
||||
showChooserDialog(activity, titles);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
case SETTINGS_DELETE_BOOKMARKS:
|
||||
showDeleteBookmarksDialog();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void showDeleteBookmarksDialog() {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.action_delete);
|
||||
builder.setMessage(R.string.action_delete_all_bookmarks);
|
||||
builder.setNegativeButton(R.string.no, null);
|
||||
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mBookmarkManager.deleteAllBookmarks();
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<String> buildTitleList(@NonNull Activity activity, @NonNull List<Source> items) {
|
||||
List<String> titles = new ArrayList<>();
|
||||
String title;
|
||||
for (Source source : items) {
|
||||
switch (source) {
|
||||
case STOCK:
|
||||
titles.add(getString(R.string.stock_browser));
|
||||
break;
|
||||
case CHROME_STABLE:
|
||||
title = getTitle(activity, "com.android.chrome");
|
||||
if (title != null) {
|
||||
titles.add(title);
|
||||
}
|
||||
break;
|
||||
case CHROME_BETA:
|
||||
title = getTitle(activity, "com.chrome.beta");
|
||||
if (title != null) {
|
||||
titles.add(title);
|
||||
}
|
||||
break;
|
||||
case CHROME_DEV:
|
||||
title = getTitle(activity, "com.chrome.beta");
|
||||
if (title != null) {
|
||||
titles.add(title);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
|
||||
private void showChooserDialog(final Activity activity, List<String> list) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
final ArrayAdapter<String> adapter = new ArrayAdapter<>(activity,
|
||||
android.R.layout.simple_list_item_1);
|
||||
for (String title : list) {
|
||||
adapter.add(title);
|
||||
}
|
||||
builder.setTitle(R.string.supported_browsers_title);
|
||||
builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String title = adapter.getItem(which);
|
||||
Source source = null;
|
||||
if (title.equals(getString(R.string.stock_browser))) {
|
||||
source = Source.STOCK;
|
||||
} else if (title.equals(getTitle(activity, "com.android.chrome"))) {
|
||||
source = Source.CHROME_STABLE;
|
||||
} else if (title.equals(getTitle(activity, "com.android.beta"))) {
|
||||
source = Source.CHROME_BETA;
|
||||
} else if (title.equals(getTitle(activity, "com.android.dev"))) {
|
||||
source = Source.CHROME_DEV;
|
||||
}
|
||||
if (source != null) {
|
||||
new ImportBookmarksTask(activity, source).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getTitle(@NonNull Activity activity, @NonNull String packageName) {
|
||||
PackageManager pm = activity.getPackageManager();
|
||||
try {
|
||||
ApplicationInfo info = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
|
||||
CharSequence title = pm.getApplicationLabel(info);
|
||||
if (title != null) {
|
||||
return title.toString();
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void loadFileList(@Nullable File path) {
|
||||
File file;
|
||||
if (path != null) {
|
||||
file = path;
|
||||
} else {
|
||||
file = mPath;
|
||||
}
|
||||
try {
|
||||
file.mkdirs();
|
||||
} catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (file.exists()) {
|
||||
mFileList = file.listFiles();
|
||||
} else {
|
||||
mFileList = new File[0];
|
||||
}
|
||||
|
||||
if (mFileList == null) {
|
||||
mFileNameList = new String[0];
|
||||
mFileList = new File[0];
|
||||
} else {
|
||||
Arrays.sort(mFileList, new SortName());
|
||||
mFileNameList = new String[mFileList.length];
|
||||
}
|
||||
for (int n = 0; n < mFileList.length; n++) {
|
||||
mFileNameList[n] = mFileList[n].getName();
|
||||
}
|
||||
}
|
||||
|
||||
private static class SortName implements Comparator<File> {
|
||||
|
||||
@Override
|
||||
public int compare(@NonNull File a, @NonNull File b) {
|
||||
if (a.isDirectory() && b.isDirectory())
|
||||
return a.getName().compareTo(b.getName());
|
||||
|
||||
if (a.isDirectory())
|
||||
return -1;
|
||||
|
||||
if (b.isDirectory())
|
||||
return 1;
|
||||
|
||||
if (a.isFile() && b.isFile())
|
||||
return a.getName().compareTo(b.getName());
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void createDialog() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
|
||||
|
||||
final String title = getString(R.string.title_chooser);
|
||||
builder.setTitle(title + ": " + Environment.getExternalStorageDirectory());
|
||||
if (mFileList == null) {
|
||||
builder.show();
|
||||
}
|
||||
builder.setItems(mFileNameList, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mFileList[which].isDirectory()) {
|
||||
builder.setTitle(title + ": " + mFileList[which]);
|
||||
loadFileList(mFileList[which]);
|
||||
builder.setItems(mFileNameList, this);
|
||||
builder.show();
|
||||
} else {
|
||||
mBookmarkManager.importBookmarksFromFile(mFileList[which], getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Transformation;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.AdapterView.OnItemLongClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.activity.ReadingActivity;
|
||||
import acr.browser.lightning.activity.TabsManager;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.async.AsyncExecutor;
|
||||
import acr.browser.lightning.bus.BookmarkEvents;
|
||||
import acr.browser.lightning.bus.BrowserEvents;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.controller.UIController;
|
||||
import acr.browser.lightning.database.BookmarkManager;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.dialog.LightningDialogBuilder;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.async.ImageDownloadTask;
|
||||
import acr.browser.lightning.react.Action;
|
||||
import acr.browser.lightning.react.Observable;
|
||||
import acr.browser.lightning.react.OnSubscribe;
|
||||
import acr.browser.lightning.react.Schedulers;
|
||||
import acr.browser.lightning.react.Subscriber;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
public class BookmarksFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
private final static String TAG = BookmarksFragment.class.getSimpleName();
|
||||
|
||||
public final static String INCOGNITO_MODE = TAG + ".INCOGNITO_MODE";
|
||||
|
||||
// Managers
|
||||
@Inject BookmarkManager mBookmarkManager;
|
||||
|
||||
// Event bus
|
||||
@Inject Bus mEventBus;
|
||||
|
||||
// Dialog builder
|
||||
@Inject LightningDialogBuilder mBookmarksDialogBuilder;
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
private TabsManager mTabsManager;
|
||||
|
||||
// Adapter
|
||||
private BookmarkViewAdapter mBookmarkAdapter;
|
||||
|
||||
// Preloaded images
|
||||
private Bitmap mWebpageBitmap, mFolderBitmap;
|
||||
|
||||
// Bookmarks
|
||||
private final List<HistoryItem> mBookmarks = new ArrayList<>();
|
||||
|
||||
// Views
|
||||
private ListView mBookmarksListView;
|
||||
private ImageView mBookmarkTitleImage, mBookmarkImage;
|
||||
|
||||
// Colors
|
||||
private int mIconColor, mScrollIndex;
|
||||
|
||||
private boolean mIsIncognito;
|
||||
|
||||
private Observable<BookmarkViewAdapter> initBookmarkManager() {
|
||||
return Observable.create(new Action<BookmarkViewAdapter>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Subscriber<BookmarkViewAdapter> subscriber) {
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
mBookmarkAdapter = new BookmarkViewAdapter(context, mBookmarks);
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
|
||||
subscriber.onNext(mBookmarkAdapter);
|
||||
}
|
||||
subscriber.onComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
final Bundle arguments = getArguments();
|
||||
final Context context = getContext();
|
||||
mTabsManager = ((UIController) context).getTabModel();
|
||||
mIsIncognito = arguments.getBoolean(INCOGNITO_MODE, false);
|
||||
boolean darkTheme = mPreferenceManager.getUseTheme() != 0 || mIsIncognito;
|
||||
mWebpageBitmap = ThemeUtils.getThemedBitmap(context, R.drawable.ic_webpage, darkTheme);
|
||||
mFolderBitmap = ThemeUtils.getThemedBitmap(context, R.drawable.ic_folder, darkTheme);
|
||||
mIconColor = darkTheme ? ThemeUtils.getIconDarkThemeColor(context) :
|
||||
ThemeUtils.getIconLightThemeColor(context);
|
||||
}
|
||||
|
||||
// Handle bookmark click
|
||||
private final OnItemClickListener mItemClickListener = new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final HistoryItem item = mBookmarks.get(position);
|
||||
if (item.isFolder()) {
|
||||
mScrollIndex = mBookmarksListView.getFirstVisiblePosition();
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(item.getTitle(), true), true);
|
||||
} else {
|
||||
mEventBus.post(new BrowserEvents.OpenUrlInCurrentTab(item.getUrl()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final HistoryItem item = mBookmarks.get(position);
|
||||
handleLongPress(item);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (mBookmarkAdapter != null) {
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.bookmark_drawer, container, false);
|
||||
mBookmarksListView = (ListView) view.findViewById(R.id.right_drawer_list);
|
||||
mBookmarksListView.setOnItemClickListener(mItemClickListener);
|
||||
mBookmarksListView.setOnItemLongClickListener(mItemLongClickListener);
|
||||
mBookmarkTitleImage = (ImageView) view.findViewById(R.id.starIcon);
|
||||
mBookmarkTitleImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
|
||||
mBookmarkImage = (ImageView) view.findViewById(R.id.icon_star);
|
||||
final View backView = view.findViewById(R.id.bookmark_back_button);
|
||||
backView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mBookmarkManager == null) return;
|
||||
if (!mBookmarkManager.isRootFolder()) {
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true);
|
||||
mBookmarksListView.setSelection(mScrollIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
setupNavigationButton(view, R.id.action_add_bookmark, R.id.icon_star);
|
||||
setupNavigationButton(view, R.id.action_reading, R.id.icon_reading);
|
||||
setupNavigationButton(view, R.id.action_toggle_desktop, R.id.icon_desktop);
|
||||
|
||||
initBookmarkManager().subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.main())
|
||||
.subscribe(new OnSubscribe<BookmarkViewAdapter>() {
|
||||
@Override
|
||||
public void onNext(@Nullable BookmarkViewAdapter item) {
|
||||
mBookmarksListView.setAdapter(mBookmarkAdapter);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
mEventBus.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
mEventBus.unregister(this);
|
||||
}
|
||||
|
||||
public void reinitializePreferences() {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
boolean darkTheme = mPreferenceManager.getUseTheme() != 0 || mIsIncognito;
|
||||
mWebpageBitmap = ThemeUtils.getThemedBitmap(activity, R.drawable.ic_webpage, darkTheme);
|
||||
mFolderBitmap = ThemeUtils.getThemedBitmap(activity, R.drawable.ic_folder, darkTheme);
|
||||
mIconColor = darkTheme ? ThemeUtils.getIconDarkThemeColor(activity) :
|
||||
ThemeUtils.getIconLightThemeColor(activity);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void addBookmark(@NonNull final BrowserEvents.BookmarkAdded event) {
|
||||
updateBookmarkIndicator(event.url);
|
||||
String folder = mBookmarkManager.getCurrentFolder();
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void currentPageInfo(@NonNull final BrowserEvents.CurrentPageUrl event) {
|
||||
updateBookmarkIndicator(event.url);
|
||||
String folder = mBookmarkManager.getCurrentFolder();
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bookmarkChanged(BookmarkEvents.BookmarkChanged event) {
|
||||
String folder = mBookmarkManager.getCurrentFolder();
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(folder, true), false);
|
||||
}
|
||||
|
||||
private void updateBookmarkIndicator(final String url) {
|
||||
if (!mBookmarkManager.isBookmark(url)) {
|
||||
mBookmarkImage.setImageResource(R.drawable.ic_action_star);
|
||||
mBookmarkImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
|
||||
} else {
|
||||
mBookmarkImage.setImageResource(R.drawable.ic_bookmark);
|
||||
mBookmarkImage.setColorFilter(ThemeUtils.getAccentColor(getContext()), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void userPressedBack(final BrowserEvents.UserPressedBack event) {
|
||||
if (mBookmarkManager.isRootFolder()) {
|
||||
mEventBus.post(new BookmarkEvents.CloseBookmarks());
|
||||
} else {
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), true);
|
||||
mBookmarksListView.setSelection(mScrollIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bookmarkDeleted(@NonNull final BookmarkEvents.Deleted event) {
|
||||
mBookmarks.remove(event.item);
|
||||
if (event.item.isFolder()) {
|
||||
setBookmarkDataSet(mBookmarkManager.getBookmarksFromFolder(null, true), false);
|
||||
} else {
|
||||
mBookmarkAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBookmarkDataSet(@NonNull List<HistoryItem> items, boolean animate) {
|
||||
mBookmarks.clear();
|
||||
mBookmarks.addAll(items);
|
||||
mBookmarkAdapter.notifyDataSetChanged();
|
||||
final int resource;
|
||||
if (mBookmarkManager.isRootFolder()) {
|
||||
resource = R.drawable.ic_action_star;
|
||||
} else {
|
||||
resource = R.drawable.ic_action_back;
|
||||
}
|
||||
|
||||
final Animation startRotation = new Animation() {
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
mBookmarkTitleImage.setRotationY(90 * interpolatedTime);
|
||||
}
|
||||
};
|
||||
final Animation finishRotation = new Animation() {
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
mBookmarkTitleImage.setRotationY((-90) + (90 * interpolatedTime));
|
||||
}
|
||||
};
|
||||
startRotation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mBookmarkTitleImage.setImageResource(resource);
|
||||
mBookmarkTitleImage.startAnimation(finishRotation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
});
|
||||
startRotation.setInterpolator(new AccelerateInterpolator());
|
||||
finishRotation.setInterpolator(new DecelerateInterpolator());
|
||||
startRotation.setDuration(250);
|
||||
finishRotation.setDuration(250);
|
||||
|
||||
if (animate) {
|
||||
mBookmarkTitleImage.startAnimation(startRotation);
|
||||
} else {
|
||||
mBookmarkTitleImage.setImageResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupNavigationButton(@NonNull View view, @IdRes int buttonId, @IdRes int imageId) {
|
||||
FrameLayout frameButton = (FrameLayout) view.findViewById(buttonId);
|
||||
frameButton.setOnClickListener(this);
|
||||
frameButton.setOnLongClickListener(this);
|
||||
ImageView buttonImage = (ImageView) view.findViewById(imageId);
|
||||
buttonImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
private void handleLongPress(@NonNull final HistoryItem item) {
|
||||
if (item.isFolder()) {
|
||||
mBookmarksDialogBuilder.showBookmarkFolderLongPressedDialog(getContext(), item);
|
||||
} else {
|
||||
mBookmarksDialogBuilder.showLongPressedDialogForBookmarkUrl(getContext(), item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.action_add_bookmark:
|
||||
mEventBus.post(new BookmarkEvents.ToggleBookmarkForCurrentPage());
|
||||
break;
|
||||
case R.id.action_reading:
|
||||
LightningView currentTab = mTabsManager.getCurrentTab();
|
||||
if (currentTab != null) {
|
||||
Intent read = new Intent(getActivity(), ReadingActivity.class);
|
||||
read.putExtra(Constants.LOAD_READING_URL, currentTab.getUrl());
|
||||
startActivity(read);
|
||||
}
|
||||
break;
|
||||
case R.id.action_toggle_desktop:
|
||||
LightningView current = mTabsManager.getCurrentTab();
|
||||
if (current != null) {
|
||||
current.toggleDesktopUA(getActivity());
|
||||
current.reload();
|
||||
// TODO add back drawer closing
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private class BookmarkViewAdapter extends ArrayAdapter<HistoryItem> {
|
||||
|
||||
final Context context;
|
||||
|
||||
public BookmarkViewAdapter(Context context, @NonNull List<HistoryItem> data) {
|
||||
super(context, R.layout.bookmark_list_item, data);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View row = convertView;
|
||||
BookmarkViewHolder holder;
|
||||
|
||||
if (row == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
row = inflater.inflate(R.layout.bookmark_list_item, parent, false);
|
||||
|
||||
holder = new BookmarkViewHolder();
|
||||
holder.txtTitle = (TextView) row.findViewById(R.id.textBookmark);
|
||||
holder.favicon = (ImageView) row.findViewById(R.id.faviconBookmark);
|
||||
row.setTag(holder);
|
||||
} else {
|
||||
holder = (BookmarkViewHolder) row.getTag();
|
||||
}
|
||||
|
||||
ViewCompat.jumpDrawablesToCurrentState(row);
|
||||
|
||||
HistoryItem web = mBookmarks.get(position);
|
||||
holder.txtTitle.setText(web.getTitle());
|
||||
if (web.isFolder()) {
|
||||
holder.favicon.setImageBitmap(mFolderBitmap);
|
||||
} else if (web.getBitmap() == null) {
|
||||
holder.favicon.setImageBitmap(mWebpageBitmap);
|
||||
new ImageDownloadTask(holder.favicon, web, mWebpageBitmap, context)
|
||||
.executeOnExecutor(AsyncExecutor.getInstance());
|
||||
} else {
|
||||
holder.favicon.setImageBitmap(web.getBitmap());
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private class BookmarkViewHolder {
|
||||
TextView txtTitle;
|
||||
ImageView favicon;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
public class DebugSettingsFragment extends PreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
|
||||
private static final String LEAK_CANARY = "leak_canary_enabled";
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
private SwitchPreference mSwitchLeakCanary;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
addPreferencesFromResource(R.xml.preference_debug);
|
||||
|
||||
mSwitchLeakCanary = (SwitchPreference) findPreference(LEAK_CANARY);
|
||||
mSwitchLeakCanary.setChecked(mPreferenceManager.getUseLeakCanary());
|
||||
mSwitchLeakCanary.setOnPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, @NonNull Object newValue) {
|
||||
switch (preference.getKey()) {
|
||||
case LEAK_CANARY:
|
||||
boolean value = Boolean.TRUE.equals(newValue);
|
||||
mPreferenceManager.setUseLeakCanary(value);
|
||||
Activity activity = getActivity();
|
||||
if (activity != null) {
|
||||
Utils.showSnackbar(activity, R.string.app_restart);
|
||||
}
|
||||
mSwitchLeakCanary.setChecked(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.Preference;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
|
||||
public class DisplaySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
|
||||
private static final String SETTINGS_HIDESTATUSBAR = "fullScreenOption";
|
||||
private static final String SETTINGS_FULLSCREEN = "fullscreen";
|
||||
private static final String SETTINGS_VIEWPORT = "wideViewPort";
|
||||
private static final String SETTINGS_OVERVIEWMODE = "overViewMode";
|
||||
private static final String SETTINGS_REFLOW = "text_reflow";
|
||||
private static final String SETTINGS_THEME = "app_theme";
|
||||
private static final String SETTINGS_TEXTSIZE = "text_size";
|
||||
private static final float XXLARGE = 30.0f;
|
||||
private static final float XLARGE = 26.0f;
|
||||
private static final float LARGE = 22.0f;
|
||||
private static final float MEDIUM = 18.0f;
|
||||
private static final float SMALL = 14.0f;
|
||||
private static final float XSMALL = 10.0f;
|
||||
|
||||
private Activity mActivity;
|
||||
private CheckBoxPreference cbstatus, cbfullscreen, cbviewport, cboverview, cbreflow;
|
||||
private Preference theme;
|
||||
private String[] mThemeOptions;
|
||||
private int mCurrentTheme;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preference_display);
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
initPrefs();
|
||||
}
|
||||
|
||||
private void initPrefs() {
|
||||
// mPreferences storage
|
||||
mThemeOptions = this.getResources().getStringArray(R.array.themes);
|
||||
mCurrentTheme = mPreferenceManager.getUseTheme();
|
||||
|
||||
theme = findPreference(SETTINGS_THEME);
|
||||
Preference textsize = findPreference(SETTINGS_TEXTSIZE);
|
||||
cbstatus = (CheckBoxPreference) findPreference(SETTINGS_HIDESTATUSBAR);
|
||||
cbfullscreen = (CheckBoxPreference) findPreference(SETTINGS_FULLSCREEN);
|
||||
cbviewport = (CheckBoxPreference) findPreference(SETTINGS_VIEWPORT);
|
||||
cboverview = (CheckBoxPreference) findPreference(SETTINGS_OVERVIEWMODE);
|
||||
cbreflow = (CheckBoxPreference) findPreference(SETTINGS_REFLOW);
|
||||
|
||||
theme.setOnPreferenceClickListener(this);
|
||||
textsize.setOnPreferenceClickListener(this);
|
||||
cbstatus.setOnPreferenceChangeListener(this);
|
||||
cbfullscreen.setOnPreferenceChangeListener(this);
|
||||
cbviewport.setOnPreferenceChangeListener(this);
|
||||
cboverview.setOnPreferenceChangeListener(this);
|
||||
cbreflow.setOnPreferenceChangeListener(this);
|
||||
|
||||
cbstatus.setChecked(mPreferenceManager.getHideStatusBarEnabled());
|
||||
cbfullscreen.setChecked(mPreferenceManager.getFullScreenEnabled());
|
||||
cbviewport.setChecked(mPreferenceManager.getUseWideViewportEnabled());
|
||||
cboverview.setChecked(mPreferenceManager.getOverviewModeEnabled());
|
||||
cbreflow.setChecked(mPreferenceManager.getTextReflowEnabled());
|
||||
|
||||
theme.setSummary(mThemeOptions[mPreferenceManager.getUseTheme()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_THEME:
|
||||
themePicker();
|
||||
return true;
|
||||
case SETTINGS_TEXTSIZE:
|
||||
textSizePicker();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
// switch preferences
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_HIDESTATUSBAR:
|
||||
mPreferenceManager.setHideStatusBarEnabled((Boolean) newValue);
|
||||
cbstatus.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_FULLSCREEN:
|
||||
mPreferenceManager.setFullScreenEnabled((Boolean) newValue);
|
||||
cbfullscreen.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_VIEWPORT:
|
||||
mPreferenceManager.setUseWideViewportEnabled((Boolean) newValue);
|
||||
cbviewport.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_OVERVIEWMODE:
|
||||
mPreferenceManager.setOverviewModeEnabled((Boolean) newValue);
|
||||
cboverview.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_REFLOW:
|
||||
mPreferenceManager.setTextReflowEnabled((Boolean) newValue);
|
||||
cbreflow.setChecked((Boolean) newValue);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void textSizePicker() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
LayoutInflater inflater = getActivity().getLayoutInflater();
|
||||
LinearLayout view = (LinearLayout) inflater.inflate(R.layout.seek_layout, null);
|
||||
final SeekBar bar = (SeekBar) view.findViewById(R.id.text_size_seekbar);
|
||||
final TextView sample = new TextView(getActivity());
|
||||
sample.setText(R.string.untitled);
|
||||
sample.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT));
|
||||
sample.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
view.addView(sample);
|
||||
bar.setOnSeekBarChangeListener(new TextSeekBarListener(sample));
|
||||
final int MAX = 5;
|
||||
bar.setMax(MAX);
|
||||
bar.setProgress(MAX - mPreferenceManager.getTextSize());
|
||||
builder.setView(view);
|
||||
builder.setTitle(R.string.title_text_size);
|
||||
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface arg0, int arg1) {
|
||||
mPreferenceManager.setTextSize(MAX - bar.getProgress());
|
||||
}
|
||||
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static float getTextSize(int size) {
|
||||
switch (size) {
|
||||
case 0:
|
||||
return XSMALL;
|
||||
case 1:
|
||||
return SMALL;
|
||||
case 2:
|
||||
return MEDIUM;
|
||||
case 3:
|
||||
return LARGE;
|
||||
case 4:
|
||||
return XLARGE;
|
||||
case 5:
|
||||
return XXLARGE;
|
||||
default:
|
||||
return MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
private void themePicker() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.theme));
|
||||
|
||||
int n = mPreferenceManager.getUseTheme();
|
||||
picker.setSingleChoiceItems(mThemeOptions, n, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setUseTheme(which);
|
||||
if (which < mThemeOptions.length) {
|
||||
theme.setSummary(mThemeOptions[which]);
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
|
||||
getActivity().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
if (mCurrentTheme != mPreferenceManager.getUseTheme()) {
|
||||
getActivity().onBackPressed();
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private static class TextSeekBarListener implements SeekBar.OnSeekBarChangeListener {
|
||||
|
||||
private final TextView sample;
|
||||
|
||||
public TextSeekBarListener(TextView sample) {this.sample = sample;}
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar view, int size, boolean user) {
|
||||
this.sample.setTextSize(getTextSize(size));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar arg0) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar arg0) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.Preference;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.download.DownloadHandler;
|
||||
import acr.browser.lightning.utils.ProxyUtils;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
public class GeneralSettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
|
||||
private static final String SETTINGS_PROXY = "proxy";
|
||||
private static final String SETTINGS_FLASH = "cb_flash";
|
||||
private static final String SETTINGS_ADS = "cb_ads";
|
||||
private static final String SETTINGS_IMAGES = "cb_images";
|
||||
private static final String SETTINGS_JAVASCRIPT = "cb_javascript";
|
||||
private static final String SETTINGS_COLORMODE = "cb_colormode";
|
||||
private static final String SETTINGS_USERAGENT = "agent";
|
||||
private static final String SETTINGS_DOWNLOAD = "download";
|
||||
private static final String SETTINGS_HOME = "home";
|
||||
private static final String SETTINGS_SEARCHENGINE = "search";
|
||||
private static final String SETTINGS_GOOGLESUGGESTIONS = "google_suggestions";
|
||||
private static final String SETTINGS_DRAWERTABS = "cb_drawertabs";
|
||||
|
||||
private Activity mActivity;
|
||||
private static final int API = android.os.Build.VERSION.SDK_INT;
|
||||
private CharSequence[] mProxyChoices;
|
||||
private Preference proxy, useragent, downloadloc, home, searchengine;
|
||||
private String mDownloadLocation;
|
||||
private int mAgentChoice;
|
||||
private String mHomepage;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preference_general);
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
initPrefs();
|
||||
}
|
||||
|
||||
private void initPrefs() {
|
||||
proxy = findPreference(SETTINGS_PROXY);
|
||||
useragent = findPreference(SETTINGS_USERAGENT);
|
||||
downloadloc = findPreference(SETTINGS_DOWNLOAD);
|
||||
home = findPreference(SETTINGS_HOME);
|
||||
searchengine = findPreference(SETTINGS_SEARCHENGINE);
|
||||
|
||||
CheckBoxPreference cbFlash = (CheckBoxPreference) findPreference(SETTINGS_FLASH);
|
||||
CheckBoxPreference cbAds = (CheckBoxPreference) findPreference(SETTINGS_ADS);
|
||||
CheckBoxPreference cbImages = (CheckBoxPreference) findPreference(SETTINGS_IMAGES);
|
||||
CheckBoxPreference cbJsScript = (CheckBoxPreference) findPreference(SETTINGS_JAVASCRIPT);
|
||||
CheckBoxPreference cbColorMode = (CheckBoxPreference) findPreference(SETTINGS_COLORMODE);
|
||||
CheckBoxPreference cbgooglesuggest = (CheckBoxPreference) findPreference(SETTINGS_GOOGLESUGGESTIONS);
|
||||
CheckBoxPreference cbDrawerTabs = (CheckBoxPreference) findPreference(SETTINGS_DRAWERTABS);
|
||||
|
||||
proxy.setOnPreferenceClickListener(this);
|
||||
useragent.setOnPreferenceClickListener(this);
|
||||
downloadloc.setOnPreferenceClickListener(this);
|
||||
home.setOnPreferenceClickListener(this);
|
||||
searchengine.setOnPreferenceClickListener(this);
|
||||
cbFlash.setOnPreferenceChangeListener(this);
|
||||
cbAds.setOnPreferenceChangeListener(this);
|
||||
cbImages.setOnPreferenceChangeListener(this);
|
||||
cbJsScript.setOnPreferenceChangeListener(this);
|
||||
cbColorMode.setOnPreferenceChangeListener(this);
|
||||
cbgooglesuggest.setOnPreferenceChangeListener(this);
|
||||
cbDrawerTabs.setOnPreferenceChangeListener(this);
|
||||
|
||||
mAgentChoice = mPreferenceManager.getUserAgentChoice();
|
||||
mHomepage = mPreferenceManager.getHomepage();
|
||||
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
|
||||
mProxyChoices = getResources().getStringArray(R.array.proxy_choices_array);
|
||||
|
||||
int choice = mPreferenceManager.getProxyChoice();
|
||||
if (choice == Constants.PROXY_MANUAL) {
|
||||
proxy.setSummary(mPreferenceManager.getProxyHost() + ':' + mPreferenceManager.getProxyPort());
|
||||
} else {
|
||||
proxy.setSummary(mProxyChoices[choice]);
|
||||
}
|
||||
|
||||
if (API >= Build.VERSION_CODES.KITKAT) {
|
||||
mPreferenceManager.setFlashSupport(0);
|
||||
}
|
||||
|
||||
setSearchEngineSummary(mPreferenceManager.getSearchChoice());
|
||||
|
||||
downloadloc.setSummary(mDownloadLocation);
|
||||
|
||||
if (mHomepage.contains("about:home")) {
|
||||
home.setSummary(getResources().getString(R.string.action_homepage));
|
||||
} else if (mHomepage.contains("about:blank")) {
|
||||
home.setSummary(getResources().getString(R.string.action_blank));
|
||||
} else if (mHomepage.contains("about:bookmarks")) {
|
||||
home.setSummary(getResources().getString(R.string.action_bookmarks));
|
||||
} else {
|
||||
home.setSummary(mHomepage);
|
||||
}
|
||||
|
||||
switch (mAgentChoice) {
|
||||
case 1:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_default));
|
||||
break;
|
||||
case 2:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_desktop));
|
||||
break;
|
||||
case 3:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_mobile));
|
||||
break;
|
||||
case 4:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_custom));
|
||||
}
|
||||
|
||||
int flashNum = mPreferenceManager.getFlashSupport();
|
||||
boolean imagesBool = mPreferenceManager.getBlockImagesEnabled();
|
||||
boolean enableJSBool = mPreferenceManager.getJavaScriptEnabled();
|
||||
|
||||
cbAds.setEnabled(Constants.FULL_VERSION);
|
||||
cbFlash.setEnabled(API < Build.VERSION_CODES.KITKAT);
|
||||
|
||||
cbImages.setChecked(imagesBool);
|
||||
cbJsScript.setChecked(enableJSBool);
|
||||
cbFlash.setChecked(flashNum > 0);
|
||||
cbAds.setChecked(Constants.FULL_VERSION && mPreferenceManager.getAdBlockEnabled());
|
||||
cbColorMode.setChecked(mPreferenceManager.getColorModeEnabled());
|
||||
cbgooglesuggest.setChecked(mPreferenceManager.getGoogleSearchSuggestionsEnabled());
|
||||
cbDrawerTabs.setChecked(mPreferenceManager.getShowTabsInDrawer(true));
|
||||
}
|
||||
|
||||
private void searchUrlPicker() {
|
||||
final AlertDialog.Builder urlPicker = new AlertDialog.Builder(mActivity);
|
||||
urlPicker.setTitle(getResources().getString(R.string.custom_url));
|
||||
final EditText getSearchUrl = new EditText(mActivity);
|
||||
String mSearchUrl = mPreferenceManager.getSearchUrl();
|
||||
getSearchUrl.setText(mSearchUrl);
|
||||
urlPicker.setView(getSearchUrl);
|
||||
urlPicker.setPositiveButton(getResources().getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String text = getSearchUrl.getText().toString();
|
||||
mPreferenceManager.setSearchUrl(text);
|
||||
searchengine.setSummary(getResources().getString(R.string.custom_url) + ": "
|
||||
+ text);
|
||||
}
|
||||
});
|
||||
urlPicker.show();
|
||||
}
|
||||
|
||||
private void getFlashChoice() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
|
||||
builder.setTitle(mActivity.getResources().getString(R.string.title_flash));
|
||||
builder.setMessage(getResources().getString(R.string.flash))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(getResources().getString(R.string.action_manual),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
mPreferenceManager.setFlashSupport(1);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(getResources().getString(R.string.action_auto),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setFlashSupport(2);
|
||||
}
|
||||
}).setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
mPreferenceManager.setFlashSupport(0);
|
||||
}
|
||||
|
||||
});
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
}
|
||||
|
||||
private void proxyChoicePicker() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.http_proxy));
|
||||
picker.setSingleChoiceItems(mProxyChoices, mPreferenceManager.getProxyChoice(),
|
||||
new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
setProxyChoice(which);
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private void setProxyChoice(int choice) {
|
||||
switch (choice) {
|
||||
case Constants.PROXY_ORBOT:
|
||||
choice = ProxyUtils.setProxyChoice(choice, mActivity);
|
||||
break;
|
||||
case Constants.PROXY_I2P:
|
||||
choice = ProxyUtils.setProxyChoice(choice, mActivity);
|
||||
break;
|
||||
case Constants.PROXY_MANUAL:
|
||||
manualProxyPicker();
|
||||
break;
|
||||
}
|
||||
|
||||
mPreferenceManager.setProxyChoice(choice);
|
||||
if (choice < mProxyChoices.length)
|
||||
proxy.setSummary(mProxyChoices[choice]);
|
||||
}
|
||||
|
||||
private void manualProxyPicker() {
|
||||
View v = mActivity.getLayoutInflater().inflate(R.layout.picker_manual_proxy, null);
|
||||
final EditText eProxyHost = (EditText) v.findViewById(R.id.proxyHost);
|
||||
final EditText eProxyPort = (EditText) v.findViewById(R.id.proxyPort);
|
||||
|
||||
// Limit the number of characters since the port needs to be of type int
|
||||
// Use input filters to limite the EditText length and determine the max
|
||||
// length by using length of integer MAX_VALUE
|
||||
int maxCharacters = Integer.toString(Integer.MAX_VALUE).length();
|
||||
InputFilter[] filterArray = new InputFilter[1];
|
||||
filterArray[0] = new InputFilter.LengthFilter(maxCharacters - 1);
|
||||
eProxyPort.setFilters(filterArray);
|
||||
|
||||
eProxyHost.setText(mPreferenceManager.getProxyHost());
|
||||
eProxyPort.setText(Integer.toString(mPreferenceManager.getProxyPort()));
|
||||
|
||||
new AlertDialog.Builder(mActivity)
|
||||
.setTitle(R.string.manual_proxy)
|
||||
.setView(v)
|
||||
.setPositiveButton(R.string.action_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
String proxyHost = eProxyHost.getText().toString();
|
||||
int proxyPort;
|
||||
try {
|
||||
// Try/Catch in case the user types an empty string or a number
|
||||
// larger than max integer
|
||||
proxyPort = Integer.parseInt(eProxyPort.getText().toString());
|
||||
} catch (NumberFormatException ignored) {
|
||||
proxyPort = mPreferenceManager.getProxyPort();
|
||||
}
|
||||
mPreferenceManager.setProxyHost(proxyHost);
|
||||
mPreferenceManager.setProxyPort(proxyPort);
|
||||
proxy.setSummary(proxyHost + ':' + proxyPort);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
|
||||
private void searchDialog() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.title_search_engine));
|
||||
CharSequence[] chars = {getResources().getString(R.string.custom_url), "Google",
|
||||
"Ask", "Bing", "Yahoo", "StartPage", "StartPage (Mobile)",
|
||||
"DuckDuckGo (Privacy)", "DuckDuckGo Lite (Privacy)", "Baidu (Chinese)",
|
||||
"Yandex (Russian)"};
|
||||
|
||||
int n = mPreferenceManager.getSearchChoice();
|
||||
|
||||
picker.setSingleChoiceItems(chars, n, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setSearchChoice(which);
|
||||
setSearchEngineSummary(which);
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private void homepageDialog() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.home));
|
||||
mHomepage = mPreferenceManager.getHomepage();
|
||||
int n;
|
||||
if (mHomepage.contains("about:home")) {
|
||||
n = 1;
|
||||
} else if (mHomepage.contains("about:blank")) {
|
||||
n = 2;
|
||||
} else if (mHomepage.contains("about:bookmarks")) {
|
||||
n = 3;
|
||||
} else {
|
||||
n = 4;
|
||||
}
|
||||
|
||||
picker.setSingleChoiceItems(R.array.homepage, n - 1,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which + 1) {
|
||||
case 1:
|
||||
mPreferenceManager.setHomepage("about:home");
|
||||
home.setSummary(getResources().getString(R.string.action_homepage));
|
||||
break;
|
||||
case 2:
|
||||
mPreferenceManager.setHomepage("about:blank");
|
||||
home.setSummary(getResources().getString(R.string.action_blank));
|
||||
break;
|
||||
case 3:
|
||||
mPreferenceManager.setHomepage("about:bookmarks");
|
||||
home.setSummary(getResources().getString(R.string.action_bookmarks));
|
||||
break;
|
||||
case 4:
|
||||
homePicker();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private void homePicker() {
|
||||
final AlertDialog.Builder homePicker = new AlertDialog.Builder(mActivity);
|
||||
homePicker.setTitle(getResources().getString(R.string.title_custom_homepage));
|
||||
final EditText getHome = new EditText(mActivity);
|
||||
mHomepage = mPreferenceManager.getHomepage();
|
||||
if (!mHomepage.startsWith("about:")) {
|
||||
getHome.setText(mHomepage);
|
||||
} else {
|
||||
String defaultUrl = "https://www.google.com";
|
||||
getHome.setText(defaultUrl);
|
||||
}
|
||||
homePicker.setView(getHome);
|
||||
homePicker.setPositiveButton(getResources().getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String text = getHome.getText().toString();
|
||||
mPreferenceManager.setHomepage(text);
|
||||
home.setSummary(text);
|
||||
}
|
||||
});
|
||||
homePicker.show();
|
||||
}
|
||||
|
||||
private void downloadLocDialog() {
|
||||
AlertDialog.Builder picker = new AlertDialog.Builder(mActivity);
|
||||
picker.setTitle(getResources().getString(R.string.title_download_location));
|
||||
mDownloadLocation = mPreferenceManager.getDownloadDirectory();
|
||||
int n;
|
||||
if (mDownloadLocation.contains(Environment.DIRECTORY_DOWNLOADS)) {
|
||||
n = 0;
|
||||
} else {
|
||||
n = 1;
|
||||
}
|
||||
|
||||
picker.setSingleChoiceItems(R.array.download_folder, n,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0:
|
||||
mPreferenceManager.setDownloadDirectory(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
|
||||
downloadloc.setSummary(DownloadHandler.DEFAULT_DOWNLOAD_PATH);
|
||||
break;
|
||||
case 1:
|
||||
downPicker();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
picker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
picker.show();
|
||||
}
|
||||
|
||||
private void agentDialog() {
|
||||
AlertDialog.Builder agentPicker = new AlertDialog.Builder(mActivity);
|
||||
agentPicker.setTitle(getResources().getString(R.string.title_user_agent));
|
||||
mAgentChoice = mPreferenceManager.getUserAgentChoice();
|
||||
agentPicker.setSingleChoiceItems(R.array.user_agent, mAgentChoice - 1,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferenceManager.setUserAgentChoice(which + 1);
|
||||
switch (which + 1) {
|
||||
case 1:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_default));
|
||||
break;
|
||||
case 2:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_desktop));
|
||||
break;
|
||||
case 3:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_mobile));
|
||||
break;
|
||||
case 4:
|
||||
useragent.setSummary(getResources().getString(R.string.agent_custom));
|
||||
agentPicker();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
agentPicker.setNeutralButton(getResources().getString(R.string.action_ok), null);
|
||||
agentPicker.show();
|
||||
}
|
||||
|
||||
private void agentPicker() {
|
||||
final AlertDialog.Builder agentStringPicker = new AlertDialog.Builder(mActivity);
|
||||
agentStringPicker.setTitle(getResources().getString(R.string.title_user_agent));
|
||||
final EditText getAgent = new EditText(mActivity);
|
||||
agentStringPicker.setView(getAgent);
|
||||
agentStringPicker.setPositiveButton(getResources().getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String text = getAgent.getText().toString();
|
||||
mPreferenceManager.setUserAgentString(text);
|
||||
useragent.setSummary(getResources().getString(R.string.agent_custom));
|
||||
}
|
||||
});
|
||||
agentStringPicker.show();
|
||||
}
|
||||
|
||||
private void downPicker() {
|
||||
final AlertDialog.Builder downLocationPicker = new AlertDialog.Builder(mActivity);
|
||||
LinearLayout layout = new LinearLayout(mActivity);
|
||||
downLocationPicker.setTitle(getResources().getString(R.string.title_download_location));
|
||||
final EditText getDownload = new EditText(mActivity);
|
||||
getDownload.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
getDownload.setText(mPreferenceManager.getDownloadDirectory());
|
||||
final int errorColor = ContextCompat.getColor(getActivity(), R.color.error_red);
|
||||
final int regularColor = ThemeUtils.getTextColor(getActivity());
|
||||
getDownload.setTextColor(regularColor);
|
||||
getDownload.addTextChangedListener(new DownloadLocationTextWatcher(getDownload, errorColor, regularColor));
|
||||
getDownload.setText(mPreferenceManager.getDownloadDirectory());
|
||||
|
||||
layout.addView(getDownload);
|
||||
downLocationPicker.setView(layout);
|
||||
downLocationPicker.setPositiveButton(getResources().getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
String text = getDownload.getText().toString();
|
||||
text = DownloadHandler.addNecessarySlashes(text);
|
||||
mPreferenceManager.setDownloadDirectory(text);
|
||||
downloadloc.setSummary(text);
|
||||
}
|
||||
});
|
||||
downLocationPicker.show();
|
||||
}
|
||||
|
||||
private void setSearchEngineSummary(int which) {
|
||||
switch (which) {
|
||||
case 0:
|
||||
searchUrlPicker();
|
||||
break;
|
||||
case 1:
|
||||
searchengine.setSummary("Google");
|
||||
break;
|
||||
case 2:
|
||||
searchengine.setSummary("Ask");
|
||||
break;
|
||||
case 3:
|
||||
searchengine.setSummary("Bing");
|
||||
break;
|
||||
case 4:
|
||||
searchengine.setSummary("Yahoo");
|
||||
break;
|
||||
case 5:
|
||||
searchengine.setSummary("StartPage");
|
||||
break;
|
||||
case 6:
|
||||
searchengine.setSummary("StartPage (Mobile)");
|
||||
break;
|
||||
case 7:
|
||||
searchengine.setSummary("DuckDuckGo");
|
||||
break;
|
||||
case 8:
|
||||
searchengine.setSummary("DuckDuckGo Lite");
|
||||
break;
|
||||
case 9:
|
||||
searchengine.setSummary("Baidu");
|
||||
break;
|
||||
case 10:
|
||||
searchengine.setSummary("Yandex");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_PROXY:
|
||||
proxyChoicePicker();
|
||||
return true;
|
||||
case SETTINGS_USERAGENT:
|
||||
agentDialog();
|
||||
return true;
|
||||
case SETTINGS_DOWNLOAD:
|
||||
downloadLocDialog();
|
||||
return true;
|
||||
case SETTINGS_HOME:
|
||||
homepageDialog();
|
||||
return true;
|
||||
case SETTINGS_SEARCHENGINE:
|
||||
searchDialog();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
boolean checked = false;
|
||||
if (newValue instanceof Boolean) {
|
||||
checked = (Boolean) newValue;
|
||||
}
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_FLASH:
|
||||
if (!Utils.isFlashInstalled(mActivity) && checked) {
|
||||
Utils.createInformativeDialog(mActivity, R.string.title_warning, R.string.dialog_adobe_not_installed);
|
||||
mPreferenceManager.setFlashSupport(0);
|
||||
return false;
|
||||
} else {
|
||||
if (checked) {
|
||||
getFlashChoice();
|
||||
} else {
|
||||
mPreferenceManager.setFlashSupport(0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
case SETTINGS_ADS:
|
||||
mPreferenceManager.setAdBlockEnabled(checked);
|
||||
return true;
|
||||
case SETTINGS_IMAGES:
|
||||
mPreferenceManager.setBlockImagesEnabled(checked);
|
||||
return true;
|
||||
case SETTINGS_JAVASCRIPT:
|
||||
mPreferenceManager.setJavaScriptEnabled(checked);
|
||||
return true;
|
||||
case SETTINGS_COLORMODE:
|
||||
mPreferenceManager.setColorModeEnabled(checked);
|
||||
return true;
|
||||
case SETTINGS_GOOGLESUGGESTIONS:
|
||||
mPreferenceManager.setGoogleSearchSuggestionsEnabled(checked);
|
||||
return true;
|
||||
case SETTINGS_DRAWERTABS:
|
||||
mPreferenceManager.setShowTabsInDrawer(checked);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DownloadLocationTextWatcher implements TextWatcher {
|
||||
private final EditText getDownload;
|
||||
private final int errorColor;
|
||||
private final int regularColor;
|
||||
|
||||
public DownloadLocationTextWatcher(EditText getDownload, int errorColor, int regularColor) {
|
||||
this.getDownload = getDownload;
|
||||
this.errorColor = errorColor;
|
||||
this.regularColor = regularColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(@NonNull Editable s) {
|
||||
if (!DownloadHandler.isWriteAccessAvailable(s.toString())) {
|
||||
this.getDownload.setTextColor(this.errorColor);
|
||||
} else {
|
||||
this.getDownload.setTextColor(this.regularColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceFragment;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
|
||||
/**
|
||||
* Simplify {@link PreferenceManager} inject in all the PreferenceFragments
|
||||
*
|
||||
* @author Stefano Pacifici
|
||||
* @date 2015/09/16
|
||||
*/
|
||||
public class LightningPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
@Inject
|
||||
PreferenceManager mPreferenceManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2014 A.C.R. Development
|
||||
*/
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.CheckBoxPreference;
|
||||
import android.preference.Preference;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.database.HistoryDatabase;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
import acr.browser.lightning.utils.WebUtils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
public class PrivacySettingsFragment extends LightningPreferenceFragment implements Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener {
|
||||
|
||||
private static final String SETTINGS_LOCATION = "location";
|
||||
private static final String SETTINGS_THIRDPCOOKIES = "third_party";
|
||||
private static final String SETTINGS_SAVEPASSWORD = "password";
|
||||
private static final String SETTINGS_CACHEEXIT = "clear_cache_exit";
|
||||
private static final String SETTINGS_HISTORYEXIT = "clear_history_exit";
|
||||
private static final String SETTINGS_COOKIEEXIT = "clear_cookies_exit";
|
||||
private static final String SETTINGS_CLEARCACHE = "clear_cache";
|
||||
private static final String SETTINGS_CLEARHISTORY = "clear_history";
|
||||
private static final String SETTINGS_CLEARCOOKIES = "clear_cookies";
|
||||
private static final String SETTINGS_CLEARWEBSTORAGE = "clear_webstorage";
|
||||
private static final String SETTINGS_WEBSTORAGEEXIT = "clear_webstorage_exit";
|
||||
private static final String SETTINGS_DONOTTRACK = "do_not_track";
|
||||
private static final String SETTINGS_IDENTIFYINGHEADERS = "remove_identifying_headers";
|
||||
|
||||
private Activity mActivity;
|
||||
private Handler mMessageHandler;
|
||||
|
||||
@Inject HistoryDatabase mHistoryDatabase;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
// Load the preferences from an XML resource
|
||||
addPreferencesFromResource(R.xml.preference_privacy);
|
||||
|
||||
mActivity = getActivity();
|
||||
|
||||
initPrefs();
|
||||
}
|
||||
|
||||
private void initPrefs() {
|
||||
Preference clearcache = findPreference(SETTINGS_CLEARCACHE);
|
||||
Preference clearhistory = findPreference(SETTINGS_CLEARHISTORY);
|
||||
Preference clearcookies = findPreference(SETTINGS_CLEARCOOKIES);
|
||||
Preference clearwebstorage = findPreference(SETTINGS_CLEARWEBSTORAGE);
|
||||
|
||||
CheckBoxPreference cblocation = (CheckBoxPreference) findPreference(SETTINGS_LOCATION);
|
||||
CheckBoxPreference cb3cookies = (CheckBoxPreference) findPreference(SETTINGS_THIRDPCOOKIES);
|
||||
CheckBoxPreference cbsavepasswords = (CheckBoxPreference) findPreference(SETTINGS_SAVEPASSWORD);
|
||||
CheckBoxPreference cbcacheexit = (CheckBoxPreference) findPreference(SETTINGS_CACHEEXIT);
|
||||
CheckBoxPreference cbhistoryexit = (CheckBoxPreference) findPreference(SETTINGS_HISTORYEXIT);
|
||||
CheckBoxPreference cbcookiesexit = (CheckBoxPreference) findPreference(SETTINGS_COOKIEEXIT);
|
||||
CheckBoxPreference cbwebstorageexit = (CheckBoxPreference) findPreference(SETTINGS_WEBSTORAGEEXIT);
|
||||
CheckBoxPreference cbDoNotTrack = (CheckBoxPreference) findPreference(SETTINGS_DONOTTRACK);
|
||||
CheckBoxPreference cbIdentifyingHeaders = (CheckBoxPreference) findPreference(SETTINGS_IDENTIFYINGHEADERS);
|
||||
|
||||
clearcache.setOnPreferenceClickListener(this);
|
||||
clearhistory.setOnPreferenceClickListener(this);
|
||||
clearcookies.setOnPreferenceClickListener(this);
|
||||
clearwebstorage.setOnPreferenceClickListener(this);
|
||||
|
||||
cblocation.setOnPreferenceChangeListener(this);
|
||||
cb3cookies.setOnPreferenceChangeListener(this);
|
||||
cbsavepasswords.setOnPreferenceChangeListener(this);
|
||||
cbcacheexit.setOnPreferenceChangeListener(this);
|
||||
cbhistoryexit.setOnPreferenceChangeListener(this);
|
||||
cbcookiesexit.setOnPreferenceChangeListener(this);
|
||||
cbwebstorageexit.setOnPreferenceChangeListener(this);
|
||||
cbDoNotTrack.setOnPreferenceChangeListener(this);
|
||||
cbIdentifyingHeaders.setOnPreferenceChangeListener(this);
|
||||
|
||||
cblocation.setChecked(mPreferenceManager.getLocationEnabled());
|
||||
cbsavepasswords.setChecked(mPreferenceManager.getSavePasswordsEnabled());
|
||||
cbcacheexit.setChecked(mPreferenceManager.getClearCacheExit());
|
||||
cbhistoryexit.setChecked(mPreferenceManager.getClearHistoryExitEnabled());
|
||||
cbcookiesexit.setChecked(mPreferenceManager.getClearCookiesExitEnabled());
|
||||
cb3cookies.setChecked(mPreferenceManager.getBlockThirdPartyCookiesEnabled());
|
||||
cbwebstorageexit.setChecked(mPreferenceManager.getClearWebStorageExitEnabled());
|
||||
cbDoNotTrack.setChecked(mPreferenceManager.getDoNotTrackEnabled() && Utils.doesSupportHeaders());
|
||||
cbIdentifyingHeaders.setChecked(mPreferenceManager.getRemoveIdentifyingHeadersEnabled() && Utils.doesSupportHeaders());
|
||||
|
||||
cbDoNotTrack.setEnabled(Utils.doesSupportHeaders());
|
||||
cbIdentifyingHeaders.setEnabled(Utils.doesSupportHeaders());
|
||||
|
||||
String identifyingHeadersSummary = LightningView.HEADER_REQUESTED_WITH + ", " + LightningView.HEADER_WAP_PROFILE;
|
||||
cbIdentifyingHeaders.setSummary(identifyingHeadersSummary);
|
||||
|
||||
cb3cookies.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
|
||||
|
||||
mMessageHandler = new MessageHandler(mActivity);
|
||||
}
|
||||
|
||||
private static class MessageHandler extends Handler {
|
||||
|
||||
final Activity mHandlerContext;
|
||||
|
||||
public MessageHandler(Activity context) {
|
||||
this.mHandlerContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
switch (msg.what) {
|
||||
case 1:
|
||||
Utils.showSnackbar(mHandlerContext, R.string.message_clear_history);
|
||||
break;
|
||||
case 2:
|
||||
Utils.showSnackbar(mHandlerContext, R.string.message_cookies_cleared);
|
||||
break;
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull Preference preference) {
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_CLEARCACHE:
|
||||
clearCache();
|
||||
return true;
|
||||
case SETTINGS_CLEARHISTORY:
|
||||
clearHistoryDialog();
|
||||
return true;
|
||||
case SETTINGS_CLEARCOOKIES:
|
||||
clearCookiesDialog();
|
||||
return true;
|
||||
case SETTINGS_CLEARWEBSTORAGE:
|
||||
clearWebStorage();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void clearHistoryDialog() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
|
||||
builder.setTitle(getResources().getString(R.string.title_clear_history));
|
||||
builder.setMessage(getResources().getString(R.string.dialog_history))
|
||||
.setPositiveButton(getResources().getString(R.string.action_yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface arg0, int arg1) {
|
||||
BrowserApp.getIOThread().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
clearHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
|
||||
}
|
||||
|
||||
private void clearCookiesDialog() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
|
||||
builder.setTitle(getResources().getString(R.string.title_clear_cookies));
|
||||
builder.setMessage(getResources().getString(R.string.dialog_cookies))
|
||||
.setPositiveButton(getResources().getString(R.string.action_yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface arg0, int arg1) {
|
||||
BrowserApp.getTaskThread().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
clearCookies();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.setNegativeButton(getResources().getString(R.string.action_no), null).show();
|
||||
}
|
||||
|
||||
private void clearCache() {
|
||||
WebView webView = new WebView(mActivity);
|
||||
webView.clearCache(true);
|
||||
webView.destroy();
|
||||
Utils.showSnackbar(mActivity, R.string.message_cache_cleared);
|
||||
}
|
||||
|
||||
private void clearHistory() {
|
||||
WebUtils.clearHistory(getActivity(), mHistoryDatabase);
|
||||
mMessageHandler.sendEmptyMessage(1);
|
||||
}
|
||||
|
||||
private void clearCookies() {
|
||||
WebUtils.clearCookies(getActivity());
|
||||
mMessageHandler.sendEmptyMessage(2);
|
||||
}
|
||||
|
||||
private void clearWebStorage() {
|
||||
WebUtils.clearWebStorage();
|
||||
Utils.showSnackbar(getActivity(), R.string.message_web_storage_cleared);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
|
||||
switch (preference.getKey()) {
|
||||
case SETTINGS_LOCATION:
|
||||
mPreferenceManager.setLocationEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_THIRDPCOOKIES:
|
||||
mPreferenceManager.setBlockThirdPartyCookiesEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_SAVEPASSWORD:
|
||||
mPreferenceManager.setSavePasswordsEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_CACHEEXIT:
|
||||
mPreferenceManager.setClearCacheExit((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_HISTORYEXIT:
|
||||
mPreferenceManager.setClearHistoryExitEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_COOKIEEXIT:
|
||||
mPreferenceManager.setClearCookiesExitEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_WEBSTORAGEEXIT:
|
||||
mPreferenceManager.setClearWebStorageExitEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_DONOTTRACK:
|
||||
mPreferenceManager.setDoNotTrackEnabled((Boolean) newValue);
|
||||
return true;
|
||||
case SETTINGS_IDENTIFYINGHEADERS:
|
||||
mPreferenceManager.setRemoveIdentifyingHeadersEnabled((Boolean) newValue);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package acr.browser.lightning.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.ColorMatrix;
|
||||
import android.graphics.ColorMatrixColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.widget.TextViewCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.LayoutManager;
|
||||
import android.support.v7.widget.SimpleItemAnimator;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.activity.TabsManager;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.browser.TabsView;
|
||||
import acr.browser.lightning.bus.NavigationEvents;
|
||||
import acr.browser.lightning.bus.TabEvents;
|
||||
import acr.browser.lightning.controller.UIController;
|
||||
import acr.browser.lightning.fragment.anim.HorizontalItemAnimator;
|
||||
import acr.browser.lightning.fragment.anim.VerticalItemAnimator;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
import acr.browser.lightning.view.LightningView;
|
||||
|
||||
/**
|
||||
* A fragment that holds and manages the tabs and interaction with the tabs.
|
||||
* It is reliant on the BrowserController in order to get the current UI state
|
||||
* of the browser. It also uses the BrowserController to signal that the UI needs
|
||||
* to change. This class contains the adapter used by both the drawer tabs and
|
||||
* the desktop tabs. It delegates touch events for the tab UI appropriately.
|
||||
*/
|
||||
public class TabsFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener, TabsView {
|
||||
|
||||
private static final String TAG = TabsFragment.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Arguments boolean to tell the fragment it is displayed in the drawner or on the tab strip
|
||||
* If true, the fragment is in the left drawner in the strip otherwise.
|
||||
*/
|
||||
public static final String VERTICAL_MODE = TAG + ".VERTICAL_MODE";
|
||||
public static final String IS_INCOGNITO = TAG + ".IS_INCOGNITO";
|
||||
|
||||
private boolean mIsIncognito, mDarkTheme;
|
||||
private int mIconColor;
|
||||
private boolean mColorMode = true;
|
||||
private boolean mShowInNavigationDrawer;
|
||||
|
||||
@Nullable private LightningViewAdapter mTabsAdapter;
|
||||
private UIController mUiController;
|
||||
private RecyclerView mRecyclerView;
|
||||
|
||||
private TabsManager mTabsManager;
|
||||
@Inject Bus mBus;
|
||||
@Inject PreferenceManager mPreferences;
|
||||
|
||||
public TabsFragment() {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final Bundle arguments = getArguments();
|
||||
final Context context = getContext();
|
||||
mUiController = (UIController) getActivity();
|
||||
mTabsManager = mUiController.getTabModel();
|
||||
mIsIncognito = arguments.getBoolean(IS_INCOGNITO, false);
|
||||
mShowInNavigationDrawer = arguments.getBoolean(VERTICAL_MODE, true);
|
||||
mDarkTheme = mPreferences.getUseTheme() != 0 || mIsIncognito;
|
||||
mColorMode = mPreferences.getColorModeEnabled();
|
||||
mColorMode &= !mDarkTheme;
|
||||
mIconColor = mDarkTheme ?
|
||||
ThemeUtils.getIconDarkThemeColor(context) :
|
||||
ThemeUtils.getIconLightThemeColor(context);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view;
|
||||
final LayoutManager layoutManager;
|
||||
if (mShowInNavigationDrawer) {
|
||||
view = inflater.inflate(R.layout.tab_drawer, container, false);
|
||||
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
|
||||
setupFrameLayoutButton(view, R.id.tab_header_button, R.id.plusIcon);
|
||||
setupFrameLayoutButton(view, R.id.new_tab_button, R.id.icon_plus);
|
||||
setupFrameLayoutButton(view, R.id.action_back, R.id.icon_back);
|
||||
setupFrameLayoutButton(view, R.id.action_forward, R.id.icon_forward);
|
||||
setupFrameLayoutButton(view, R.id.action_home, R.id.icon_home);
|
||||
} else {
|
||||
view = inflater.inflate(R.layout.tab_strip, container, false);
|
||||
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
|
||||
ImageView newTab = (ImageView) view.findViewById(R.id.new_tab_button);
|
||||
newTab.setColorFilter(ThemeUtils.getIconDarkThemeColor(getActivity()));
|
||||
newTab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
mUiController.newTabClicked();
|
||||
}
|
||||
});
|
||||
}
|
||||
mRecyclerView = (RecyclerView) view.findViewById(R.id.tabs_list);
|
||||
SimpleItemAnimator animator;
|
||||
if (mShowInNavigationDrawer) {
|
||||
animator = new VerticalItemAnimator();
|
||||
} else {
|
||||
animator = new HorizontalItemAnimator();
|
||||
}
|
||||
animator.setSupportsChangeAnimations(false);
|
||||
animator.setAddDuration(200);
|
||||
animator.setChangeDuration(0);
|
||||
animator.setRemoveDuration(200);
|
||||
animator.setMoveDuration(200);
|
||||
mRecyclerView.setLayerType(View.LAYER_TYPE_NONE, null);
|
||||
mRecyclerView.setItemAnimator(animator);
|
||||
mRecyclerView.setLayoutManager(layoutManager);
|
||||
mTabsAdapter = new LightningViewAdapter(mShowInNavigationDrawer);
|
||||
mRecyclerView.setAdapter(mTabsAdapter);
|
||||
mRecyclerView.setHasFixedSize(true);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void setupFrameLayoutButton(@NonNull final View root, @IdRes final int buttonId,
|
||||
@IdRes final int imageId) {
|
||||
final View frameButton = root.findViewById(buttonId);
|
||||
final ImageView buttonImage = (ImageView) root.findViewById(imageId);
|
||||
frameButton.setOnClickListener(this);
|
||||
frameButton.setOnLongClickListener(this);
|
||||
buttonImage.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
mTabsAdapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
mBus.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
// Force adapter refresh
|
||||
if (mTabsAdapter != null) {
|
||||
mTabsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
mBus.unregister(this);
|
||||
}
|
||||
|
||||
public void reinitializePreferences() {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
mDarkTheme = mPreferences.getUseTheme() != 0 || mIsIncognito;
|
||||
mColorMode = mPreferences.getColorModeEnabled();
|
||||
mColorMode &= !mDarkTheme;
|
||||
mIconColor = mDarkTheme ?
|
||||
ThemeUtils.getIconDarkThemeColor(activity) :
|
||||
ThemeUtils.getIconLightThemeColor(activity);
|
||||
if (mTabsAdapter != null) {
|
||||
mTabsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.tab_header_button:
|
||||
mUiController.showCloseDialog(mTabsManager.indexOfCurrentTab());
|
||||
break;
|
||||
case R.id.new_tab_button:
|
||||
mBus.post(new TabEvents.NewTab());
|
||||
break;
|
||||
case R.id.action_back:
|
||||
mBus.post(new NavigationEvents.GoBack());
|
||||
break;
|
||||
case R.id.action_forward:
|
||||
mBus.post(new NavigationEvents.GoForward());
|
||||
break;
|
||||
case R.id.action_home:
|
||||
mBus.post(new NavigationEvents.GoHome());
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(@NonNull View v) {
|
||||
switch (v.getId()) {
|
||||
case R.id.action_new_tab:
|
||||
mBus.post(new TabEvents.NewTabLongPress());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tabAdded() {
|
||||
if (mTabsAdapter != null) {
|
||||
mTabsAdapter.notifyItemInserted(mTabsManager.last());
|
||||
mRecyclerView.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mRecyclerView.smoothScrollToPosition(mTabsAdapter.getItemCount() - 1);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tabRemoved(int position) {
|
||||
if (mTabsAdapter != null) {
|
||||
mTabsAdapter.notifyItemRemoved(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tabChanged(int position) {
|
||||
if (mTabsAdapter != null) {
|
||||
mTabsAdapter.notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
private class LightningViewAdapter extends RecyclerView.Adapter<LightningViewAdapter.LightningViewHolder> {
|
||||
|
||||
private final int mLayoutResourceId;
|
||||
@Nullable private final Drawable mBackgroundTabDrawable;
|
||||
@Nullable private final Drawable mForegroundTabDrawable;
|
||||
@Nullable private final Bitmap mForegroundTabBitmap;
|
||||
private ColorMatrix mColorMatrix;
|
||||
private Paint mPaint;
|
||||
private ColorFilter mFilter;
|
||||
private static final float DESATURATED = 0.5f;
|
||||
|
||||
private final boolean mDrawerTabs;
|
||||
|
||||
public LightningViewAdapter(final boolean vertical) {
|
||||
this.mLayoutResourceId = vertical ? R.layout.tab_list_item : R.layout.tab_list_item_horizontal;
|
||||
this.mDrawerTabs = vertical;
|
||||
|
||||
if (vertical) {
|
||||
mBackgroundTabDrawable = null;
|
||||
mForegroundTabBitmap = null;
|
||||
mForegroundTabDrawable = ThemeUtils.getSelectedBackground(getContext(), mDarkTheme);
|
||||
} else {
|
||||
int backgroundColor = Utils.mixTwoColors(ThemeUtils.getPrimaryColor(getContext()), Color.BLACK, 0.75f);
|
||||
Bitmap backgroundTabBitmap = Bitmap.createBitmap(Utils.dpToPx(175), Utils.dpToPx(30), Bitmap.Config.ARGB_8888);
|
||||
Utils.drawTrapezoid(new Canvas(backgroundTabBitmap), backgroundColor, true);
|
||||
mBackgroundTabDrawable = new BitmapDrawable(getResources(), backgroundTabBitmap);
|
||||
|
||||
int foregroundColor = ThemeUtils.getPrimaryColor(getContext());
|
||||
mForegroundTabBitmap = Bitmap.createBitmap(Utils.dpToPx(175), Utils.dpToPx(30), Bitmap.Config.ARGB_8888);
|
||||
Utils.drawTrapezoid(new Canvas(mForegroundTabBitmap), foregroundColor, false);
|
||||
mForegroundTabDrawable = null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LightningViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
|
||||
View view = inflater.inflate(mLayoutResourceId, viewGroup, false);
|
||||
return new LightningViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final LightningViewHolder holder, int position) {
|
||||
holder.exitButton.setTag(position);
|
||||
|
||||
ViewCompat.jumpDrawablesToCurrentState(holder.exitButton);
|
||||
|
||||
LightningView web = mTabsManager.getTabAtPosition(position);
|
||||
if (web == null) {
|
||||
return;
|
||||
}
|
||||
holder.txtTitle.setText(web.getTitle());
|
||||
|
||||
final Bitmap favicon = web.getFavicon();
|
||||
if (web.isForegroundTab()) {
|
||||
TextViewCompat.setTextAppearance(holder.txtTitle, R.style.boldText);
|
||||
Drawable foregroundDrawable;
|
||||
if (!mDrawerTabs) {
|
||||
foregroundDrawable = new BitmapDrawable(getResources(), mForegroundTabBitmap);
|
||||
if (!mIsIncognito && mColorMode) {
|
||||
foregroundDrawable.setColorFilter(mUiController.getUiColor(), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
} else {
|
||||
foregroundDrawable = mForegroundTabDrawable;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
holder.layout.setBackground(foregroundDrawable);
|
||||
} else {
|
||||
holder.layout.setBackgroundDrawable(foregroundDrawable);
|
||||
}
|
||||
if (!mIsIncognito && mColorMode) {
|
||||
mUiController.changeToolbarBackground(favicon, foregroundDrawable);
|
||||
}
|
||||
holder.favicon.setImageBitmap(favicon);
|
||||
} else {
|
||||
TextViewCompat.setTextAppearance(holder.txtTitle, R.style.normalText);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
holder.layout.setBackground(mBackgroundTabDrawable);
|
||||
} else {
|
||||
holder.layout.setBackgroundDrawable(mBackgroundTabDrawable);
|
||||
}
|
||||
holder.favicon.setImageBitmap(getDesaturatedBitmap(favicon));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mTabsManager.size();
|
||||
}
|
||||
|
||||
public Bitmap getDesaturatedBitmap(@NonNull Bitmap favicon) {
|
||||
Bitmap grayscaleBitmap = Bitmap.createBitmap(favicon.getWidth(),
|
||||
favicon.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
|
||||
Canvas c = new Canvas(grayscaleBitmap);
|
||||
if (mColorMatrix == null || mFilter == null || mPaint == null) {
|
||||
mPaint = new Paint();
|
||||
mColorMatrix = new ColorMatrix();
|
||||
mColorMatrix.setSaturation(DESATURATED);
|
||||
mFilter = new ColorMatrixColorFilter(mColorMatrix);
|
||||
mPaint.setColorFilter(mFilter);
|
||||
}
|
||||
|
||||
c.drawBitmap(favicon, 0, 0, mPaint);
|
||||
return grayscaleBitmap;
|
||||
}
|
||||
|
||||
public class LightningViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||
|
||||
public LightningViewHolder(@NonNull View view) {
|
||||
super(view);
|
||||
txtTitle = (TextView) view.findViewById(R.id.textTab);
|
||||
favicon = (ImageView) view.findViewById(R.id.faviconTab);
|
||||
exit = (ImageView) view.findViewById(R.id.deleteButton);
|
||||
layout = (LinearLayout) view.findViewById(R.id.tab_item_background);
|
||||
exitButton = (FrameLayout) view.findViewById(R.id.deleteAction);
|
||||
exit.setColorFilter(mIconColor, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
exitButton.setOnClickListener(this);
|
||||
layout.setOnClickListener(this);
|
||||
layout.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
@NonNull final TextView txtTitle;
|
||||
@NonNull final ImageView favicon;
|
||||
@NonNull final ImageView exit;
|
||||
@NonNull final FrameLayout exitButton;
|
||||
@NonNull final LinearLayout layout;
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v == exitButton) {
|
||||
// Close tab
|
||||
mBus.post(new TabEvents.CloseTab(getAdapterPosition()));
|
||||
}
|
||||
if (v == layout) {
|
||||
mBus.post(new TabEvents.ShowTab(getAdapterPosition()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
// Show close dialog
|
||||
mBus.post(new TabEvents.ShowCloseDialog(getAdapterPosition()));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 acr.browser.lightning.fragment.anim;
|
||||
|
||||
import android.support.v4.animation.AnimatorCompatHelper;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorListener;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.support.v7.widget.SimpleItemAnimator;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
|
||||
* animations on remove, add, and move events that happen to the items in
|
||||
* a RecyclerView. RecyclerView uses a HorizontalItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class HorizontalItemAnimator extends SimpleItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
|
||||
|
||||
private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
|
||||
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
|
||||
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
|
||||
|
||||
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
|
||||
|
||||
private static class MoveInfo {
|
||||
public ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChangeInfo {
|
||||
public ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{" +
|
||||
"oldHolder=" + oldHolder +
|
||||
", newHolder=" + newHolder +
|
||||
", fromX=" + fromX +
|
||||
", fromY=" + fromY +
|
||||
", toX=" + toX +
|
||||
", toY=" + toY +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||
boolean movesPending = !mPendingMoves.isEmpty();
|
||||
boolean changesPending = !mPendingChanges.isEmpty();
|
||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
// nothing to animate
|
||||
return;
|
||||
}
|
||||
// First, remove stuff
|
||||
for (ViewHolder holder : mPendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
mPendingRemovals.clear();
|
||||
// Next, move stuff
|
||||
if (movesPending) {
|
||||
final ArrayList<MoveInfo> moves = new ArrayList<>();
|
||||
moves.addAll(mPendingMoves);
|
||||
mMovesList.add(moves);
|
||||
mPendingMoves.clear();
|
||||
Runnable mover = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (MoveInfo moveInfo : moves) {
|
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||
moveInfo.toX, moveInfo.toY);
|
||||
}
|
||||
moves.clear();
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
View view = moves.get(0).holder.itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
||||
} else {
|
||||
mover.run();
|
||||
}
|
||||
}
|
||||
// Next, change stuff, to run in parallel with move animations
|
||||
if (changesPending) {
|
||||
final ArrayList<ChangeInfo> changes = new ArrayList<>();
|
||||
changes.addAll(mPendingChanges);
|
||||
mChangesList.add(changes);
|
||||
mPendingChanges.clear();
|
||||
Runnable changer = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (ChangeInfo change : changes) {
|
||||
animateChangeImpl(change);
|
||||
}
|
||||
changes.clear();
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
ViewHolder holder = changes.get(0).oldHolder;
|
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff
|
||||
if (additionsPending) {
|
||||
final ArrayList<ViewHolder> additions = new ArrayList<>();
|
||||
additions.addAll(mPendingAdditions);
|
||||
mAdditionsList.add(additions);
|
||||
mPendingAdditions.clear();
|
||||
Runnable adder = new Runnable() {
|
||||
public void run() {
|
||||
for (ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
View view = additions.get(0).itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mRemoveAnimations.add(holder);
|
||||
animation.setDuration(getRemoveDuration())
|
||||
.alpha(0).translationY(holder.itemView.getHeight())
|
||||
.setInterpolator(new AccelerateInterpolator()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
ViewCompat.setAlpha(holder.itemView, 0);
|
||||
ViewCompat.setTranslationY(holder.itemView, holder.itemView.getHeight());
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateAddImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mAddAnimations.add(holder);
|
||||
animation.alpha(1).translationY(0)
|
||||
.setInterpolator(new DecelerateInterpolator()).setDuration(getAddDuration())
|
||||
.setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchAddFinished(holder);
|
||||
mAddAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += ViewCompat.getTranslationX(holder.itemView);
|
||||
fromY += ViewCompat.getTranslationY(holder.itemView);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
resetAnimation(holder);
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, -deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, -deltaY);
|
||||
}
|
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.animate(view).translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.animate(view).translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mMoveAnimations.add(holder);
|
||||
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
mMoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
// if (oldHolder != newHolder) {
|
||||
// if (oldHolder != null) {
|
||||
// dispatchChangeFinished(oldHolder, true);
|
||||
// }
|
||||
// if (newHolder != null) {
|
||||
// dispatchChangeFinished(newHolder, false);
|
||||
// }
|
||||
// } else if (oldHolder != null) {
|
||||
// dispatchChangeFinished(oldHolder, true);
|
||||
// }
|
||||
// return false;
|
||||
if (oldHolder == newHolder) {
|
||||
// Don't know how to run change animations when the same view holder is re-used.
|
||||
// run a move animation to handle position changes.
|
||||
if ((fromX - toX) == 0 && (fromY - toY) == 0) {
|
||||
dispatchMoveFinished(oldHolder);
|
||||
return false;
|
||||
}
|
||||
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
||||
}
|
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
|
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
|
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
|
||||
resetAnimation(oldHolder);
|
||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// recover prev translation state after ending animation
|
||||
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
|
||||
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
|
||||
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
|
||||
if (newHolder != null) {
|
||||
// carry over translation values
|
||||
resetAnimation(newHolder);
|
||||
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
|
||||
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
|
||||
ViewCompat.setAlpha(newHolder.itemView, 0);
|
||||
}
|
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder == null ? null : holder.itemView;
|
||||
final ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
if (view != null) {
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
|
||||
getChangeDuration());
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
oldViewAnim.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
if (newView != null) {
|
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
|
||||
alpha(1).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
newViewAnimation.setListener(null);
|
||||
ViewCompat.setAlpha(newView, 1);
|
||||
ViewCompat.setTranslationX(newView, 0);
|
||||
ViewCompat.setTranslationY(newView, 0);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
mChangeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
|
||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||
ChangeInfo changeInfo = infoList.get(i);
|
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||
infoList.remove(changeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||
if (changeInfo.oldHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||
}
|
||||
if (changeInfo.newHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
|
||||
boolean oldItem = false;
|
||||
if (changeInfo.newHolder == item) {
|
||||
changeInfo.newHolder = null;
|
||||
} else if (changeInfo.oldHolder == item) {
|
||||
changeInfo.oldHolder = null;
|
||||
oldItem = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
ViewCompat.setAlpha(item.itemView, 1);
|
||||
ViewCompat.setTranslationX(item.itemView, 0);
|
||||
ViewCompat.setTranslationY(item.itemView, 0);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(ViewHolder item) {
|
||||
final View view = item.itemView;
|
||||
// this will trigger end callback which should set properties to their target values.
|
||||
ViewCompat.animate(view).cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(mPendingChanges, item);
|
||||
if (mPendingRemovals.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (mPendingAdditions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
endChangeAnimation(changes, item);
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(i);
|
||||
}
|
||||
}
|
||||
for (int i = mMovesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
for (int j = moves.size() - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
if (additions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animations should be ended by the cancel above.
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mRemoveAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mAddAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mAddAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mChangeAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mMoveAnimations list");
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
private void resetAnimation(ViewHolder holder) {
|
||||
AnimatorCompatHelper.clearInterpolator(holder.itemView);
|
||||
endAnimation(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!mPendingAdditions.isEmpty() ||
|
||||
!mPendingChanges.isEmpty() ||
|
||||
!mPendingMoves.isEmpty() ||
|
||||
!mPendingRemovals.isEmpty() ||
|
||||
!mMoveAnimations.isEmpty() ||
|
||||
!mRemoveAnimations.isEmpty() ||
|
||||
!mAddAnimations.isEmpty() ||
|
||||
!mChangeAnimations.isEmpty() ||
|
||||
!mMovesList.isEmpty() ||
|
||||
!mAdditionsList.isEmpty() ||
|
||||
!mChangesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
private void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = mPendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
MoveInfo item = mPendingMoves.get(i);
|
||||
View view = item.holder.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item.holder);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
count = mPendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingRemovals.get(i);
|
||||
dispatchRemoveFinished(item);
|
||||
mPendingRemovals.remove(i);
|
||||
}
|
||||
count = mPendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingAdditions.get(i);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
mPendingAdditions.remove(i);
|
||||
}
|
||||
count = mPendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||
}
|
||||
mPendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = mMovesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
count = moves.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
ViewHolder item = moveInfo.holder;
|
||||
View view = item.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(moveInfo.holder);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mAdditionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
count = additions.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
ViewHolder item = additions.get(j);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mChangesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
count = changes.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
endChangeAnimationIfNecessary(changes.get(j));
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelAll(mRemoveAnimations);
|
||||
cancelAll(mMoveAnimations);
|
||||
cancelAll(mAddAnimations);
|
||||
cancelAll(mChangeAnimations);
|
||||
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
|
||||
static void cancelAll(List<ViewHolder> viewHolders) {
|
||||
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
||||
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
/*
|
||||
* Copyright (C) 2014 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 acr.browser.lightning.fragment.anim;
|
||||
|
||||
import android.support.v4.animation.AnimatorCompatHelper;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorListener;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.support.v7.widget.SimpleItemAnimator;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
|
||||
* animations on remove, add, and move events that happen to the items in
|
||||
* a RecyclerView. RecyclerView uses a VerticalItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class VerticalItemAnimator extends SimpleItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
|
||||
|
||||
private ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
|
||||
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
|
||||
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
|
||||
|
||||
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
|
||||
|
||||
private static class MoveInfo {
|
||||
public ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChangeInfo {
|
||||
public ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{" +
|
||||
"oldHolder=" + oldHolder +
|
||||
", newHolder=" + newHolder +
|
||||
", fromX=" + fromX +
|
||||
", fromY=" + fromY +
|
||||
", toX=" + toX +
|
||||
", toY=" + toY +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||
boolean movesPending = !mPendingMoves.isEmpty();
|
||||
boolean changesPending = !mPendingChanges.isEmpty();
|
||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
// nothing to animate
|
||||
return;
|
||||
}
|
||||
// First, remove stuff
|
||||
for (ViewHolder holder : mPendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
mPendingRemovals.clear();
|
||||
// Next, move stuff
|
||||
if (movesPending) {
|
||||
final ArrayList<MoveInfo> moves = new ArrayList<>();
|
||||
moves.addAll(mPendingMoves);
|
||||
mMovesList.add(moves);
|
||||
mPendingMoves.clear();
|
||||
Runnable mover = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (MoveInfo moveInfo : moves) {
|
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||
moveInfo.toX, moveInfo.toY);
|
||||
}
|
||||
moves.clear();
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
View view = moves.get(0).holder.itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
||||
} else {
|
||||
mover.run();
|
||||
}
|
||||
}
|
||||
// Next, change stuff, to run in parallel with move animations
|
||||
if (changesPending) {
|
||||
final ArrayList<ChangeInfo> changes = new ArrayList<>();
|
||||
changes.addAll(mPendingChanges);
|
||||
mChangesList.add(changes);
|
||||
mPendingChanges.clear();
|
||||
Runnable changer = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (ChangeInfo change : changes) {
|
||||
animateChangeImpl(change);
|
||||
}
|
||||
changes.clear();
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
ViewHolder holder = changes.get(0).oldHolder;
|
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff
|
||||
if (additionsPending) {
|
||||
final ArrayList<ViewHolder> additions = new ArrayList<>();
|
||||
additions.addAll(mPendingAdditions);
|
||||
mAdditionsList.add(additions);
|
||||
mPendingAdditions.clear();
|
||||
Runnable adder = new Runnable() {
|
||||
public void run() {
|
||||
for (ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
View view = additions.get(0).itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mRemoveAnimations.add(holder);
|
||||
animation.setDuration(getRemoveDuration())
|
||||
.alpha(0).translationX(-holder.itemView.getWidth() / 2)
|
||||
.setInterpolator(new AccelerateInterpolator()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
ViewCompat.setAlpha(holder.itemView, 0);
|
||||
ViewCompat.setTranslationX(holder.itemView, -holder.itemView.getWidth() / 2);
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateAddImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mAddAnimations.add(holder);
|
||||
animation.alpha(1).translationX(0).setDuration(getAddDuration())
|
||||
.setInterpolator(new DecelerateInterpolator()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchAddFinished(holder);
|
||||
mAddAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += ViewCompat.getTranslationX(holder.itemView);
|
||||
fromY += ViewCompat.getTranslationY(holder.itemView);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
resetAnimation(holder);
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, -deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, -deltaY);
|
||||
}
|
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.animate(view).translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.animate(view).translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mMoveAnimations.add(holder);
|
||||
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
mMoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
// if (oldHolder != newHolder) {
|
||||
// if (oldHolder != null) {
|
||||
// dispatchChangeFinished(oldHolder, true);
|
||||
// }
|
||||
// if (newHolder != null) {
|
||||
// dispatchChangeFinished(newHolder, false);
|
||||
// }
|
||||
// } else if (oldHolder != null) {
|
||||
// dispatchChangeFinished(oldHolder, true);
|
||||
// }
|
||||
// return false;
|
||||
if (oldHolder == newHolder) {
|
||||
// Don't know how to run change animations when the same view holder is re-used.
|
||||
// run a move animation to handle position changes.
|
||||
if ((fromX - toX) == 0 && (fromY - toY) == 0) {
|
||||
dispatchMoveFinished(oldHolder);
|
||||
return false;
|
||||
}
|
||||
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
||||
}
|
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
|
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
|
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
|
||||
resetAnimation(oldHolder);
|
||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// recover prev translation state after ending animation
|
||||
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
|
||||
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
|
||||
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
|
||||
if (newHolder != null) {
|
||||
// carry over translation values
|
||||
resetAnimation(newHolder);
|
||||
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
|
||||
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
|
||||
ViewCompat.setAlpha(newHolder.itemView, 0);
|
||||
}
|
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder == null ? null : holder.itemView;
|
||||
final ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
if (view != null) {
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
|
||||
getChangeDuration());
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
oldViewAnim.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
if (newView != null) {
|
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
|
||||
alpha(1).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
newViewAnimation.setListener(null);
|
||||
ViewCompat.setAlpha(newView, 1);
|
||||
ViewCompat.setTranslationX(newView, 0);
|
||||
ViewCompat.setTranslationY(newView, 0);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
mChangeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
|
||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||
ChangeInfo changeInfo = infoList.get(i);
|
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||
infoList.remove(changeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||
if (changeInfo.oldHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||
}
|
||||
if (changeInfo.newHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
|
||||
boolean oldItem = false;
|
||||
if (changeInfo.newHolder == item) {
|
||||
changeInfo.newHolder = null;
|
||||
} else if (changeInfo.oldHolder == item) {
|
||||
changeInfo.oldHolder = null;
|
||||
oldItem = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
ViewCompat.setAlpha(item.itemView, 1);
|
||||
ViewCompat.setTranslationX(item.itemView, 0);
|
||||
ViewCompat.setTranslationY(item.itemView, 0);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(ViewHolder item) {
|
||||
final View view = item.itemView;
|
||||
// this will trigger end callback which should set properties to their target values.
|
||||
ViewCompat.animate(view).cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(mPendingChanges, item);
|
||||
if (mPendingRemovals.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (mPendingAdditions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
endChangeAnimation(changes, item);
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(i);
|
||||
}
|
||||
}
|
||||
for (int i = mMovesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
for (int j = moves.size() - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
if (additions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animations should be ended by the cancel above.
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mRemoveAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mAddAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mAddAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mChangeAnimations list");
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression,ConstantConditions
|
||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mMoveAnimations list");
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
private void resetAnimation(ViewHolder holder) {
|
||||
AnimatorCompatHelper.clearInterpolator(holder.itemView);
|
||||
endAnimation(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!mPendingAdditions.isEmpty() ||
|
||||
!mPendingChanges.isEmpty() ||
|
||||
!mPendingMoves.isEmpty() ||
|
||||
!mPendingRemovals.isEmpty() ||
|
||||
!mMoveAnimations.isEmpty() ||
|
||||
!mRemoveAnimations.isEmpty() ||
|
||||
!mAddAnimations.isEmpty() ||
|
||||
!mChangeAnimations.isEmpty() ||
|
||||
!mMovesList.isEmpty() ||
|
||||
!mAdditionsList.isEmpty() ||
|
||||
!mChangesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
private void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = mPendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
MoveInfo item = mPendingMoves.get(i);
|
||||
View view = item.holder.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item.holder);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
count = mPendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingRemovals.get(i);
|
||||
dispatchRemoveFinished(item);
|
||||
mPendingRemovals.remove(i);
|
||||
}
|
||||
count = mPendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingAdditions.get(i);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
mPendingAdditions.remove(i);
|
||||
}
|
||||
count = mPendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||
}
|
||||
mPendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = mMovesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
count = moves.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
ViewHolder item = moveInfo.holder;
|
||||
View view = item.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(moveInfo.holder);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mAdditionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
count = additions.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
ViewHolder item = additions.get(j);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mChangesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i);
|
||||
count = changes.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
endChangeAnimationIfNecessary(changes.get(j));
|
||||
if (changes.isEmpty()) {
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelAll(mRemoveAnimations);
|
||||
cancelAll(mMoveAnimations);
|
||||
cancelAll(mAddAnimations);
|
||||
cancelAll(mChangeAnimations);
|
||||
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
|
||||
static void cancelAll(List<ViewHolder> viewHolders) {
|
||||
for (int i = viewHolders.size() - 1; i >= 0; i--) {
|
||||
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package acr.browser.lightning.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.download.DownloadHandler;
|
||||
|
||||
@Singleton
|
||||
public class PreferenceManager {
|
||||
|
||||
private static class Name {
|
||||
public static final String ADOBE_FLASH_SUPPORT = "enableflash";
|
||||
public static final String BLOCK_ADS = "AdBlock";
|
||||
public static final String BLOCK_IMAGES = "blockimages";
|
||||
public static final String CLEAR_CACHE_EXIT = "cache";
|
||||
public static final String COOKIES = "cookies";
|
||||
public static final String DOWNLOAD_DIRECTORY = "downloadLocation";
|
||||
public static final String FULL_SCREEN = "fullscreen";
|
||||
public static final String HIDE_STATUS_BAR = "hidestatus";
|
||||
public static final String HOMEPAGE = "home";
|
||||
public static final String INCOGNITO_COOKIES = "incognitocookies";
|
||||
public static final String JAVASCRIPT = "java";
|
||||
public static final String LOCATION = "location";
|
||||
public static final String OVERVIEW_MODE = "overviewmode";
|
||||
public static final String POPUPS = "newwindows";
|
||||
public static final String RESTORE_LOST_TABS = "restoreclosed";
|
||||
public static final String SAVE_PASSWORDS = "passwords";
|
||||
public static final String SEARCH = "search";
|
||||
public static final String SEARCH_URL = "searchurl";
|
||||
public static final String TEXT_REFLOW = "textreflow";
|
||||
public static final String TEXT_SIZE = "textsize";
|
||||
public static final String USE_WIDE_VIEWPORT = "wideviewport";
|
||||
public static final String USER_AGENT = "agentchoose";
|
||||
public static final String USER_AGENT_STRING = "userAgentString";
|
||||
public static final String GOOGLE_SEARCH_SUGGESTIONS = "GoogleSearchSuggestions";
|
||||
public static final String CLEAR_HISTORY_EXIT = "clearHistoryExit";
|
||||
public static final String CLEAR_COOKIES_EXIT = "clearCookiesExit";
|
||||
public static final String SAVE_URL = "saveUrl";
|
||||
public static final String RENDERING_MODE = "renderMode";
|
||||
public static final String BLOCK_THIRD_PARTY = "thirdParty";
|
||||
public static final String ENABLE_COLOR_MODE = "colorMode";
|
||||
public static final String URL_BOX_CONTENTS = "urlContent";
|
||||
public static final String INVERT_COLORS = "invertColors";
|
||||
public static final String READING_TEXT_SIZE = "readingTextSize";
|
||||
public static final String THEME = "Theme";
|
||||
public static final String TEXT_ENCODING = "textEncoding";
|
||||
public static final String CLEAR_WEBSTORAGE_EXIT = "clearWebStorageExit";
|
||||
public static final String SHOW_TABS_IN_DRAWER = "showTabsInDrawer";
|
||||
public static final String DO_NOT_TRACK = "doNotTrack";
|
||||
public static final String IDENTIFYING_HEADERS = "removeIdentifyingHeaders";
|
||||
|
||||
public static final String USE_PROXY = "useProxy";
|
||||
public static final String PROXY_CHOICE = "proxyChoice";
|
||||
public static final String USE_PROXY_HOST = "useProxyHost";
|
||||
public static final String USE_PROXY_PORT = "useProxyPort";
|
||||
public static final String INITIAL_CHECK_FOR_TOR = "checkForTor";
|
||||
public static final String INITIAL_CHECK_FOR_I2P = "checkForI2P";
|
||||
|
||||
public static final String LEAK_CANARY = "leakCanary";
|
||||
}
|
||||
|
||||
@NonNull private final SharedPreferences mPrefs;
|
||||
|
||||
private static final String PREFERENCES = "settings";
|
||||
|
||||
@Inject
|
||||
PreferenceManager(@NonNull final Context context) {
|
||||
mPrefs = context.getSharedPreferences(PREFERENCES, 0);
|
||||
}
|
||||
|
||||
public boolean getAdBlockEnabled() {
|
||||
return mPrefs.getBoolean(Name.BLOCK_ADS, false);
|
||||
}
|
||||
|
||||
public boolean getBlockImagesEnabled() {
|
||||
return mPrefs.getBoolean(Name.BLOCK_IMAGES, false);
|
||||
}
|
||||
|
||||
public boolean getBlockThirdPartyCookiesEnabled() {
|
||||
return mPrefs.getBoolean(Name.BLOCK_THIRD_PARTY, false);
|
||||
}
|
||||
|
||||
public boolean getCheckedForTor() {
|
||||
return mPrefs.getBoolean(Name.INITIAL_CHECK_FOR_TOR, false);
|
||||
}
|
||||
|
||||
public boolean getCheckedForI2P() {
|
||||
return mPrefs.getBoolean(Name.INITIAL_CHECK_FOR_I2P, false);
|
||||
}
|
||||
|
||||
public boolean getClearCacheExit() {
|
||||
return mPrefs.getBoolean(Name.CLEAR_CACHE_EXIT, false);
|
||||
}
|
||||
|
||||
public boolean getClearCookiesExitEnabled() {
|
||||
return mPrefs.getBoolean(Name.CLEAR_COOKIES_EXIT, false);
|
||||
}
|
||||
|
||||
public boolean getClearWebStorageExitEnabled() {
|
||||
return mPrefs.getBoolean(Name.CLEAR_WEBSTORAGE_EXIT, false);
|
||||
}
|
||||
|
||||
public boolean getClearHistoryExitEnabled() {
|
||||
return mPrefs.getBoolean(Name.CLEAR_HISTORY_EXIT, false);
|
||||
}
|
||||
|
||||
public boolean getColorModeEnabled() {
|
||||
return mPrefs.getBoolean(Name.ENABLE_COLOR_MODE, true);
|
||||
}
|
||||
|
||||
public boolean getCookiesEnabled() {
|
||||
return mPrefs.getBoolean(Name.COOKIES, true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getDownloadDirectory() {
|
||||
return mPrefs.getString(Name.DOWNLOAD_DIRECTORY, DownloadHandler.DEFAULT_DOWNLOAD_PATH);
|
||||
}
|
||||
|
||||
public int getFlashSupport() {
|
||||
return mPrefs.getInt(Name.ADOBE_FLASH_SUPPORT, 0);
|
||||
}
|
||||
|
||||
public boolean getFullScreenEnabled() {
|
||||
return mPrefs.getBoolean(Name.FULL_SCREEN, false);
|
||||
}
|
||||
|
||||
public boolean getGoogleSearchSuggestionsEnabled() {
|
||||
return mPrefs.getBoolean(Name.GOOGLE_SEARCH_SUGGESTIONS, true);
|
||||
}
|
||||
|
||||
public boolean getHideStatusBarEnabled() {
|
||||
return mPrefs.getBoolean(Name.HIDE_STATUS_BAR, false);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getHomepage() {
|
||||
return mPrefs.getString(Name.HOMEPAGE, Constants.HOMEPAGE);
|
||||
}
|
||||
|
||||
public boolean getIncognitoCookiesEnabled() {
|
||||
return mPrefs.getBoolean(Name.INCOGNITO_COOKIES, false);
|
||||
}
|
||||
|
||||
public boolean getInvertColors() {
|
||||
return mPrefs.getBoolean(Name.INVERT_COLORS, false);
|
||||
}
|
||||
|
||||
public boolean getJavaScriptEnabled() {
|
||||
return mPrefs.getBoolean(Name.JAVASCRIPT, true);
|
||||
}
|
||||
|
||||
public boolean getLocationEnabled() {
|
||||
return mPrefs.getBoolean(Name.LOCATION, false);
|
||||
}
|
||||
|
||||
public boolean getOverviewModeEnabled() {
|
||||
return mPrefs.getBoolean(Name.OVERVIEW_MODE, true);
|
||||
}
|
||||
|
||||
public boolean getPopupsEnabled() {
|
||||
return mPrefs.getBoolean(Name.POPUPS, true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getProxyHost() {
|
||||
return mPrefs.getString(Name.USE_PROXY_HOST, "localhost");
|
||||
}
|
||||
|
||||
public int getProxyPort() {
|
||||
return mPrefs.getInt(Name.USE_PROXY_PORT, 8118);
|
||||
}
|
||||
|
||||
public int getReadingTextSize() {
|
||||
return mPrefs.getInt(Name.READING_TEXT_SIZE, 2);
|
||||
}
|
||||
|
||||
public int getRenderingMode() {
|
||||
return mPrefs.getInt(Name.RENDERING_MODE, 0);
|
||||
}
|
||||
|
||||
public boolean getRestoreLostTabsEnabled() {
|
||||
return mPrefs.getBoolean(Name.RESTORE_LOST_TABS, true);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getSavedUrl() {
|
||||
return mPrefs.getString(Name.SAVE_URL, null);
|
||||
}
|
||||
|
||||
public boolean getSavePasswordsEnabled() {
|
||||
return mPrefs.getBoolean(Name.SAVE_PASSWORDS, true);
|
||||
}
|
||||
|
||||
public int getSearchChoice() {
|
||||
return mPrefs.getInt(Name.SEARCH, 1);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getSearchUrl() {
|
||||
return mPrefs.getString(Name.SEARCH_URL, Constants.GOOGLE_SEARCH);
|
||||
}
|
||||
|
||||
public boolean getTextReflowEnabled() {
|
||||
return mPrefs.getBoolean(Name.TEXT_REFLOW, false);
|
||||
}
|
||||
|
||||
public int getTextSize() {
|
||||
return mPrefs.getInt(Name.TEXT_SIZE, 3);
|
||||
}
|
||||
|
||||
public int getUrlBoxContentChoice() {
|
||||
return mPrefs.getInt(Name.URL_BOX_CONTENTS, 0);
|
||||
}
|
||||
|
||||
public int getUseTheme() {
|
||||
return mPrefs.getInt(Name.THEME, 0);
|
||||
}
|
||||
|
||||
public boolean getUseProxy() {
|
||||
return mPrefs.getBoolean(Name.USE_PROXY, false);
|
||||
}
|
||||
|
||||
public int getProxyChoice() {
|
||||
return mPrefs.getInt(Name.PROXY_CHOICE, Constants.NO_PROXY);
|
||||
}
|
||||
|
||||
public int getUserAgentChoice() {
|
||||
return mPrefs.getInt(Name.USER_AGENT, 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getUserAgentString(@Nullable String def) {
|
||||
return mPrefs.getString(Name.USER_AGENT_STRING, def);
|
||||
}
|
||||
|
||||
public boolean getUseWideViewportEnabled() {
|
||||
return mPrefs.getBoolean(Name.USE_WIDE_VIEWPORT, true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getTextEncoding() {
|
||||
return mPrefs.getString(Name.TEXT_ENCODING, Constants.DEFAULT_ENCODING);
|
||||
}
|
||||
|
||||
public boolean getShowTabsInDrawer(boolean defaultValue) {
|
||||
return mPrefs.getBoolean(Name.SHOW_TABS_IN_DRAWER, defaultValue);
|
||||
}
|
||||
|
||||
public boolean getDoNotTrackEnabled() {
|
||||
return mPrefs.getBoolean(Name.DO_NOT_TRACK, false);
|
||||
}
|
||||
|
||||
public boolean getRemoveIdentifyingHeadersEnabled() {
|
||||
return mPrefs.getBoolean(Name.IDENTIFYING_HEADERS, false);
|
||||
}
|
||||
|
||||
private void putBoolean(@NonNull String name, boolean value) {
|
||||
mPrefs.edit().putBoolean(name, value).apply();
|
||||
}
|
||||
|
||||
private void putInt(@NonNull String name, int value) {
|
||||
mPrefs.edit().putInt(name, value).apply();
|
||||
}
|
||||
|
||||
private void putString(@NonNull String name, @Nullable String value) {
|
||||
mPrefs.edit().putString(name, value).apply();
|
||||
}
|
||||
|
||||
public void setRemoveIdentifyingHeadersEnabled(boolean enabled) {
|
||||
putBoolean(Name.IDENTIFYING_HEADERS, enabled);
|
||||
}
|
||||
|
||||
public void setDoNotTrackEnabled(boolean doNotTrack) {
|
||||
putBoolean(Name.DO_NOT_TRACK, doNotTrack);
|
||||
}
|
||||
|
||||
public void setShowTabsInDrawer(boolean show) {
|
||||
putBoolean(Name.SHOW_TABS_IN_DRAWER, show);
|
||||
}
|
||||
|
||||
public void setTextEncoding(@NonNull String encoding) {
|
||||
putString(Name.TEXT_ENCODING, encoding);
|
||||
}
|
||||
|
||||
public void setAdBlockEnabled(boolean enable) {
|
||||
putBoolean(Name.BLOCK_ADS, enable);
|
||||
}
|
||||
|
||||
public void setBlockImagesEnabled(boolean enable) {
|
||||
putBoolean(Name.BLOCK_IMAGES, enable);
|
||||
}
|
||||
|
||||
public void setBlockThirdPartyCookiesEnabled(boolean enable) {
|
||||
putBoolean(Name.BLOCK_THIRD_PARTY, enable);
|
||||
}
|
||||
|
||||
public void setCheckedForTor(boolean check) {
|
||||
putBoolean(Name.INITIAL_CHECK_FOR_TOR, check);
|
||||
}
|
||||
|
||||
public void setCheckedForI2P(boolean check) {
|
||||
putBoolean(Name.INITIAL_CHECK_FOR_I2P, check);
|
||||
}
|
||||
|
||||
public void setClearCacheExit(boolean enable) {
|
||||
putBoolean(Name.CLEAR_CACHE_EXIT, enable);
|
||||
}
|
||||
|
||||
public void setClearCookiesExitEnabled(boolean enable) {
|
||||
putBoolean(Name.CLEAR_COOKIES_EXIT, enable);
|
||||
}
|
||||
|
||||
public void setClearWebStorageExitEnabled(boolean enable) {
|
||||
putBoolean(Name.CLEAR_WEBSTORAGE_EXIT, enable);
|
||||
}
|
||||
|
||||
public void setClearHistoryExitEnabled(boolean enable) {
|
||||
putBoolean(Name.CLEAR_HISTORY_EXIT, enable);
|
||||
}
|
||||
|
||||
public void setColorModeEnabled(boolean enable) {
|
||||
putBoolean(Name.ENABLE_COLOR_MODE, enable);
|
||||
}
|
||||
|
||||
public void setCookiesEnabled(boolean enable) {
|
||||
putBoolean(Name.COOKIES, enable);
|
||||
}
|
||||
|
||||
public void setDownloadDirectory(@NonNull String directory) {
|
||||
putString(Name.DOWNLOAD_DIRECTORY, directory);
|
||||
}
|
||||
|
||||
public void setFlashSupport(int n) {
|
||||
putInt(Name.ADOBE_FLASH_SUPPORT, n);
|
||||
}
|
||||
|
||||
public void setFullScreenEnabled(boolean enable) {
|
||||
putBoolean(Name.FULL_SCREEN, enable);
|
||||
}
|
||||
|
||||
public void setGoogleSearchSuggestionsEnabled(boolean enabled) {
|
||||
putBoolean(Name.GOOGLE_SEARCH_SUGGESTIONS, enabled);
|
||||
}
|
||||
|
||||
public void setHideStatusBarEnabled(boolean enable) {
|
||||
putBoolean(Name.HIDE_STATUS_BAR, enable);
|
||||
}
|
||||
|
||||
public void setHomepage(@NonNull String homepage) {
|
||||
putString(Name.HOMEPAGE, homepage);
|
||||
}
|
||||
|
||||
public void setIncognitoCookiesEnabled(boolean enable) {
|
||||
putBoolean(Name.INCOGNITO_COOKIES, enable);
|
||||
}
|
||||
|
||||
public void setInvertColors(boolean enable) {
|
||||
putBoolean(Name.INVERT_COLORS, enable);
|
||||
}
|
||||
|
||||
public void setJavaScriptEnabled(boolean enable) {
|
||||
putBoolean(Name.JAVASCRIPT, enable);
|
||||
}
|
||||
|
||||
public void setLocationEnabled(boolean enable) {
|
||||
putBoolean(Name.LOCATION, enable);
|
||||
}
|
||||
|
||||
public void setOverviewModeEnabled(boolean enable) {
|
||||
putBoolean(Name.OVERVIEW_MODE, enable);
|
||||
}
|
||||
|
||||
public void setPopupsEnabled(boolean enable) {
|
||||
putBoolean(Name.POPUPS, enable);
|
||||
}
|
||||
|
||||
public void setReadingTextSize(int size) {
|
||||
putInt(Name.READING_TEXT_SIZE, size);
|
||||
}
|
||||
|
||||
public void setRenderingMode(int mode) {
|
||||
putInt(Name.RENDERING_MODE, mode);
|
||||
}
|
||||
|
||||
public void setRestoreLostTabsEnabled(boolean enable) {
|
||||
putBoolean(Name.RESTORE_LOST_TABS, enable);
|
||||
}
|
||||
|
||||
public void setSavedUrl(@Nullable String url) {
|
||||
putString(Name.SAVE_URL, url);
|
||||
}
|
||||
|
||||
public void setSavePasswordsEnabled(boolean enable) {
|
||||
putBoolean(Name.SAVE_PASSWORDS, enable);
|
||||
}
|
||||
|
||||
public void setSearchChoice(int choice) {
|
||||
putInt(Name.SEARCH, choice);
|
||||
}
|
||||
|
||||
public void setSearchUrl(@NonNull String url) {
|
||||
putString(Name.SEARCH_URL, url);
|
||||
}
|
||||
|
||||
public void setTextReflowEnabled(boolean enable) {
|
||||
putBoolean(Name.TEXT_REFLOW, enable);
|
||||
}
|
||||
|
||||
public void setTextSize(int size) {
|
||||
putInt(Name.TEXT_SIZE, size);
|
||||
}
|
||||
|
||||
public void setUrlBoxContentChoice(int choice) {
|
||||
putInt(Name.URL_BOX_CONTENTS, choice);
|
||||
}
|
||||
|
||||
public void setUseTheme(int theme) {
|
||||
putInt(Name.THEME, theme);
|
||||
}
|
||||
|
||||
public void setUseLeakCanary(boolean useLeakCanary) {
|
||||
putBoolean(Name.LEAK_CANARY, useLeakCanary);
|
||||
}
|
||||
|
||||
public boolean getUseLeakCanary() {
|
||||
return mPrefs.getBoolean(Name.LEAK_CANARY, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid choices:
|
||||
* <ul>
|
||||
* <li>{@link Constants#NO_PROXY}</li>
|
||||
* <li>{@link Constants#PROXY_ORBOT}</li>
|
||||
* <li>{@link Constants#PROXY_I2P}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param choice the proxy to use.
|
||||
*/
|
||||
public void setProxyChoice(int choice) {
|
||||
putBoolean(Name.USE_PROXY, choice != Constants.NO_PROXY);
|
||||
putInt(Name.PROXY_CHOICE, choice);
|
||||
}
|
||||
|
||||
public void setProxyHost(@NonNull String proxyHost) {
|
||||
putString(Name.USE_PROXY_HOST, proxyHost);
|
||||
}
|
||||
|
||||
public void setProxyPort(int proxyPort) {
|
||||
putInt(Name.USE_PROXY_PORT, proxyPort);
|
||||
}
|
||||
|
||||
public void setUserAgentChoice(int choice) {
|
||||
putInt(Name.USER_AGENT, choice);
|
||||
}
|
||||
|
||||
public void setUserAgentString(@Nullable String agent) {
|
||||
putString(Name.USER_AGENT_STRING, agent);
|
||||
}
|
||||
|
||||
public void setUseWideViewportEnabled(boolean enable) {
|
||||
putBoolean(Name.USE_WIDE_VIEWPORT, enable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public interface Action<T> {
|
||||
/**
|
||||
* Should be overridden to send the subscriber
|
||||
* events such as {@link Subscriber#onNext(Object)}
|
||||
* or {@link Subscriber#onComplete()}.
|
||||
*
|
||||
* @param subscriber the subscriber that is sent in
|
||||
* when the user of the Observable
|
||||
* subscribes.
|
||||
*/
|
||||
void onSubscribe(@NonNull Subscriber<T> subscriber);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import acr.browser.lightning.utils.Preconditions;
|
||||
|
||||
/**
|
||||
* An RxJava implementation. This class allows work
|
||||
* to be done on a certain thread and then allows
|
||||
* items to be emitted on a different thread. It is
|
||||
* a replacement for {@link android.os.AsyncTask}.
|
||||
*
|
||||
* @param <T> the type that the Observable will emit.
|
||||
*/
|
||||
public class Observable<T> {
|
||||
|
||||
private static final String TAG = Observable.class.getSimpleName();
|
||||
|
||||
@NonNull private final Action<T> mAction;
|
||||
@Nullable private Executor mSubscriberThread;
|
||||
@Nullable private Executor mObserverThread;
|
||||
@NonNull private final Executor mDefault;
|
||||
|
||||
private Observable(@NonNull Action<T> action) {
|
||||
mAction = action;
|
||||
Looper looper = Looper.myLooper();
|
||||
Preconditions.checkNonNull(looper);
|
||||
mDefault = new ThreadExecutor(looper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static creator method that creates an Observable from the
|
||||
* {@link Action} that is passed in as the parameter. Action
|
||||
* must not be null.
|
||||
*
|
||||
* @param action the Action to perform
|
||||
* @param <T> the type that will be emitted to the onSubscribe
|
||||
* @return a valid non-null Observable.
|
||||
*/
|
||||
@NonNull
|
||||
public static <T> Observable<T> create(@NonNull Action<T> action) {
|
||||
Preconditions.checkNonNull(action);
|
||||
return new Observable<>(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Observable what Executor that the onSubscribe
|
||||
* work should run on.
|
||||
*
|
||||
* @param subscribeExecutor the Executor to run the work on.
|
||||
* @return returns this so that calls can be conveniently chained.
|
||||
*/
|
||||
public Observable<T> subscribeOn(@NonNull Executor subscribeExecutor) {
|
||||
mSubscriberThread = subscribeExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Observable what Executor the onSubscribe should observe
|
||||
* the work on.
|
||||
*
|
||||
* @param observerExecutor the Executor to run to callback on.
|
||||
* @return returns this so that calls can be conveniently chained.
|
||||
*/
|
||||
public Observable<T> observeOn(@NonNull Executor observerExecutor) {
|
||||
mObserverThread = observerExecutor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes immediately to the Observable and ignores
|
||||
* all onComplete and onNext calls.
|
||||
*/
|
||||
public void subscribe() {
|
||||
executeOnSubscriberThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mAction.onSubscribe(new Subscriber<T>() {
|
||||
@Override
|
||||
public void unsubscribe() {}
|
||||
|
||||
@Override
|
||||
public void onComplete() {}
|
||||
|
||||
@Override
|
||||
public void onStart() {}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable throwable) {}
|
||||
|
||||
@Override
|
||||
public void onNext(T item) {}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately subscribes to the Observable and starts
|
||||
* sending events from the Observable to the {@link OnSubscribe}.
|
||||
*
|
||||
* @param onSubscribe the class that wishes to receive onNext and
|
||||
* onComplete callbacks from the Observable.
|
||||
*/
|
||||
public Subscription subscribe(@NonNull OnSubscribe<T> onSubscribe) {
|
||||
|
||||
Preconditions.checkNonNull(onSubscribe);
|
||||
|
||||
final Subscriber<T> subscriber = new SubscriberImpl<>(onSubscribe, this);
|
||||
|
||||
subscriber.onStart();
|
||||
|
||||
executeOnSubscriberThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mAction.onSubscribe(subscriber);
|
||||
}
|
||||
});
|
||||
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
private void executeOnObserverThread(@NonNull Runnable runnable) {
|
||||
if (mObserverThread != null) {
|
||||
mObserverThread.execute(runnable);
|
||||
} else {
|
||||
mDefault.execute(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private void executeOnSubscriberThread(@NonNull Runnable runnable) {
|
||||
if (mSubscriberThread != null) {
|
||||
mSubscriberThread.execute(runnable);
|
||||
} else {
|
||||
mDefault.execute(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SubscriberImpl<T> implements Subscriber<T> {
|
||||
|
||||
@Nullable private volatile OnSubscribe<T> mOnSubscribe;
|
||||
@NonNull private final Observable<T> mObservable;
|
||||
private boolean mOnCompleteExecuted = false;
|
||||
private boolean mOnError = false;
|
||||
|
||||
public SubscriberImpl(@NonNull OnSubscribe<T> onSubscribe, @NonNull Observable<T> observable) {
|
||||
mOnSubscribe = onSubscribe;
|
||||
mObservable = observable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe() {
|
||||
mOnSubscribe = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
OnSubscribe<T> onSubscribe = mOnSubscribe;
|
||||
if (!mOnCompleteExecuted && onSubscribe != null && !mOnError) {
|
||||
mOnCompleteExecuted = true;
|
||||
mObservable.executeOnObserverThread(new OnCompleteRunnable<>(onSubscribe));
|
||||
} else if (!mOnError) {
|
||||
Log.e(TAG, "onComplete called more than once");
|
||||
throw new RuntimeException("onComplete called more than once");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
OnSubscribe<T> onSubscribe = mOnSubscribe;
|
||||
if (onSubscribe != null) {
|
||||
mObservable.executeOnObserverThread(new OnStartRunnable<>(onSubscribe));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull final Throwable throwable) {
|
||||
OnSubscribe<T> onSubscribe = mOnSubscribe;
|
||||
if (onSubscribe != null) {
|
||||
mOnError = true;
|
||||
mObservable.executeOnObserverThread(new OnErrorRunnable<>(onSubscribe, throwable));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(final T item) {
|
||||
OnSubscribe<T> onSubscribe = mOnSubscribe;
|
||||
if (!mOnCompleteExecuted && onSubscribe != null) {
|
||||
mObservable.executeOnObserverThread(new OnNextRunnable<>(onSubscribe, item));
|
||||
} else {
|
||||
Log.e(TAG, "onComplete has been already called, onNext should not be called");
|
||||
throw new RuntimeException("onNext should not be called after onComplete has been called");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnCompleteRunnable<T> implements Runnable {
|
||||
private final OnSubscribe<T> onSubscribe;
|
||||
|
||||
public OnCompleteRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
onSubscribe.onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnNextRunnable<T> implements Runnable {
|
||||
private final OnSubscribe<T> onSubscribe;
|
||||
private final T item;
|
||||
|
||||
public OnNextRunnable(@NonNull OnSubscribe<T> onSubscribe, T item) {
|
||||
this.onSubscribe = onSubscribe;
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
onSubscribe.onNext(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnErrorRunnable<T> implements Runnable {
|
||||
private final OnSubscribe<T> onSubscribe;
|
||||
private final Throwable throwable;
|
||||
|
||||
public OnErrorRunnable(@NonNull OnSubscribe<T> onSubscribe, @NonNull Throwable throwable) {
|
||||
this.onSubscribe = onSubscribe;
|
||||
this.throwable = throwable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
onSubscribe.onError(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnStartRunnable<T> implements Runnable {
|
||||
private final OnSubscribe<T> onSubscribe;
|
||||
|
||||
public OnStartRunnable(@NonNull OnSubscribe<T> onSubscribe) {this.onSubscribe = onSubscribe;}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
onSubscribe.onStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public abstract class OnSubscribe<T> {
|
||||
|
||||
/**
|
||||
* Called when the observable
|
||||
* runs into an error that will
|
||||
* cause it to abort and not finish.
|
||||
* Receiving this callback means that
|
||||
* the observable is dead and no
|
||||
* {@link #onComplete()} or {@link #onNext(Object)}
|
||||
* callbacks will be called.
|
||||
*
|
||||
* @param throwable an optional throwable that could
|
||||
* be sent.
|
||||
*/
|
||||
public void onError(@NonNull Throwable throwable) {}
|
||||
|
||||
/**
|
||||
* Called before the observer begins
|
||||
* to process and emit items or complete.
|
||||
*/
|
||||
public void onStart() {}
|
||||
|
||||
/**
|
||||
* Called when the Observer emits an
|
||||
* item. It can be called multiple times.
|
||||
* It cannot be called after onComplete
|
||||
* has been called.
|
||||
*
|
||||
* @param item the item that has been emitted,
|
||||
* can be null.
|
||||
*/
|
||||
public void onNext(@Nullable T item) {}
|
||||
|
||||
/**
|
||||
* This method is called when the observer is
|
||||
* finished sending the subscriber events. It
|
||||
* is guaranteed that no other methods will be
|
||||
* called on the OnSubscribe after this method
|
||||
* has been called.
|
||||
*/
|
||||
public void onComplete() {}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class Schedulers {
|
||||
private static final Executor sWorker = Executors.newFixedThreadPool(4);
|
||||
private static final Executor sIOWorker = Executors.newSingleThreadExecutor();
|
||||
private static final Executor sMain = new ThreadExecutor(Looper.getMainLooper());
|
||||
|
||||
/**
|
||||
* The worker thread executor, will
|
||||
* execute work on any one of multiple
|
||||
* threads.
|
||||
*
|
||||
* @return a non-null executor.
|
||||
*/
|
||||
@NonNull
|
||||
public static Executor worker() {
|
||||
return sWorker;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main thread.
|
||||
*
|
||||
* @return a non-null executor that does work on the main thread.
|
||||
*/
|
||||
@NonNull
|
||||
public static Executor main() {
|
||||
return sMain;
|
||||
}
|
||||
|
||||
/**
|
||||
* The io thread.
|
||||
*
|
||||
* @return a non-null executor that does
|
||||
* work on a single thread off the main thread.
|
||||
*/
|
||||
@NonNull
|
||||
public static Executor io() {
|
||||
return sIOWorker;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
public interface Subscriber<T> extends Subscription {
|
||||
|
||||
/**
|
||||
* Called immediately upon subscribing
|
||||
* and before the Observable begins
|
||||
* emitting items. This should not be
|
||||
* called by the creator of the Observable
|
||||
* and is rather called internally by the
|
||||
* Observable class itself.
|
||||
*/
|
||||
void onStart();
|
||||
|
||||
/**
|
||||
* Called when the observable
|
||||
* runs into an error that will
|
||||
* cause it to abort and not finish.
|
||||
* Receiving this callback means that
|
||||
* the observable is dead and no
|
||||
* {@link #onComplete()} or {@link #onNext(Object)}
|
||||
* callbacks will be called.
|
||||
*
|
||||
* @param throwable an optional throwable that could
|
||||
* be sent.
|
||||
*/
|
||||
void onError(@NonNull Throwable throwable);
|
||||
|
||||
/**
|
||||
* Called when the Observer emits an
|
||||
* item. It can be called multiple times.
|
||||
* It cannot be called after onComplete
|
||||
* has been called.
|
||||
*
|
||||
* @param item the item that has been emitted,
|
||||
* can be null.
|
||||
*/
|
||||
void onNext(@Nullable T item);
|
||||
|
||||
/**
|
||||
* This method is called when the observer is
|
||||
* finished sending the subscriber events. It
|
||||
* is guaranteed that no other methods will be
|
||||
* called on the OnSubscribe after this method
|
||||
* has been called.
|
||||
*/
|
||||
void onComplete();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
public interface Subscription {
|
||||
|
||||
void unsubscribe();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package acr.browser.lightning.react;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class ThreadExecutor implements Executor {
|
||||
|
||||
private final Handler mHandler;
|
||||
|
||||
public ThreadExecutor(@NonNull Looper looper) {
|
||||
mHandler = new Handler(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(@NonNull Runnable command) {
|
||||
mHandler.post(command);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2011 Peter Karich
|
||||
*
|
||||
* 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 acr.browser.lightning.reading;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
|
||||
/**
|
||||
* This class is not thread safe. Use one new instance every time due to
|
||||
* encoding variable.
|
||||
*
|
||||
* @author Peter Karich
|
||||
*/
|
||||
public class Converter {
|
||||
|
||||
private final static String UTF8 = "UTF-8";
|
||||
private final static String ISO = "ISO-8859-1";
|
||||
private final static int K2 = 2048;
|
||||
private int maxBytes = 1000000 / 2;
|
||||
private String encoding;
|
||||
private String url;
|
||||
|
||||
public Converter(String urlOnlyHint) {
|
||||
url = urlOnlyHint;
|
||||
}
|
||||
|
||||
public Converter() {
|
||||
}
|
||||
|
||||
public Converter setMaxBytes(int maxBytes) {
|
||||
this.maxBytes = maxBytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static String extractEncoding(String contentType) {
|
||||
String[] values;
|
||||
if (contentType != null)
|
||||
values = contentType.split(";");
|
||||
else
|
||||
values = new String[0];
|
||||
|
||||
String charset = "";
|
||||
|
||||
for (String value : values) {
|
||||
value = value.trim().toLowerCase(Locale.getDefault());
|
||||
|
||||
if (value.startsWith("charset="))
|
||||
charset = value.substring("charset=".length());
|
||||
}
|
||||
|
||||
// http1.1 says ISO-8859-1 is the default charset
|
||||
if (charset.isEmpty())
|
||||
charset = ISO;
|
||||
|
||||
return charset;
|
||||
}
|
||||
|
||||
public String getEncoding() {
|
||||
if (encoding == null)
|
||||
return "";
|
||||
return encoding.toLowerCase(Locale.getDefault());
|
||||
}
|
||||
|
||||
public String streamToString(InputStream is) {
|
||||
return streamToString(is, maxBytes, encoding);
|
||||
}
|
||||
|
||||
public String streamToString(InputStream is, String enc) {
|
||||
return streamToString(is, maxBytes, enc);
|
||||
}
|
||||
|
||||
/**
|
||||
* reads bytes off the string and returns a string
|
||||
*
|
||||
* @param is input stream to read
|
||||
* @param maxBytes
|
||||
* The max bytes that we want to read from the input stream
|
||||
* @return String
|
||||
*/
|
||||
private String streamToString(InputStream is, int maxBytes, String enc) {
|
||||
encoding = enc;
|
||||
// Http 1.1. standard is iso-8859-1 not utf8 :(
|
||||
// but we force utf-8 as youtube assumes it ;)
|
||||
if (encoding == null || encoding.isEmpty())
|
||||
encoding = UTF8;
|
||||
|
||||
BufferedInputStream in = null;
|
||||
try {
|
||||
in = new BufferedInputStream(is, K2);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
// detect encoding with the help of meta tag
|
||||
try {
|
||||
in.mark(K2 * 2);
|
||||
String tmpEnc = detectCharset("charset=", output, in, encoding);
|
||||
if (tmpEnc != null)
|
||||
encoding = tmpEnc;
|
||||
else {
|
||||
Log.d(Constants.TAG, "no charset found in first stage");
|
||||
// detect with the help of xml beginning ala
|
||||
// encoding="charset"
|
||||
tmpEnc = detectCharset("encoding=", output, in, encoding);
|
||||
if (tmpEnc != null)
|
||||
encoding = tmpEnc;
|
||||
else
|
||||
Log.d(Constants.TAG, "no charset found in second stage");
|
||||
}
|
||||
|
||||
if (!Charset.isSupported(encoding))
|
||||
throw new UnsupportedEncodingException(encoding);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.d(Constants.TAG,
|
||||
"Using default encoding:" + UTF8 + " problem:" + e.getMessage()
|
||||
+ " encoding:" + encoding + ' ' + url);
|
||||
encoding = UTF8;
|
||||
}
|
||||
|
||||
// SocketException: Connection reset
|
||||
// IOException: missing CR => problem on server (probably some xml
|
||||
// character thing?)
|
||||
// IOException: Premature EOF => socket unexpectly closed from
|
||||
// server
|
||||
int bytesRead = output.size();
|
||||
byte[] arr = new byte[K2];
|
||||
while (true) {
|
||||
if (bytesRead >= maxBytes) {
|
||||
Log.d(Constants.TAG, "Maxbyte of " + maxBytes
|
||||
+ " exceeded! Maybe html is now broken but try it nevertheless. Url: "
|
||||
+ url);
|
||||
break;
|
||||
}
|
||||
|
||||
int n = in.read(arr);
|
||||
if (n < 0)
|
||||
break;
|
||||
bytesRead += n;
|
||||
output.write(arr, 0, n);
|
||||
}
|
||||
|
||||
return output.toString(encoding);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, e.toString() + " url:" + url);
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* This method detects the charset even if the first call only returns some
|
||||
* bytes. It will read until 4K bytes are reached and then try to determine
|
||||
* the encoding
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private static String detectCharset(String key, ByteArrayOutputStream bos, BufferedInputStream in,
|
||||
String enc) throws IOException {
|
||||
|
||||
// Grab better encoding from stream
|
||||
byte[] arr = new byte[K2];
|
||||
int nSum = 0;
|
||||
while (nSum < K2) {
|
||||
int n = in.read(arr);
|
||||
if (n < 0)
|
||||
break;
|
||||
|
||||
nSum += n;
|
||||
bos.write(arr, 0, n);
|
||||
}
|
||||
|
||||
String str = bos.toString(enc);
|
||||
int encIndex = str.indexOf(key);
|
||||
int clength = key.length();
|
||||
if (encIndex > 0) {
|
||||
char startChar = str.charAt(encIndex + clength);
|
||||
int lastEncIndex;
|
||||
if (startChar == '\'')
|
||||
// if we have charset='something'
|
||||
lastEncIndex = str.indexOf('\'', ++encIndex + clength);
|
||||
else if (startChar == '\"')
|
||||
// if we have charset="something"
|
||||
lastEncIndex = str.indexOf('\"', ++encIndex + clength);
|
||||
else {
|
||||
// if we have "text/html; charset=utf-8"
|
||||
int first = str.indexOf('\"', encIndex + clength);
|
||||
if (first < 0)
|
||||
first = Integer.MAX_VALUE;
|
||||
|
||||
// or "text/html; charset=utf-8 "
|
||||
int sec = str.indexOf(' ', encIndex + clength);
|
||||
if (sec < 0)
|
||||
sec = Integer.MAX_VALUE;
|
||||
lastEncIndex = Math.min(first, sec);
|
||||
|
||||
// or "text/html; charset=utf-8 '
|
||||
int third = str.indexOf('\'', encIndex + clength);
|
||||
if (third > 0)
|
||||
lastEncIndex = Math.min(lastEncIndex, third);
|
||||
}
|
||||
|
||||
// re-read byte array with different encoding
|
||||
// assume that the encoding string cannot be greater than 40 chars
|
||||
if (lastEncIndex > encIndex + clength && lastEncIndex < encIndex + clength + 40) {
|
||||
String tmpEnc = SHelper.encodingCleanup(str.substring(encIndex + clength,
|
||||
lastEncIndex));
|
||||
try {
|
||||
in.reset();
|
||||
bos.reset();
|
||||
return tmpEnc;
|
||||
} catch (IOException ex) {
|
||||
Log.e(Constants.TAG, "Couldn't reset stream to re-read with new encoding "
|
||||
+ tmpEnc + ' ' + ex.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
/*
|
||||
* Copyright 2011 Peter Karich
|
||||
*
|
||||
* 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 acr.browser.lightning.reading;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
/**
|
||||
* Class to fetch articles. This class is thread safe.
|
||||
*
|
||||
* @author Peter Karich
|
||||
*/
|
||||
public class HtmlFetcher {
|
||||
|
||||
private static final Pattern SPACE = Pattern.compile(" ");
|
||||
|
||||
static {
|
||||
SHelper.enableCookieMgmt();
|
||||
SHelper.enableUserAgentOverwrite();
|
||||
SHelper.enableAnySSL();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
BufferedReader reader = null;
|
||||
BufferedWriter writer = null;
|
||||
try {
|
||||
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
reader = new BufferedReader(new FileReader("urls.txt"));
|
||||
String line;
|
||||
Set<String> existing = new LinkedHashSet<>();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
int index1 = line.indexOf('\"');
|
||||
int index2 = line.indexOf('\"', index1 + 1);
|
||||
String url = line.substring(index1 + 1, index2);
|
||||
String domainStr = SHelper.extractDomain(url, true);
|
||||
String counterStr = "";
|
||||
// TODO more similarities
|
||||
if (existing.contains(domainStr))
|
||||
counterStr = "2";
|
||||
else
|
||||
existing.add(domainStr);
|
||||
|
||||
String html = new HtmlFetcher().fetchAsString(url, 2000);
|
||||
String outFile = domainStr + counterStr + ".html";
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
writer = new BufferedWriter(new FileWriter(outFile));
|
||||
writer.write(html);
|
||||
}
|
||||
} finally {
|
||||
Utils.close(reader);
|
||||
Utils.close(writer);
|
||||
}
|
||||
}
|
||||
|
||||
private String referrer = "http://jetsli.de/crawler";
|
||||
private String userAgent = "Mozilla/5.0 (compatible; Jetslide; +" + referrer + ')';
|
||||
private String cacheControl = "max-age=0";
|
||||
private String language = "en-us";
|
||||
private String accept = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
|
||||
private String charset = "UTF-8";
|
||||
private SCache cache;
|
||||
private final AtomicInteger cacheCounter = new AtomicInteger(0);
|
||||
private int maxTextLength = -1;
|
||||
private ArticleTextExtractor extractor = new ArticleTextExtractor();
|
||||
private Set<String> furtherResolveNecessary = new LinkedHashSet<String>() {
|
||||
{
|
||||
add("bit.ly");
|
||||
add("cli.gs");
|
||||
add("deck.ly");
|
||||
add("fb.me");
|
||||
add("feedproxy.google.com");
|
||||
add("flic.kr");
|
||||
add("fur.ly");
|
||||
add("goo.gl");
|
||||
add("is.gd");
|
||||
add("ink.co");
|
||||
add("j.mp");
|
||||
add("lnkd.in");
|
||||
add("on.fb.me");
|
||||
add("ow.ly");
|
||||
add("plurl.us");
|
||||
add("sns.mx");
|
||||
add("snurl.com");
|
||||
add("su.pr");
|
||||
add("t.co");
|
||||
add("tcrn.ch");
|
||||
add("tl.gd");
|
||||
add("tiny.cc");
|
||||
add("tinyurl.com");
|
||||
add("tmi.me");
|
||||
add("tr.im");
|
||||
add("twurl.nl");
|
||||
}
|
||||
};
|
||||
|
||||
public HtmlFetcher() {
|
||||
}
|
||||
|
||||
public void setExtractor(ArticleTextExtractor extractor) {
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
public ArticleTextExtractor getExtractor() {
|
||||
return extractor;
|
||||
}
|
||||
|
||||
public HtmlFetcher setCache(SCache cache) {
|
||||
this.cache = cache;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SCache getCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
public int getCacheCounter() {
|
||||
return cacheCounter.get();
|
||||
}
|
||||
|
||||
public HtmlFetcher clearCacheCounter() {
|
||||
cacheCounter.set(0);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HtmlFetcher setMaxTextLength(int maxTextLength) {
|
||||
this.maxTextLength = maxTextLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getMaxTextLength() {
|
||||
return maxTextLength;
|
||||
}
|
||||
|
||||
public void setAccept(String accept) {
|
||||
this.accept = accept;
|
||||
}
|
||||
|
||||
public void setCharset(String charset) {
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
public void setCacheControl(String cacheControl) {
|
||||
this.cacheControl = cacheControl;
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
public String getReferrer() {
|
||||
return referrer;
|
||||
}
|
||||
|
||||
public HtmlFetcher setReferrer(String referrer) {
|
||||
this.referrer = referrer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
public void setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
public String getAccept() {
|
||||
return accept;
|
||||
}
|
||||
|
||||
public String getCacheControl() {
|
||||
return cacheControl;
|
||||
}
|
||||
|
||||
public String getCharset() {
|
||||
return charset;
|
||||
}
|
||||
|
||||
public JResult fetchAndExtract(String url, int timeout, boolean resolve) throws Exception {
|
||||
return fetchAndExtract(url, timeout, resolve, 0, false);
|
||||
}
|
||||
|
||||
// main workhorse to call externally
|
||||
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
|
||||
private JResult fetchAndExtract(String url, int timeout, boolean resolve,
|
||||
int maxContentSize, boolean forceReload) throws Exception {
|
||||
String originalUrl = url;
|
||||
url = SHelper.removeHashbang(url);
|
||||
String gUrl = SHelper.getUrlFromUglyGoogleRedirect(url);
|
||||
if (gUrl != null)
|
||||
url = gUrl;
|
||||
else {
|
||||
gUrl = SHelper.getUrlFromUglyFacebookRedirect(url);
|
||||
if (gUrl != null)
|
||||
url = gUrl;
|
||||
}
|
||||
|
||||
if (resolve) {
|
||||
// check if we can avoid resolving the URL (which hits the website!)
|
||||
JResult res = getFromCache(url, originalUrl);
|
||||
if (res != null)
|
||||
return res;
|
||||
|
||||
String resUrl = getResolvedUrl(url, timeout, 0);
|
||||
if (resUrl.isEmpty()) {
|
||||
|
||||
JResult result = new JResult();
|
||||
if (cache != null)
|
||||
cache.put(url, result);
|
||||
return result.setUrl(url);
|
||||
}
|
||||
|
||||
// if resolved url is different then use it!
|
||||
if (!resUrl.equals(url)) {
|
||||
// this is necessary e.g. for some homebaken url resolvers which return
|
||||
// the resolved url relative to url!
|
||||
url = SHelper.useDomainOfFirstArg4Second(url, resUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// check if we have the (resolved) URL in cache
|
||||
JResult res = getFromCache(url, originalUrl);
|
||||
if (res != null)
|
||||
return res;
|
||||
|
||||
JResult result = new JResult();
|
||||
// or should we use? <link rel="canonical" href="http://www.N24.de/news/newsitem_6797232.html"/>
|
||||
result.setUrl(url);
|
||||
result.setOriginalUrl(originalUrl);
|
||||
|
||||
// Immediately put the url into the cache as extracting content takes time.
|
||||
if (cache != null) {
|
||||
cache.put(originalUrl, result);
|
||||
cache.put(url, result);
|
||||
}
|
||||
|
||||
// extract content to the extent appropriate for content type
|
||||
String lowerUrl = url.toLowerCase();
|
||||
if (SHelper.isDoc(lowerUrl) || SHelper.isApp(lowerUrl) || SHelper.isPackage(lowerUrl)) {
|
||||
// skip
|
||||
} else if (SHelper.isVideo(lowerUrl) || SHelper.isAudio(lowerUrl)) {
|
||||
result.setVideoUrl(url);
|
||||
} else if (SHelper.isImage(lowerUrl)) {
|
||||
result.setImageUrl(url);
|
||||
} else {
|
||||
try {
|
||||
String urlToDownload = url;
|
||||
if (forceReload) {
|
||||
urlToDownload = getURLtoBreakCache(url);
|
||||
}
|
||||
extractor.extractContent(result, fetchAsString(urlToDownload, timeout), maxContentSize);
|
||||
} catch (IOException io) {
|
||||
// do nothing
|
||||
}
|
||||
if (result.getFaviconUrl().isEmpty())
|
||||
result.setFaviconUrl(SHelper.getDefaultFavicon(url));
|
||||
|
||||
// some links are relative to root and do not include the domain of the url :(
|
||||
if (!result.getFaviconUrl().isEmpty())
|
||||
result.setFaviconUrl(fixUrl(url, result.getFaviconUrl()));
|
||||
|
||||
if (!result.getImageUrl().isEmpty())
|
||||
result.setImageUrl(fixUrl(url, result.getImageUrl()));
|
||||
|
||||
if (!result.getVideoUrl().isEmpty())
|
||||
result.setVideoUrl(fixUrl(url, result.getVideoUrl()));
|
||||
|
||||
if (!result.getRssUrl().isEmpty())
|
||||
result.setRssUrl(fixUrl(url, result.getRssUrl()));
|
||||
}
|
||||
result.setText(lessText(result.getText()));
|
||||
synchronized (result) {
|
||||
result.notifyAll();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Ugly hack to break free from any cached versions, a few URLs required this.
|
||||
private static String getURLtoBreakCache(String url) {
|
||||
try {
|
||||
URL aURL = new URL(url);
|
||||
if (aURL.getQuery() != null && aURL.getQuery().isEmpty()) {
|
||||
return url + "?1";
|
||||
} else {
|
||||
return url + "&1";
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private String lessText(String text) {
|
||||
if (text == null)
|
||||
return "";
|
||||
|
||||
if (maxTextLength >= 0 && text.length() > maxTextLength)
|
||||
return text.substring(0, maxTextLength);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static String fixUrl(String url, String urlOrPath) {
|
||||
return SHelper.useDomainOfFirstArg4Second(url, urlOrPath);
|
||||
}
|
||||
|
||||
private String fetchAsString(String urlAsString, int timeout)
|
||||
throws IOException {
|
||||
return fetchAsString(urlAsString, timeout, true);
|
||||
}
|
||||
|
||||
// main routine to get raw webpage content
|
||||
private String fetchAsString(String urlAsString, int timeout, boolean includeSomeGooseOptions)
|
||||
throws IOException {
|
||||
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, includeSomeGooseOptions);
|
||||
hConn.setInstanceFollowRedirects(true);
|
||||
String encoding = hConn.getContentEncoding();
|
||||
InputStream is;
|
||||
if ("gzip".equalsIgnoreCase(encoding)) {
|
||||
is = new GZIPInputStream(hConn.getInputStream());
|
||||
} else if ("deflate".equalsIgnoreCase(encoding)) {
|
||||
is = new InflaterInputStream(hConn.getInputStream(), new Inflater(true));
|
||||
} else {
|
||||
is = hConn.getInputStream();
|
||||
}
|
||||
|
||||
String enc = Converter.extractEncoding(hConn.getContentType());
|
||||
return createConverter(urlAsString).streamToString(is, enc);
|
||||
}
|
||||
|
||||
private static Converter createConverter(String url) {
|
||||
return new Converter(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* On some devices we have to hack:
|
||||
* http://developers.sun.com/mobility/reference/techart/design_guidelines/http_redirection.html
|
||||
*
|
||||
* @param timeout Sets a specified timeout value, in milliseconds
|
||||
* @return the resolved url if any. Or null if it couldn't resolve the url
|
||||
* (within the specified time) or the same url if response code is OK
|
||||
*/
|
||||
private String getResolvedUrl(String urlAsString, int timeout,
|
||||
int num_redirects) {
|
||||
String newUrl = null;
|
||||
int responseCode = -1;
|
||||
try {
|
||||
HttpURLConnection hConn = createUrlConnection(urlAsString, timeout, true);
|
||||
// force no follow
|
||||
hConn.setInstanceFollowRedirects(false);
|
||||
// the program doesn't care what the content actually is !!
|
||||
// http://java.sun.com/developer/JDCTechTips/2003/tt0422.html
|
||||
hConn.setRequestMethod("HEAD");
|
||||
hConn.connect();
|
||||
responseCode = hConn.getResponseCode();
|
||||
hConn.getInputStream().close();
|
||||
if (responseCode == HttpURLConnection.HTTP_OK)
|
||||
return urlAsString;
|
||||
|
||||
newUrl = hConn.getHeaderField("Location");
|
||||
// Note that the max recursion level is 5.
|
||||
if (responseCode / 100 == 3 && newUrl != null && num_redirects < 5) {
|
||||
newUrl = SPACE.matcher(newUrl).replaceAll("+");
|
||||
// some services use (none-standard) utf8 in their location header
|
||||
if (urlAsString.contains("://bit.ly")
|
||||
|| urlAsString.contains("://is.gd"))
|
||||
newUrl = encodeUriFromHeader(newUrl);
|
||||
|
||||
// AP: This code is not longer need, instead we always follow
|
||||
// multiple redirects.
|
||||
//
|
||||
// fix problems if shortened twice. as it is often the case after twitters' t.co bullshit
|
||||
//if (furtherResolveNecessary.contains(SHelper.extractDomain(newUrl, true)))
|
||||
// newUrl = getResolvedUrl(newUrl, timeout);
|
||||
|
||||
// Add support for URLs with multiple levels of redirection,
|
||||
// call getResolvedUrl until there is no more redirects or a
|
||||
// max number of redirects is reached.
|
||||
newUrl = SHelper.useDomainOfFirstArg4Second(urlAsString, newUrl);
|
||||
newUrl = getResolvedUrl(newUrl, timeout, num_redirects + 1);
|
||||
return newUrl;
|
||||
} else
|
||||
return urlAsString;
|
||||
|
||||
} catch (Exception ex) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a URI that was decoded as ISO-8859-1 and applies percent-encoding
|
||||
* to non-ASCII characters. Workaround for broken origin servers that send
|
||||
* UTF-8 in the Location: header.
|
||||
*/
|
||||
private static String encodeUriFromHeader(String badLocation) {
|
||||
StringBuilder sb = new StringBuilder(badLocation.length());
|
||||
|
||||
for (char ch : badLocation.toCharArray()) {
|
||||
if (ch < (char) 128) {
|
||||
sb.append(ch);
|
||||
} else {
|
||||
// this is ONLY valid if the uri was decoded using ISO-8859-1
|
||||
sb.append(String.format("%%%02X", (int) ch));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private HttpURLConnection createUrlConnection(String urlAsStr, int timeout,
|
||||
boolean includeSomeGooseOptions) throws IOException {
|
||||
URL url = new URL(urlAsStr);
|
||||
//using proxy may increase latency
|
||||
HttpURLConnection hConn = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
|
||||
hConn.setRequestProperty("User-Agent", userAgent);
|
||||
hConn.setRequestProperty("Accept", accept);
|
||||
|
||||
if (includeSomeGooseOptions) {
|
||||
hConn.setRequestProperty("Accept-Language", language);
|
||||
hConn.setRequestProperty("content-charset", charset);
|
||||
hConn.addRequestProperty("Referer", referrer);
|
||||
// avoid the cache for testing purposes only?
|
||||
hConn.setRequestProperty("Cache-Control", cacheControl);
|
||||
}
|
||||
|
||||
// suggest respond to be gzipped or deflated (which is just another compression)
|
||||
// http://stackoverflow.com/q/3932117
|
||||
hConn.setRequestProperty("Accept-Encoding", "gzip, deflate");
|
||||
hConn.setConnectTimeout(timeout);
|
||||
hConn.setReadTimeout(timeout);
|
||||
return hConn;
|
||||
}
|
||||
|
||||
private JResult getFromCache(String url, String originalUrl) {
|
||||
if (cache != null) {
|
||||
JResult res = cache.get(url);
|
||||
if (res != null) {
|
||||
// e.g. the cache returned a shortened url as original url now we want to store the
|
||||
// current original url! Also it can be that the cache response to url but the JResult
|
||||
// does not contain it so overwrite it:
|
||||
res.setUrl(url);
|
||||
res.setOriginalUrl(originalUrl);
|
||||
cacheCounter.addAndGet(1);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package acr.browser.lightning.reading;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
/**
|
||||
* Class which encapsulates the data from an image found under an element
|
||||
*
|
||||
* @author Chris Alexander, chris@chris-alexander.co.uk
|
||||
*/
|
||||
class ImageResult {
|
||||
|
||||
private final String src;
|
||||
public final Integer weight;
|
||||
private final String title;
|
||||
private final int height;
|
||||
private final int width;
|
||||
private final String alt;
|
||||
private final boolean noFollow;
|
||||
public Element element;
|
||||
|
||||
public ImageResult(String src, Integer weight, String title, int height, int width, String alt,
|
||||
boolean noFollow) {
|
||||
this.src = src;
|
||||
this.weight = weight;
|
||||
this.title = title;
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
this.alt = alt;
|
||||
this.noFollow = noFollow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2011 Peter Karich
|
||||
*
|
||||
* 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 acr.browser.lightning.reading;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Parsed result from web page containing important title, text and image.
|
||||
*
|
||||
* @author Peter Karich
|
||||
*/
|
||||
public class JResult implements Serializable {
|
||||
|
||||
private String title;
|
||||
private String url;
|
||||
private String originalUrl;
|
||||
private String canonicalUrl;
|
||||
private String imageUrl;
|
||||
private String videoUrl;
|
||||
private String rssUrl;
|
||||
private String text;
|
||||
private String faviconUrl;
|
||||
private String description;
|
||||
private String authorName;
|
||||
private String authorDescription;
|
||||
private Date date;
|
||||
private Collection<String> keywords;
|
||||
private List<ImageResult> images = null;
|
||||
private final List<Map<String, String>> links = new ArrayList<>();
|
||||
private String type;
|
||||
private String sitename;
|
||||
private String language;
|
||||
|
||||
public JResult() {
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
if (url == null)
|
||||
return "";
|
||||
return url;
|
||||
}
|
||||
|
||||
public JResult setUrl(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JResult setOriginalUrl(String originalUrl) {
|
||||
this.originalUrl = originalUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getOriginalUrl() {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
public JResult setCanonicalUrl(String canonicalUrl) {
|
||||
this.canonicalUrl = canonicalUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCanonicalUrl() {
|
||||
return canonicalUrl;
|
||||
}
|
||||
|
||||
public String getFaviconUrl() {
|
||||
if (faviconUrl == null)
|
||||
return "";
|
||||
return faviconUrl;
|
||||
}
|
||||
|
||||
public JResult setFaviconUrl(String faviconUrl) {
|
||||
this.faviconUrl = faviconUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JResult setRssUrl(String rssUrl) {
|
||||
this.rssUrl = rssUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getRssUrl() {
|
||||
if (rssUrl == null)
|
||||
return "";
|
||||
return rssUrl;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
if (description == null)
|
||||
return "";
|
||||
return description;
|
||||
}
|
||||
|
||||
public JResult setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAuthorName() {
|
||||
if (authorName == null)
|
||||
return "";
|
||||
return authorName;
|
||||
}
|
||||
|
||||
public JResult setAuthorName(String authorName) {
|
||||
this.authorName = authorName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAuthorDescription() {
|
||||
if (authorDescription == null)
|
||||
return "";
|
||||
return authorDescription;
|
||||
}
|
||||
|
||||
public JResult setAuthorDescription(String authorDescription) {
|
||||
this.authorDescription = authorDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
if (imageUrl == null)
|
||||
return "";
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public JResult setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
if (text == null)
|
||||
return "";
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
public JResult setText(String text) {
|
||||
this.text = text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
if (title == null)
|
||||
return "";
|
||||
return title;
|
||||
}
|
||||
|
||||
public JResult setTitle(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getVideoUrl() {
|
||||
if (videoUrl == null)
|
||||
return "";
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
public JResult setVideoUrl(String videoUrl) {
|
||||
this.videoUrl = videoUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JResult setDate(Date date) {
|
||||
this.date = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Collection<String> getKeywords() {
|
||||
return keywords;
|
||||
}
|
||||
|
||||
public void setKeywords(Collection<String> keywords) {
|
||||
this.keywords = keywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return get date from url or guessed from text
|
||||
*/
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return images list
|
||||
*/
|
||||
public List<ImageResult> getImages() {
|
||||
if (images == null)
|
||||
return Collections.emptyList();
|
||||
return images;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return images count
|
||||
*/
|
||||
public int getImagesCount() {
|
||||
if (images == null)
|
||||
return 0;
|
||||
return images.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* set images list
|
||||
*/
|
||||
public void setImages(List<ImageResult> images) {
|
||||
this.images = images;
|
||||
}
|
||||
|
||||
public void addLink(String url, String text, Integer pos) {
|
||||
Map<String, String> link = new HashMap<>();
|
||||
link.put("url", url);
|
||||
link.put("text", text);
|
||||
link.put("offset", String.valueOf(pos));
|
||||
links.add(link);
|
||||
}
|
||||
|
||||
public List<Map<String, String>> getLinks() {
|
||||
if (links == null)
|
||||
return Collections.emptyList();
|
||||
return links;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getSitename() {
|
||||
return sitename;
|
||||
}
|
||||
|
||||
public void setSitename(String sitename) {
|
||||
this.sitename = sitename;
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public void setLanguage(String language) {
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "title:" + getTitle() + " imageUrl:" + getImageUrl() + " text:" + text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (C) 2010 Peter Karich <>
|
||||
* <p/>
|
||||
* 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
|
||||
* <p/>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p/>
|
||||
* 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 acr.browser.lightning.reading;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Simple impl of Map.Entry. So that we can have ordered maps.
|
||||
*
|
||||
* @author Peter Karich, peat_hal ‘at’ users ‘dot’ sourceforge ‘dot’
|
||||
* net
|
||||
*/
|
||||
public class MapEntry<K, V> implements Map.Entry<K, V>, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final K key;
|
||||
private V value;
|
||||
|
||||
public MapEntry(K key, V value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V setValue(V value) {
|
||||
this.value = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return key + ", " + value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (!(obj instanceof Map<?, ?>))
|
||||
return false;
|
||||
final MapEntry<?, ?> other = (MapEntry<?, ?>) obj;
|
||||
|
||||
return !(this.key != other.key && (this.key == null || !this.key.equals(other.key))) &&
|
||||
!(this.value != other.value && (this.value == null || !this.value.equals(other.value)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = 7;
|
||||
hash = 19 * hash + (this.key != null ? this.key.hashCode() : 0);
|
||||
hash = 19 * hash + (this.value != null ? this.value.hashCode() : 0);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package acr.browser.lightning.reading;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
|
||||
/**
|
||||
* @author goose | jim
|
||||
* @author karussell
|
||||
* <p/>
|
||||
* this class will be responsible for taking our top node and stripping out junk
|
||||
* we don't want and getting it ready for how we want it presented to the user
|
||||
*/
|
||||
public class OutputFormatter {
|
||||
|
||||
private static final int MIN_FIRST_PARAGRAPH_TEXT = 50; // Min size of first paragraph
|
||||
private static final int MIN_PARAGRAPH_TEXT = 30; // Min size of any other paragraphs
|
||||
private static final List<String> NODES_TO_REPLACE = Arrays.asList("strong", "b", "i");
|
||||
private Pattern unlikelyPattern = Pattern.compile("display:none|visibility:hidden");
|
||||
private final int minFirstParagraphText;
|
||||
private final int minParagraphText;
|
||||
private final List<String> nodesToReplace;
|
||||
private String nodesToKeepCssSelector = "p, ol";
|
||||
|
||||
public OutputFormatter() {
|
||||
this(MIN_FIRST_PARAGRAPH_TEXT, MIN_PARAGRAPH_TEXT, NODES_TO_REPLACE);
|
||||
}
|
||||
|
||||
public OutputFormatter(int minParagraphText) {
|
||||
this(minParagraphText, minParagraphText, NODES_TO_REPLACE);
|
||||
}
|
||||
|
||||
public OutputFormatter(int minFirstParagraphText, int minParagraphText) {
|
||||
this(minFirstParagraphText, minParagraphText, NODES_TO_REPLACE);
|
||||
}
|
||||
|
||||
private OutputFormatter(int minFirstParagraphText, int minParagraphText,
|
||||
List<String> nodesToReplace) {
|
||||
this.minFirstParagraphText = minFirstParagraphText;
|
||||
this.minParagraphText = minParagraphText;
|
||||
this.nodesToReplace = nodesToReplace;
|
||||
}
|
||||
|
||||
/**
|
||||
* set elements to keep in output text
|
||||
*/
|
||||
public void setNodesToKeepCssSelector(String nodesToKeepCssSelector) {
|
||||
this.nodesToKeepCssSelector = nodesToKeepCssSelector;
|
||||
}
|
||||
|
||||
/**
|
||||
* takes an element and turns the P tags into \n\n
|
||||
*/
|
||||
public String getFormattedText(Element topNode) {
|
||||
setParagraphIndex(topNode, nodesToKeepCssSelector);
|
||||
removeNodesWithNegativeScores(topNode);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int countOfP = append(topNode, sb, nodesToKeepCssSelector);
|
||||
String str = SHelper.innerTrim(sb.toString());
|
||||
|
||||
int topNodeLength = topNode.text().length();
|
||||
if (topNodeLength == 0) {
|
||||
topNodeLength = 1;
|
||||
}
|
||||
|
||||
|
||||
boolean lowTextRatio = ((str.length() / (topNodeLength * 1.0)) < 0.25);
|
||||
if (str.length() > 100 && countOfP > 0 && !lowTextRatio)
|
||||
return str;
|
||||
|
||||
// no subelements
|
||||
if (str.isEmpty() || (!topNode.text().isEmpty()
|
||||
&& str.length() <= topNode.ownText().length())
|
||||
|| countOfP == 0 || lowTextRatio) {
|
||||
str = topNode.text();
|
||||
}
|
||||
|
||||
// if jsoup failed to parse the whole html now parse this smaller
|
||||
// snippet again to avoid html tags disturbing our text:
|
||||
return Jsoup.parse(str).text();
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are elements inside our top node that have a negative gravity
|
||||
* score remove them
|
||||
*/
|
||||
private void removeNodesWithNegativeScores(Element topNode) {
|
||||
Elements gravityItems = topNode.select("*[gravityScore]");
|
||||
for (Element item : gravityItems) {
|
||||
int score = getScore(item);
|
||||
int paragraphIndex = getParagraphIndex(item);
|
||||
if (score < 0 || item.text().length() < getMinParagraph(paragraphIndex)) {
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int append(Element node, StringBuilder sb, String tagName) {
|
||||
int countOfP = 0; // Number of P elements in the article
|
||||
int paragraphWithTextIndex = 0;
|
||||
// is select more costly then getElementsByTag?
|
||||
MAIN:
|
||||
for (Element e : node.select(tagName)) {
|
||||
Element tmpEl = e;
|
||||
// check all elements until 'node'
|
||||
while (tmpEl != null && !tmpEl.equals(node)) {
|
||||
if (unlikely(tmpEl))
|
||||
continue MAIN;
|
||||
tmpEl = tmpEl.parent();
|
||||
}
|
||||
|
||||
String text = node2Text(e);
|
||||
if (text.isEmpty() || text.length() < getMinParagraph(paragraphWithTextIndex)
|
||||
|| text.length() > SHelper.countLetters(text) * 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.tagName().equals("p")) {
|
||||
countOfP++;
|
||||
}
|
||||
|
||||
sb.append(text);
|
||||
sb.append("\n\n");
|
||||
paragraphWithTextIndex += 1;
|
||||
}
|
||||
|
||||
return countOfP;
|
||||
}
|
||||
|
||||
private static void setParagraphIndex(Element node, String tagName) {
|
||||
int paragraphIndex = 0;
|
||||
for (Element e : node.select(tagName)) {
|
||||
e.attr("paragraphIndex", Integer.toString(paragraphIndex++));
|
||||
}
|
||||
}
|
||||
|
||||
private int getMinParagraph(int paragraphIndex) {
|
||||
if (paragraphIndex < 1) {
|
||||
return minFirstParagraphText;
|
||||
} else {
|
||||
return minParagraphText;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getParagraphIndex(Element el) {
|
||||
try {
|
||||
return Integer.parseInt(el.attr("paragraphIndex"));
|
||||
} catch (NumberFormatException ex) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getScore(Element el) {
|
||||
try {
|
||||
return Integer.parseInt(el.attr("gravityScore"));
|
||||
} catch (Exception ex) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean unlikely(Node e) {
|
||||
if (e.attr("class") != null && e.attr("class").toLowerCase().contains("caption"))
|
||||
return true;
|
||||
|
||||
String style = e.attr("style");
|
||||
String clazz = e.attr("class");
|
||||
return unlikelyPattern.matcher(style).find() || unlikelyPattern.matcher(clazz).find();
|
||||
}
|
||||
|
||||
private void appendTextSkipHidden(Element e, StringBuilder accum, int indent) {
|
||||
for (Node child : e.childNodes()) {
|
||||
if (unlikely(child)) {
|
||||
continue;
|
||||
}
|
||||
if (child instanceof TextNode) {
|
||||
TextNode textNode = (TextNode) child;
|
||||
String txt = textNode.text();
|
||||
accum.append(txt);
|
||||
} else if (child instanceof Element) {
|
||||
Element element = (Element) child;
|
||||
if (accum.length() > 0 && element.isBlock()
|
||||
&& !lastCharIsWhitespace(accum))
|
||||
accum.append(' ');
|
||||
else if (element.tagName().equals("br"))
|
||||
accum.append(' ');
|
||||
appendTextSkipHidden(element, accum, indent + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean lastCharIsWhitespace(StringBuilder accum) {
|
||||
return accum.length() != 0 && Character.isWhitespace(accum.charAt(accum.length() - 1));
|
||||
}
|
||||
|
||||
private String node2Text(Element el) {
|
||||
StringBuilder sb = new StringBuilder(200);
|
||||
appendTextSkipHidden(el, sb, 0);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private OutputFormatter setUnlikelyPattern(String unlikelyPattern) {
|
||||
this.unlikelyPattern = Pattern.compile(unlikelyPattern);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OutputFormatter appendUnlikelyPattern(String str) {
|
||||
return setUnlikelyPattern(unlikelyPattern.toString() + '|' + str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2011 Peter Karich
|
||||
*
|
||||
* 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 acr.browser.lightning.reading;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Peter Karich
|
||||
*/
|
||||
public interface SCache {
|
||||
|
||||
JResult get(String url);
|
||||
|
||||
void put(String url, JResult res);
|
||||
|
||||
int getSize();
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* Copyright 2011 Peter Karich
|
||||
*
|
||||
* 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 acr.browser.lightning.reading;
|
||||
|
||||
import org.jsoup.nodes.Element;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
/**
|
||||
* @author Peter Karich
|
||||
*/
|
||||
class SHelper {
|
||||
|
||||
private static final String UTF8 = "UTF-8";
|
||||
private static final Pattern SPACE = Pattern.compile(" ");
|
||||
|
||||
public static String replaceSpaces(String url) {
|
||||
if (!url.isEmpty()) {
|
||||
url = url.trim();
|
||||
if (url.contains(" ")) {
|
||||
Matcher spaces = SPACE.matcher(url);
|
||||
url = spaces.replaceAll("%20");
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public static int count(String str, String substring) {
|
||||
int c = 0;
|
||||
int index1 = str.indexOf(substring);
|
||||
if (index1 >= 0) {
|
||||
c++;
|
||||
c += count(str.substring(index1 + substring.length()), substring);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* remove more than two spaces or newlines
|
||||
*/
|
||||
public static String innerTrim(String str) {
|
||||
if (str.isEmpty())
|
||||
return "";
|
||||
|
||||
StringBuilder sb = new StringBuilder(str.length());
|
||||
boolean previousSpace = false;
|
||||
for (int i = 0, length = str.length(); i < length; i++) {
|
||||
char c = str.charAt(i);
|
||||
if (c == ' ' || (int) c == 9 || c == '\n') {
|
||||
previousSpace = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousSpace)
|
||||
sb.append(' ');
|
||||
|
||||
previousSpace = false;
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts reading the encoding from the first valid character until an
|
||||
* invalid encoding character occurs.
|
||||
*/
|
||||
public static String encodingCleanup(String str) {
|
||||
StringBuilder sb = new StringBuilder(str.length());
|
||||
boolean startedWithCorrectString = false;
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
if (Character.isDigit(c) || Character.isLetter(c) || c == '-' || c == '_') {
|
||||
startedWithCorrectString = true;
|
||||
sb.append(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startedWithCorrectString)
|
||||
break;
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the longest substring as str1.substring(result[0], result[1]);
|
||||
*/
|
||||
public static String getLongestSubstring(String str1, String str2) {
|
||||
int res[] = longestSubstring(str1, str2);
|
||||
if (res == null || res[0] >= res[1])
|
||||
return "";
|
||||
|
||||
return str1.substring(res[0], res[1]);
|
||||
}
|
||||
|
||||
private static int[] longestSubstring(String str1, String str2) {
|
||||
if (str1 == null || str1.isEmpty() || str2 == null || str2.isEmpty())
|
||||
return null;
|
||||
|
||||
// dynamic programming => save already identical length into array
|
||||
// to understand this algo simply print identical length in every entry of the array
|
||||
// i+1, j+1 then reuses information from i,j
|
||||
// java initializes them already with 0
|
||||
int[][] num = new int[str1.length()][str2.length()];
|
||||
int maxlen = 0;
|
||||
int lastSubstrBegin = 0;
|
||||
int endIndex = 0;
|
||||
for (int i = 0; i < str1.length(); i++) {
|
||||
for (int j = 0; j < str2.length(); j++) {
|
||||
if (str1.charAt(i) == str2.charAt(j)) {
|
||||
if ((i == 0) || (j == 0))
|
||||
num[i][j] = 1;
|
||||
else
|
||||
num[i][j] = 1 + num[i - 1][j - 1];
|
||||
|
||||
if (num[i][j] > maxlen) {
|
||||
maxlen = num[i][j];
|
||||
// generate substring from str1 => i
|
||||
lastSubstrBegin = i - num[i][j] + 1;
|
||||
endIndex = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new int[]{lastSubstrBegin, endIndex};
|
||||
}
|
||||
|
||||
public static String getDefaultFavicon(String url) {
|
||||
return useDomainOfFirstArg4Second(url, "/favicon.ico");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param urlForDomain extract the domain from this url
|
||||
* @param path this url does not have a domain
|
||||
* @return
|
||||
*/
|
||||
public static String useDomainOfFirstArg4Second(String urlForDomain, String path) {
|
||||
try {
|
||||
// See: http://stackoverflow.com/questions/1389184/building-an-absolute-url-from-a-relative-url-in-java
|
||||
URL baseUrl = new URL(urlForDomain);
|
||||
URL relativeurl = new URL(baseUrl, path);
|
||||
return relativeurl.toString();
|
||||
} catch (MalformedURLException ex) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public static String extractHost(String url) {
|
||||
return extractDomain(url, false);
|
||||
}
|
||||
|
||||
public static String extractDomain(String url, boolean aggressive) {
|
||||
if (url.startsWith("http://"))
|
||||
url = url.substring("http://".length());
|
||||
else if (url.startsWith("https://"))
|
||||
url = url.substring("https://".length());
|
||||
|
||||
if (aggressive) {
|
||||
if (url.startsWith("www."))
|
||||
url = url.substring("www.".length());
|
||||
|
||||
// strip mobile from start
|
||||
if (url.startsWith("m."))
|
||||
url = url.substring("m.".length());
|
||||
}
|
||||
|
||||
int slashIndex = url.indexOf('/');
|
||||
if (slashIndex > 0)
|
||||
url = url.substring(0, slashIndex);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
public static boolean isVideoLink(String url) {
|
||||
url = extractDomain(url, true);
|
||||
return url.startsWith("youtube.com") || url.startsWith("video.yahoo.com")
|
||||
|| url.startsWith("vimeo.com") || url.startsWith("blip.tv");
|
||||
}
|
||||
|
||||
public static boolean isVideo(String url) {
|
||||
return url.endsWith(".mpeg") || url.endsWith(".mpg") || url.endsWith(".avi") || url.endsWith(".mov")
|
||||
|| url.endsWith(".mpg4") || url.endsWith(".mp4") || url.endsWith(".flv") || url.endsWith(".wmv");
|
||||
}
|
||||
|
||||
public static boolean isAudio(String url) {
|
||||
return url.endsWith(".mp3") || url.endsWith(".ogg") || url.endsWith(".m3u") || url.endsWith(".wav");
|
||||
}
|
||||
|
||||
public static boolean isDoc(String url) {
|
||||
return url.endsWith(".pdf") || url.endsWith(".ppt") || url.endsWith(".doc")
|
||||
|| url.endsWith(".swf") || url.endsWith(".rtf") || url.endsWith(".xls");
|
||||
}
|
||||
|
||||
public static boolean isPackage(String url) {
|
||||
return url.endsWith(".gz") || url.endsWith(".tgz") || url.endsWith(".zip")
|
||||
|| url.endsWith(".rar") || url.endsWith(".deb") || url.endsWith(".rpm") || url.endsWith(".7z");
|
||||
}
|
||||
|
||||
public static boolean isApp(String url) {
|
||||
return url.endsWith(".exe") || url.endsWith(".bin") || url.endsWith(".bat") || url.endsWith(".dmg");
|
||||
}
|
||||
|
||||
public static boolean isImage(String url) {
|
||||
return url.endsWith(".png") || url.endsWith(".jpeg") || url.endsWith(".gif")
|
||||
|| url.endsWith(".jpg") || url.endsWith(".bmp") || url.endsWith(".ico") || url.endsWith(".eps");
|
||||
}
|
||||
|
||||
/**
|
||||
* @see "http://blogs.sun.com/CoreJavaTechTips/entry/cookie_handling_in_java_se"
|
||||
*/
|
||||
public static void enableCookieMgmt() {
|
||||
CookieManager manager = new CookieManager();
|
||||
manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
|
||||
CookieHandler.setDefault(manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see "http://stackoverflow.com/questions/2529682/setting-user-agent-of-a-java-urlconnection"
|
||||
*/
|
||||
public static void enableUserAgentOverwrite() {
|
||||
System.setProperty("http.agent", "");
|
||||
}
|
||||
|
||||
public static String getUrlFromUglyGoogleRedirect(String url) {
|
||||
if (url.startsWith("https://www.google.com/url?")) {
|
||||
url = url.substring("https://www.google.com/url?".length());
|
||||
String arr[] = urlDecode(url).split("&");
|
||||
for (String str : arr) {
|
||||
if (str.startsWith("q="))
|
||||
return str.substring("q=".length());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getUrlFromUglyFacebookRedirect(String url) {
|
||||
if (url.startsWith("https://www.facebook.com/l.php?u=")) {
|
||||
url = url.substring("https://www.facebook.com/l.php?u=".length());
|
||||
return urlDecode(url);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String urlEncode(String str) {
|
||||
try {
|
||||
return URLEncoder.encode(str, UTF8);
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
private static String urlDecode(String str) {
|
||||
try {
|
||||
return URLDecoder.decode(str, UTF8);
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Popular sites uses the #! to indicate the importance of the following
|
||||
* chars. Ugly but true. Such as: facebook, twitter, gizmodo, ...
|
||||
*/
|
||||
public static String removeHashbang(String url) {
|
||||
return url.replaceFirst("#!", "");
|
||||
}
|
||||
|
||||
public static String printNode(Element root) {
|
||||
return printNode(root, 0);
|
||||
}
|
||||
|
||||
private static String printNode(Element root, int indentation) {
|
||||
StringBuilder sb = new StringBuilder(indentation);
|
||||
for (int i = 0; i < indentation; i++) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(root.tagName());
|
||||
sb.append(':');
|
||||
sb.append(root.ownText());
|
||||
sb.append('\n');
|
||||
for (Element el : root.children()) {
|
||||
sb.append(printNode(el, indentation + 1));
|
||||
sb.append('\n');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String estimateDate(String url) {
|
||||
int index = url.indexOf("://");
|
||||
if (index > 0)
|
||||
url = url.substring(index + 3);
|
||||
|
||||
int year = -1;
|
||||
int yearCounter = -1;
|
||||
int month = -1;
|
||||
int monthCounter = -1;
|
||||
int day = -1;
|
||||
String strs[] = url.split("/");
|
||||
for (int counter = 0; counter < strs.length; counter++) {
|
||||
String str = strs[counter];
|
||||
if (str.length() == 4) {
|
||||
try {
|
||||
year = Integer.parseInt(str);
|
||||
} catch (Exception ex) {
|
||||
continue;
|
||||
}
|
||||
if (year < 1970 || year > 3000) {
|
||||
year = -1;
|
||||
continue;
|
||||
}
|
||||
yearCounter = counter;
|
||||
} else if (str.length() == 2) {
|
||||
if (monthCounter < 0 && counter == yearCounter + 1) {
|
||||
try {
|
||||
month = Integer.parseInt(str);
|
||||
} catch (Exception ex) {
|
||||
continue;
|
||||
}
|
||||
if (month < 1 || month > 12) {
|
||||
month = -1;
|
||||
continue;
|
||||
}
|
||||
monthCounter = counter;
|
||||
} else if (counter == monthCounter + 1) {
|
||||
try {
|
||||
day = Integer.parseInt(str);
|
||||
} catch (Exception ignored) {
|
||||
// ignored
|
||||
}
|
||||
if (day < 1 || day > 31) {
|
||||
day = -1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (year < 0)
|
||||
return null;
|
||||
|
||||
StringBuilder str = new StringBuilder(year);
|
||||
if (month < 1)
|
||||
return str.toString();
|
||||
|
||||
str.append('/');
|
||||
if (month < 10)
|
||||
str.append('0');
|
||||
str.append(month);
|
||||
if (day < 1)
|
||||
return str.toString();
|
||||
|
||||
str.append('/');
|
||||
if (day < 10)
|
||||
str.append('0');
|
||||
str.append(day);
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static String completeDate(String dateStr) {
|
||||
if (dateStr == null)
|
||||
return null;
|
||||
|
||||
int index = dateStr.indexOf('/');
|
||||
if (index > 0) {
|
||||
index = dateStr.indexOf('/', index + 1);
|
||||
if (index > 0)
|
||||
return dateStr;
|
||||
else
|
||||
return dateStr + "/01";
|
||||
}
|
||||
return dateStr + "/01/01";
|
||||
}
|
||||
|
||||
// with the help of http://stackoverflow.com/questions/1828775/httpclient-and-ssl
|
||||
public static void enableAnySSL() {
|
||||
try {
|
||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||
ctx.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom());
|
||||
SSLContext.setDefault(ctx);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultTrustManager implements X509TrustManager {
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int countLetters(String str) {
|
||||
int len = str.length();
|
||||
int chars = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (Character.isLetter(str.charAt(i)))
|
||||
chars++;
|
||||
}
|
||||
return chars;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package acr.browser.lightning.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class NetworkReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {}
|
||||
|
||||
public static boolean isConnected(@NonNull Context context) {
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cm == null)
|
||||
return false;
|
||||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
return activeNetwork != null && activeNetwork.isConnected();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package acr.browser.lightning.search;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.utils.Utils;
|
||||
|
||||
class RetrieveSuggestionsTask extends AsyncTask<Void, Void, List<HistoryItem>> {
|
||||
|
||||
private static final String TAG = RetrieveSuggestionsTask.class.getSimpleName();
|
||||
|
||||
private static final Pattern SPACE_PATTERN = Pattern.compile(" ", Pattern.LITERAL);
|
||||
private static final String CACHE_FILE_TYPE = ".sgg";
|
||||
private static final String ENCODING = "ISO-8859-1";
|
||||
private static final long INTERVAL_DAY = 86400000;
|
||||
private static final String DEFAULT_LANGUAGE = "en";
|
||||
@Nullable private static XmlPullParser sXpp;
|
||||
@Nullable private static String sLanguage;
|
||||
@NonNull private final WeakReference<SuggestionsResult> mResultCallback;
|
||||
@NonNull private final Application mApplication;
|
||||
@NonNull private final String mSearchSubtitle;
|
||||
@NonNull private String mQuery;
|
||||
|
||||
public RetrieveSuggestionsTask(@NonNull String query,
|
||||
@NonNull SuggestionsResult callback,
|
||||
@NonNull Application application) {
|
||||
mQuery = query;
|
||||
mResultCallback = new WeakReference<>(callback);
|
||||
mApplication = application;
|
||||
mSearchSubtitle = mApplication.getString(R.string.suggestion);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static synchronized String getLanguage() {
|
||||
if (sLanguage == null) {
|
||||
sLanguage = Locale.getDefault().getLanguage();
|
||||
}
|
||||
if (TextUtils.isEmpty(sLanguage)) {
|
||||
sLanguage = DEFAULT_LANGUAGE;
|
||||
}
|
||||
return sLanguage;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static synchronized XmlPullParser getParser() throws XmlPullParserException {
|
||||
if (sXpp == null) {
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
sXpp = factory.newPullParser();
|
||||
}
|
||||
return sXpp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected List<HistoryItem> doInBackground(Void... voids) {
|
||||
List<HistoryItem> filter = new ArrayList<>(5);
|
||||
try {
|
||||
mQuery = SPACE_PATTERN.matcher(mQuery).replaceAll("+");
|
||||
URLEncoder.encode(mQuery, ENCODING);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
File cache = downloadSuggestionsForQuery(mQuery, getLanguage(), mApplication);
|
||||
if (!cache.exists()) {
|
||||
return filter;
|
||||
}
|
||||
InputStream fileInput = null;
|
||||
try {
|
||||
fileInput = new BufferedInputStream(new FileInputStream(cache));
|
||||
XmlPullParser parser = getParser();
|
||||
parser.setInput(fileInput, ENCODING);
|
||||
int eventType = parser.getEventType();
|
||||
int counter = 0;
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (eventType == XmlPullParser.START_TAG && "suggestion".equals(parser.getName())) {
|
||||
String suggestion = parser.getAttributeValue(null, "data");
|
||||
filter.add(new HistoryItem(mSearchSubtitle + " \"" + suggestion + '"',
|
||||
suggestion, R.drawable.ic_search));
|
||||
counter++;
|
||||
if (counter >= 5) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
eventType = parser.next();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return filter;
|
||||
} finally {
|
||||
Utils.close(fileInput);
|
||||
}
|
||||
return filter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@NonNull List<HistoryItem> result) {
|
||||
SuggestionsResult callback = mResultCallback.get();
|
||||
if (callback != null) {
|
||||
callback.resultReceived(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method downloads the search suggestions for the specific query.
|
||||
* NOTE: This is a blocking operation, do not run on the UI thread.
|
||||
*
|
||||
* @param query the query to get suggestions for
|
||||
* @return the cache file containing the suggestions
|
||||
*/
|
||||
@NonNull
|
||||
private static File downloadSuggestionsForQuery(@NonNull String query, String language, @NonNull Application app) {
|
||||
File cacheFile = new File(app.getCacheDir(), query.hashCode() + CACHE_FILE_TYPE);
|
||||
if (System.currentTimeMillis() - INTERVAL_DAY < cacheFile.lastModified()) {
|
||||
return cacheFile;
|
||||
}
|
||||
if (!isNetworkConnected(app)) {
|
||||
return cacheFile;
|
||||
}
|
||||
InputStream in = null;
|
||||
FileOutputStream fos = null;
|
||||
try {
|
||||
// Old API that doesn't support HTTPS
|
||||
// http://google.com/complete/search?q= + query + &output=toolbar&hl= + language
|
||||
URL url = new URL("https://suggestqueries.google.com/complete/search?output=toolbar&hl="
|
||||
+ language + "&q=" + query);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() >= HttpURLConnection.HTTP_MULT_CHOICE ||
|
||||
connection.getResponseCode() < HttpURLConnection.HTTP_OK) {
|
||||
Log.e(TAG, "Search API Responded with code: " + connection.getResponseCode());
|
||||
connection.disconnect();
|
||||
return cacheFile;
|
||||
}
|
||||
in = connection.getInputStream();
|
||||
|
||||
if (in != null) {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
fos = new FileOutputStream(cacheFile);
|
||||
int buffer;
|
||||
while ((buffer = in.read()) != -1) {
|
||||
fos.write(buffer);
|
||||
}
|
||||
fos.flush();
|
||||
}
|
||||
connection.disconnect();
|
||||
cacheFile.setLastModified(System.currentTimeMillis());
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Problem getting search suggestions", e);
|
||||
} finally {
|
||||
Utils.close(in);
|
||||
Utils.close(fos);
|
||||
}
|
||||
return cacheFile;
|
||||
}
|
||||
|
||||
private static boolean isNetworkConnected(@NonNull Context context) {
|
||||
NetworkInfo networkInfo = getActiveNetworkInfo(context);
|
||||
return networkInfo != null && networkInfo.isConnected();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static NetworkInfo getActiveNetworkInfo(@NonNull Context context) {
|
||||
ConnectivityManager connectivity = (ConnectivityManager) context
|
||||
.getApplicationContext()
|
||||
.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (connectivity == null) {
|
||||
return null;
|
||||
}
|
||||
return connectivity.getActiveNetworkInfo();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package acr.browser.lightning.search;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.database.BookmarkManager;
|
||||
import acr.browser.lightning.database.HistoryDatabase;
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import acr.browser.lightning.utils.ThemeUtils;
|
||||
|
||||
public class SuggestionsAdapter extends BaseAdapter implements Filterable, SuggestionsResult {
|
||||
|
||||
private static final String TAG = SuggestionsAdapter.class.getSimpleName();
|
||||
|
||||
private final List<HistoryItem> mHistory = new ArrayList<>(5);
|
||||
private final List<HistoryItem> mBookmarks = new ArrayList<>(5);
|
||||
private final List<HistoryItem> mSuggestions = new ArrayList<>(5);
|
||||
private final List<HistoryItem> mFilteredList = new ArrayList<>(5);
|
||||
private final List<HistoryItem> mAllBookmarks = new ArrayList<>(5);
|
||||
|
||||
private boolean mUseGoogle = true;
|
||||
private boolean mIsExecuting = false;
|
||||
private final boolean mDarkTheme;
|
||||
private final boolean mIncognito;
|
||||
private static final String CACHE_FILE_TYPE = ".sgg";
|
||||
private static final long INTERVAL_DAY = 86400000;
|
||||
private static final int MAX_SUGGESTIONS = 5;
|
||||
private static final SuggestionsComparator sComparator = new SuggestionsComparator();
|
||||
|
||||
@NonNull private final Context mContext;
|
||||
@Nullable private SearchFilter mFilter;
|
||||
@NonNull private final Drawable mSearchDrawable;
|
||||
@NonNull private final Drawable mHistoryDrawable;
|
||||
@NonNull private final Drawable mBookmarkDrawable;
|
||||
|
||||
@Inject HistoryDatabase mDatabaseHandler;
|
||||
@Inject BookmarkManager mBookmarkManager;
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
public SuggestionsAdapter(@NonNull Context context, boolean dark, boolean incognito) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
|
||||
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled();
|
||||
mContext = context;
|
||||
mDarkTheme = dark || incognito;
|
||||
mIncognito = incognito;
|
||||
BrowserApp.getTaskThread().execute(new ClearCacheRunnable(BrowserApp.get(context)));
|
||||
mSearchDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_search, mDarkTheme);
|
||||
mBookmarkDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_bookmark, mDarkTheme);
|
||||
mHistoryDrawable = ThemeUtils.getThemedDrawable(context, R.drawable.ic_history, mDarkTheme);
|
||||
}
|
||||
|
||||
public void refreshPreferences() {
|
||||
mUseGoogle = mPreferenceManager.getGoogleSearchSuggestionsEnabled();
|
||||
if (!mUseGoogle) {
|
||||
synchronized (mSuggestions) {
|
||||
mSuggestions.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshBookmarks() {
|
||||
synchronized (SuggestionsAdapter.this) {
|
||||
mAllBookmarks.clear();
|
||||
mAllBookmarks.addAll(mBookmarkManager.getAllBookmarks(true));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mFilteredList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mFilteredList.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static class SuggestionHolder {
|
||||
ImageView mImage;
|
||||
TextView mTitle;
|
||||
TextView mUrl;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, ViewGroup parent) {
|
||||
SuggestionHolder holder;
|
||||
|
||||
if (convertView == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(mContext);
|
||||
convertView = inflater.inflate(R.layout.two_line_autocomplete, parent, false);
|
||||
|
||||
holder = new SuggestionHolder();
|
||||
holder.mTitle = (TextView) convertView.findViewById(R.id.title);
|
||||
holder.mUrl = (TextView) convertView.findViewById(R.id.url);
|
||||
holder.mImage = (ImageView) convertView.findViewById(R.id.suggestionIcon);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (SuggestionHolder) convertView.getTag();
|
||||
}
|
||||
HistoryItem web;
|
||||
web = mFilteredList.get(position);
|
||||
holder.mTitle.setText(web.getTitle());
|
||||
holder.mUrl.setText(web.getUrl());
|
||||
|
||||
Drawable image;
|
||||
switch (web.getImageId()) {
|
||||
case R.drawable.ic_bookmark: {
|
||||
if (mDarkTheme)
|
||||
holder.mTitle.setTextColor(Color.WHITE);
|
||||
image = mBookmarkDrawable;
|
||||
break;
|
||||
}
|
||||
case R.drawable.ic_search: {
|
||||
if (mDarkTheme)
|
||||
holder.mTitle.setTextColor(Color.WHITE);
|
||||
image = mSearchDrawable;
|
||||
break;
|
||||
}
|
||||
case R.drawable.ic_history: {
|
||||
if (mDarkTheme)
|
||||
holder.mTitle.setTextColor(Color.WHITE);
|
||||
image = mHistoryDrawable;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (mDarkTheme)
|
||||
holder.mTitle.setTextColor(Color.WHITE);
|
||||
image = mSearchDrawable;
|
||||
break;
|
||||
}
|
||||
|
||||
holder.mImage.setImageDrawable(image);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
if (mFilter == null) {
|
||||
mFilter = new SearchFilter();
|
||||
}
|
||||
return mFilter;
|
||||
}
|
||||
|
||||
private static class ClearCacheRunnable implements Runnable {
|
||||
|
||||
@NonNull
|
||||
private final Application app;
|
||||
|
||||
public ClearCacheRunnable(@NonNull Application app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
File dir = new File(app.getCacheDir().toString());
|
||||
String[] fileList = dir.list(new NameFilter());
|
||||
long earliestTimeAllowed = System.currentTimeMillis() - INTERVAL_DAY;
|
||||
for (String fileName : fileList) {
|
||||
File file = new File(dir.getPath() + fileName);
|
||||
if (earliestTimeAllowed > file.lastModified()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NameFilter implements FilenameFilter {
|
||||
|
||||
@Override
|
||||
public boolean accept(File dir, @NonNull String filename) {
|
||||
return filename.endsWith(CACHE_FILE_TYPE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SearchFilter extends Filter {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected FilterResults performFiltering(@Nullable CharSequence constraint) {
|
||||
FilterResults results = new FilterResults();
|
||||
if (constraint == null) {
|
||||
return results;
|
||||
}
|
||||
String query = constraint.toString().toLowerCase(Locale.getDefault());
|
||||
if (mUseGoogle && !mIncognito && !mIsExecuting) {
|
||||
mIsExecuting = true;
|
||||
new RetrieveSuggestionsTask(query, SuggestionsAdapter.this, BrowserApp.get(mContext)).executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||
}
|
||||
|
||||
int counter = 0;
|
||||
synchronized (mBookmarks) {
|
||||
mBookmarks.clear();
|
||||
synchronized (SuggestionsAdapter.this) {
|
||||
for (int n = 0; n < mAllBookmarks.size(); n++) {
|
||||
if (counter >= 5) {
|
||||
break;
|
||||
}
|
||||
if (mAllBookmarks.get(n).getTitle().toLowerCase(Locale.getDefault())
|
||||
.startsWith(query)) {
|
||||
mBookmarks.add(mAllBookmarks.get(n));
|
||||
counter++;
|
||||
} else if (mAllBookmarks.get(n).getUrl().contains(query)) {
|
||||
mBookmarks.add(mAllBookmarks.get(n));
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<HistoryItem> historyList = mDatabaseHandler.findItemsContaining(constraint.toString());
|
||||
synchronized (mHistory) {
|
||||
mHistory.clear();
|
||||
mHistory.addAll(historyList);
|
||||
}
|
||||
results.count = 1;
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence convertResultToString(@NonNull Object resultValue) {
|
||||
return ((HistoryItem) resultValue).getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
synchronized (mFilteredList) {
|
||||
mFilteredList.clear();
|
||||
List<HistoryItem> filtered = getFilteredList();
|
||||
Collections.sort(filtered, sComparator);
|
||||
mFilteredList.addAll(filtered);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resultReceived(@NonNull List<HistoryItem> searchResults) {
|
||||
mIsExecuting = false;
|
||||
synchronized (mSuggestions) {
|
||||
mSuggestions.clear();
|
||||
mSuggestions.addAll(searchResults);
|
||||
}
|
||||
synchronized (mFilteredList) {
|
||||
mFilteredList.clear();
|
||||
List<HistoryItem> filtered = getFilteredList();
|
||||
Collections.sort(filtered, sComparator);
|
||||
mFilteredList.addAll(filtered);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private synchronized List<HistoryItem> getFilteredList() {
|
||||
List<HistoryItem> list = new ArrayList<>(5);
|
||||
synchronized (mBookmarks) {
|
||||
synchronized (mHistory) {
|
||||
synchronized (mSuggestions) {
|
||||
Iterator<HistoryItem> bookmark = mBookmarks.iterator();
|
||||
Iterator<HistoryItem> history = mHistory.iterator();
|
||||
Iterator<HistoryItem> suggestion = mSuggestions.listIterator();
|
||||
while (list.size() < MAX_SUGGESTIONS) {
|
||||
if (!bookmark.hasNext() && !suggestion.hasNext() && !history.hasNext()) {
|
||||
return list;
|
||||
}
|
||||
if (bookmark.hasNext()) {
|
||||
list.add(bookmark.next());
|
||||
}
|
||||
if (suggestion.hasNext() && list.size() < MAX_SUGGESTIONS) {
|
||||
list.add(suggestion.next());
|
||||
}
|
||||
if (history.hasNext() && list.size() < MAX_SUGGESTIONS) {
|
||||
list.add(history.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static class SuggestionsComparator implements Comparator<HistoryItem> {
|
||||
|
||||
@Override
|
||||
public int compare(@NonNull HistoryItem lhs, @NonNull HistoryItem rhs) {
|
||||
if (lhs.getImageId() == rhs.getImageId()) return 0;
|
||||
if (lhs.getImageId() == R.drawable.ic_bookmark) return -1;
|
||||
if (rhs.getImageId() == R.drawable.ic_bookmark) return 1;
|
||||
if (lhs.getImageId() == R.drawable.ic_history) return -1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package acr.browser.lightning.search;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import acr.browser.lightning.database.HistoryItem;
|
||||
|
||||
interface SuggestionsResult {
|
||||
|
||||
/**
|
||||
* Called when the search suggestions have
|
||||
* been retrieved from the server.
|
||||
*
|
||||
* @param searchResults the results, a valid
|
||||
* list of results. May
|
||||
* be empty.
|
||||
*/
|
||||
void resultReceived(@NonNull List<HistoryItem> searchResults);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
|
||||
@Singleton
|
||||
public class AdBlock {
|
||||
|
||||
private static final String TAG = "AdBlock";
|
||||
private static final String BLOCKED_DOMAINS_LIST_FILE_NAME = "hosts.txt";
|
||||
private static final String LOCAL_IP_V4 = "127.0.0.1";
|
||||
private static final String LOCAL_IP_V4_ALT = "0.0.0.0";
|
||||
private static final String LOCAL_IP_V6 = "::1";
|
||||
private static final String LOCALHOST = "localhost";
|
||||
private static final String COMMENT = "#";
|
||||
private static final String TAB = "\t";
|
||||
private static final String SPACE = " ";
|
||||
private static final String EMPTY = "";
|
||||
private final Set<String> mBlockedDomainsList = new HashSet<>();
|
||||
private boolean mBlockAds;
|
||||
private static final Locale mLocale = Locale.getDefault();
|
||||
|
||||
@Inject PreferenceManager mPreferenceManager;
|
||||
|
||||
@Inject
|
||||
public AdBlock(@NonNull Context context) {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
if (mBlockedDomainsList.isEmpty() && Constants.FULL_VERSION) {
|
||||
loadHostsFile(context);
|
||||
}
|
||||
mBlockAds = mPreferenceManager.getAdBlockEnabled();
|
||||
}
|
||||
|
||||
public void updatePreference() {
|
||||
mBlockAds = mPreferenceManager.getAdBlockEnabled();
|
||||
}
|
||||
|
||||
private void loadBlockedDomainsList(@NonNull final Context context) {
|
||||
BrowserApp.getTaskThread().execute(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
AssetManager asset = context.getAssets();
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
reader = new BufferedReader(new InputStreamReader(
|
||||
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
mBlockedDomainsList.add(line.trim().toLowerCase(mLocale));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "Reading blocked domains list from file '"
|
||||
+ BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e);
|
||||
} finally {
|
||||
Utils.close(reader);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* a method that determines if the given URL is an ad or not. It performs
|
||||
* a search of the URL's domain on the blocked domain hash set.
|
||||
*
|
||||
* @param url the URL to check for being an ad
|
||||
* @return true if it is an ad, false if it is not an ad
|
||||
*/
|
||||
public boolean isAd(@Nullable String url) {
|
||||
if (!mBlockAds || url == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String domain;
|
||||
try {
|
||||
domain = getDomainName(url);
|
||||
} catch (URISyntaxException e) {
|
||||
Log.d(TAG, "URL '" + url + "' is invalid", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isOnBlacklist = mBlockedDomainsList.contains(domain.toLowerCase(mLocale));
|
||||
if (isOnBlacklist) {
|
||||
Log.d(TAG, "URL '" + url + "' is an ad");
|
||||
}
|
||||
return isOnBlacklist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the probable domain name for a given URL
|
||||
*
|
||||
* @param url the url to parse
|
||||
* @return returns the domain
|
||||
* @throws URISyntaxException throws an exception if the string cannot form a URI
|
||||
*/
|
||||
@NonNull
|
||||
private static String getDomainName(@NonNull String url) throws URISyntaxException {
|
||||
int index = url.indexOf('/', 8);
|
||||
if (index != -1) {
|
||||
url = url.substring(0, index);
|
||||
}
|
||||
|
||||
URI uri = new URI(url);
|
||||
String domain = uri.getHost();
|
||||
if (domain == null) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return domain.startsWith("www.") ? domain.substring(4) : domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method reads through a hosts file and extracts the domains that should
|
||||
* be redirected to localhost (a.k.a. IP address 127.0.0.1). It can handle files that
|
||||
* simply have a list of hostnames to block, or it can handle a full blown hosts file.
|
||||
* It will strip out comments, references to the base IP address and just extract the
|
||||
* domains to be used
|
||||
*
|
||||
* @param context the context needed to read the file
|
||||
*/
|
||||
private void loadHostsFile(@NonNull final Context context) {
|
||||
BrowserApp.getTaskThread().execute(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
AssetManager asset = context.getAssets();
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
reader = new BufferedReader(new InputStreamReader(
|
||||
asset.open(BLOCKED_DOMAINS_LIST_FILE_NAME)));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!line.isEmpty() && !line.startsWith(COMMENT)) {
|
||||
line = line.replace(LOCAL_IP_V4, EMPTY)
|
||||
.replace(LOCAL_IP_V4_ALT, EMPTY)
|
||||
.replace(LOCAL_IP_V6, EMPTY)
|
||||
.replace(TAB, EMPTY);
|
||||
int comment = line.indexOf(COMMENT);
|
||||
if (comment >= 0) {
|
||||
line = line.substring(0, comment);
|
||||
}
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && !line.equals(LOCALHOST)) {
|
||||
while (line.contains(SPACE)) {
|
||||
int space = line.indexOf(SPACE);
|
||||
String host = line.substring(0, space);
|
||||
mBlockedDomainsList.add(host.trim());
|
||||
line = line.substring(space, line.length()).trim();
|
||||
}
|
||||
mBlockedDomainsList.add(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "Reading blocked domains list from file '"
|
||||
+ BLOCKED_DOMAINS_LIST_FILE_NAME + "' failed.", e);
|
||||
} finally {
|
||||
Utils.close(reader);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
public class DrawableUtils {
|
||||
|
||||
public static Bitmap getRoundedNumberImage(int number, int width, int height, int color, int thickness) {
|
||||
String text;
|
||||
|
||||
if (number > 99) {
|
||||
text = "\u221E";
|
||||
} else {
|
||||
text = String.valueOf(number);
|
||||
}
|
||||
|
||||
Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(image);
|
||||
Paint paint = new Paint();
|
||||
paint.setColor(color);
|
||||
Typeface boldText = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
|
||||
paint.setTypeface(boldText);
|
||||
paint.setTextSize(Utils.dpToPx(14));
|
||||
paint.setAntiAlias(true);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
|
||||
|
||||
int radius = Utils.dpToPx(2);
|
||||
|
||||
RectF outer = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
canvas.drawRoundRect(outer, radius, radius, paint);
|
||||
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
radius--;
|
||||
RectF inner = new RectF(thickness, thickness, canvas.getWidth() - thickness, canvas.getHeight() - thickness);
|
||||
canvas.drawRoundRect(inner, radius, radius, paint);
|
||||
|
||||
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
|
||||
|
||||
int xPos = (canvas.getWidth() / 2);
|
||||
int yPos = (int) ((canvas.getHeight() / 2) - ((paint.descent() + paint.ascent()) / 2));
|
||||
|
||||
canvas.drawText(String.valueOf(text), xPos, yPos, paint);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
|
||||
public static int mixColor(float fraction, int startValue, int endValue) {
|
||||
int startInt = startValue;
|
||||
int startA = (startInt >> 24) & 0xff;
|
||||
int startR = (startInt >> 16) & 0xff;
|
||||
int startG = (startInt >> 8) & 0xff;
|
||||
int startB = startInt & 0xff;
|
||||
|
||||
int endInt = endValue;
|
||||
int endA = (endInt >> 24) & 0xff;
|
||||
int endR = (endInt >> 16) & 0xff;
|
||||
int endG = (endInt >> 8) & 0xff;
|
||||
int endB = endInt & 0xff;
|
||||
|
||||
return (startA + (int)(fraction * (endA - startA))) << 24 |
|
||||
(startR + (int)(fraction * (endR - startR))) << 16 |
|
||||
(startG + (int)(fraction * (endG - startG))) << 8 |
|
||||
(startB + (int)(fraction * (endB - startB)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
|
||||
/**
|
||||
* A utility class containing helpful methods
|
||||
* pertaining to file storage.
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
/**
|
||||
* Writes a bundle to persistent storage in the files directory
|
||||
* using the specified file name. This method is a blocking
|
||||
* operation.
|
||||
*
|
||||
* @param app the application needed to obtain the file directory.
|
||||
* @param bundle the bundle to store in persistent storage.
|
||||
* @param name the name of the file to store the bundle in.
|
||||
*/
|
||||
public static void writeBundleToStorage(final @NonNull Application app, final Bundle bundle, final @NonNull String name) {
|
||||
BrowserApp.getIOThread().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
File outputFile = new File(app.getFilesDir(), name);
|
||||
FileOutputStream outputStream = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
outputStream = new FileOutputStream(outputFile);
|
||||
Parcel parcel = Parcel.obtain();
|
||||
parcel.writeBundle(bundle);
|
||||
outputStream.write(parcel.marshall());
|
||||
outputStream.flush();
|
||||
parcel.recycle();
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Unable to write bundle to storage");
|
||||
} finally {
|
||||
Utils.close(outputStream);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this method to delete the bundle with the specified name.
|
||||
* This is a blocking call and should be used within a worker
|
||||
* thread unless immediate deletion is necessary.
|
||||
*
|
||||
* @param app the application object needed to get the file.
|
||||
* @param name the name of the file.
|
||||
*/
|
||||
public static void deleteBundleInStorage(final @NonNull Application app, final @NonNull String name) {
|
||||
File outputFile = new File(app.getFilesDir(), name);
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a bundle from the file with the specified
|
||||
* name in the peristent storage files directory.
|
||||
* This method is a blocking operation.
|
||||
*
|
||||
* @param app the application needed to obtain the files directory.
|
||||
* @param name the name of the file to read from.
|
||||
* @return a valid Bundle loaded using the system class loader
|
||||
* or null if the method was unable to read the Bundle from storage.
|
||||
*/
|
||||
@Nullable
|
||||
public static Bundle readBundleFromStorage(@NonNull Application app, @NonNull String name) {
|
||||
File inputFile = new File(app.getFilesDir(), name);
|
||||
FileInputStream inputStream = null;
|
||||
try {
|
||||
//noinspection IOResourceOpenedButNotSafelyClosed
|
||||
inputStream = new FileInputStream(inputFile);
|
||||
Parcel parcel = Parcel.obtain();
|
||||
byte[] data = new byte[(int) inputStream.getChannel().size()];
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
inputStream.read(data, 0, data.length);
|
||||
parcel.unmarshall(data, 0, data.length);
|
||||
parcel.setDataPosition(0);
|
||||
Bundle out = parcel.readBundle(ClassLoader.getSystemClassLoader());
|
||||
out.putAll(out);
|
||||
parcel.recycle();
|
||||
return out;
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(Constants.TAG, "Unable to read bundle from storage");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
inputFile.delete();
|
||||
Utils.close(inputStream);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
|
||||
public class IntentUtils {
|
||||
|
||||
private final Activity mActivity;
|
||||
|
||||
private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile("(?i)"
|
||||
+ // switch on case insensitive matching
|
||||
'('
|
||||
+ // begin group for schema
|
||||
"(?:http|https|file)://" + "|(?:inline|data|about|javascript):" + "|(?:.*:.*@)"
|
||||
+ ')' + "(.*)");
|
||||
|
||||
public IntentUtils(Activity activity) {
|
||||
mActivity = activity;
|
||||
}
|
||||
|
||||
public boolean startActivityForUrl(@Nullable WebView tab, @NonNull String url) {
|
||||
Intent intent;
|
||||
try {
|
||||
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
|
||||
} catch (URISyntaxException ex) {
|
||||
Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
intent.setComponent(null);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||
intent.setSelector(null);
|
||||
}
|
||||
|
||||
if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
|
||||
String packagename = intent.getPackage();
|
||||
if (packagename != null) {
|
||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://search?q=pname:"
|
||||
+ packagename));
|
||||
intent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
mActivity.startActivity(intent);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (tab != null) {
|
||||
intent.putExtra(Constants.INTENT_ORIGIN, 1);
|
||||
}
|
||||
|
||||
Matcher m = ACCEPTED_URI_SCHEMA.matcher(url);
|
||||
if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (mActivity.startActivityIfNeeded(intent, -1)) {
|
||||
return true;
|
||||
}
|
||||
} catch (ActivityNotFoundException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for intent handlers that are specific to this URL aka, specialized
|
||||
* apps like google maps or youtube
|
||||
*/
|
||||
private boolean isSpecializedHandlerAvailable(Intent intent) {
|
||||
PackageManager pm = mActivity.getPackageManager();
|
||||
List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
|
||||
PackageManager.GET_RESOLVED_FILTER);
|
||||
if (handlers == null || handlers.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (ResolveInfo resolveInfo : handlers) {
|
||||
IntentFilter filter = resolveInfo.filter;
|
||||
if (filter == null) {
|
||||
// No intent filter matches this intent?
|
||||
// Error on the side of staying in the browser, ignore
|
||||
continue;
|
||||
}
|
||||
// NOTICE: Use of && instead of || will cause the browser
|
||||
// to launch a new intent for every URL, using OR only
|
||||
// launches a new one if there is a non-browser app that
|
||||
// can handle it.
|
||||
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) {
|
||||
// Generic handler, skip
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
public class KeyboardHelper {
|
||||
|
||||
public interface KeyboardListener {
|
||||
/**
|
||||
* Called when the visibility of the keyboard changes.
|
||||
* Parameter tells whether the keyboard has been shown
|
||||
* or hidden.
|
||||
*
|
||||
* @param visible true if the keyboard has been shown,
|
||||
* false otherwise.
|
||||
*/
|
||||
void keyboardVisibilityChanged(boolean visible);
|
||||
}
|
||||
|
||||
@NonNull private final View mView;
|
||||
private int mLastRight = -1;
|
||||
private int mLastBottom = -1;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param view the view to listen on, should be
|
||||
* the {@link android.R.id#content} view.
|
||||
*/
|
||||
public KeyboardHelper(@NonNull View view) {
|
||||
mView = view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link KeyboardListener} to receive
|
||||
* callbacks when the keyboard is shown for the specific
|
||||
* view. The view used should be the content view as it
|
||||
* will receive resize events from the system.
|
||||
*
|
||||
* @param listener the listener to register to receive events.
|
||||
*/
|
||||
public void registerKeyboardListener(@NonNull final KeyboardListener listener) {
|
||||
mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
Rect rect = new Rect();
|
||||
if (mLastBottom == -1) {
|
||||
mLastBottom = rect.bottom;
|
||||
}
|
||||
if (mLastRight == -1) {
|
||||
mLastRight = rect.right;
|
||||
}
|
||||
mView.getWindowVisibleDisplayFrame(rect);
|
||||
if (mLastRight == rect.right && rect.bottom < mLastBottom) {
|
||||
listener.keyboardVisibilityChanged(true);
|
||||
} else if (mLastRight == rect.right && rect.bottom > mLastBottom) {
|
||||
listener.keyboardVisibilityChanged(false);
|
||||
}
|
||||
mLastBottom = rect.bottom;
|
||||
mLastRight = rect.right;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class MemoryLeakUtils {
|
||||
|
||||
private static final String TAG = MemoryLeakUtils.class.getSimpleName();
|
||||
|
||||
private static Method sFinishInputLocked = null;
|
||||
|
||||
/**
|
||||
* Clears the mNextServedView and mServedView in
|
||||
* InputMethodManager and keeps them from leaking.
|
||||
*
|
||||
* @param application the application needed to get
|
||||
* the InputMethodManager that is
|
||||
* leaking the views.
|
||||
*/
|
||||
public static void clearNextServedView(@NonNull Application application) {
|
||||
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||
// This shouldn't be a problem on N
|
||||
return;
|
||||
}
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) application.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
if (sFinishInputLocked == null) {
|
||||
try {
|
||||
sFinishInputLocked = InputMethodManager.class.getDeclaredMethod("finishInputLocked");
|
||||
} catch (NoSuchMethodException e) {
|
||||
Log.d(TAG, "Unable to find method in clearNextServedView", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (sFinishInputLocked != null) {
|
||||
sFinishInputLocked.setAccessible(true);
|
||||
try {
|
||||
sFinishInputLocked.invoke(imm);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Unable to invoke method in clearNextServedView", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static abstract class LifecycleAdapter implements Application.ActivityLifecycleCallbacks {
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
public class Preconditions {
|
||||
/**
|
||||
* Ensure that an object is not null
|
||||
* and throw a RuntimeException if it
|
||||
* is null.
|
||||
*
|
||||
* @param object check nullness on this object.
|
||||
*/
|
||||
public static void checkNonNull(Object object) {
|
||||
if (object == null) {
|
||||
throw new RuntimeException("Object must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
|
||||
import net.i2p.android.ui.I2PAndroidHelper;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
import acr.browser.lightning.app.BrowserApp;
|
||||
import acr.browser.lightning.bus.BrowserEvents;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.preference.PreferenceManager;
|
||||
import info.guardianproject.netcipher.proxy.OrbotHelper;
|
||||
import info.guardianproject.netcipher.web.WebkitProxy;
|
||||
|
||||
@Singleton
|
||||
public class ProxyUtils {
|
||||
// Helper
|
||||
private static boolean mI2PHelperBound;
|
||||
private static boolean mI2PProxyInitialized;
|
||||
|
||||
@Inject PreferenceManager mPreferences;
|
||||
@Inject I2PAndroidHelper mI2PHelper;
|
||||
@Inject Bus mBus;
|
||||
|
||||
@Inject
|
||||
public ProxyUtils() {
|
||||
BrowserApp.getAppComponent().inject(this);
|
||||
}
|
||||
|
||||
/*
|
||||
* If Orbot/Tor or I2P is installed, prompt the user if they want to enable
|
||||
* proxying for this session
|
||||
*/
|
||||
public void checkForProxy(@NonNull final Activity activity) {
|
||||
boolean useProxy = mPreferences.getUseProxy();
|
||||
|
||||
final boolean orbotInstalled = OrbotHelper.isOrbotInstalled(activity);
|
||||
boolean orbotChecked = mPreferences.getCheckedForTor();
|
||||
boolean orbot = orbotInstalled && !orbotChecked;
|
||||
|
||||
boolean i2pInstalled = mI2PHelper.isI2PAndroidInstalled();
|
||||
boolean i2pChecked = mPreferences.getCheckedForI2P();
|
||||
boolean i2p = i2pInstalled && !i2pChecked;
|
||||
|
||||
// TODO Is the idea to show this per-session, or only once?
|
||||
if (!useProxy && (orbot || i2p)) {
|
||||
if (orbot) mPreferences.setCheckedForTor(true);
|
||||
if (i2p) mPreferences.setCheckedForI2P(true);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
|
||||
if (orbotInstalled && i2pInstalled) {
|
||||
String[] proxyChoices = activity.getResources().getStringArray(R.array.proxy_choices_array);
|
||||
builder.setTitle(activity.getResources().getString(R.string.http_proxy))
|
||||
.setSingleChoiceItems(proxyChoices, mPreferences.getProxyChoice(),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
mPreferences.setProxyChoice(which);
|
||||
}
|
||||
})
|
||||
.setNeutralButton(activity.getResources().getString(R.string.action_ok),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (mPreferences.getUseProxy())
|
||||
initializeProxy(activity);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
mPreferences.setProxyChoice(orbotInstalled ?
|
||||
Constants.PROXY_ORBOT : Constants.PROXY_I2P);
|
||||
initializeProxy(activity);
|
||||
break;
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
mPreferences.setProxyChoice(Constants.NO_PROXY);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
builder.setMessage(orbotInstalled ? R.string.use_tor_prompt : R.string.use_i2p_prompt)
|
||||
.setPositiveButton(R.string.yes, dialogClickListener)
|
||||
.setNegativeButton(R.string.no, dialogClickListener);
|
||||
}
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize WebKit Proxying
|
||||
*/
|
||||
private void initializeProxy(@NonNull Activity activity) {
|
||||
String host;
|
||||
int port;
|
||||
|
||||
switch (mPreferences.getProxyChoice()) {
|
||||
case Constants.NO_PROXY:
|
||||
// We shouldn't be here
|
||||
return;
|
||||
|
||||
case Constants.PROXY_ORBOT:
|
||||
if (!OrbotHelper.isOrbotRunning(activity))
|
||||
OrbotHelper.requestStartTor(activity);
|
||||
host = "localhost";
|
||||
port = 8118;
|
||||
break;
|
||||
|
||||
case Constants.PROXY_I2P:
|
||||
mI2PProxyInitialized = true;
|
||||
if (mI2PHelperBound && !mI2PHelper.isI2PAndroidRunning()) {
|
||||
mI2PHelper.requestI2PAndroidStart(activity);
|
||||
}
|
||||
host = "localhost";
|
||||
port = 4444;
|
||||
break;
|
||||
|
||||
default:
|
||||
host = mPreferences.getProxyHost();
|
||||
port = mPreferences.getProxyPort();
|
||||
}
|
||||
|
||||
try {
|
||||
WebkitProxy.setProxy(BrowserApp.class.getName(), activity.getApplicationContext(), null, host, port);
|
||||
} catch (Exception e) {
|
||||
Log.d(Constants.TAG, "error enabling web proxying", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean isProxyReady() {
|
||||
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
|
||||
if (!mI2PHelper.isI2PAndroidRunning()) {
|
||||
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_not_running));
|
||||
return false;
|
||||
} else if (!mI2PHelper.areTunnelsActive()) {
|
||||
mBus.post(new BrowserEvents.ShowSnackBarMessage(R.string.i2p_tunnels_not_ready));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void updateProxySettings(@NonNull Activity activity) {
|
||||
if (mPreferences.getUseProxy()) {
|
||||
initializeProxy(activity);
|
||||
} else {
|
||||
try {
|
||||
WebkitProxy.resetProxy(BrowserApp.class.getName(), activity.getApplicationContext());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mI2PProxyInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void onStop() {
|
||||
mI2PHelper.unbind();
|
||||
mI2PHelperBound = false;
|
||||
}
|
||||
|
||||
public void onStart(final Activity activity) {
|
||||
if (mPreferences.getProxyChoice() == Constants.PROXY_I2P) {
|
||||
// Try to bind to I2P Android
|
||||
mI2PHelper.bind(new I2PAndroidHelper.Callback() {
|
||||
@Override
|
||||
public void onI2PAndroidBound() {
|
||||
mI2PHelperBound = true;
|
||||
if (mI2PProxyInitialized && !mI2PHelper.isI2PAndroidRunning())
|
||||
mI2PHelper.requestI2PAndroidStart(activity);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static int setProxyChoice(int choice, @NonNull Activity activity) {
|
||||
switch (choice) {
|
||||
case Constants.PROXY_ORBOT:
|
||||
if (!OrbotHelper.isOrbotInstalled(activity)) {
|
||||
choice = Constants.NO_PROXY;
|
||||
Utils.showSnackbar(activity, R.string.install_orbot);
|
||||
}
|
||||
break;
|
||||
|
||||
case Constants.PROXY_I2P:
|
||||
I2PAndroidHelper ih = new I2PAndroidHelper(BrowserApp.get(activity));
|
||||
if (!ih.isI2PAndroidInstalled()) {
|
||||
choice = Constants.NO_PROXY;
|
||||
ih.promptToInstall(activity);
|
||||
}
|
||||
break;
|
||||
case Constants.PROXY_MANUAL:
|
||||
break;
|
||||
}
|
||||
return choice;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package acr.browser.lightning.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import acr.browser.lightning.R;
|
||||
|
||||
public class ThemeUtils {
|
||||
|
||||
private static final TypedValue sTypedValue = new TypedValue();
|
||||
|
||||
public static int getPrimaryColor(@NonNull Context context) {
|
||||
return getColor(context, R.attr.colorPrimary);
|
||||
}
|
||||
|
||||
public static int getPrimaryColorDark(@NonNull Context context) {
|
||||
return getColor(context, R.attr.colorPrimaryDark);
|
||||
}
|
||||
|
||||
public static int getAccentColor(@NonNull Context context) {
|
||||
return getColor(context, R.attr.colorAccent);
|
||||
}
|
||||
|
||||
private static int getColor(@NonNull Context context, @AttrRes int resource) {
|
||||
TypedArray a = context.obtainStyledAttributes(sTypedValue.data, new int[]{resource});
|
||||
int color = a.getColor(0, 0);
|
||||
a.recycle();
|
||||
return color;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getIconLightThemeColor(@NonNull Context context) {
|
||||
return ContextCompat.getColor(context, R.color.icon_light_theme);
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getIconDarkThemeColor(@NonNull Context context) {
|
||||
return ContextCompat.getColor(context, R.color.icon_dark_theme);
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getIconThemeColor(@NonNull Context context, boolean dark) {
|
||||
return (dark) ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
|
||||
}
|
||||
|
||||
public static void themeImageView(@NonNull ImageView icon, @NonNull Context context, boolean dark) {
|
||||
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
|
||||
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Bitmap getThemedBitmap(@NonNull Context context, @DrawableRes int res, boolean dark) {
|
||||
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
|
||||
Bitmap sourceBitmap = BitmapFactory.decodeResource(context.getResources(), res);
|
||||
Bitmap resultBitmap = Bitmap.createBitmap(sourceBitmap.getWidth(), sourceBitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Paint p = new Paint();
|
||||
ColorFilter filter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
p.setColorFilter(filter);
|
||||
Canvas canvas = new Canvas(resultBitmap);
|
||||
canvas.drawBitmap(sourceBitmap, 0, 0, p);
|
||||
sourceBitmap.recycle();
|
||||
return resultBitmap;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Drawable getThemedDrawable(@NonNull Context context, @DrawableRes int res, boolean dark) {
|
||||
int color = dark ? getIconDarkThemeColor(context) : getIconLightThemeColor(context);
|
||||
final Drawable drawable = ContextCompat.getDrawable(context, res);
|
||||
drawable.mutate();
|
||||
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ColorDrawable getSelectedBackground(@NonNull Context context, boolean dark) {
|
||||
@ColorInt final int color = (dark) ? ContextCompat.getColor(context, R.color.selected_dark) :
|
||||
ContextCompat.getColor(context, R.color.selected_light);
|
||||
return new ColorDrawable(color);
|
||||
}
|
||||
|
||||
public static int getThemedTextHintColor(boolean dark){
|
||||
return 0x80ffffff & (dark ? Color.WHITE : Color.BLACK);
|
||||
}
|
||||
|
||||
public static int getTextColor(@NonNull Context context) {
|
||||
return getColor(context, android.R.attr.editTextColor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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 acr.browser.lightning.utils;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Patterns;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import acr.browser.lightning.constant.BookmarkPage;
|
||||
import acr.browser.lightning.constant.Constants;
|
||||
import acr.browser.lightning.constant.HistoryPage;
|
||||
import acr.browser.lightning.constant.StartPage;
|
||||
|
||||
/**
|
||||
* Utility methods for Url manipulation
|
||||
*/
|
||||
public class UrlUtils {
|
||||
private static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
|
||||
"(?i)" + // switch on case insensitive matching
|
||||
'(' + // begin group for schema
|
||||
"(?:http|https|file)://" +
|
||||
"|(?:inline|data|about|javascript):" +
|
||||
"|(?:.*:.*@)" +
|
||||
')' +
|
||||
"(.*)");
|
||||
// Google search
|
||||
public final static String QUERY_PLACE_HOLDER = "%s";
|
||||
// Regular expression to strip http:// and optionally
|
||||
// the trailing slash
|
||||
private static final Pattern STRIP_URL_PATTERN =
|
||||
Pattern.compile("^http://(.*?)/?$");
|
||||
|
||||
private UrlUtils() { /* cannot be instantiated */ }
|
||||
|
||||
/**
|
||||
* Strips the provided url of preceding "http://" and any trailing "/". Does not
|
||||
* strip "https://". If the provided string cannot be stripped, the original string
|
||||
* is returned.
|
||||
* <p/>
|
||||
* TODO: Put this in TextUtils to be used by other packages doing something similar.
|
||||
*
|
||||
* @param url a url to strip, like "http://www.google.com/"
|
||||
* @return a stripped url like "www.google.com", or the original string if it could
|
||||
* not be stripped
|
||||
*/
|
||||
@Nullable
|
||||
public static String stripUrl(@Nullable String url) {
|
||||
if (url == null) return null;
|
||||
Matcher m = STRIP_URL_PATTERN.matcher(url);
|
||||
if (m.matches()) {
|
||||
return m.group(1);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to determine whether user input is a URL or search
|
||||
* terms. Anything with a space is passed to search if canBeSearch is true.
|
||||
* <p/>
|
||||
* Converts to lowercase any mistakenly uppercased schema (i.e.,
|
||||
* "Http://" converts to "http://"
|
||||
*
|
||||
* @param canBeSearch If true, will return a search url if it isn't a valid
|
||||
* URL. If false, invalid URLs will return null
|
||||
* @return Original or modified URL
|
||||
*/
|
||||
@NonNull
|
||||
public static String smartUrlFilter(@NonNull String url, boolean canBeSearch, String searchUrl) {
|
||||
String inUrl = url.trim();
|
||||
boolean hasSpace = inUrl.indexOf(' ') != -1;
|
||||
Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
|
||||
if (matcher.matches()) {
|
||||
// force scheme to lowercase
|
||||
String scheme = matcher.group(1);
|
||||
String lcScheme = scheme.toLowerCase();
|
||||
if (!lcScheme.equals(scheme)) {
|
||||
inUrl = lcScheme + matcher.group(2);
|
||||
}
|
||||
if (hasSpace && Patterns.WEB_URL.matcher(inUrl).matches()) {
|
||||
inUrl = inUrl.replace(" ", "%20");
|
||||
}
|
||||
return inUrl;
|
||||
}
|
||||
if (!hasSpace) {
|
||||
if (Patterns.WEB_URL.matcher(inUrl).matches()) {
|
||||
return URLUtil.guessUrl(inUrl);
|
||||
}
|
||||
}
|
||||
if (canBeSearch) {
|
||||
return URLUtil.composeSearchUrl(inUrl,
|
||||
searchUrl, QUERY_PLACE_HOLDER);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/* package */
|
||||
@NonNull
|
||||
static String fixUrl(@NonNull String inUrl) {
|
||||
// FIXME: Converting the url to lower case
|
||||
// duplicates functionality in smartUrlFilter().
|
||||
// However, changing all current callers of fixUrl to
|
||||
// call smartUrlFilter in addition may have unwanted
|
||||
// consequences, and is deferred for now.
|
||||
int colon = inUrl.indexOf(':');
|
||||
boolean allLower = true;
|
||||
for (int index = 0; index < colon; index++) {
|
||||
char ch = inUrl.charAt(index);
|
||||
if (!Character.isLetter(ch)) {
|
||||
break;
|
||||
}
|
||||
allLower &= Character.isLowerCase(ch);
|
||||
if (index == colon - 1 && !allLower) {
|
||||
inUrl = inUrl.substring(0, colon).toLowerCase()
|
||||
+ inUrl.substring(colon);
|
||||
}
|
||||
}
|
||||
if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
|
||||
return inUrl;
|
||||
if (inUrl.startsWith("http:") ||
|
||||
inUrl.startsWith("https:")) {
|
||||
if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
|
||||
inUrl = inUrl.replaceFirst("/", "//");
|
||||
} else inUrl = inUrl.replaceFirst(":", "://");
|
||||
}
|
||||
return inUrl;
|
||||
}
|
||||
|
||||
// Returns the filtered URL. Cannot return null, but can return an empty string
|
||||
/* package */
|
||||
@Nullable
|
||||
static String filteredUrl(@Nullable String inUrl) {
|
||||
if (inUrl == null) {
|
||||
return "";
|
||||
}
|
||||
if (inUrl.startsWith("content:")
|
||||
|| inUrl.startsWith("browser:")) {
|
||||
return "";
|
||||
}
|
||||
return inUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given url is the bookmarks/history page or a normal website
|
||||
*/
|
||||
public static boolean isSpecialUrl(@Nullable String url) {
|
||||
return url != null && url.startsWith(Constants.FILE) &&
|
||||
(url.endsWith(BookmarkPage.FILENAME) ||
|
||||
url.endsWith(HistoryPage.FILENAME) ||
|
||||
url.endsWith(StartPage.FILENAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the url is a url for the bookmark page.
|
||||
*
|
||||
* @param url the url to check, may be null.
|
||||
* @return true if the url is a bookmark url, false otherwise.
|
||||
*/
|
||||
public static boolean isBookmarkUrl(@Nullable String url) {
|
||||
return url != null && url.startsWith(Constants.FILE) && url.endsWith(BookmarkPage.FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the url is a url for the history page.
|
||||
*
|
||||
* @param url the url to check, may be null.
|
||||
* @return true if the url is a history url, false otherwise.
|
||||
*/
|
||||
public static boolean isHistoryUrl(@Nullable String url) {
|
||||
return url != null && url.startsWith(Constants.FILE) && url.endsWith(HistoryPage.FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the url is a url for the start page.
|
||||
*
|
||||
* @param url the url to check, may be null.
|
||||
* @return true if the url is a start page url, false otherwise.
|
||||
*/
|
||||
public static boolean isStartPageUrl(@Nullable String url) {
|
||||
return url != null && url.startsWith(Constants.FILE) && url.endsWith(StartPage.FILENAME);
|
||||
}
|
||||
}
|
||||