Compare commits
835 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22a1895050 | |||
| 2df42c772b | |||
| 82fea04460 | |||
| 6ccd70dad2 | |||
| ec2d14bffe | |||
| 1d544ff558 | |||
| 867cd29e0f | |||
| d1cf660af6 | |||
| adabe3a227 | |||
| b57ef6c3b4 | |||
| 1b50823843 | |||
| 18ddad522a | |||
| 85b034ecfb | |||
| 5a04749ef2 | |||
| bcd32dde85 | |||
| 7832cb810b | |||
| cb652721a7 | |||
| 4ad005fb53 | |||
| ce312aea72 | |||
| 6c5dc556e8 | |||
| 95dc18f1d0 | |||
| f284cc8676 | |||
| 0cc456c4a1 | |||
| 0666146374 | |||
| 5860ca720e | |||
| 91ee8362ba | |||
| dd81460826 | |||
| 5180916d9f | |||
| 4d92ddcc75 | |||
| f1f0900e13 | |||
| 6edf68d03c | |||
| c643ae507b | |||
| fdc843a93a | |||
| 383c06c72a | |||
| a255ce3e36 | |||
| fa58e820c1 | |||
| fad011e2fc | |||
| 9fad040e14 | |||
| 8d0ab82f15 | |||
| 3d7e994b93 | |||
| 4d40c0f431 | |||
| d9bb60e66b | |||
| 52433ba347 | |||
| 4290846698 | |||
| 6bbd7ec00d | |||
| dc535ddb38 | |||
| dfe087e702 | |||
| 930e0b92eb | |||
| 3199bc6118 | |||
| b1384375a3 | |||
| 81dd98cefd | |||
| 02108b3273 | |||
| c5deb87c23 | |||
| 572274a2e3 | |||
| 7f303b362d | |||
| 41ce6cce05 | |||
| f6e1c55749 | |||
| d24bcf8e6e | |||
| 2b497ff679 | |||
| 627c20f6fd | |||
| cfab918bea | |||
| 89746203fe | |||
| 8bc6947c94 | |||
| 5a264804da | |||
| 1f5e025fea | |||
| 3201e220a0 | |||
| d41129011b | |||
| 6eb1aefb1c | |||
| 433646b455 | |||
| 257e7d1d27 | |||
| f668d6492a | |||
| b8ea968a58 | |||
| 5b361afc67 | |||
| 0947cd1e24 | |||
| f8ab1f108b | |||
| 84f53bbf81 | |||
| ad2656a24b | |||
| 4734fbd651 | |||
| a947755383 | |||
| f4a1283073 | |||
| f37a2c5b24 | |||
| cca7d770f1 | |||
| 2c7f3f2321 | |||
| cc92e025f2 | |||
| ce0e28dad6 | |||
| b483f7cdb6 | |||
| 8ce0694099 | |||
| 910b356a05 | |||
| d74e0310f8 | |||
| ee19879577 | |||
| 5e3d7d2f2b | |||
| f292eb0fc6 | |||
| 475de2281b | |||
| 949c8331d2 | |||
| a5e0fa8402 | |||
| f0066b3dac | |||
| 10480a533c | |||
| d36dcac817 | |||
| d6d49ac7ad | |||
| 573e127fc7 | |||
| 9cb6e599a4 | |||
| f42dd1013e | |||
| 70441658e5 | |||
| 0ca71f25f6 | |||
| 5d3a4e24f9 | |||
| 3b8faf47c4 | |||
| ac21776d3b | |||
| e5127c9040 | |||
| 8533d06943 | |||
| 066dd6103f | |||
| 670f9c280c | |||
| 5ea3c58b77 | |||
| 6e15571ef4 | |||
| ad46fced4f | |||
| 5acf42423e | |||
| 2a2bef8a47 | |||
| 2ffb0f59a2 | |||
| 400bdaa6f5 | |||
| fa449966f3 | |||
| f4078493f6 | |||
| 5007f7e83d | |||
| ca1a8e15cb | |||
| b9f299805f | |||
| acde49012f | |||
| 76c69f0d19 | |||
| c0b91b4a63 | |||
| f6a194ae92 | |||
| 9e7d178972 | |||
| 17b2e2dca7 | |||
| eb73bcd25f | |||
| f96c090120 | |||
| aefdaee424 | |||
| 27ceee37c0 | |||
| e1c2e3c984 | |||
| 48e3718fc2 | |||
| ee20f65a9b | |||
| 235a13855b | |||
| acec42c6af | |||
| 6d009eddf5 | |||
| b0dde281ea | |||
| 1e7d43bc72 | |||
| 8576a1edaf | |||
| 5dc97d2191 | |||
| a32b45e067 | |||
| ff8210c72b | |||
| 85031e89b6 | |||
| f2676532ce | |||
| 7a996ee8f4 | |||
| d09e28d653 | |||
| e446f4190f | |||
| f53534b803 | |||
| fb6792f71b | |||
| 5d66d55db1 | |||
| d3bdd48425 | |||
| c89f675ccf | |||
| f0b989d20e | |||
| 3b950b6b28 | |||
| d10a0f956e | |||
| 2317228642 | |||
| 78a4571a8a | |||
| c7b16ce91b | |||
| 2e84aeaccf | |||
| 49e9a532b8 | |||
| 47f7d70cfb | |||
| a3b03efcbe | |||
| 814d4faf3e | |||
| c487dc2f03 | |||
| f552a06bd7 | |||
| 4de5593802 | |||
| 4294e9f98d | |||
| 05a911f1e3 | |||
| 464f8d7851 | |||
| 370b07be31 | |||
| cb49c71279 | |||
| f637fb9596 | |||
| 5c8072cc9e | |||
| 40cd057c28 | |||
| 0bcd37c1f1 | |||
| 6fce6fd80b | |||
| bc13a6056d | |||
| 00879964e4 | |||
| 64711d6175 | |||
| 0d3704f55d | |||
| 0856eed7fc | |||
| 8efaa9e724 | |||
| 98983ff4eb | |||
| 0ae36ed7a7 | |||
| 8e8b20c3dd | |||
| 82e2aa45c6 | |||
| e4be47e7e7 | |||
| 404c605a1e | |||
| 4966d85634 | |||
| 043eebf013 | |||
| 3b521ddd08 | |||
| bd74d3a1a3 | |||
| 0dc867ce64 | |||
| dde08e2d67 | |||
| 3c5871526d | |||
| 49f4ba2da8 | |||
| 77b660576a | |||
| a15ab62f03 | |||
| c65250973c | |||
| d5872aad55 | |||
| 9d27bba6fd | |||
| caeecce50b | |||
| f6c82dd767 | |||
| f88205f957 | |||
| 3a0a4726b9 | |||
| a113fc1c73 | |||
| 92d122b0a3 | |||
| d96a2d22e0 | |||
| 8d96b9fe7f | |||
| fdf7bdd64d | |||
| 4f4c90f792 | |||
| f42674ed6f | |||
| e66b697663 | |||
| a21e40a4f1 | |||
| 03dd58ca41 | |||
| 6f3827db15 | |||
| fd2b333bc7 | |||
| ba3e882f47 | |||
| b9a80e12a8 | |||
| 979ee209dd | |||
| 10b1380aad | |||
| b239407c25 | |||
| 475221a011 | |||
| 658ee78213 | |||
| 5244fe73ea | |||
| 243e024d1a | |||
| 5930996e1c | |||
| a05e6917b8 | |||
| d05e9e57c3 | |||
| 1b88e9c238 | |||
| ce32b8d63a | |||
| 2bc26c9046 | |||
| 714a64975f | |||
| 7a6354e781 | |||
| 5f11c2d0b6 | |||
| 9c6814482d | |||
| 94a5736939 | |||
| bc496d8277 | |||
| 2b23665cb5 | |||
| 1d1c9666dc | |||
| 1b14f3f608 | |||
| d370e817c0 | |||
| ea4c6c95a1 | |||
| f97ccd5c5d | |||
| 0b2a672f2d | |||
| f960b07ee1 | |||
| 5cbf06f6a6 | |||
| 70ea517a3a | |||
| 957305d6f5 | |||
| bbd1d429a6 | |||
| 56ba47a844 | |||
| 8101d394a8 | |||
| 496144f8ad | |||
| 4cbee71636 | |||
| 7aed643eba | |||
| 829da52101 | |||
| 9dd449c2ef | |||
| f78bdaa358 | |||
| 16236e1361 | |||
| 8e52d45037 | |||
| 2246670487 | |||
| cde325198e | |||
| faa0710fd5 | |||
| d1bb27ad6b | |||
| 8f1f679077 | |||
| b632cbba0f | |||
| de438e90c4 | |||
| 7a3b1d4dbe | |||
| 44e9b4da4d | |||
| 559710b4b4 | |||
| a05c5b4976 | |||
| 51cfe767ee | |||
| 0891e78b8d | |||
| 5c834ec09c | |||
| fc5b6ae4e4 | |||
| 5e668d9e13 | |||
| b3d63c5698 | |||
| 079c160268 | |||
| f5425c2bea | |||
| 8c4967abc4 | |||
| 8dd069b38d | |||
| 1160fb999e | |||
| 948329f79e | |||
| a61ef37a22 | |||
| 66e8de06f0 | |||
| b5c3daea0c | |||
| ba70ecc063 | |||
| 3b1145133a | |||
| 3e0e4caa0c | |||
| a55a969588 | |||
| 8b46435808 | |||
| 1a6966e522 | |||
| 2fcee4cea0 | |||
| 223b7c6069 | |||
| 771946918a | |||
| d7e113108b | |||
| e3d9fac1b0 | |||
| 7c78109bf2 | |||
| b7ccf66418 | |||
| 8c5265f9b5 | |||
| e81bbf5195 | |||
| f219b62fcc | |||
| 6347260522 | |||
| 9fe80cfb66 | |||
| 62975e5e1b | |||
| 9b4e62149f | |||
| 8643a12744 | |||
| 9369218821 | |||
| 0c1b48cee2 | |||
| 5865fdbd23 | |||
| 6b5fe82d47 | |||
| 49624d5afd | |||
| a92de9ffa0 | |||
| d4133cc51d | |||
| 611068a9b0 | |||
| 7930ecaea9 | |||
| 3c72080802 | |||
| 4c762a1aa8 | |||
| 4f99477d83 | |||
| 6d4de6795e | |||
| 4f8b93cb6a | |||
| 48a4aefe96 | |||
| dc530be9be | |||
| bb5e9e739b | |||
| 7264249ef8 | |||
| 5eb5f6807e | |||
| 8bb50a6e2c | |||
| 7b7987b574 | |||
| a209edc181 | |||
| e744a475c3 | |||
| b280e1bf89 | |||
| decb763a5f | |||
| 28f685ae10 | |||
| 6270d59f3f | |||
| 11c6f33392 | |||
| a75b613744 | |||
| ccca070dd2 | |||
| bd2ce2e82d | |||
| a88c920ef0 | |||
| 6aee2ae6b9 | |||
| 0ec14da2ac | |||
| 375f538bc1 | |||
| 1c6907373b | |||
| 64fe9048f0 | |||
| c96193b3d0 | |||
| b1035c24bc | |||
| d69e75480e | |||
| 4da12055e9 | |||
| 7ca9272fe7 | |||
| 5f0bf827ae | |||
| 87cd020e94 | |||
| 4fd7095fbf | |||
| c1ebcf33e5 | |||
| d0f72a8d96 | |||
| 6e5ac76b52 | |||
| 8ae3b93307 | |||
| 7246e06973 | |||
| 0ae824b9f5 | |||
| deee22b6db | |||
| c8e01182f4 | |||
| dec6bd6779 | |||
| b00b791107 | |||
| 4db2b1cc29 | |||
| 471ebb3b6e | |||
| e057b6530b | |||
| b5328324f8 | |||
| 18aa853948 | |||
| c8559bd36a | |||
| b71e717da1 | |||
| 60677b36f6 | |||
| 339d32b1dd | |||
| 9432e34571 | |||
| 153f33f44b | |||
| fc2f20ee6e | |||
| cfa70feac5 | |||
| 6a851f7247 | |||
| a0cae1b7b8 | |||
| 5798ba9e89 | |||
| 919e3c386d | |||
| e85bb40904 | |||
| d23a2df809 | |||
| b9185cce15 | |||
| c14605b598 | |||
| 68c2f3c754 | |||
| 2725a59826 | |||
| 9ef8f5d1f2 | |||
| 39cd66032d | |||
| ee53e7218f | |||
| 72a1cded27 | |||
| 3295b6a7b7 | |||
| 4f64325f2b | |||
| de9c9cbdcb | |||
| 1fe9e259b4 | |||
| 6b4f68128b | |||
| 8b637fbeec | |||
| 7267535ccf | |||
| 8f99987696 | |||
| 515473e5dc | |||
| 8b83e939ce | |||
| c71bd36de2 | |||
| 9fdc8f0da3 | |||
| 8ba099024e | |||
| e656acec1d | |||
| 6fa4994e04 | |||
| a929706219 | |||
| 62effd1cc1 | |||
| 417b597d1d | |||
| 2fabb9013a | |||
| 835dcc6446 | |||
| fba0a6ec71 | |||
| 9a2f4c984c | |||
| d870e68001 | |||
| 8e7fcfb746 | |||
| a995e132f3 | |||
| 6b9921c5a7 | |||
| 75695812ee | |||
| eae0414cb4 | |||
| 7b203bedfc | |||
| da20b817d2 | |||
| ef1f2251b5 | |||
| c907835ce7 | |||
| 3be623dd63 | |||
| 045c1e0305 | |||
| c292e79d94 | |||
| cb54d87daf | |||
| a29c0bd466 | |||
| 575ba33e43 | |||
| 30bb750d94 | |||
| c045d4e2cc | |||
| b2d568d646 | |||
| 22c2b47506 | |||
| 2ddaf3ee66 | |||
| d4dbee023e | |||
| 560c34afed | |||
| b3fc66f6b9 | |||
| 0ca70316dc | |||
| dd0f475944 | |||
| 80bb4a1888 | |||
| d7e72a96f9 | |||
| e552987663 | |||
| 33dea97475 | |||
| 4cbaae1567 | |||
| a8a9e58fe9 | |||
| 93ec154b7e | |||
| ed71d3e348 | |||
| 62fb61a8bb | |||
| de75e67de7 | |||
| 4fc10d360a | |||
| 40615976e1 | |||
| c6bd428c46 | |||
| 95000d718a | |||
| 1e077a7b0c | |||
| 659d6fc263 | |||
| d56fa926d2 | |||
| 321121d5ef | |||
| a50b4ba041 | |||
| 94b4d730e3 | |||
| f4a946f5e2 | |||
| 23a062abea | |||
| 986f67adf6 | |||
| 750c73fef9 | |||
| 419a4d830f | |||
| 814daa5d3c | |||
| 308bd369bc | |||
| 80670a9c4b | |||
| c01db35297 | |||
| e8cbbbb4bc | |||
| b79955107b | |||
| 67e646408b | |||
| f2500aeb6e | |||
| 0df714b9ea | |||
| 3ebbb27c3a | |||
| 8e9cf4603c | |||
| be4fc50ae0 | |||
| bb8a33f1a8 | |||
| c9147dc64d | |||
| aabca8f771 | |||
| e2e4f53d3e | |||
| 4b2e3d876b | |||
| a294484fb9 | |||
| 8e0892498e | |||
| d571081625 | |||
| 46668e32f6 | |||
| 627f513da0 | |||
| 5b5c87a07b | |||
| df1eec07e5 | |||
| 3f98bcea33 | |||
| 11ac9f5f3e | |||
| 66816e1a4c | |||
| 920bacd5aa | |||
| f05f95dc41 | |||
| 8b0faa57aa | |||
| e29741cf17 | |||
| 3cbc850d12 | |||
| 70e12db7b7 | |||
| 3b5f921c01 | |||
| 3c0b56c99a | |||
| ce841476ae | |||
| 110e7c9c29 | |||
| 4f0f1672c7 | |||
| b18146f699 | |||
| c7ceb5cf82 | |||
| d02a136e2f | |||
| 1a9b23ca10 | |||
| 3d82162b4d | |||
| 39a42e224a | |||
| f93e5d38f2 | |||
| 2f4688cb13 | |||
| 24c41107d2 | |||
| 9eef163e70 | |||
| e82f202a17 | |||
| da8ca7dbfb | |||
| 0fcf70a2bf | |||
| 68f8bee146 | |||
| e35a0ffc17 | |||
| 21f1a161c9 | |||
| a31d2cd28b | |||
| 47f978a4fa | |||
| c2f2672421 | |||
| aaf6598e43 | |||
| f77a4eb3cb | |||
| 3435da0124 | |||
| d705a3172b | |||
| 82cf8cb877 | |||
| 2dabd5492f | |||
| 64827028b5 | |||
| 753ca3abc0 | |||
| a6bf61bc48 | |||
| 0d79bc2600 | |||
| 3273e9879b | |||
| 9c8898faa4 | |||
| 0ed314ed81 | |||
| 14fa92e0be | |||
| aeb5346606 | |||
| 0ab734f777 | |||
| 72a4b4e267 | |||
| 7052c6f68b | |||
| 4beba85de4 | |||
| 51edbd9baa | |||
| e249968662 | |||
| 567634c958 | |||
| 2a92c10777 | |||
| 7c801366ef | |||
| bbffe7737b | |||
| 88bb8949dd | |||
| d7d9027afa | |||
| 1917462ea3 | |||
| 4c69888938 | |||
| 639ca433a2 | |||
| 44c1a2adb6 | |||
| 6369c480f9 | |||
| b2a7de0a72 | |||
| 19825aa844 | |||
| 14ece26b85 | |||
| 9a17352156 | |||
| b9eb8de88a | |||
| 04cfbcb7e4 | |||
| d74d91532b | |||
| f28a54506e | |||
| b6797131e7 | |||
| 20a5f25901 | |||
| 6ccc3e5cc6 | |||
| 9eb68e641c | |||
| 3f5a94e710 | |||
| 22e51f1216 | |||
| 7060c906a7 | |||
| e61c265418 | |||
| 1149751ae4 | |||
| d35a798a80 | |||
| 175b07875a | |||
| 8cb2ecd113 | |||
| 362de65b36 | |||
| c753663079 | |||
| f243d98094 | |||
| d711a9a14c | |||
| e399a81ee3 | |||
| f030f88029 | |||
| 54f2959d6a | |||
| d5a63da257 | |||
| 2a09f39fdb | |||
| 61a22cc910 | |||
| 7c3a8fda4b | |||
| c8ff5bf624 | |||
| dfd791ffb5 | |||
| 345800ba25 | |||
| 4c3a5492c7 | |||
| 7862426144 | |||
| 6e196262a5 | |||
| 11abd6d60f | |||
| 54df9912eb | |||
| b46e1f3291 | |||
| 98eb063ea1 | |||
| ac29a19ee8 | |||
| 553d005ed0 | |||
| 96e59d764a | |||
| cb3c3eb046 | |||
| 8d50f12886 | |||
| c8821f2f54 | |||
| b3e2f3545d | |||
| 383b7bce21 | |||
| ab9a8fd106 | |||
| 2954260722 | |||
| 6f78ba604f | |||
| 4471d95482 | |||
| 929038fc30 | |||
| c2cf64dfa0 | |||
| 719177b816 | |||
| 190296e533 | |||
| f1f33112fc | |||
| b1fdeeef65 | |||
| 63b8e9551c | |||
| f6758d4d3a | |||
| e873b539eb | |||
| 95a3d7b494 | |||
| 1578925613 | |||
| f3ee2d2cf0 | |||
| 39d71f3627 | |||
| e52caa013d | |||
| 18d88ab298 | |||
| b768f5f661 | |||
| b756410982 | |||
| bcc58481ed | |||
| 2c6fd5d0d8 | |||
| 24238acf0e | |||
| 089399307f | |||
| 385d9155f7 | |||
| 6eaef8388b | |||
| 3100d6aede | |||
| 35b62c2bc6 | |||
| ded2fcdc7d | |||
| 0479cfca18 | |||
| 4f0a2dd3c8 | |||
| e082ad97ac | |||
| 40d8f17ce7 | |||
| 140f876164 | |||
| 5fa0fa8d17 | |||
| da8ac35932 | |||
| 9e6491b86b | |||
| fcdb0e3d80 | |||
| e42e4ee96d | |||
| a9d42403d9 | |||
| 561dd33044 | |||
| c5618bb3db | |||
| d9f5ba517d | |||
| 054ceda63a | |||
| ad0efd98ff | |||
| f0b473db2c | |||
| 58324c5d24 | |||
| 82d960c586 | |||
| ec9219ac7f | |||
| 28e7bd5304 | |||
| 4e4c2cd788 | |||
| 896c4e980c | |||
| 4262ec7386 | |||
| 08744c55ce | |||
| 6031161452 | |||
| 55c657aba6 | |||
| c4e01140a2 | |||
| c9cdf51b51 | |||
| de3ab708e9 | |||
| 52f2dd22f2 | |||
| 8aa90935d8 | |||
| b8ae608d79 | |||
| 15070aab45 | |||
| 9fb2da406a | |||
| 4488726b12 | |||
| 6ade459c40 | |||
| 47914f94ac | |||
| e5e6a07e24 | |||
| dda8fe032c | |||
| 09a0eabc48 | |||
| 6546b580e9 | |||
| 93521840bc | |||
| 5f2b84d88c | |||
| 794784e253 | |||
| 96bfeda954 | |||
| ada909ec8a | |||
| 5064a76aa5 | |||
| 20cc2f478f | |||
| f818e4033d | |||
| 0736fc6b91 | |||
| 7bef401e6a | |||
| ea51e5e443 | |||
| eccc0eb167 | |||
| afa3f1d705 | |||
| ce94de1214 | |||
| 3d3b596962 | |||
| b42e0df218 | |||
| 7da78b6e4a | |||
| 0b63f8d4a2 | |||
| 1aa9ddbb48 | |||
| 5e44460112 | |||
| 39350b2cfb | |||
| 154f7b5a54 | |||
| 94adc2c922 | |||
| 80e8ca0c36 | |||
| 358a3f84e6 | |||
| 7629d9f429 | |||
| da87e974f4 | |||
| 6cd2397b27 | |||
| d053fc9fea | |||
| 979a4b808b | |||
| bdfda12d68 | |||
| c8211dbdd8 | |||
| 4a0332a948 | |||
| c17f13d467 | |||
| 9efa6f163d | |||
| 560dee201f | |||
| f6371537ed | |||
| 5a31813a45 | |||
| 384af8a9db | |||
| 186f594d1d | |||
| 497cd6a494 | |||
| 471afcf0be | |||
| 8724d3eff7 | |||
| 29ba7a512e | |||
| 9fbd5ee72b | |||
| ed867884b0 | |||
| 8019b22fc4 | |||
| 0bcef534d0 | |||
| 69033c0657 | |||
| 63c86bbacb | |||
| 251e19d751 | |||
| 12279501b4 | |||
| 3c512273f0 | |||
| 597651248a | |||
| a80c577f1f | |||
| a09c8e329a | |||
| 2174451e97 | |||
| ee562eb3b7 | |||
| 03e05b3e35 | |||
| 385a206f6b | |||
| 202a78526c | |||
| eb7fe78410 | |||
| e0f79490eb | |||
| 1acf79baae | |||
| 405056cbd3 | |||
| fc68b9add5 | |||
| d95d5d5e6b | |||
| 69d86affd0 | |||
| 9f5374f4fd | |||
| f858ba0b70 | |||
| b9a2374f8f | |||
| 73c0af47db | |||
| 72f62b595d | |||
| 5316164f00 | |||
| a14646d2e2 | |||
| 1135334a95 | |||
| 5462167d85 | |||
| b7c3856785 | |||
| 92ad0838e7 | |||
| ae405d54d5 | |||
| d9ea9cf1ba | |||
| 5c4a65d278 | |||
| cbc543b981 | |||
| a22f59aab6 | |||
| 9b7c5e7756 | |||
| e2591a54d6 | |||
| 9180089787 | |||
| bd892a4cf1 | |||
| 1dd895ebef | |||
| c416adc980 | |||
| 6737b17e43 | |||
| 3300d61a53 | |||
| defbbc7b44 | |||
| a77e1ebf61 | |||
| 6834be0909 | |||
| 9d3640f6b9 | |||
| 4e392eeb14 | |||
| 4ce778f76f | |||
| 160bb20eed | |||
| 27c73369da | |||
| 6cbb033326 | |||
| bfd31516ba | |||
| da664e5869 | |||
| cae1f78025 | |||
| 8b282eaf8a | |||
| b5b65ecd41 | |||
| 87b47a9c31 | |||
| 6dd0c75cdc | |||
| 5f3f2139c1 | |||
| ef97c913cf | |||
| b1df05ea47 | |||
| a270946ce2 | |||
| efc19441cd | |||
| 803ec482fb | |||
| dac71dfd55 | |||
| 85b7ebdcc7 | |||
| 800bc52c0e | |||
| d4498fb573 | |||
| 2bd6c38bf6 | |||
| 5dfbfe6cf5 | |||
| f47768b8db | |||
| 2ba951bb42 | |||
| 39733689c6 | |||
| c97bdbe203 | |||
| 6cfe7ff889 | |||
| d5763ddbd6 | |||
| 59ac399f9e | |||
| 094a96ab78 | |||
| 26b5d00a72 | |||
| 74e281b0c4 | |||
| c6bd6e4b0c | |||
| e3fc761414 | |||
| 16a65ffa93 | |||
| 63f0b0528c | |||
| 000c97881c | |||
| 20e9fcf172 | |||
| 04851d5df3 | |||
| 9c29be3378 | |||
| 9d1c5a1184 | |||
| 56f7fd9a31 | |||
| 8d9d28e1c9 | |||
| 47a14dfd40 | |||
| 9d0e0f4f2d | |||
| 2543736e82 | |||
| 84f8008167 | |||
| 11676bff83 | |||
| a7c9ec51ed | |||
| d077b6d880 | |||
| 4ce9e8be3f | |||
| b2ee32b4ab | |||
| 0b705bd311 | |||
| 9ae9f307bc | |||
| 83cbb60cb7 | |||
| 43d476786f | |||
| 322f13072c | |||
| 0861208b64 | |||
| e08b9469d0 | |||
| c9a7379847 | |||
| b211d15b17 | |||
| 99cfa2080f |
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,8 +1,9 @@
|
||||
/.idea
|
||||
.idea/misc.xml
|
||||
.idea/
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
.gradle/
|
||||
local.properties
|
||||
# sign.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
captures/
|
||||
build/
|
||||
release-app/
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[submodule "libraries/LGLibrary"]
|
||||
path = libraries/LGLibrary
|
||||
url = git@gitlab.ghzhushou.com:client/client-common.git
|
||||
branch = master
|
||||
1
.idea/.name
generated
1
.idea/.name
generated
@ -1 +0,0 @@
|
||||
GH-ASSISTv1.45
|
||||
22
.idea/compiler.xml
generated
22
.idea/compiler.xml
generated
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<resourceExtensions />
|
||||
<wildcardResourcePatterns>
|
||||
<entry name="!?*.java" />
|
||||
<entry name="!?*.form" />
|
||||
<entry name="!?*.class" />
|
||||
<entry name="!?*.groovy" />
|
||||
<entry name="!?*.scala" />
|
||||
<entry name="!?*.flex" />
|
||||
<entry name="!?*.kt" />
|
||||
<entry name="!?*.clj" />
|
||||
<entry name="!?*.aj" />
|
||||
</wildcardResourcePatterns>
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="false">
|
||||
<processorPath useClasspath="true" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/copyright/profiles_settings.xml
generated
3
.idea/copyright/profiles_settings.xml
generated
@ -1,3 +0,0 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="" />
|
||||
</component>
|
||||
6
.idea/encodings.xml
generated
6
.idea/encodings.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="PROJECT" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
28
.idea/gradle.xml
generated
28
.idea/gradle.xml
generated
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.10" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/hackdex" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="myModules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/hackdex" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
46
.idea/misc.xml
generated
46
.idea/misc.xml
generated
@ -1,46 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="EntryPointsManager">
|
||||
<entry_points version="2.0" />
|
||||
</component>
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="4">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
|
||||
<OptionsSetting value="true" id="Add" />
|
||||
<OptionsSetting value="true" id="Remove" />
|
||||
<OptionsSetting value="true" id="Checkout" />
|
||||
<OptionsSetting value="true" id="Update" />
|
||||
<OptionsSetting value="true" id="Status" />
|
||||
<OptionsSetting value="true" id="Edit" />
|
||||
<ConfirmationsSetting value="0" id="Add" />
|
||||
<ConfirmationsSetting value="0" id="Remove" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/modules.xml
generated
12
.idea/modules.xml
generated
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/GH-ASSISTv1.45.iml" filepath="$PROJECT_DIR$/GH-ASSISTv1.45.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/GH-ASSISTv1.50.iml" filepath="$PROJECT_DIR$/GH-ASSISTv1.50.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/buildSrc/buildSrc.iml" filepath="$PROJECT_DIR$/buildSrc/buildSrc.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/hackdex/hackdex.iml" filepath="$PROJECT_DIR$/hackdex/hackdex.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
12
.idea/runConfigurations.xml
generated
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
@ -0,0 +1,8 @@
|
||||
### Ver 3.0
|
||||
* x
|
||||
|
||||
### Ver 2.6
|
||||
* xx
|
||||
|
||||
### Ver 2.5
|
||||
* 此处写本次更新所做的业务和代码修改
|
||||
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# 光环助手Android客户端
|
||||
|
||||
### Sourceset/debug/release
|
||||
* https://developer.android.com/studio/build/build-variants.html#sourcesets
|
||||
|
||||
### 多渠道打包配置
|
||||
* 使用[ApkChannelPackage](https://github.com/ltlovezh/ApkChannelPackage)的方案
|
||||
* 正式打包命令:请使用./gradlew channelPubRelease打包渠道包
|
||||
|
||||
### 混淆配置
|
||||
* 配置文件:Android默认配置+proguard-rules.txt等
|
||||
* 参考libraries下每个项目独立的配置文件``proguard-project.txt``
|
||||
|
||||
### apk大小优化
|
||||
* 限制resConfig资源集
|
||||
* 开启ShrinkResources
|
||||
* 开启混淆,使用minifyEnabled(仅在release开启)
|
||||
* pngquant对png压缩、png/jpg->webp(未尝试)
|
||||
|
||||
### 第三方appkey等配置
|
||||
* 修改``gradle.properties``文件将各种key填入其中,实现统一管理
|
||||
* 通过gradle文件内的resValue/buildConfigField/manifestPlaceHolder方式实现编译期间修改,具体情况请参考``./build.gradle``和``./app/build.gradle``配置
|
||||
|
||||
### 拉取代码步骤
|
||||
1. 拉取主项目代码: `git clone -b dev git@gitlab.ghzhushou.com:halo/assistant-android.git`
|
||||
2. 初始化公用类库: `bash ./scripts/init_submodules.sh`
|
||||
|
||||
### submodule管理方式(只拉取master)
|
||||
* 提交代码,需要cd到submodule文件夹去做修改
|
||||
|
||||
|
||||
|
||||
### TODO
|
||||
* GSON 序列化用统一的一个, GsonUtil fromJson
|
||||
* CleanApkAdapter 转化字符串size工具函数 比如SpeedUtils
|
||||
* getString 解决 字符串hardcode问题
|
||||
* Adapter 里面clicklistener 用接口传参将点击操作委托给controller
|
||||
* Adapter ViewHolder的功能,部分重写到ViewHolder类本身
|
||||
|
||||
* activity 统一入口未完成(外部入口相关)
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/build
|
||||
316
app/build.gradle
316
app/build.gradle
@ -1,113 +1,263 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
task('processWithJavassist') << {
|
||||
String classPath = file('build/intermediates/classes/debug')//项目编译class所在目录
|
||||
dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir
|
||||
.absolutePath + '/intermediates/classes/debug')//第二个参数是hackdex的class所在目录
|
||||
}
|
||||
apply plugin: 'kotlin-android' // kotlin
|
||||
|
||||
task buildJar(dependsOn: ['compileReleaseJavaWithJavac'], type: Jar) {
|
||||
// apkChannelPackage
|
||||
apply plugin: 'channel'
|
||||
|
||||
baseName = "news"
|
||||
//后缀名
|
||||
extension = "jar"
|
||||
//最终的 Jar 包名,如果没设置,默认为 [baseName]-[appendix]-[version]-[classifier].[extension]
|
||||
archiveName = "news.jar";
|
||||
|
||||
//需打包的资源所在的路径集
|
||||
def srcClassDir = [project.buildDir.absolutePath + "/intermediates/classes/debug"];
|
||||
from srcClassDir
|
||||
|
||||
//去除路径集下部分的资源
|
||||
exclude "com/gh/gamecenter/BuildConfig.class"
|
||||
exclude "com/gh/gamecenter/R.class"
|
||||
exclude "com/gh/gamecenter/BuildConfig/\$*.class"
|
||||
exclude "com/gh/gamecenter/R/\$*.class"
|
||||
|
||||
//只导入资源路径集下的部分资源
|
||||
include "com/gh/gamecenter/NewsActivity.class"
|
||||
include "com/gh/gamecenter/NewsActivity\$*.class"
|
||||
//注: exclude include 支持可变长参数
|
||||
}
|
||||
apply from: 'tinker-support.gradle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion "23.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.gh.gamecenter"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 19
|
||||
versionCode 14
|
||||
versionName "1.51"
|
||||
|
||||
// 默认的渠道
|
||||
// manifestPlaceholders = [CHANNEL_VALUE: "GH_TEST"]
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
/**
|
||||
* 签名设置
|
||||
*/
|
||||
dexOptions {
|
||||
jumboMode = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
||||
multiDexEnabled true
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = [eventBusIndex: 'com.gh.EventBusIndex']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 只支持两种架构,减少apk大小,有疑问请参考:
|
||||
* https://developer.android.com/ndk/guides/abis.html
|
||||
* http://allenfeng.com/2016/11/06/what-you-should-know-about-android-abi-and-so/
|
||||
* (为了性能考虑,armeabi可以考虑替换成armeabi-v7a[需要先收集用户设备情况])
|
||||
*/
|
||||
ndk {
|
||||
abiFilters "armeabi", "x86"
|
||||
}
|
||||
|
||||
// 由于app只针对中文用户,所以仅保留zh资源,其他删掉
|
||||
resConfigs "zh"
|
||||
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode rootProject.ext.versionCode
|
||||
versionName rootProject.ext.versionName
|
||||
applicationId rootProject.ext.applicationId
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-fresco.txt'
|
||||
|
||||
/**
|
||||
* All third-party appid/appkey
|
||||
*/
|
||||
buildConfigField "String", "WECHAT_APPID", "\"${WECHAT_APPID}\""
|
||||
buildConfigField "String", "WECHAT_SECRET", "\"${WECHAT_SECRET}\""
|
||||
buildConfigField "String", "TENCENT_APPID", "\"${TENCENT_APPID}\""
|
||||
buildConfigField "String", "WEIBO_APPKEY", "\"${WEIBO_APPKEY}\""
|
||||
buildConfigField "String", "MTA_APPKEY", "\"${MTA_APPKEY}\""
|
||||
buildConfigField "String", "TD_APPID", "\"${TD_APPID}\""
|
||||
buildConfigField "String", "PATCH_VERSION_NAME", "\"${PATCH_VERSION_NAME}\""
|
||||
|
||||
}
|
||||
|
||||
// gradle 2.2以上默认同时启用v1和v2(优先用于Android N)
|
||||
signingConfigs {
|
||||
debug {
|
||||
v1SigningEnabled true
|
||||
v2SigningEnabled true
|
||||
}
|
||||
release {
|
||||
storeFile file("gh.keystore")
|
||||
keyAlias "gh.keystore"
|
||||
keyPassword "20150318"
|
||||
storePassword "20150318"
|
||||
v1SigningEnabled true
|
||||
v2SigningEnabled true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
debuggable true
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
zipAlignEnabled false
|
||||
versionNameSuffix "-debug"
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${DEBUG_UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEBUG_UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${DEBUG_MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${DEBUG_MIPUSH_APPKEY}\""
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
zipAlignEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.release
|
||||
|
||||
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
|
||||
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
|
||||
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
|
||||
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants.all { variant ->
|
||||
// variant.dex.dependsOn << processWithJavassist //在执行dx命令之前将代码打入到class中
|
||||
// }
|
||||
|
||||
/**
|
||||
* 渠道打包
|
||||
* 多渠道打包,渠道请参考"channel.txt"文件,所有渠道值均通过java code设置
|
||||
*/
|
||||
productFlavors {
|
||||
GH_100 {}
|
||||
GH_101 {}
|
||||
GH_102 {}
|
||||
GH_103 {}
|
||||
GH_104 {}
|
||||
GH_106 {}
|
||||
GH_109 {}
|
||||
GH_110 {}
|
||||
GH_113 {}
|
||||
GH_114 {}
|
||||
GH_115 {}
|
||||
GH_116 {}
|
||||
GH_118 {}
|
||||
GH_119 {}
|
||||
GH_120 {}
|
||||
GH_121 {}
|
||||
GH_123 {}
|
||||
GH_200 {}
|
||||
GH_201 {}
|
||||
GH_202 {}
|
||||
GH_203 {}
|
||||
GH_307 {}
|
||||
GH_127 {}
|
||||
}
|
||||
productFlavors.all { flavor ->
|
||||
flavor.manifestPlaceholders = [CHANNEL_VALUE: name]//命令 gradlew assembleRelease
|
||||
// publish release host
|
||||
publish {
|
||||
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
|
||||
buildConfigField "String", "USER_HOST", "\"${USER_HOST}\""
|
||||
buildConfigField "String", "COMMENT_HOST", "\"${COMMENT_HOST}\""
|
||||
buildConfigField "String", "LIBAO_HOST", "\"${LIBAO_HOST}\""
|
||||
buildConfigField "String", "MESSAGE_HOST", "\"${MESSAGE_HOST}\""
|
||||
buildConfigField "String", "DATA_HOST", "\"${DATA_HOST}\""
|
||||
buildConfigField "String", "USERSEA_HOST", "\"${USERSEA_HOST}\""
|
||||
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
|
||||
|
||||
buildConfigField "String", "USERSEA_APP_ID", "\"${USERSEA_APP_ID}\""
|
||||
buildConfigField "String", "USERSEA_APP_SECRET", "\"${USERSEA_APP_SECRET}\""
|
||||
}
|
||||
// internal test dev host
|
||||
internal {
|
||||
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
|
||||
buildConfigField "String", "USER_HOST", "\"${DEV_USER_HOST}\""
|
||||
buildConfigField "String", "COMMENT_HOST", "\"${DEV_COMMENT_HOST}\""
|
||||
buildConfigField "String", "LIBAO_HOST", "\"${DEV_LIBAO_HOST}\""
|
||||
buildConfigField "String", "MESSAGE_HOST", "\"${DEV_MESSAGE_HOST}\""
|
||||
buildConfigField "String", "DATA_HOST", "\"${DEV_DATA_HOST}\""
|
||||
buildConfigField "String", "USERSEA_HOST", "\"${DEV_USERSEA_HOST}\""
|
||||
|
||||
buildConfigField "String", "BUGLY_APPID", "\"${DEBUG_BUGLY_APPID}\""
|
||||
|
||||
buildConfigField "String", "USERSEA_APP_ID", "\"${DEV_USERSEA_APP_ID}\""
|
||||
buildConfigField "String", "USERSEA_APP_SECRET", "\"${DEV_USERSEA_APP_SECRET}\""
|
||||
}
|
||||
}
|
||||
|
||||
// productFlavors.all { flavor ->
|
||||
// flavor.manifestPlaceholders = [CHANNEL_VALUE: name]//命令 gradlew assembleRelease
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
// apkChannelPackage
|
||||
channel {
|
||||
//多渠道包的输出目录,默认为new File(project.buildDir,"channel")
|
||||
baseOutputDir = new File(project.buildDir, "channel")
|
||||
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
|
||||
apkNameFormat = '${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
|
||||
}
|
||||
|
||||
rebuildChannel {
|
||||
// baseDebugApk = 已有Debug APK
|
||||
// baseReleaseApk = 已有Release APK
|
||||
// //默认为new File(project.buildDir, "rebuildChannel/debug")
|
||||
// debugOutputDir = Debug渠道包输出目录
|
||||
// //默认为new File(project.buildDir, "rebuildChannel/release")
|
||||
// releaseOutputDir = Release渠道包输出目录
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'pl.droidsonroids.gif:android-gif-drawable:1.1.16'
|
||||
}
|
||||
|
||||
compile fileTree(include: '*.jar', dir: 'libs')
|
||||
|
||||
debugCompile "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
|
||||
debugCompile "com.facebook.stetho:stetho:${stetho}"
|
||||
debugCompile "com.facebook.stetho:stetho-okhttp3:${stetho}"
|
||||
debugCompile "com.squareup.okhttp3:logging-interceptor:${okHttp}"
|
||||
|
||||
compile "com.android.support:multidex:${multidex}"
|
||||
compile "com.android.support:design:${androidSupport}"
|
||||
compile "com.android.support:support-v4:${androidSupport}"
|
||||
compile "com.android.support:appcompat-v7:${androidSupport}"
|
||||
compile "com.android.support:support-annotations:${androidSupport}"
|
||||
compile "com.android.support:percent:${androidSupport}"
|
||||
|
||||
compile "com.kyleduo.switchbutton:library:${switchButton}"
|
||||
compile "com.readystatesoftware.systembartint:systembartint:${systemBarTint}"
|
||||
|
||||
compile "com.facebook.fresco:fresco:${fresco}"
|
||||
compile "com.facebook.fresco:animated-gif:${fresco}"
|
||||
|
||||
compile "com.squareup.okhttp3:okhttp:${okHttp}"
|
||||
|
||||
compile "com.leon.channel:helper:${apkChannelPackage}"
|
||||
|
||||
compile "com.squareup.retrofit2:retrofit:${retrofit}"
|
||||
compile "com.squareup.retrofit2:converter-gson:${retrofit}" // include gson 2.7
|
||||
compile "com.squareup.retrofit2:adapter-rxjava:${retrofit}"
|
||||
// compile "com.google.code.gson:gson:${gson}"
|
||||
|
||||
compile "com.j256.ormlite:ormlite-android:${ormlite}"
|
||||
compile "com.j256.ormlite:ormlite-core:${ormlite}"
|
||||
|
||||
compile "com.jakewharton:butterknife:${butterKnife}"
|
||||
annotationProcessor "com.jakewharton:butterknife-compiler:${butterKnife}"
|
||||
|
||||
compile "org.greenrobot:eventbus:${eventbus}"
|
||||
annotationProcessor "org.greenrobot:eventbus-annotation-processor:${eventbusApt}"
|
||||
|
||||
compile "io.reactivex:rxjava:${rxJava}"
|
||||
compile "io.reactivex:rxandroid:${rxAndroid}"
|
||||
compile "com.jakewharton.rxbinding:rxbinding:${rxBinding}"
|
||||
|
||||
//TODO update to rx 2.x
|
||||
// compile "io.reactivex.rxjava2:rxjava:${rxJava2}"
|
||||
// compile "io.reactivex.rxjava2:rxandroid:${rxAndroid2}"
|
||||
// compile "com.jakewharton.rxbinding2:rxbinding:${rxBinding2}"
|
||||
|
||||
compile "com.google.zxing:core:${zxing}"
|
||||
compile "com.google.zxing:android-core:${zxing}"
|
||||
|
||||
compile "com.daimajia.swipelayout:library:${swipeLayout}"
|
||||
compile("cn.trinea.android.view.autoscrollviewpager:android-auto-scroll-view-pager:${autoScrollViewPager}") {
|
||||
exclude module: 'support-v4'
|
||||
}
|
||||
|
||||
compile "com.sina.weibo.sdk:core:${weiboSDK}"
|
||||
|
||||
// bugly with tinker support
|
||||
compile "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
|
||||
|
||||
compile "pub.devrel:easypermissions:${easypermissions}"
|
||||
|
||||
compile project(':libraries:LGLibrary')
|
||||
compile project(':libraries:MiPush')
|
||||
compile project(':libraries:MTA')
|
||||
compile project(':libraries:QQShare')
|
||||
compile project(':libraries:TalkingData')
|
||||
compile project(':libraries:UmengPush')
|
||||
compile project(':libraries:WechatShare')
|
||||
compile project(':libraries:iosched')
|
||||
|
||||
}
|
||||
File propFile = file('sign.properties');
|
||||
if (propFile.exists()) {
|
||||
def Properties props = new Properties()
|
||||
props.load(new FileInputStream(propFile))
|
||||
|
||||
if (props.containsKey('keyAlias') && props.containsKey('keyPassword') &&
|
||||
props.containsKey('storeFile') && props.containsKey('storePassword')) {
|
||||
android.signingConfigs {
|
||||
// debug 不要使用正式签名,这样tinker才不会打补丁。
|
||||
debug {
|
||||
keyAlias props.get('keyAlias')
|
||||
keyPassword props.get('keyPassword')
|
||||
storeFile file(props.get('storeFile'))
|
||||
storePassword props.get('storePassword')
|
||||
}
|
||||
release {
|
||||
keyAlias props.get('keyAlias')
|
||||
keyPassword props.get('keyPassword')
|
||||
storeFile file(props.get('storeFile'))
|
||||
storePassword props.get('storePassword')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
} else {
|
||||
android.buildTypes.release.signingConfig = null
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
21
app/proguard-fresco.txt
Normal file
21
app/proguard-fresco.txt
Normal file
@ -0,0 +1,21 @@
|
||||
# Keep our interfaces so they can be used by other ProGuard rules.
|
||||
# See http://sourceforge.net/p/proguard/bugs/466/
|
||||
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
|
||||
|
||||
# Do not strip any method/class that is annotated with @DoNotStrip
|
||||
-keep @com.facebook.common.internal.DoNotStrip class *
|
||||
-keepclassmembers class * {
|
||||
@com.facebook.common.internal.DoNotStrip *;
|
||||
}
|
||||
|
||||
# Keep native methods
|
||||
-keepclassmembers class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
-dontwarn okio.**
|
||||
-dontwarn com.squareup.okhttp.**
|
||||
-dontwarn okhttp3.**
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn com.android.volley.toolbox.**
|
||||
-dontwarn com.facebook.infer.**
|
||||
187
app/proguard-rules.txt
Normal file
187
app/proguard-rules.txt
Normal file
@ -0,0 +1,187 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in C:\Android\sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
#--------- remove logs start ----------------
|
||||
-assumenosideeffects class com.lightgame.config.CommonDebug {
|
||||
private static String getLogTag(...);
|
||||
private static String getMethodName();
|
||||
public static void logMethodName(...);
|
||||
public static void logParams(...);
|
||||
public static void logFields(...);
|
||||
public static void logMethodWithParams(...);
|
||||
}
|
||||
|
||||
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
|
||||
|
||||
#-dontoptimize
|
||||
#--------- remove logs end ----------------
|
||||
|
||||
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
|
||||
-dontwarn InnerClasses
|
||||
|
||||
# OrmLite uses reflection
|
||||
-keep class com.j256.**
|
||||
-keepclassmembers class com.j256.** { *; }
|
||||
-keep enum com.j256.**
|
||||
-keepclassmembers enum com.j256.** { *; }
|
||||
-keep interface com.j256.**
|
||||
-keepclassmembers interface com.j256.** { *; }
|
||||
-dontwarn com.j256.**
|
||||
|
||||
#okhttp3
|
||||
-dontwarn com.squareup.okhttp3.**
|
||||
-dontwarn okio.**
|
||||
-keep class com.squareup.okhttp3.** { *;}
|
||||
|
||||
# stetho
|
||||
-keep class com.facebook.stetho.** { *; }
|
||||
-dontwarn com.facebook.stetho.**
|
||||
|
||||
# Retrofit 2.2
|
||||
# Platform calls Class.forName on types which do not exist on Android to determine platform.
|
||||
-dontnote retrofit2.Platform
|
||||
# Platform used when running on Java 8 VMs. Will not be used at runtime.
|
||||
-dontwarn retrofit2.Platform$Java8
|
||||
# Retain generic type information for use by reflection by converters and adapters.
|
||||
-keepattributes Signature
|
||||
# Retain declared checked exceptions for use by a Proxy instance.
|
||||
-keepattributes Exceptions
|
||||
|
||||
# Retrofit 2.X
|
||||
## https://square.github.io/retrofit/ ##
|
||||
|
||||
-dontwarn retrofit2.**
|
||||
-keep class retrofit2.** { *; }
|
||||
-keepattributes Signature
|
||||
-keepattributes Exceptions
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
|
||||
# rxjava
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.ImmediateScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.TestScheduler {
|
||||
public <methods>;
|
||||
}
|
||||
-keep class rx.schedulers.Schedulers {
|
||||
public static ** test();
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||
long producerIndex;
|
||||
long consumerIndex;
|
||||
}
|
||||
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
|
||||
long producerNode;
|
||||
long consumerNode;
|
||||
}
|
||||
-dontwarn rx.internal.util.**
|
||||
|
||||
## AutoScrollViewPager
|
||||
-keep class cn.trinea.android.** { *; }
|
||||
-keepclassmembers class cn.trinea.android.** { *; }
|
||||
-dontwarn cn.trinea.android.**
|
||||
|
||||
## butterknife
|
||||
# Retain generated class which implement Unbinder.
|
||||
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
|
||||
#
|
||||
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
|
||||
## is used to reflectively look up the generated ViewBinding.
|
||||
#-keep class butterknife.*
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
|
||||
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
|
||||
|
||||
-dontwarn butterknife.internal.**
|
||||
-keep class **$$ViewInjector { *; }
|
||||
-keepnames class * { @butterknife.InjectView *;}
|
||||
-dontwarn butterknife.Views$InjectViewProcessor
|
||||
-dontwarn com.gc.materialdesign.views.**
|
||||
|
||||
# eventbus
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class ** {
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
# Only required if you use AsyncExecutor
|
||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
||||
<init>(java.lang.Throwable);
|
||||
}
|
||||
|
||||
# weiboSdk
|
||||
-keep class com.sina.weibo.sdk.** { *; }
|
||||
-dontwarn android.webkit.WebView
|
||||
-dontwarn android.webkit.WebViewClient
|
||||
|
||||
# app models
|
||||
-keep class com.gh.common.view.** {*;}
|
||||
-keep class com.gh.gamecenter.db.info.** {*;}
|
||||
-keep class com.gh.gamecenter.entity.** {*;}
|
||||
-keep class com.gh.gamecenter.retrofit.** {*;}
|
||||
-keep class com.gh.gamecenter.eventbus.** {*;}
|
||||
-keep class * extends rx.Subscriber
|
||||
|
||||
#---------------------------------webview------------------------------------
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
|
||||
public boolean *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String);
|
||||
}
|
||||
#----------------------------------------------------------------------------
|
||||
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
-keepclassmembers enum * { *; }
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
# ------ bugly ---------
|
||||
-dontwarn com.tencent.bugly.**
|
||||
-keep public class com.tencent.bugly.**{*;}
|
||||
|
||||
# 重命名文件为SourceFile,再配合mapping符号表,可以拿到真实的类名
|
||||
-renamesourcefileattribute SourceFile
|
||||
# 保留源文件行号
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
4
app/sign.properties
Normal file
4
app/sign.properties
Normal file
@ -0,0 +1,4 @@
|
||||
storeFile=gh.keystore
|
||||
storePassword=20150318
|
||||
keyAlias=gh.keystore
|
||||
keyPassword=20150318
|
||||
@ -1,13 +0,0 @@
|
||||
package com.gh.gamecenter;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
||||
69
app/src/debug/java/com/gh/gamecenter/Injection.java
Normal file
69
app/src/debug/java/com/gh/gamecenter/Injection.java
Normal file
@ -0,0 +1,69 @@
|
||||
package com.gh.gamecenter;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
import com.lightgame.utils.Utils;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
|
||||
/**
|
||||
* @author CsHeng
|
||||
* @Date 03/09/2017
|
||||
* @Time 4:34 PM
|
||||
*/
|
||||
|
||||
public class Injection {
|
||||
|
||||
public static boolean appInit(Application application) {
|
||||
|
||||
// init leakcanary
|
||||
if (LeakCanary.isInAnalyzerProcess(application)) {
|
||||
// This process is dedicated to LeakCanary for heap analysis.
|
||||
// You should not init your app in this process.
|
||||
return false;
|
||||
}
|
||||
LeakCanary.install(application);
|
||||
|
||||
// init stetho
|
||||
Stetho.initializeWithDefaults(application);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static OkHttpClient.Builder provideRetrofitBuilder() {
|
||||
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
//分段打印retrofit日志
|
||||
if (message.startsWith("{") || message.startsWith("["))
|
||||
if (message.length() > 4000) {
|
||||
for (int i = 0; i < message.length(); i += 4000) {
|
||||
if (i + 4000 < message.length())
|
||||
Utils.log("OkHttp_Body::" + i, message.substring(i, i + 4000));
|
||||
else
|
||||
Utils.log("OkHttp_Body::" + i, message.substring(i, message.length()));
|
||||
}
|
||||
} else
|
||||
Utils.log("OkHttp_Body::", message);
|
||||
}
|
||||
});
|
||||
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder();
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
|
||||
builder.addNetworkInterceptor(interceptor);
|
||||
builder.addNetworkInterceptor(new StethoInterceptor());
|
||||
if (BuildConfig.DEBUG) {
|
||||
builder.addNetworkInterceptor(loggingInterceptor);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,234 +1,296 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.gh.gamecenter" >
|
||||
<manifest xmlns:android = "http://schemas.android.com/apk/res/android"
|
||||
package = "com.gh.gamecenter" >
|
||||
|
||||
<!-- 允许应用程序访问网络连接 -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name = "android.permission.INTERNET" />
|
||||
<!-- 允许应用程序写入外部存储,如SD卡上写文件 -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<!-- 允许应用程序读取扩展存储器 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name = "android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<!-- 允许挂载和反挂载文件系统可移动存储 -->
|
||||
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
|
||||
<uses-permission android:name = "android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
|
||||
<!-- 允许应用程序访问Wi-Fi网络状态信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" />
|
||||
<!-- 允许应用程序获取网络信息状态 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- 允许应用程序读取电话状态 -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name = "android.permission.READ_PHONE_STATE" />
|
||||
<!-- 允许应用程序获取当前或最近运行的应用 -->
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
<uses-permission android:name = "android.permission.GET_TASKS" />
|
||||
<!-- 允许访问振动设备 -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name = "android.permission.VIBRATE" />
|
||||
<!-- 允许应用程序通过WiFi或移动基站获取粗略的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- 允许应用程序通过GPS获取精确的位置信息 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_FINE_LOCATION" />
|
||||
<!-- 允许应用程序改变Wi-Fi连接状态 -->
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name = "android.permission.CHANGE_WIFI_STATE" />
|
||||
<!-- 允许应用程序管理AccountManager中的账户列表 -->
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name = "android.permission.MANAGE_ACCOUNTS" />
|
||||
<!-- 允许应用程序访问GMail账户列表 -->
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name = "android.permission.GET_ACCOUNTS" />
|
||||
<!-- 允许应用程序连接配对过的蓝牙设备 -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name = "android.permission.BLUETOOTH" />
|
||||
<!-- 允许应用程序管理蓝牙,搜索和配对新的蓝牙设备 -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name = "android.permission.BLUETOOTH_ADMIN" />
|
||||
<!-- 允许应用程序打开系统窗口,显示其他应用程序 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- 小米推送需要的权限 -->
|
||||
<uses-permission android:name="com.gh.gamecenter.permission.MIPUSH_RECEIVE" />
|
||||
<!-- 获取网路状态 -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name = "android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- 修改系统设置的权限 -->
|
||||
<uses-permission android:name = "android.permission.WRITE_SETTINGS" />
|
||||
|
||||
<permission
|
||||
android:name="com.gh.gamecenter.permission.MIPUSH_RECEIVE"
|
||||
android:protectionLevel="signature" />
|
||||
<!-- bugly with tinker -->
|
||||
<uses-permission android:name = "android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name = "android.permission.INTERNET" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name = "android.permission.READ_LOGS" />
|
||||
<uses-permission android:name = "android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:resizeable="true"
|
||||
android:smallScreens="true" />
|
||||
android:anyDensity = "true"
|
||||
android:largeScreens = "true"
|
||||
android:normalScreens = "true"
|
||||
android:resizeable = "true"
|
||||
android:smallScreens = "true" />
|
||||
|
||||
<!--android:largeHeap = "true"-->
|
||||
<application
|
||||
android:name="com.gh.base.AppController"
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/logo"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppThemeNormal" >
|
||||
<!-- TalkingData -->
|
||||
<meta-data
|
||||
android:name="TD_APP_ID"
|
||||
android:value="81DB144D555386A38A70B833537EC256" />
|
||||
<meta-data
|
||||
android:name="TD_CHANNEL_ID"
|
||||
android:value="GH_TEST"
|
||||
/>
|
||||
<!--android:value="${CHANNEL_VALUE}"-->
|
||||
android:name = "com.halo.assistant.TinkerApp"
|
||||
android:allowBackup = "true"
|
||||
android:icon = "@drawable/logo"
|
||||
android:label = "@string/app_name"
|
||||
android:theme = "@style/AppCompatTheme.APP" >
|
||||
|
||||
<!-- MTA -->
|
||||
<meta-data
|
||||
android:name="TA_APPKEY"
|
||||
android:value="APV567FTBS7J"/>
|
||||
<meta-data
|
||||
android:name="InstallChannel"
|
||||
android:value="GH_TEST"/>
|
||||
<!--android:value="${CHANNEL_VALUE}"-->
|
||||
<!--android:launchMode = "singleTask"-->
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SplashScreenActivity"
|
||||
android:configChanges = "keyboardHidden|orientation|screenSize"
|
||||
android:noHistory = "true"
|
||||
android:screenOrientation = "portrait"
|
||||
android:theme = "@style/AppGuideTheme" >
|
||||
<intent-filter >
|
||||
<action android:name = "android.intent.action.MAIN" />
|
||||
|
||||
<category android:name = "android.intent.category.LAUNCHER" />
|
||||
</intent-filter >
|
||||
</activity >
|
||||
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SplashScreenActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme_Guide"
|
||||
android:uiOptions="splitActionBarWhenNarrow" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
android:name = "com.gh.gamecenter.MainActivity"
|
||||
android:launchMode = "singleTask"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateAlwaysHidden|adjustResize" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.mob.tools.MobUIShell"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize" >
|
||||
<intent-filter>
|
||||
<data android:scheme="tencent100371282" />
|
||||
android:name = "com.gh.gamecenter.DownloadManagerActivity"
|
||||
android:launchMode = "singleTask"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ViewImageActivity"
|
||||
android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SearchActivity"
|
||||
android:configChanges = "keyboardHidden"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.NewsDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SettingActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ConcernActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SubjectActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.PluginActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.NewsSearchActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.GameNewsActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CropImageActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.WebActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareCardPicActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareCardActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.LibaoActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.LibaoDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareGhWfifActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ShareGhActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CleanApkActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.KcSelectGameActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ChooseReceiverActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ReceiverWaitingActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.FileSenderActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.FileReceiverActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SelectUserIconActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.AboutActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CommentDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.GameDetailActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SuggestSelectActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SuggestionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.VoteActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateAlwaysHidden|adjustResize" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.ToolBoxActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".WXEntryActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.DownloadManagerActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ViewImageActivity"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SearchActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateVisible" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.GameDetailsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.NewsActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.GameActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SettingActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SuggestionActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateVisible" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.ConcernActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.SubjectActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="com.gh.gamecenter.PluginActivity"
|
||||
android:screenOrientation="portrait"/>
|
||||
android:name = "com.gh.gamecenter.WeiBoShareActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
|
||||
<receiver android:name="com.gh.gamecenter.receiver.InstallAndUninstallReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.InstallActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.LoginActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.UserInfoActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.UserRegionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.CollectionActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.MessageActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.UserInfoEditActivity"
|
||||
android:screenOrientation = "portrait"
|
||||
android:windowSoftInputMode = "stateHidden" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.KaiFuActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.NormalActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<activity
|
||||
android:name = "com.gh.gamecenter.SkipActivity"
|
||||
android:theme = "@style/Theme.AppCompat.Light.Fullscreen.Transparent" >
|
||||
<intent-filter >
|
||||
<data android:scheme = "ghzhushou" />
|
||||
|
||||
<category android:name = "android.intent.category.DEFAULT" />
|
||||
<action android:name = "android.intent.action.VIEW" />
|
||||
<category android:name = "android.intent.category.BROWSABLE" />
|
||||
</intent-filter >
|
||||
</activity >
|
||||
|
||||
<activity
|
||||
android:name = ".CommonActivity"
|
||||
android:screenOrientation = "portrait" />
|
||||
|
||||
<receiver android:name = "com.gh.gamecenter.receiver.InstallAndUninstallReceiver" >
|
||||
<intent-filter >
|
||||
<action android:name = "android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name = "android.intent.action.PACKAGE_REMOVED" />
|
||||
<action android:name = "android.intent.action.PACKAGE_REPLACED" />
|
||||
|
||||
<data android:scheme = "package" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
<receiver
|
||||
android:name="com.gh.gamecenter.receiver.NotificationReceiver"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.NOTIFICATION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
android:name = "com.gh.gamecenter.receiver.NotificationReceiver"
|
||||
android:exported = "false" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.NOTIFICATION" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
<receiver
|
||||
android:name="com.gh.gamecenter.receiver.UninstallReceiver"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="com.gh.gamecenter.UNINSTALL" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name="com.gh.gamecenter.receiver.NetworkStateReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
android:name = "com.gh.gamecenter.receiver.DownloadReceiver"
|
||||
android:exported = "false" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.DOWNLOAD" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
<receiver
|
||||
android:name="com.xiaomi.push.service.receivers.PingReceiver"
|
||||
android:exported="false"
|
||||
android:process=":pushservice" >
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.push.PING_TIMER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="com.gh.base.GHPushMessageReceiver"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.mipush.ERROR" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
android:name = "com.gh.gamecenter.receiver.InstallReceiver"
|
||||
android:exported = "false" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.INSTALL" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
<receiver android:name = "com.gh.gamecenter.receiver.NetworkStateReceiver" >
|
||||
<intent-filter >
|
||||
<action android:name = "android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name="com.xiaomi.push.service.receivers.PingReceiver"
|
||||
android:exported="false"
|
||||
android:process=":pushservice" >
|
||||
<intent-filter>
|
||||
<action android:name="com.xiaomi.push.PING_TIMER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
android:name = "com.gh.gamecenter.receiver.ActivitySkipReceiver"
|
||||
android:exported = "true" >
|
||||
<intent-filter >
|
||||
<action android:name = "com.gh.gamecenter.ACTIVITYSKIP" />
|
||||
</intent-filter >
|
||||
</receiver >
|
||||
|
||||
<service android:name="com.gh.download.DownloadService" />
|
||||
<service
|
||||
android:name="com.xiaomi.push.service.XMPushService"
|
||||
android:enabled="true"
|
||||
android:process=":pushservice" />
|
||||
<service
|
||||
android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
|
||||
android:enabled="true"
|
||||
android:exported="true" />
|
||||
<service
|
||||
android:name="com.xiaomi.mipush.sdk.MessageHandleService"
|
||||
android:enabled="true" />
|
||||
</application>
|
||||
<!--<service android:name = "com.gh.gamecenter.statistics.AppStaticService" />-->
|
||||
|
||||
</manifest>
|
||||
</application >
|
||||
|
||||
</manifest >
|
||||
69
app/src/main/assets/Home.html
Normal file
69
app/src/main/assets/Home.html
Normal file
@ -0,0 +1,69 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>光环助手</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<style>
|
||||
body {
|
||||
font: 100%/1.0 'Microsoft YaHei','Helvetica Neue',Helvetica,Arial,sans-serif;
|
||||
background-color: #fff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
}
|
||||
|
||||
article {
|
||||
width:100%;
|
||||
max-width:720px;
|
||||
clear: both;
|
||||
margin: 0 auto;
|
||||
margin-top: 20%;
|
||||
text-align: center;
|
||||
margin-bottom:20%;
|
||||
}
|
||||
.title{margin-top: 4%;font-size:1.7em;color:#191919;text-align:center;}
|
||||
.info{margin-top: 18%;font-size:1.0em;color:#191919;line-height:1.3em;}
|
||||
.download {text-align: center;}
|
||||
.download a{font-size:1.8em;padding:0.2em; text-align:center;color:#ffffff;margin: 0 auto;width:56%;background-color:#2999f9;border-radius:8px; text-decoration:none;display:block;line-height:1.8em;}
|
||||
|
||||
@media only screen and (min-width: 1080px) {
|
||||
article {
|
||||
width:100%;
|
||||
max-width:720px;
|
||||
clear: both;
|
||||
margin: 0 auto;
|
||||
margin-top: 5%;
|
||||
text-align: center;
|
||||
margin-bottom:20%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
<img src="http://192.168.43.1:3100/image/gh_icon.png" width="28%">
|
||||
<p class="title">光环助手</p>
|
||||
<br class="info">乐于分享的人是最帅的^_^ </p>
|
||||
<div class="download">
|
||||
<a href="http://192.168.43.1:3100/download/ghzs.apk">免流量下载</a>
|
||||
</div>
|
||||
<p class="title"><font color="#9A9A9A" size="3em">仅限安卓系统 </font></p>
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
1365
app/src/main/assets/LocList.txt
Normal file
1365
app/src/main/assets/LocList.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,129 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<DevInfor>
|
||||
<!--
|
||||
说明:
|
||||
|
||||
1、表格中的第一项
|
||||
<ShareSDK
|
||||
AppKey="api20" />
|
||||
是必须的,其中的AppKey是您在ShareSDK上注册的开发者帐号的AppKey
|
||||
|
||||
2、所有集成到您项目的平台都应该为其在表格中填写相对应的开发者信息,以新浪微博为例:
|
||||
<SinaWeibo
|
||||
Id="1"
|
||||
SortId="1"
|
||||
AppKey="568898243"
|
||||
AppSecret="38a4f8204cc784f81f9f0daaf31e02e3"
|
||||
RedirectUrl="http://www.mob.com"
|
||||
Enable="true" />
|
||||
其中的SortId是此平台在分享列表中的位置,由开发者自行定义,可以是任何整型数字,数值越大
|
||||
越靠后AppKey、AppSecret和RedirectUrl是您在新浪微博上注册开发者信息和应用后得到的信息
|
||||
Id是一个保留的识别符,整型,ShareSDK不使用此字段,供您在自己的项目中当作平台的识别符。
|
||||
Enable字段表示此平台是否有效,布尔值,默认为true,如果Enable为false,即便平台的jar包
|
||||
已经添加到应用中,平台实例依然不可获取。
|
||||
|
||||
各个平台注册应用信息的地址如下:
|
||||
新浪微博 http://open.weibo.com
|
||||
腾讯微博 http://dev.t.qq.com
|
||||
QQ空间 http://connect.qq.com/intro/login/
|
||||
微信好友 http://open.weixin.qq.com
|
||||
Facebook https://developers.facebook.com
|
||||
Twitter https://dev.twitter.com
|
||||
人人网 http://dev.renren.com
|
||||
开心网 http://open.kaixin001.com
|
||||
搜狐微博 http://open.t.sohu.com
|
||||
网易微博 http://open.t.163.com
|
||||
豆瓣 http://developers.douban.com
|
||||
|
||||
有道云笔记 http://note.youdao.com/open/developguide.html#app
|
||||
印象笔记 https://dev.evernote.com/
|
||||
Linkedin https://developer.linkedin.com
|
||||
FourSquare https://developer.foursquare.com/
|
||||
搜狐随身看 https://open.sohu.com/
|
||||
Flickr http://www.flickr.com/services/
|
||||
Pinterest http://developers.pinterest.com/
|
||||
Tumblr http://www.tumblr.com/developers
|
||||
Dropbox https://www.dropbox.com/developers
|
||||
Instagram http://instagram.com/developer#
|
||||
VKontakte http://vk.com/dev
|
||||
易信好友 http://open.yixin.im/
|
||||
明道 http://open.mingdao.com/
|
||||
Line http://media.line.me/zh-hant/
|
||||
Pocket http://getpocket.com/developer/apps/new
|
||||
-->
|
||||
|
||||
<ShareSDK
|
||||
AppKey = "6f286c8a261a"/> <!-- 修改成你在sharesdk后台注册的应用的appkey"-->
|
||||
|
||||
<!-- ShareByAppClient标识是否使用微博客户端分享,默认是false -->
|
||||
<SinaWeibo
|
||||
Id="1"
|
||||
SortId="1"
|
||||
AppKey="568898243"
|
||||
AppSecret="38a4f8204cc784f81f9f0daaf31e02e3"
|
||||
RedirectUrl="http://www.sharesdk.cn"
|
||||
ShareByAppClient="false"
|
||||
Enable="true" />
|
||||
|
||||
<TencentWeibo
|
||||
Id="2"
|
||||
SortId="2"
|
||||
AppKey="801307650"
|
||||
AppSecret="ae36f4ee3946e1cbb98d6965b0b2ff5c"
|
||||
RedirectUri="http://sharesdk.cn"
|
||||
Enable="true" />
|
||||
|
||||
<!-- ShareByAppClient标识是否使用微博客户端分享,默认是false -->
|
||||
<QZone
|
||||
Id="3"
|
||||
SortId="3"
|
||||
AppId="1104659243"
|
||||
AppKey="OfjHS7bWyxPiH0t8"
|
||||
ShareByAppClient="true"
|
||||
Enable="true" />
|
||||
|
||||
<!--
|
||||
Wechat微信和WechatMoments微信朋友圈的appid是一样的;
|
||||
|
||||
注意:开发者不能用我们这两个平台的appid,否则分享不了
|
||||
|
||||
微信测试的时候,微信测试需要先签名打包出apk,
|
||||
sample测试微信,要先签名打包,keystore在sample项目中,密码123456
|
||||
|
||||
BypassApproval是绕过审核的标记,设置为true后AppId将被忽略,故不经过
|
||||
审核的应用也可以执行分享,但是仅限于分享文字和图片,不能分享其他类型,
|
||||
默认值为false。此外,微信收藏不支持此字段。
|
||||
-->
|
||||
<Wechat
|
||||
Id="4"
|
||||
SortId="4"
|
||||
AppId="wx4868b35061f87885"
|
||||
AppSecret="64020361b8ec4c99936c0e3999a9f249"
|
||||
BypassApproval="true"
|
||||
Enable="true" />
|
||||
|
||||
<WechatMoments
|
||||
Id="5"
|
||||
SortId="5"
|
||||
AppId="wx4868b35061f87885"
|
||||
AppSecret="64020361b8ec4c99936c0e3999a9f249"
|
||||
BypassApproval="true"
|
||||
Enable="true" />
|
||||
|
||||
<WechatFavorite
|
||||
Id="6"
|
||||
SortId="6"
|
||||
AppId="wx4868b35061f87885"
|
||||
AppSecret="64020361b8ec4c99936c0e3999a9f249"
|
||||
Enable="true" />
|
||||
|
||||
<!-- ShareByAppClient标识是否使用微博客户端分享,默认是false -->
|
||||
<QQ
|
||||
Id="7"
|
||||
SortId="7"
|
||||
AppId="1104659243"
|
||||
AppKey="OfjHS7bWyxPiH0t8"
|
||||
ShareByAppClient="true"
|
||||
Enable="true" />
|
||||
|
||||
</DevInfor>
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -1,30 +1,149 @@
|
||||
9u=\u4E5D\u6E38\u7248
|
||||
360=360\u7248
|
||||
baidu=\u767E\u5EA6\u7248
|
||||
dangle=\u5F53\u4E50\u7248
|
||||
ouwan=\u5076\u73A9\u7248
|
||||
gf=\u5B98\u65B9\u7248
|
||||
mi=\u5C0F\u7C73\u7248
|
||||
oppo=OPPO\u7248
|
||||
91=91\u7248
|
||||
wdj=\u8C4C\u8C46\u835A\u7248
|
||||
vivo=VIVO\u7248
|
||||
pps=PPS\u7248
|
||||
37wan=37\u73A9\u7248
|
||||
anzhi=\u5B89\u667A\u7248
|
||||
ewan=\u76CA\u73A9\u7248
|
||||
huawei=\u534E\u4E3A\u7248
|
||||
gf-h=\u5B98\u65B9-\u4E13\u670D\u7248
|
||||
gf-z=\u5B98\u65B9-\u6DF7\u670D\u7248
|
||||
shuizhu=\u6C34\u716E\u7248
|
||||
jifeng=\u673A\u950B\u7248
|
||||
azsc=\u5B89\u5353\u5E02\u573A\u7248
|
||||
lenvono=\u8054\u60F3\u7248
|
||||
jinshan=\u91D1\u5C71\u7248
|
||||
mumayi=\u6728\u8682\u8681\u7248
|
||||
gf-n=\u5B98\u7F51-\u5185\u7248
|
||||
gf-w=\u5B98\u7F51-\u5916\u7248
|
||||
duoku=\u591A\u9177\u7248
|
||||
pptv=PPTV\u7248
|
||||
\u5B98\u65B9\u7248=\u5B98\u65B9\u7248
|
||||
\u5B98\u65B9=\u5B98\u65B9\u7248
|
||||
9u=\u4e5d\u6e38
|
||||
4399=\u0034\u0033\u0039\u0039
|
||||
yyb=\u5e94\u7528\u5b9d
|
||||
xm=\u5c0f\u7c73
|
||||
dl=\u5f53\u4e50
|
||||
91=\u0039\u0031
|
||||
yw=\u76ca\u73a9
|
||||
gf-w=\u5b98\u65b9\u5916
|
||||
az=\u5b89\u667a
|
||||
oppo=\u006f\u0070\u0070\u006f
|
||||
wdj=\u8c4c\u8c46\u835a
|
||||
360=\u0033\u0036\u0030
|
||||
vivo=\u0076\u0069\u0076\u006f
|
||||
pps=\u0050\u0050\u0053
|
||||
hw=\u534e\u4e3a
|
||||
37=\u0033\u0037\u73a9
|
||||
baidu=\u767e\u5ea6
|
||||
ow=\u5076\u73a9
|
||||
gf=\u5b98\u65b9
|
||||
lenovo=\u8054\u60f3
|
||||
pptv=\u0050\u0050\u0054\u0056
|
||||
jf=\u673a\u950b
|
||||
mumayi=\u6728\u8682\u8681
|
||||
8868=\u0038\u0038\u0036\u0038
|
||||
19196=\u0031\u0039\u0031\u0039\u0036
|
||||
07073=\u0030\u0037\u0030\u0037\u0033
|
||||
gp=\u679c\u76d8
|
||||
mzw=\u62c7\u6307\u73a9
|
||||
af=\u5b89\u950b
|
||||
lb=\u730e\u5b9d
|
||||
ayx=\u963f\u6e38\u620f
|
||||
tt=\u0054\u0054
|
||||
xiongmao=\u718a\u732b\u73a9
|
||||
aq=\u5b89\u8da3
|
||||
ls=\u4e50\u89c6
|
||||
jl=\u91d1\u7acb
|
||||
lehh=\u4e50\u55e8\u55e8
|
||||
pyw=\u670b\u53cb\u73a9
|
||||
azsc=\u5b89\u5353\u5e02\u573a
|
||||
hyx=\u548c\u6e38\u620f
|
||||
aiyouxi=\u7231\u6e38\u620f
|
||||
woyouxi=\u6c83\u6e38\u620f
|
||||
lg=\u4e50\u8d2d\u0028\u70b9\u70b9\u0029
|
||||
gf-h=\u0028\u65e7\u0029\u5b98\u65b9\u4e13\u670d
|
||||
gf-z=\u0028\u65e7\u0029\u5b98\u65b9\u6df7\u670d
|
||||
gfzf=\u5b98\u65b9\u4e13\u670d
|
||||
gfhf=\u5b98\u65b9\u6df7\u670d
|
||||
owzf=\u5076\u73a9\u4e13\u670d
|
||||
gf-n=\u5b98\u65b9\u5185
|
||||
my=\u9b54\u9047
|
||||
jinshan=\u91d1\u5c71
|
||||
duoku=\u591a\u9177
|
||||
yk=\u4f18\u9177
|
||||
xl=\u65b0\u6d6a
|
||||
sougou=\u641c\u72d7
|
||||
dy=\u6597\u9c7c
|
||||
dw=\u591a\u73a9
|
||||
tf=\u53f0\u670d
|
||||
xunlei=\u8fc5\u96f7
|
||||
ky=\u5feb\u7528
|
||||
lenvono=\u0028\u65e7\u0029\u8054\u60f3
|
||||
jifeng=\u0028\u65e7\u0029\u673a\u950b
|
||||
anzhi=\u0028\u65e7\u0029\u5b89\u667a
|
||||
37wan=\u0028\u65e7\u0029\u0033\u0037\u73a9
|
||||
ewan=\u0028\u65e7\u0029\u76ca\u73a9
|
||||
mi=\u0028\u65e7\u0029\u5c0f\u7c73
|
||||
dangle=\u0028\u65e7\u0029\u5f53\u4e50
|
||||
ouwan=\u0028\u65e7\u0029\u5076\u73a9
|
||||
ttyy=\u0028\u65e7\u0029\u0054\u0054
|
||||
huawei=\u0028\u65e7\u0029\u534e\u4e3a
|
||||
shuizhu=\u0028\u65e7\u0029\u6c34\u716e
|
||||
43997=\u0034\u0033\u0039\u0039\u0037
|
||||
19196zf=\u0031\u0039\u0031\u0039\u0036\u4e13\u670d
|
||||
51508=\u0035\u0031\u0035\u0030\u0038
|
||||
zzb=\u81f3\u5c0a\u5b9d
|
||||
wkd=\u73a9\u5ba2\u7248
|
||||
yq=\u4f18\u8da3
|
||||
tiantian=\u5929\u5929
|
||||
ddw=\u70b9\u70b9\u73a9
|
||||
by=\u7206\u6e38
|
||||
as=\u7231\u4e0a
|
||||
flb=\u5c0f\u7b28\u6e38\u620f
|
||||
cf=\u695a\u98ce
|
||||
itools=\u0049\u0054\u004f\u004f\u004c\u0053
|
||||
ayw=\u7231\u7ea6\u73a9
|
||||
cc=\u866b\u866b
|
||||
kpzs=\u9760\u8c31\u52a9\u624b
|
||||
xtt=\u65b0\u0074\u0074
|
||||
yt=\u6e38\u9014\u7248
|
||||
9665=\u0039\u0036\u0036\u0035
|
||||
lehhkf=\u4e50\u55e8\u55e8\u006b\u0066
|
||||
lehhol=\u4e50\u55e8\u55e8\u006f\u006c
|
||||
afly=\u5b89\u950b\u006c\u0079
|
||||
360hf=\u0033\u0036\u0030\u6df7\u670d\u7248
|
||||
qql=\u9f50\u9f50\u4e50
|
||||
yehuo=\u91ce\u706b\u7248
|
||||
jizhi=\u6781\u81f4\u7248
|
||||
shengxun=\u76db\u8baf\u6e38\u620f
|
||||
9upt=\u4e5d\u6e38\u666e\u901a\u7248
|
||||
9ujs=\u4e5d\u6e38\u52a0\u901f\u7248
|
||||
xmpt=\u5c0f\u7c73\u666e\u901a\u7248
|
||||
xmjs=\u5c0f\u7c73\u52a0\u901f\u7248
|
||||
gfpt=\u5b98\u65b9\u666e\u901a\u7248
|
||||
gfjs=\u5b98\u65b9\u52a0\u901f\u7248
|
||||
wkd1=\u73a9\u5ba2\u65b0\u7248
|
||||
5288=\u0035\u0032\u0038\u0038
|
||||
7guo=\u4e03\u679c
|
||||
zhanyou=\u5c55\u6e38\u7248
|
||||
babie=\u5df4\u522b\u65f6\u4ee3\u7248
|
||||
yueyou=\u7ea6\u6e38\u7248
|
||||
kuniu=\u9177\u725b\u7248
|
||||
chukong=\u89e6\u63a7\u7248
|
||||
changxiang=\u7545\u60f3\u7248
|
||||
zhuohua=\u707c\u534e\u7248
|
||||
duokemeng=\u54c6\u53ef\u68a6\u7248
|
||||
xuanyun=\u7384\u4e91\u7248
|
||||
hanqu=\u701a\u8da3\u7248
|
||||
wapu=\u86d9\u6251\u7248
|
||||
shengli=\u80dc\u5229\u6e38\u620f\u7248
|
||||
heitao=\u9ed1\u6843\u4e92\u52a8\u7248
|
||||
xumei=\u65ed\u6885\u6e38\u620f\u7248
|
||||
weixun=\u5fae\u8baf\u7248
|
||||
tianxiang=\u5929\u8c61\u4e92\u52a8\u7248
|
||||
taiqi=\u6cf0\u5947\u7248
|
||||
chujian=\u521d\u89c1\u7248
|
||||
gaiya=\u76d6\u5a05\u7248
|
||||
wanmei=\u5b8c\u7f8e\u4e16\u754c\u7248
|
||||
zhuoyue=\u5353\u8d8a\u7248
|
||||
meifeng=\u7f8e\u5cf0\u7248
|
||||
xuanji=\u7384\u673a\u7248
|
||||
changyou=\u7545\u6e38\u7248
|
||||
syg=\u624b\u6e38\u72d7
|
||||
youzu=\u6e38\u65cf\u7248
|
||||
bili=\u0062\u0069\u006c\u0069\u0062\u0069\u006c\u0069\u7248
|
||||
ly=\u4e50\u6e38
|
||||
gfwy=\u7f51\u9875\u7248
|
||||
miqi=\u7c73\u5947\u73a9
|
||||
ayw=\u6e38\u620f\u0066\u0061\u006e
|
||||
ys=\u591c\u795e\u7248
|
||||
aofei=\u5965\u98de
|
||||
mgw=\u8611\u83c7\u73a9
|
||||
longc=\u9f99\u57ce\u7248
|
||||
16y=\u0031\u0036\u6e38
|
||||
xq=\u5c0f\u4e03
|
||||
yuwan=\u9c7c\u4e38\u7248
|
||||
jgp=\u679c\u76d8\u65e7
|
||||
lequ=\u6dd8\u8da3
|
||||
jianguo=\u575a\u679c\u7248
|
||||
yanmeng=\u5ef6\u68a6\u7248
|
||||
@ -1,697 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.support.v4.util.Pools;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
|
||||
/**
|
||||
* Helper class that can enqueue and process adapter update operations.
|
||||
* <p>
|
||||
* To support animations, RecyclerView presents an older version the Adapter to best represent
|
||||
* previous state of the layout. Sometimes, this is not trivial when items are removed that were
|
||||
* not laid out, in which case, RecyclerView has no way of providing that item's view for
|
||||
* animations.
|
||||
* <p>
|
||||
* AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
|
||||
* pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
|
||||
* and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
|
||||
* according to previously deferred operation and dispatch them before the first layout pass. It
|
||||
* also takes care of updating deferred UpdateOps since order of operations is changed by this
|
||||
* process.
|
||||
* <p>
|
||||
* Although operations may be forwarded to LayoutManager in different orders, resulting data set
|
||||
* is guaranteed to be the consistent.
|
||||
*/
|
||||
class AdapterHelper implements OpReorderer.Callback {
|
||||
|
||||
final static int POSITION_TYPE_INVISIBLE = 0;
|
||||
|
||||
final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String TAG = "AHT";
|
||||
|
||||
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
|
||||
|
||||
final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
|
||||
|
||||
final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
Runnable mOnItemProcessedCallback;
|
||||
|
||||
final boolean mDisableRecycler;
|
||||
|
||||
final OpReorderer mOpReorderer;
|
||||
|
||||
AdapterHelper(Callback callback) {
|
||||
this(callback, false);
|
||||
}
|
||||
|
||||
AdapterHelper(Callback callback, boolean disableRecycler) {
|
||||
mCallback = callback;
|
||||
mDisableRecycler = disableRecycler;
|
||||
mOpReorderer = new OpReorderer(this);
|
||||
}
|
||||
|
||||
AdapterHelper addUpdateOp(UpdateOp... ops) {
|
||||
Collections.addAll(mPendingUpdates, ops);
|
||||
return this;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
recycleUpdateOpsAndClearList(mPendingUpdates);
|
||||
recycleUpdateOpsAndClearList(mPostponedList);
|
||||
}
|
||||
|
||||
void preProcess() {
|
||||
mOpReorderer.reorderOps(mPendingUpdates);
|
||||
final int count = mPendingUpdates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
applyAdd(op);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
applyRemove(op);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
applyUpdate(op);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
applyMove(op);
|
||||
break;
|
||||
}
|
||||
if (mOnItemProcessedCallback != null) {
|
||||
mOnItemProcessedCallback.run();
|
||||
}
|
||||
}
|
||||
mPendingUpdates.clear();
|
||||
}
|
||||
|
||||
void consumePostponedUpdates() {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
mCallback.onDispatchSecondPass(mPostponedList.get(i));
|
||||
}
|
||||
recycleUpdateOpsAndClearList(mPostponedList);
|
||||
}
|
||||
|
||||
private void applyMove(UpdateOp op) {
|
||||
// MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
|
||||
// otherwise, it would be converted into a REMOVE operation
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
|
||||
private void applyRemove(UpdateOp op) {
|
||||
int tmpStart = op.positionStart;
|
||||
int tmpCount = 0;
|
||||
int tmpEnd = op.positionStart + op.itemCount;
|
||||
int type = -1;
|
||||
for (int position = op.positionStart; position < tmpEnd; position++) {
|
||||
boolean typeChanged = false;
|
||||
ViewHolder vh = mCallback.findViewHolder(position);
|
||||
if (vh != null || canFindInPreLayout(position)) {
|
||||
// If a ViewHolder exists or this is a newly added item, we can defer this update
|
||||
// to post layout stage.
|
||||
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
|
||||
// * For items that are added and removed in the same process cycle, they won't
|
||||
// have any effect in pre-layout since their add ops are already deferred to
|
||||
// post-layout pass.
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
// Looks like we have other updates that we cannot merge with this one.
|
||||
// Create an UpdateOp and dispatch it to LayoutManager.
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
dispatchAndUpdateViewHolders(newOp);
|
||||
typeChanged = true;
|
||||
}
|
||||
type = POSITION_TYPE_NEW_OR_LAID_OUT;
|
||||
} else {
|
||||
// This update cannot be recovered because we don't have a ViewHolder representing
|
||||
// this position. Instead, post it to LayoutManager immediately
|
||||
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
|
||||
// Looks like we have other updates that we cannot merge with this one.
|
||||
// Create UpdateOp op and dispatch it to LayoutManager.
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
postponeAndUpdateViewHolders(newOp);
|
||||
typeChanged = true;
|
||||
}
|
||||
type = POSITION_TYPE_INVISIBLE;
|
||||
}
|
||||
if (typeChanged) {
|
||||
position -= tmpCount; // also equal to tmpStart
|
||||
tmpEnd -= tmpCount;
|
||||
tmpCount = 1;
|
||||
} else {
|
||||
tmpCount++;
|
||||
}
|
||||
}
|
||||
if (tmpCount != op.itemCount) { // all 1 effect
|
||||
recycleUpdateOp(op);
|
||||
op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
}
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
dispatchAndUpdateViewHolders(op);
|
||||
} else {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyUpdate(UpdateOp op) {
|
||||
int tmpStart = op.positionStart;
|
||||
int tmpCount = 0;
|
||||
int tmpEnd = op.positionStart + op.itemCount;
|
||||
int type = -1;
|
||||
for (int position = op.positionStart; position < tmpEnd; position++) {
|
||||
ViewHolder vh = mCallback.findViewHolder(position);
|
||||
if (vh != null || canFindInPreLayout(position)) { // deferred
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
dispatchAndUpdateViewHolders(newOp);
|
||||
tmpCount = 0;
|
||||
tmpStart = position;
|
||||
}
|
||||
type = POSITION_TYPE_NEW_OR_LAID_OUT;
|
||||
} else { // applied
|
||||
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
postponeAndUpdateViewHolders(newOp);
|
||||
tmpCount = 0;
|
||||
tmpStart = position;
|
||||
}
|
||||
type = POSITION_TYPE_INVISIBLE;
|
||||
}
|
||||
tmpCount++;
|
||||
}
|
||||
if (tmpCount != op.itemCount) { // all 1 effect
|
||||
recycleUpdateOp(op);
|
||||
op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
}
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
dispatchAndUpdateViewHolders(op);
|
||||
} else {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchAndUpdateViewHolders(UpdateOp op) {
|
||||
// tricky part.
|
||||
// traverse all postpones and revert their changes on this op if necessary, apply updated
|
||||
// dispatch to them since now they are after this op.
|
||||
if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
|
||||
throw new IllegalArgumentException("should not dispatch add or move for pre layout");
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatch (pre)" + op);
|
||||
Log.d(TAG, "postponed state before:");
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
|
||||
// handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
|
||||
// TODO Since move ops are pushed to end, we should not need this anymore
|
||||
int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
|
||||
}
|
||||
int tmpCnt = 1;
|
||||
int offsetPositionForPartial = op.positionStart;
|
||||
final int positionMultiplier;
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.UPDATE:
|
||||
positionMultiplier = 1;
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
positionMultiplier = 0;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("op should be remove or update." + op);
|
||||
}
|
||||
for (int p = 1; p < op.itemCount; p++) {
|
||||
final int pos = op.positionStart + (positionMultiplier * p);
|
||||
int updatedPos = updatePositionWithPostponed(pos, op.cmd);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
|
||||
}
|
||||
boolean continuous = false;
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.UPDATE:
|
||||
continuous = updatedPos == tmpStart + 1;
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
continuous = updatedPos == tmpStart;
|
||||
break;
|
||||
}
|
||||
if (continuous) {
|
||||
tmpCnt++;
|
||||
} else {
|
||||
// need to dispatch this separately
|
||||
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "need to dispatch separately " + tmp);
|
||||
}
|
||||
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
|
||||
recycleUpdateOp(tmp);
|
||||
if (op.cmd == UpdateOp.UPDATE) {
|
||||
offsetPositionForPartial += tmpCnt;
|
||||
}
|
||||
tmpStart = updatedPos;// need to remove previously dispatched
|
||||
tmpCnt = 1;
|
||||
}
|
||||
}
|
||||
recycleUpdateOp(op);
|
||||
if (tmpCnt > 0) {
|
||||
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatching:" + tmp);
|
||||
}
|
||||
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
|
||||
recycleUpdateOp(tmp);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "post dispatch");
|
||||
Log.d(TAG, "postponed state after:");
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
|
||||
mCallback.onDispatchFirstPass(op);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("only remove and update ops can be dispatched"
|
||||
+ " in first pass");
|
||||
}
|
||||
}
|
||||
|
||||
private int updatePositionWithPostponed(int pos, int cmd) {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
UpdateOp postponed = mPostponedList.get(i);
|
||||
if (postponed.cmd == UpdateOp.MOVE) {
|
||||
int start, end;
|
||||
if (postponed.positionStart < postponed.itemCount) {
|
||||
start = postponed.positionStart;
|
||||
end = postponed.itemCount;
|
||||
} else {
|
||||
start = postponed.itemCount;
|
||||
end = postponed.positionStart;
|
||||
}
|
||||
if (pos >= start && pos <= end) {
|
||||
//i'm affected
|
||||
if (start == postponed.positionStart) {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.itemCount++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.itemCount--;
|
||||
}
|
||||
// op moved to left, move it right to revert
|
||||
pos++;
|
||||
} else {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
}
|
||||
// op was moved right, move left to revert
|
||||
pos--;
|
||||
}
|
||||
} else if (pos < postponed.positionStart) {
|
||||
// postponed MV is outside the dispatched OP. if it is before, offset
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
postponed.itemCount++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
postponed.itemCount--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (postponed.positionStart <= pos) {
|
||||
if (postponed.cmd == UpdateOp.ADD) {
|
||||
pos -= postponed.itemCount;
|
||||
} else if (postponed.cmd == UpdateOp.REMOVE) {
|
||||
pos += postponed.itemCount;
|
||||
}
|
||||
} else {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispath (step" + i + ")");
|
||||
Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
}
|
||||
for (int i = mPostponedList.size() - 1; i >= 0; i--) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (op.itemCount == op.positionStart || op.itemCount < 0) {
|
||||
mPostponedList.remove(i);
|
||||
recycleUpdateOp(op);
|
||||
}
|
||||
} else if (op.itemCount <= 0) {
|
||||
mPostponedList.remove(i);
|
||||
recycleUpdateOp(op);
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private boolean canFindInPreLayout(int position) {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (findPositionOffset(op.itemCount, i + 1) == position) {
|
||||
return true;
|
||||
}
|
||||
} else if (op.cmd == UpdateOp.ADD) {
|
||||
// TODO optimize.
|
||||
final int end = op.positionStart + op.itemCount;
|
||||
for (int pos = op.positionStart; pos < end; pos++) {
|
||||
if (findPositionOffset(pos, i + 1) == position) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void applyAdd(UpdateOp op) {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
|
||||
private void postponeAndUpdateViewHolders(UpdateOp op) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "postponing " + op);
|
||||
}
|
||||
mPostponedList.add(op);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
|
||||
op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown update op type for " + op);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasPendingUpdates() {
|
||||
return mPendingUpdates.size() > 0;
|
||||
}
|
||||
|
||||
int findPositionOffset(int position) {
|
||||
return findPositionOffset(position, 0);
|
||||
}
|
||||
|
||||
int findPositionOffset(int position, int firstPostponedItem) {
|
||||
int count = mPostponedList.size();
|
||||
for (int i = firstPostponedItem; i < count; ++i) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (op.positionStart == position) {
|
||||
position = op.itemCount;
|
||||
} else {
|
||||
if (op.positionStart < position) {
|
||||
position--; // like a remove
|
||||
}
|
||||
if (op.itemCount <= position) {
|
||||
position++; // like an add
|
||||
}
|
||||
}
|
||||
} else if (op.positionStart <= position) {
|
||||
if (op.cmd == UpdateOp.REMOVE) {
|
||||
if (position < op.positionStart + op.itemCount) {
|
||||
return -1;
|
||||
}
|
||||
position -= op.itemCount;
|
||||
} else if (op.cmd == UpdateOp.ADD) {
|
||||
position += op.itemCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeChanged(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeInserted(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeMoved(int from, int to, int itemCount) {
|
||||
if (from == to) {
|
||||
return false;//no-op
|
||||
}
|
||||
if (itemCount != 1) {
|
||||
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
|
||||
}
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips pre-processing and applies all updates in one pass.
|
||||
*/
|
||||
void consumeUpdatesInOnePass() {
|
||||
// we still consume postponed updates (if there is) in case there was a pre-process call
|
||||
// w/o a matching consumePostponedUpdates.
|
||||
consumePostponedUpdates();
|
||||
final int count = mPendingUpdates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
|
||||
break;
|
||||
}
|
||||
if (mOnItemProcessedCallback != null) {
|
||||
mOnItemProcessedCallback.run();
|
||||
}
|
||||
}
|
||||
recycleUpdateOpsAndClearList(mPendingUpdates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queued operation to happen when child views are updated.
|
||||
*/
|
||||
static class UpdateOp {
|
||||
|
||||
static final int ADD = 0;
|
||||
|
||||
static final int REMOVE = 1;
|
||||
|
||||
static final int UPDATE = 2;
|
||||
|
||||
static final int MOVE = 3;
|
||||
|
||||
static final int POOL_SIZE = 30;
|
||||
|
||||
int cmd;
|
||||
|
||||
int positionStart;
|
||||
|
||||
// holds the target position if this is a MOVE
|
||||
int itemCount;
|
||||
|
||||
UpdateOp(int cmd, int positionStart, int itemCount) {
|
||||
this.cmd = cmd;
|
||||
this.positionStart = positionStart;
|
||||
this.itemCount = itemCount;
|
||||
}
|
||||
|
||||
String cmdToString() {
|
||||
switch (cmd) {
|
||||
case ADD:
|
||||
return "add";
|
||||
case REMOVE:
|
||||
return "rm";
|
||||
case UPDATE:
|
||||
return "up";
|
||||
case MOVE:
|
||||
return "mv";
|
||||
}
|
||||
return "??";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateOp op = (UpdateOp) o;
|
||||
|
||||
if (cmd != op.cmd) {
|
||||
return false;
|
||||
}
|
||||
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
|
||||
// reverse of this is also true
|
||||
if (itemCount == op.positionStart && positionStart == op.itemCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (itemCount != op.itemCount) {
|
||||
return false;
|
||||
}
|
||||
if (positionStart != op.positionStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = cmd;
|
||||
result = 31 * result + positionStart;
|
||||
result = 31 * result + itemCount;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
|
||||
UpdateOp op = mUpdateOpPool.acquire();
|
||||
if (op == null) {
|
||||
op = new UpdateOp(cmd, positionStart, itemCount);
|
||||
} else {
|
||||
op.cmd = cmd;
|
||||
op.positionStart = positionStart;
|
||||
op.itemCount = itemCount;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycleUpdateOp(UpdateOp op) {
|
||||
if (!mDisableRecycler) {
|
||||
mUpdateOpPool.release(op);
|
||||
}
|
||||
}
|
||||
|
||||
void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
|
||||
final int count = ops.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
recycleUpdateOp(ops.get(i));
|
||||
}
|
||||
ops.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract between AdapterHelper and RecyclerView.
|
||||
*/
|
||||
static interface Callback {
|
||||
|
||||
ViewHolder findViewHolder(int position);
|
||||
|
||||
void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
|
||||
|
||||
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
|
||||
|
||||
void markViewHoldersUpdated(int positionStart, int itemCount);
|
||||
|
||||
void onDispatchFirstPass(UpdateOp updateOp);
|
||||
|
||||
void onDispatchSecondPass(UpdateOp updateOp);
|
||||
|
||||
void offsetPositionsForAdd(int positionStart, int itemCount);
|
||||
|
||||
void offsetPositionsForMove(int from, int to);
|
||||
}
|
||||
}
|
||||
@ -1,484 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to manage children.
|
||||
* <p>
|
||||
* It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
|
||||
* provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
|
||||
* like getChildAt, getChildCount etc. These methods ignore hidden children.
|
||||
* <p>
|
||||
* When RecyclerView needs direct access to the view group children, it can call unfiltered
|
||||
* methods like get getUnfilteredChildCount or getUnfilteredChildAt.
|
||||
*/
|
||||
class ChildHelper {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String TAG = "ChildrenHelper";
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
final Bucket mBucket;
|
||||
|
||||
final List<View> mHiddenViews;
|
||||
|
||||
ChildHelper(Callback callback) {
|
||||
mCallback = callback;
|
||||
mBucket = new Bucket();
|
||||
mHiddenViews = new ArrayList<View>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a view to the ViewGroup
|
||||
*
|
||||
* @param child View to add.
|
||||
* @param hidden If set to true, this item will be invisible from regular methods.
|
||||
*/
|
||||
void addView(View child, boolean hidden) {
|
||||
addView(child, -1, hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a view to the ViewGroup at an index
|
||||
*
|
||||
* @param child View to add.
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
* @param hidden If set to true, this item will be invisible from regular methods.
|
||||
*/
|
||||
void addView(View child, int index, boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mCallback.addView(child, offset);
|
||||
mBucket.insert(offset, hidden);
|
||||
if (hidden) {
|
||||
mHiddenViews.add(child);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
private int getOffset(int index) {
|
||||
if (index < 0) {
|
||||
return -1; //anything below 0 won't work as diff will be undefined.
|
||||
}
|
||||
final int limit = mCallback.getChildCount();
|
||||
int offset = index;
|
||||
while (offset < limit) {
|
||||
final int removedBefore = mBucket.countOnesBefore(offset);
|
||||
final int diff = index - (offset - removedBefore);
|
||||
if (diff == 0) {
|
||||
while (mBucket.get(offset)) { // ensure this offset is not hidden
|
||||
offset ++;
|
||||
}
|
||||
return offset;
|
||||
} else {
|
||||
offset += diff;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided View from underlying RecyclerView.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
*/
|
||||
void removeView(View view) {
|
||||
int index = mCallback.indexOfChild(view);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
mCallback.removeViewAt(index);
|
||||
if (mBucket.remove(index)) {
|
||||
mHiddenViews.remove(view);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "remove View off:" + index + "," + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the view at the provided index from RecyclerView.
|
||||
*
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
*/
|
||||
void removeViewAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
final View view = mCallback.getChildAt(offset);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
mCallback.removeViewAt(offset);
|
||||
if (mBucket.remove(offset)) {
|
||||
mHiddenViews.remove(view);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child at provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
View getChildAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
return mCallback.getChildAt(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all views from the ViewGroup including the hidden ones.
|
||||
*/
|
||||
void removeAllViewsUnfiltered() {
|
||||
mCallback.removeAllViews();
|
||||
mBucket.reset();
|
||||
mHiddenViews.clear();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeAllViewsUnfiltered");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be used to find a disappearing view by position.
|
||||
*
|
||||
* @param position The adapter position of the item.
|
||||
* @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
|
||||
* @return A hidden view with a valid ViewHolder that matches the position and type.
|
||||
*/
|
||||
View findHiddenNonRemovedView(int position, int type) {
|
||||
final int count = mHiddenViews.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View view = mHiddenViews.get(i);
|
||||
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
|
||||
if (holder.getPosition() == position && !holder.isInvalid() &&
|
||||
(type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the provided view to the underlying ViewGroup.
|
||||
*
|
||||
* @param child Child to attach.
|
||||
* @param index Index of the child to attach in regular perspective.
|
||||
* @param layoutParams LayoutParams for the child.
|
||||
* @param hidden If set to true, this item will be invisible to the regular methods.
|
||||
*/
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
|
||||
boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mCallback.attachViewToParent(child, offset, layoutParams);
|
||||
mBucket.insert(offset, hidden);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," +
|
||||
"h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of children that are not hidden.
|
||||
*
|
||||
* @return Number of children that are not hidden.
|
||||
* @see #getChildAt(int)
|
||||
*/
|
||||
int getChildCount() {
|
||||
return mCallback.getChildCount() - mHiddenViews.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of children.
|
||||
*
|
||||
* @return The total number of children including the hidden views.
|
||||
* @see #getUnfilteredChildAt(int)
|
||||
*/
|
||||
int getUnfilteredChildCount() {
|
||||
return mCallback.getChildCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
|
||||
*
|
||||
* @param index ViewGroup index of the child to return.
|
||||
* @return The view in the provided index.
|
||||
*/
|
||||
View getUnfilteredChildAt(int index) {
|
||||
return mCallback.getChildAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the view at the provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
void detachViewFromParent(int index) {
|
||||
final int offset = getOffset(index);
|
||||
mCallback.detachViewFromParent(offset);
|
||||
mBucket.remove(offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the child in regular perspective.
|
||||
*
|
||||
* @param child The child whose index will be returned.
|
||||
* @return The regular perspective index of the child or -1 if it does not exists.
|
||||
*/
|
||||
int indexOfChild(View child) {
|
||||
final int index = mCallback.indexOfChild(child);
|
||||
if (index == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
if (DEBUG) {
|
||||
throw new IllegalArgumentException("cannot get index of a hidden child");
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// reverse the index
|
||||
return index - mBucket.countOnesBefore(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a View is visible to LayoutManager or not.
|
||||
*
|
||||
* @param view The child view to check. Should be a child of the Callback.
|
||||
* @return True if the View is not visible to LayoutManager
|
||||
*/
|
||||
boolean isHidden(View view) {
|
||||
return mHiddenViews.contains(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a child view as hidden.
|
||||
*
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
void hide(View view) {
|
||||
final int offset = mCallback.indexOfChild(view);
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
|
||||
}
|
||||
if (DEBUG && mBucket.get(offset)) {
|
||||
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
|
||||
}
|
||||
mBucket.set(offset);
|
||||
mHiddenViews.add(view);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mBucket.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a view from the ViewGroup if it is hidden.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
* @return True if the View is found and it is hidden. False otherwise.
|
||||
*/
|
||||
boolean removeViewIfHidden(View view) {
|
||||
final int index = mCallback.indexOfChild(view);
|
||||
if (index == -1) {
|
||||
if (mHiddenViews.remove(view) && DEBUG) {
|
||||
throw new IllegalStateException("view is in hidden list but not in view group");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
mBucket.remove(index);
|
||||
mCallback.removeViewAt(index);
|
||||
if (!mHiddenViews.remove(view) && DEBUG) {
|
||||
throw new IllegalStateException(
|
||||
"removed a hidden view but it is not in hidden views list");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitset implementation that provides methods to offset indices.
|
||||
*/
|
||||
static class Bucket {
|
||||
|
||||
final static int BITS_PER_WORD = Long.SIZE;
|
||||
|
||||
final static long LAST_BIT = 1L << (Long.SIZE - 1);
|
||||
|
||||
long mData = 0;
|
||||
|
||||
Bucket next;
|
||||
|
||||
void set(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
next.set(index - BITS_PER_WORD);
|
||||
} else {
|
||||
mData |= 1L << index;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNext() {
|
||||
if (next == null) {
|
||||
next = new Bucket();
|
||||
}
|
||||
}
|
||||
|
||||
void clear(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
if (next != null) {
|
||||
next.clear(index - BITS_PER_WORD);
|
||||
}
|
||||
} else {
|
||||
mData &= ~(1L << index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean get(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return next.get(index - BITS_PER_WORD);
|
||||
} else {
|
||||
return (mData & (1L << index)) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
mData = 0;
|
||||
if (next != null) {
|
||||
next.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void insert(int index, boolean value) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
next.insert(index - BITS_PER_WORD, value);
|
||||
} else {
|
||||
final boolean lastBit = (mData & LAST_BIT) != 0;
|
||||
long mask = (1L << index) - 1;
|
||||
final long before = mData & mask;
|
||||
final long after = ((mData & ~mask)) << 1;
|
||||
mData = before | after;
|
||||
if (value) {
|
||||
set(index);
|
||||
} else {
|
||||
clear(index);
|
||||
}
|
||||
if (lastBit || next != null) {
|
||||
ensureNext();
|
||||
next.insert(0, lastBit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return next.remove(index - BITS_PER_WORD);
|
||||
} else {
|
||||
long mask = (1L << index);
|
||||
final boolean value = (mData & mask) != 0;
|
||||
mData &= ~mask;
|
||||
mask = mask - 1;
|
||||
final long before = mData & mask;
|
||||
// cannot use >> because it adds one.
|
||||
final long after = Long.rotateRight(mData & ~mask, 1);
|
||||
mData = before | after;
|
||||
if (next != null) {
|
||||
if (next.get(0)) {
|
||||
set(BITS_PER_WORD - 1);
|
||||
}
|
||||
next.remove(0);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int countOnesBefore(int index) {
|
||||
if (next == null) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
return Long.bitCount(mData);
|
||||
}
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
}
|
||||
if (index < BITS_PER_WORD) {
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
} else {
|
||||
return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return next == null ? Long.toBinaryString(mData)
|
||||
: next.toString() + "xx" + Long.toBinaryString(mData);
|
||||
}
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
|
||||
int getChildCount();
|
||||
|
||||
void addView(View child, int index);
|
||||
|
||||
int indexOfChild(View view);
|
||||
|
||||
void removeViewAt(int index);
|
||||
|
||||
View getChildAt(int offset);
|
||||
|
||||
void removeAllViews();
|
||||
|
||||
RecyclerView.ViewHolder getChildViewHolder(View view);
|
||||
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
|
||||
|
||||
void detachViewFromParent(int offset);
|
||||
}
|
||||
}
|
||||
@ -1,628 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorListener;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
|
||||
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 DefaultItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
|
||||
|
||||
private ArrayList<ArrayList<ViewHolder>> mAdditionsList =
|
||||
new ArrayList<ArrayList<ViewHolder>>();
|
||||
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>();
|
||||
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>();
|
||||
|
||||
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>();
|
||||
|
||||
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<MoveInfo>();
|
||||
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<ChangeInfo>();
|
||||
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<ViewHolder>();
|
||||
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) {
|
||||
endAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
animation.setDuration(getRemoveDuration())
|
||||
.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
mRemoveAnimations.add(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final ViewHolder holder) {
|
||||
endAnimation(holder);
|
||||
ViewCompat.setAlpha(holder.itemView, 0);
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateAddImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
mAddAnimations.add(holder);
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
animation.alpha(1).setDuration(getAddDuration()).
|
||||
setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
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);
|
||||
endAnimation(holder);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
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.
|
||||
mMoveAnimations.add(holder);
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
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) {
|
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
|
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
|
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
|
||||
endAnimation(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 && newHolder.itemView != null) {
|
||||
// carry over translation values
|
||||
endAnimation(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.itemView;
|
||||
final ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
|
||||
getChangeDuration());
|
||||
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) {
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
|
||||
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(item);
|
||||
}
|
||||
}
|
||||
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(changes);
|
||||
}
|
||||
}
|
||||
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(moves);
|
||||
}
|
||||
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(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animations should be ended by the cancel above.
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mRemoveAnimations list");
|
||||
}
|
||||
|
||||
if (mAddAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mAddAnimations list");
|
||||
}
|
||||
|
||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mChangeAnimations list");
|
||||
}
|
||||
|
||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mMoveAnimations list");
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
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) {}
|
||||
};
|
||||
}
|
||||
@ -1,816 +0,0 @@
|
||||
/*
|
||||
* 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 languag`e governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
|
||||
* <p>
|
||||
* By default, each item occupies 1 span. You can change it by providing a custom
|
||||
* {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
|
||||
*/
|
||||
public class GridLayoutManager extends LinearLayoutManager {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "GridLayoutManager";
|
||||
public static final int DEFAULT_SPAN_COUNT = -1;
|
||||
/**
|
||||
* The measure spec for the scroll direction.
|
||||
*/
|
||||
static final int MAIN_DIR_SPEC =
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
int mSpanCount = DEFAULT_SPAN_COUNT;
|
||||
/**
|
||||
* The size of each span
|
||||
*/
|
||||
int mSizePerSpan;
|
||||
/**
|
||||
* Temporary array to keep views in layoutChunk method
|
||||
*/
|
||||
View[] mSet;
|
||||
final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
|
||||
final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
|
||||
SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
|
||||
// re-used variable to acquire decor insets from RecyclerView
|
||||
final Rect mDecorInsets = new Rect();
|
||||
|
||||
/**
|
||||
* Creates a vertical GridLayoutManager
|
||||
*
|
||||
* @param context Current context, will be used to access resources.
|
||||
* @param spanCount The number of columns in the grid
|
||||
*/
|
||||
public GridLayoutManager(Context context, int spanCount) {
|
||||
super(context);
|
||||
setSpanCount(spanCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context Current context, will be used to access resources.
|
||||
* @param spanCount The number of columns or rows in the grid
|
||||
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
|
||||
* #VERTICAL}.
|
||||
* @param reverseLayout When set to true, layouts from end to start.
|
||||
*/
|
||||
public GridLayoutManager(Context context, int spanCount, int orientation,
|
||||
boolean reverseLayout) {
|
||||
super(context, orientation, reverseLayout);
|
||||
setSpanCount(spanCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* stackFromEnd is not supported by GridLayoutManager. Consider using
|
||||
* {@link #setReverseLayout(boolean)}.
|
||||
*/
|
||||
@Override
|
||||
public void setStackFromEnd(boolean stackFromEnd) {
|
||||
if (stackFromEnd) {
|
||||
throw new UnsupportedOperationException(
|
||||
"GridLayoutManager does not support stack from end."
|
||||
+ " Consider using reverse layout");
|
||||
}
|
||||
super.setStackFromEnd(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state) {
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
return mSpanCount;
|
||||
}
|
||||
if (state.getItemCount() < 1) {
|
||||
return 0;
|
||||
}
|
||||
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state) {
|
||||
if (mOrientation == VERTICAL) {
|
||||
return mSpanCount;
|
||||
}
|
||||
if (state.getItemCount() < 1) {
|
||||
return 0;
|
||||
}
|
||||
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
|
||||
RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
|
||||
ViewGroup.LayoutParams lp = host.getLayoutParams();
|
||||
if (!(lp instanceof LayoutParams)) {
|
||||
super.onInitializeAccessibilityNodeInfoForItem(host, info);
|
||||
return;
|
||||
}
|
||||
LayoutParams glp = (LayoutParams) lp;
|
||||
int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewPosition());
|
||||
if (mOrientation == HORIZONTAL) {
|
||||
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
|
||||
glp.getSpanIndex(), glp.getSpanSize(),
|
||||
spanGroupIndex, 1,
|
||||
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
|
||||
} else { // VERTICAL
|
||||
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
|
||||
spanGroupIndex , 1,
|
||||
glp.getSpanIndex(), glp.getSpanSize(),
|
||||
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
|
||||
if (state.isPreLayout()) {
|
||||
cachePreLayoutSpanMapping();
|
||||
}
|
||||
super.onLayoutChildren(recycler, state);
|
||||
if (DEBUG) {
|
||||
validateChildOrder();
|
||||
}
|
||||
clearPreLayoutSpanMappingCache();
|
||||
}
|
||||
|
||||
private void clearPreLayoutSpanMappingCache() {
|
||||
mPreLayoutSpanSizeCache.clear();
|
||||
mPreLayoutSpanIndexCache.clear();
|
||||
}
|
||||
|
||||
private void cachePreLayoutSpanMapping() {
|
||||
final int childCount = getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
|
||||
final int viewPosition = lp.getViewPosition();
|
||||
mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
|
||||
mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsChanged(RecyclerView recyclerView) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
|
||||
return new LayoutParams(c, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
|
||||
if (lp instanceof ViewGroup.MarginLayoutParams) {
|
||||
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
|
||||
} else {
|
||||
return new LayoutParams(lp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
|
||||
return lp instanceof LayoutParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the source to get the number of spans occupied by each item in the adapter.
|
||||
*
|
||||
* @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
|
||||
* occupied by each item
|
||||
*/
|
||||
public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
|
||||
mSpanSizeLookup = spanSizeLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
|
||||
*
|
||||
* @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
|
||||
*/
|
||||
public SpanSizeLookup getSpanSizeLookup() {
|
||||
return mSpanSizeLookup;
|
||||
}
|
||||
|
||||
private void updateMeasurements() {
|
||||
int totalSpace;
|
||||
if (getOrientation() == VERTICAL) {
|
||||
totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
|
||||
} else {
|
||||
totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
|
||||
}
|
||||
mSizePerSpan = totalSpace / mSpanCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) {
|
||||
super.onAnchorReady(state, anchorInfo);
|
||||
updateMeasurements();
|
||||
if (state.getItemCount() > 0 && !state.isPreLayout()) {
|
||||
ensureAnchorIsInFirstSpan(anchorInfo);
|
||||
}
|
||||
if (mSet == null || mSet.length != mSpanCount) {
|
||||
mSet = new View[mSpanCount];
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo) {
|
||||
int span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
|
||||
while (span > 0 && anchorInfo.mPosition > 0) {
|
||||
anchorInfo.mPosition--;
|
||||
span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
|
||||
}
|
||||
}
|
||||
|
||||
private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
|
||||
int viewPosition) {
|
||||
if (!state.isPreLayout()) {
|
||||
return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
|
||||
}
|
||||
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
|
||||
if (adapterPosition == -1) {
|
||||
if (DEBUG) {
|
||||
throw new RuntimeException("Cannot find span group index for position "
|
||||
+ viewPosition);
|
||||
}
|
||||
Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
|
||||
return 0;
|
||||
}
|
||||
return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
|
||||
}
|
||||
|
||||
private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
|
||||
if (!state.isPreLayout()) {
|
||||
return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
|
||||
}
|
||||
final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
|
||||
if (cached != -1) {
|
||||
return cached;
|
||||
}
|
||||
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
|
||||
if (adapterPosition == -1) {
|
||||
if (DEBUG) {
|
||||
throw new RuntimeException("Cannot find span index for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
}
|
||||
Log.w(TAG, "Cannot find span size for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
return 0;
|
||||
}
|
||||
return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
|
||||
}
|
||||
|
||||
private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
|
||||
if (!state.isPreLayout()) {
|
||||
return mSpanSizeLookup.getSpanSize(pos);
|
||||
}
|
||||
final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
|
||||
if (cached != -1) {
|
||||
return cached;
|
||||
}
|
||||
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
|
||||
if (adapterPosition == -1) {
|
||||
if (DEBUG) {
|
||||
throw new RuntimeException("Cannot find span size for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
}
|
||||
Log.w(TAG, "Cannot find span size for pre layout position. It is"
|
||||
+ " not cached, not in the adapter. Pos:" + pos);
|
||||
return 1;
|
||||
}
|
||||
return mSpanSizeLookup.getSpanSize(adapterPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
|
||||
LayoutState layoutState, LayoutChunkResult result) {
|
||||
final boolean layingOutInPrimaryDirection =
|
||||
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
|
||||
int count = 0;
|
||||
int consumedSpanCount = 0;
|
||||
int remainingSpan = mSpanCount;
|
||||
if (!layingOutInPrimaryDirection) {
|
||||
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
|
||||
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
|
||||
remainingSpan = itemSpanIndex + itemSpanSize;
|
||||
}
|
||||
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
|
||||
int pos = layoutState.mCurrentPosition;
|
||||
final int spanSize = getSpanSize(recycler, state, pos);
|
||||
if (spanSize > mSpanCount) {
|
||||
throw new IllegalArgumentException("Item at position " + pos + " requires " +
|
||||
spanSize + " spans but GridLayoutManager has only " + mSpanCount
|
||||
+ " spans.");
|
||||
}
|
||||
remainingSpan -= spanSize;
|
||||
if (remainingSpan < 0) {
|
||||
break; // item did not fit into this row or column
|
||||
}
|
||||
View view = layoutState.next(recycler);
|
||||
if (view == null) {
|
||||
break;
|
||||
}
|
||||
consumedSpanCount += spanSize;
|
||||
mSet[count] = view;
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
result.mFinished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int maxSize = 0;
|
||||
|
||||
// we should assign spans before item decor offsets are calculated
|
||||
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
|
||||
for (int i = 0; i < count; i++) {
|
||||
View view = mSet[i];
|
||||
if (layoutState.mScrapList == null) {
|
||||
if (layingOutInPrimaryDirection) {
|
||||
addView(view);
|
||||
} else {
|
||||
addView(view, 0);
|
||||
}
|
||||
} else {
|
||||
if (layingOutInPrimaryDirection) {
|
||||
addDisappearingView(view);
|
||||
} else {
|
||||
addDisappearingView(view, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int spanSize = getSpanSize(recycler, state, getPosition(view));
|
||||
final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
|
||||
View.MeasureSpec.EXACTLY);
|
||||
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
|
||||
if (mOrientation == VERTICAL) {
|
||||
measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height));
|
||||
} else {
|
||||
measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec);
|
||||
}
|
||||
final int size = mOrientationHelper.getDecoratedMeasurement(view);
|
||||
if (size > maxSize) {
|
||||
maxSize = size;
|
||||
}
|
||||
}
|
||||
|
||||
// views that did not measure the maxSize has to be re-measured
|
||||
final int maxMeasureSpec = getMainDirSpec(maxSize);
|
||||
for (int i = 0; i < count; i ++) {
|
||||
final View view = mSet[i];
|
||||
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
|
||||
int spanSize = getSpanSize(recycler, state, getPosition(view));
|
||||
final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
|
||||
View.MeasureSpec.EXACTLY);
|
||||
if (mOrientation == VERTICAL) {
|
||||
measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec);
|
||||
} else {
|
||||
measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.mConsumed = maxSize;
|
||||
|
||||
int left = 0, right = 0, top = 0, bottom = 0;
|
||||
if (mOrientation == VERTICAL) {
|
||||
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
|
||||
bottom = layoutState.mOffset;
|
||||
top = bottom - maxSize;
|
||||
} else {
|
||||
top = layoutState.mOffset;
|
||||
bottom = top + maxSize;
|
||||
}
|
||||
} else {
|
||||
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
|
||||
right = layoutState.mOffset;
|
||||
left = right - maxSize;
|
||||
} else {
|
||||
left = layoutState.mOffset;
|
||||
right = left + maxSize;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
View view = mSet[i];
|
||||
LayoutParams params = (LayoutParams) view.getLayoutParams();
|
||||
if (mOrientation == VERTICAL) {
|
||||
left = getPaddingLeft() + mSizePerSpan * params.mSpanIndex;
|
||||
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
|
||||
} else {
|
||||
top = getPaddingTop() + mSizePerSpan * params.mSpanIndex;
|
||||
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
|
||||
}
|
||||
// We calculate everything with View's bounding box (which includes decor and margins)
|
||||
// To calculate correct layout position, we subtract margins.
|
||||
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
|
||||
right - params.rightMargin, bottom - params.bottomMargin);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
|
||||
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
|
||||
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
|
||||
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
|
||||
}
|
||||
// Consume the available space if the view is not removed OR changed
|
||||
if (params.isItemRemoved() || params.isItemChanged()) {
|
||||
result.mIgnoreConsumed = true;
|
||||
}
|
||||
result.mFocusable |= view.isFocusable();
|
||||
}
|
||||
Arrays.fill(mSet, null);
|
||||
}
|
||||
|
||||
private int getMainDirSpec(int dim) {
|
||||
if (dim < 0) {
|
||||
return MAIN_DIR_SPEC;
|
||||
} else {
|
||||
return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
|
||||
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
|
||||
calculateItemDecorationsForChild(child, mDecorInsets);
|
||||
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
|
||||
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
|
||||
lp.rightMargin + mDecorInsets.right);
|
||||
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
|
||||
lp.bottomMargin + mDecorInsets.bottom);
|
||||
child.measure(widthSpec, heightSpec);
|
||||
}
|
||||
|
||||
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
|
||||
if (startInset == 0 && endInset == 0) {
|
||||
return spec;
|
||||
}
|
||||
final int mode = View.MeasureSpec.getMode(spec);
|
||||
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
|
||||
return View.MeasureSpec.makeMeasureSpec(
|
||||
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
|
||||
int consumedSpanCount, boolean layingOutInPrimaryDirection) {
|
||||
int span, spanDiff, start, end, diff;
|
||||
// make sure we traverse from min position to max position
|
||||
if (layingOutInPrimaryDirection) {
|
||||
start = 0;
|
||||
end = count;
|
||||
diff = 1;
|
||||
} else {
|
||||
start = count - 1;
|
||||
end = -1;
|
||||
diff = -1;
|
||||
}
|
||||
if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
|
||||
span = consumedSpanCount - 1;
|
||||
spanDiff = -1;
|
||||
} else {
|
||||
span = 0;
|
||||
spanDiff = 1;
|
||||
}
|
||||
for (int i = start; i != end; i += diff) {
|
||||
View view = mSet[i];
|
||||
LayoutParams params = (LayoutParams) view.getLayoutParams();
|
||||
params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
|
||||
if (spanDiff == -1 && params.mSpanSize > 1) {
|
||||
params.mSpanIndex = span - (params.mSpanSize - 1);
|
||||
} else {
|
||||
params.mSpanIndex = span;
|
||||
}
|
||||
span += spanDiff * params.mSpanSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of spans laid out by this grid.
|
||||
*
|
||||
* @return The number of spans
|
||||
* @see #setSpanCount(int)
|
||||
*/
|
||||
public int getSpanCount() {
|
||||
return mSpanCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of spans to be laid out.
|
||||
* <p>
|
||||
* If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
|
||||
* If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
|
||||
*
|
||||
* @param spanCount The total number of spans in the grid
|
||||
* @see #getSpanCount()
|
||||
*/
|
||||
public void setSpanCount(int spanCount) {
|
||||
if (spanCount == mSpanCount) {
|
||||
return;
|
||||
}
|
||||
if (spanCount < 1) {
|
||||
throw new IllegalArgumentException("Span count should be at least 1. Provided "
|
||||
+ spanCount);
|
||||
}
|
||||
mSpanCount = spanCount;
|
||||
mSpanSizeLookup.invalidateSpanIndexCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class to provide the number of spans each item occupies.
|
||||
* <p>
|
||||
* Default implementation sets each item to occupy exactly 1 span.
|
||||
*
|
||||
* @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
|
||||
*/
|
||||
public static abstract class SpanSizeLookup {
|
||||
|
||||
final SparseIntArray mSpanIndexCache = new SparseIntArray();
|
||||
|
||||
private boolean mCacheSpanIndices = false;
|
||||
|
||||
/**
|
||||
* Returns the number of span occupied by the item at <code>position</code>.
|
||||
*
|
||||
* @param position The adapter position of the item
|
||||
* @return The number of spans occupied by the item at the provided position
|
||||
*/
|
||||
abstract public int getSpanSize(int position);
|
||||
|
||||
/**
|
||||
* Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
|
||||
* not. By default these values are not cached. If you are not overriding
|
||||
* {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
|
||||
*
|
||||
* @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
|
||||
*/
|
||||
public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
|
||||
mCacheSpanIndices = cacheSpanIndices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the span index cache. GridLayoutManager automatically calls this method when
|
||||
* adapter changes occur.
|
||||
*/
|
||||
public void invalidateSpanIndexCache() {
|
||||
mSpanIndexCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
|
||||
*
|
||||
* @return True if results of {@link #getSpanIndex(int, int)} are cached.
|
||||
*/
|
||||
public boolean isSpanIndexCacheEnabled() {
|
||||
return mCacheSpanIndices;
|
||||
}
|
||||
|
||||
int getCachedSpanIndex(int position, int spanCount) {
|
||||
if (!mCacheSpanIndices) {
|
||||
return getSpanIndex(position, spanCount);
|
||||
}
|
||||
final int existing = mSpanIndexCache.get(position, -1);
|
||||
if (existing != -1) {
|
||||
return existing;
|
||||
}
|
||||
final int value = getSpanIndex(position, spanCount);
|
||||
mSpanIndexCache.put(position, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final span index of the provided position.
|
||||
* <p>
|
||||
* If you have a faster way to calculate span index for your items, you should override
|
||||
* this method. Otherwise, you should enable span index cache
|
||||
* ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
|
||||
* disabled, default implementation traverses all items from 0 to
|
||||
* <code>position</code>. When caching is enabled, it calculates from the closest cached
|
||||
* value before the <code>position</code>.
|
||||
* <p>
|
||||
* If you override this method, you need to make sure it is consistent with
|
||||
* {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
|
||||
* each item. It is called only for the reference item and rest of the items
|
||||
* are assigned to spans based on the reference item. For example, you cannot assign a
|
||||
* position to span 2 while span 1 is empty.
|
||||
* <p>
|
||||
* Note that span offsets always start with 0 and are not affected by RTL.
|
||||
*
|
||||
* @param position The position of the item
|
||||
* @param spanCount The total number of spans in the grid
|
||||
* @return The final span position of the item. Should be between 0 (inclusive) and
|
||||
* <code>spanCount</code>(exclusive)
|
||||
*/
|
||||
public int getSpanIndex(int position, int spanCount) {
|
||||
int positionSpanSize = getSpanSize(position);
|
||||
if (positionSpanSize == spanCount) {
|
||||
return 0; // quick return for full-span items
|
||||
}
|
||||
int span = 0;
|
||||
int startPos = 0;
|
||||
// If caching is enabled, try to jump
|
||||
if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
|
||||
int prevKey = findReferenceIndexFromCache(position);
|
||||
if (prevKey >= 0) {
|
||||
span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
|
||||
startPos = prevKey + 1;
|
||||
}
|
||||
}
|
||||
for (int i = startPos; i < position; i++) {
|
||||
int size = getSpanSize(i);
|
||||
span += size;
|
||||
if (span == spanCount) {
|
||||
span = 0;
|
||||
} else if (span > spanCount) {
|
||||
// did not fit, moving to next row / column
|
||||
span = size;
|
||||
}
|
||||
}
|
||||
if (span + positionSpanSize <= spanCount) {
|
||||
return span;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int findReferenceIndexFromCache(int position) {
|
||||
int lo = 0;
|
||||
int hi = mSpanIndexCache.size() - 1;
|
||||
|
||||
while (lo <= hi) {
|
||||
final int mid = (lo + hi) >>> 1;
|
||||
final int midVal = mSpanIndexCache.keyAt(mid);
|
||||
if (midVal < position) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
int index = lo - 1;
|
||||
if (index >= 0 && index < mSpanIndexCache.size()) {
|
||||
return mSpanIndexCache.keyAt(index);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the group this position belongs.
|
||||
* <p>
|
||||
* For example, if grid has 3 columns and each item occupies 1 span, span group index
|
||||
* for item 1 will be 0, item 5 will be 1.
|
||||
*
|
||||
* @param adapterPosition The position in adapter
|
||||
* @param spanCount The total number of spans in the grid
|
||||
* @return The index of the span group including the item at the given adapter position
|
||||
*/
|
||||
public int getSpanGroupIndex(int adapterPosition, int spanCount) {
|
||||
int span = 0;
|
||||
int group = 0;
|
||||
int positionSpanSize = getSpanSize(adapterPosition);
|
||||
for (int i = 0; i < adapterPosition; i++) {
|
||||
int size = getSpanSize(i);
|
||||
span += size;
|
||||
if (span == spanCount) {
|
||||
span = 0;
|
||||
group++;
|
||||
} else if (span > spanCount) {
|
||||
// did not fit, moving to next row / column
|
||||
span = size;
|
||||
group++;
|
||||
}
|
||||
}
|
||||
if (span + positionSpanSize > spanCount) {
|
||||
group++;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPredictiveItemAnimations() {
|
||||
return mPendingSavedState == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
|
||||
*/
|
||||
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
|
||||
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpanIndex(int position, int spanCount) {
|
||||
return position % spanCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LayoutParams used by GridLayoutManager.
|
||||
*/
|
||||
public static class LayoutParams extends RecyclerView.LayoutParams {
|
||||
|
||||
/**
|
||||
* Span Id for Views that are not laid out yet.
|
||||
*/
|
||||
public static final int INVALID_SPAN_ID = -1;
|
||||
|
||||
private int mSpanIndex = INVALID_SPAN_ID;
|
||||
|
||||
private int mSpanSize = 0;
|
||||
|
||||
public LayoutParams(Context c, AttributeSet attrs) {
|
||||
super(c, attrs);
|
||||
}
|
||||
|
||||
public LayoutParams(int width, int height) {
|
||||
super(width, height);
|
||||
}
|
||||
|
||||
public LayoutParams(ViewGroup.MarginLayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
public LayoutParams(ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
public LayoutParams(RecyclerView.LayoutParams source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current span index of this View. If the View is not laid out yet, the return
|
||||
* value is <code>undefined</code>.
|
||||
* <p>
|
||||
* Note that span index may change by whether the RecyclerView is RTL or not. For
|
||||
* example, if the number of spans is 3 and layout is RTL, the rightmost item will have
|
||||
* span index of 2. If the layout changes back to LTR, span index for this view will be 0.
|
||||
* If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
|
||||
* <p>
|
||||
* If the View occupies multiple spans, span with the minimum index is returned.
|
||||
*
|
||||
* @return The span index of the View.
|
||||
*/
|
||||
public int getSpanIndex() {
|
||||
return mSpanIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of spans occupied by this View. If the View not laid out yet, the
|
||||
* return value is <code>undefined</code>.
|
||||
*
|
||||
* @return The number of spans occupied by this View.
|
||||
*/
|
||||
public int getSpanSize() {
|
||||
return mSpanSize;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
/*
|
||||
* 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 languag`e governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
|
||||
* space.
|
||||
*/
|
||||
class LayoutState {
|
||||
|
||||
final static String TAG = "LayoutState";
|
||||
|
||||
final static int LAYOUT_START = -1;
|
||||
|
||||
final static int LAYOUT_END = 1;
|
||||
|
||||
final static int INVALID_LAYOUT = Integer.MIN_VALUE;
|
||||
|
||||
final static int ITEM_DIRECTION_HEAD = -1;
|
||||
|
||||
final static int ITEM_DIRECTION_TAIL = 1;
|
||||
|
||||
final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
|
||||
|
||||
/**
|
||||
* Number of pixels that we should fill, in the layout direction.
|
||||
*/
|
||||
int mAvailable;
|
||||
|
||||
/**
|
||||
* Current position on the adapter to get the next item.
|
||||
*/
|
||||
int mCurrentPosition;
|
||||
|
||||
/**
|
||||
* Defines the direction in which the data adapter is traversed.
|
||||
* Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
|
||||
*/
|
||||
int mItemDirection;
|
||||
|
||||
/**
|
||||
* Defines the direction in which the layout is filled.
|
||||
* Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
|
||||
*/
|
||||
int mLayoutDirection;
|
||||
|
||||
/**
|
||||
* Used if you want to pre-layout items that are not yet visible.
|
||||
* The difference with {@link #mAvailable} is that, when recycling, distance rendered for
|
||||
* {@link #mExtra} is not considered not to recycle visible children.
|
||||
*/
|
||||
int mExtra = 0;
|
||||
|
||||
/**
|
||||
* @return true if there are more items in the data adapter
|
||||
*/
|
||||
boolean hasMore(RecyclerView.State state) {
|
||||
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the view for the next element that we should render.
|
||||
* Also updates current item index to the next item, based on {@link #mItemDirection}
|
||||
*
|
||||
* @return The next element that we should render.
|
||||
*/
|
||||
View next(RecyclerView.Recycler recycler) {
|
||||
final View view = recycler.getViewForPosition(mCurrentPosition);
|
||||
mCurrentPosition += mItemDirection;
|
||||
return view;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,338 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
/**
|
||||
* {@link RecyclerView.SmoothScroller} implementation which uses
|
||||
* {@link LinearInterpolator} until the target position becames a child of
|
||||
* the RecyclerView and then uses
|
||||
* {@link DecelerateInterpolator} to slowly approach to target position.
|
||||
*/
|
||||
abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
|
||||
|
||||
private static final String TAG = "LinearSmoothScroller";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final float MILLISECONDS_PER_INCH = 25f;
|
||||
|
||||
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
|
||||
|
||||
/**
|
||||
* Align child view's left or top with parent view's left or top
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(View, int)
|
||||
* @see #calculateDyToMakeVisible(View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_START = -1;
|
||||
|
||||
/**
|
||||
* Align child view's right or bottom with parent view's right or bottom
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(View, int)
|
||||
* @see #calculateDyToMakeVisible(View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_END = 1;
|
||||
|
||||
/**
|
||||
* <p>Decides if the child should be snapped from start or end, depending on where it
|
||||
* currently is in relation to its parent.</p>
|
||||
* <p>For instance, if the view is virtually on the left of RecyclerView, using
|
||||
* {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(View, int)
|
||||
* @see #calculateDyToMakeVisible(View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_ANY = 0;
|
||||
|
||||
// Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
|
||||
// view is not laid out until interim target position is reached, we can detect the case before
|
||||
// scrolling slows down and reschedule another interim target scroll
|
||||
private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
|
||||
|
||||
protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
|
||||
|
||||
protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
|
||||
|
||||
protected PointF mTargetVector;
|
||||
|
||||
private final float MILLISECONDS_PER_PX;
|
||||
|
||||
// Temporary variables to keep track of the interim scroll target. These values do not
|
||||
// point to a real item position, rather point to an estimated location pixels.
|
||||
protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
|
||||
|
||||
public LinearSmoothScroller(Context context) {
|
||||
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onStart() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
|
||||
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
|
||||
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
|
||||
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
|
||||
final int time = calculateTimeForDeceleration(distance);
|
||||
if (time > 0) {
|
||||
action.update(-dx, -dy, time, mDecelerateInterpolator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
|
||||
if (getChildCount() == 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
if (DEBUG && mTargetVector != null
|
||||
&& ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
|
||||
throw new IllegalStateException("Scroll happened in the opposite direction"
|
||||
+ " of the target. Some calculations are wrong");
|
||||
}
|
||||
mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
|
||||
mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
|
||||
|
||||
if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
|
||||
updateActionForInterimTarget(action);
|
||||
} // everything is valid, keep going
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onStop() {
|
||||
mInterimTargetDx = mInterimTargetDy = 0;
|
||||
mTargetVector = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scroll speed.
|
||||
*
|
||||
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
|
||||
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
|
||||
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
|
||||
*/
|
||||
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
|
||||
* DecelerateInterpolator looks smooth.</p>
|
||||
*
|
||||
* @param dx Distance to scroll
|
||||
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
|
||||
* from LinearInterpolation
|
||||
*/
|
||||
protected int calculateTimeForDeceleration(int dx) {
|
||||
// we want to cover same area with the linear interpolator for the first 10% of the
|
||||
// interpolation. After that, deceleration will take control.
|
||||
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
|
||||
// which gives 0.100028 when x = .3356
|
||||
// this is why we divide linear scrolling time with .3356
|
||||
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time it should take to scroll the given distance (in pixels)
|
||||
*
|
||||
* @param dx Distance in pixels that we want to scroll
|
||||
* @return Time in milliseconds
|
||||
* @see #calculateSpeedPerPixel(DisplayMetrics)
|
||||
*/
|
||||
protected int calculateTimeForScrolling(int dx) {
|
||||
// In a case where dx is very small, rounding may return 0 although dx > 0.
|
||||
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
|
||||
// time.
|
||||
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling towards a child view, this method defines whether we should align the left
|
||||
* or the right edge of the child with the parent RecyclerView.
|
||||
*
|
||||
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
|
||||
* @see #SNAP_TO_START
|
||||
* @see #SNAP_TO_END
|
||||
* @see #SNAP_TO_ANY
|
||||
*/
|
||||
protected int getHorizontalSnapPreference() {
|
||||
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
|
||||
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling towards a child view, this method defines whether we should align the top
|
||||
* or the bottom edge of the child with the parent RecyclerView.
|
||||
*
|
||||
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
|
||||
* @see #SNAP_TO_START
|
||||
* @see #SNAP_TO_END
|
||||
* @see #SNAP_TO_ANY
|
||||
*/
|
||||
protected int getVerticalSnapPreference() {
|
||||
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
|
||||
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the target scroll position is not a child of the RecyclerView, this method calculates
|
||||
* a direction vector towards that child and triggers a smooth scroll.
|
||||
*
|
||||
* @see #computeScrollVectorForPosition(int)
|
||||
*/
|
||||
protected void updateActionForInterimTarget(Action action) {
|
||||
// find an interim target position
|
||||
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
|
||||
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
|
||||
Log.e(TAG, "To support smooth scrolling, you should override \n"
|
||||
+ "LayoutManager#computeScrollVectorForPosition.\n"
|
||||
+ "Falling back to instant scroll");
|
||||
final int target = getTargetPosition();
|
||||
stop();
|
||||
instantScrollToPosition(target);
|
||||
return;
|
||||
}
|
||||
normalize(scrollVector);
|
||||
mTargetVector = scrollVector;
|
||||
|
||||
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
|
||||
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
|
||||
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
|
||||
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
|
||||
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
|
||||
// won't actually scroll more than what we need.
|
||||
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
|
||||
, (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
|
||||
, (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
|
||||
}
|
||||
|
||||
private int clampApplyScroll(int tmpDt, int dt) {
|
||||
final int before = tmpDt;
|
||||
tmpDt -= dt;
|
||||
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
|
||||
return 0;
|
||||
}
|
||||
return tmpDt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for {@link #calculateDxToMakeVisible(View, int)} and
|
||||
* {@link #calculateDyToMakeVisible(View, int)}
|
||||
*/
|
||||
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
|
||||
snapPreference) {
|
||||
switch (snapPreference) {
|
||||
case SNAP_TO_START:
|
||||
return boxStart - viewStart;
|
||||
case SNAP_TO_END:
|
||||
return boxEnd - viewEnd;
|
||||
case SNAP_TO_ANY:
|
||||
final int dtStart = boxStart - viewStart;
|
||||
if (dtStart > 0) {
|
||||
return dtStart;
|
||||
}
|
||||
final int dtEnd = boxEnd - viewEnd;
|
||||
if (dtEnd < 0) {
|
||||
return dtEnd;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("snap preference should be one of the"
|
||||
+ " constants defined in SmoothScroller, starting with SNAP_");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the vertical scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}.
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDyToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (!layoutManager.canScrollVertically()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
final int start = layoutManager.getPaddingTop();
|
||||
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
|
||||
return calculateDtToFit(top, bottom, start, end, snapPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the horizontal scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDxToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (!layoutManager.canScrollHorizontally()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
final int start = layoutManager.getPaddingLeft();
|
||||
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
|
||||
return calculateDtToFit(left, right, start, end, snapPreference);
|
||||
}
|
||||
|
||||
abstract public PointF computeScrollVectorForPosition(int targetPosition);
|
||||
}
|
||||
@ -1,238 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.support.v7.widget.AdapterHelper.UpdateOp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.UPDATE;
|
||||
|
||||
class OpReorderer {
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
public OpReorderer(Callback callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void reorderOps(List<UpdateOp> ops) {
|
||||
// since move operations breaks continuity, their effects on ADD/RM are hard to handle.
|
||||
// we push them to the end of the list so that they can be handled easily.
|
||||
int badMove;
|
||||
while ((badMove = getLastMoveOutOfOrder(ops)) != -1) {
|
||||
swapMoveOp(ops, badMove, badMove + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void swapMoveOp(List<UpdateOp> list, int badMove, int next) {
|
||||
final UpdateOp moveOp = list.get(badMove);
|
||||
final UpdateOp nextOp = list.get(next);
|
||||
switch (nextOp.cmd) {
|
||||
case REMOVE:
|
||||
swapMoveRemove(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
case ADD:
|
||||
swapMoveAdd(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
case UPDATE:
|
||||
swapMoveUpdate(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
|
||||
int removePos, UpdateOp removeOp) {
|
||||
UpdateOp extraRm = null;
|
||||
// check if move is nulled out by remove
|
||||
boolean revertedMove = false;
|
||||
final boolean moveIsBackwards;
|
||||
|
||||
if (moveOp.positionStart < moveOp.itemCount) {
|
||||
moveIsBackwards = false;
|
||||
if (removeOp.positionStart == moveOp.positionStart
|
||||
&& removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
|
||||
revertedMove = true;
|
||||
}
|
||||
} else {
|
||||
moveIsBackwards = true;
|
||||
if (removeOp.positionStart == moveOp.itemCount + 1 &&
|
||||
removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
|
||||
revertedMove = true;
|
||||
}
|
||||
}
|
||||
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < removeOp.positionStart) {
|
||||
removeOp.positionStart--;
|
||||
} else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
|
||||
// move is removed.
|
||||
removeOp.itemCount --;
|
||||
moveOp.cmd = REMOVE;
|
||||
moveOp.itemCount = 1;
|
||||
if (removeOp.itemCount == 0) {
|
||||
list.remove(removePos);
|
||||
mCallback.recycleUpdateOp(removeOp);
|
||||
}
|
||||
// no need to swap, it is already a remove
|
||||
return;
|
||||
}
|
||||
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= removeOp.positionStart) {
|
||||
removeOp.positionStart++;
|
||||
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
|
||||
final int remaining = removeOp.positionStart + removeOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining);
|
||||
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
|
||||
}
|
||||
|
||||
// if effects of move is reverted by remove, we are done.
|
||||
if (revertedMove) {
|
||||
list.set(movePos, removeOp);
|
||||
list.remove(removePos);
|
||||
mCallback.recycleUpdateOp(moveOp);
|
||||
return;
|
||||
}
|
||||
|
||||
// now find out the new locations for move actions
|
||||
if (moveIsBackwards) {
|
||||
if (extraRm != null) {
|
||||
if (moveOp.positionStart > extraRm.positionStart) {
|
||||
moveOp.positionStart -= extraRm.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount > extraRm.positionStart) {
|
||||
moveOp.itemCount -= extraRm.itemCount;
|
||||
}
|
||||
}
|
||||
if (moveOp.positionStart > removeOp.positionStart) {
|
||||
moveOp.positionStart -= removeOp.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount > removeOp.positionStart) {
|
||||
moveOp.itemCount -= removeOp.itemCount;
|
||||
}
|
||||
} else {
|
||||
if (extraRm != null) {
|
||||
if (moveOp.positionStart >= extraRm.positionStart) {
|
||||
moveOp.positionStart -= extraRm.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount >= extraRm.positionStart) {
|
||||
moveOp.itemCount -= extraRm.itemCount;
|
||||
}
|
||||
}
|
||||
if (moveOp.positionStart >= removeOp.positionStart) {
|
||||
moveOp.positionStart -= removeOp.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount >= removeOp.positionStart) {
|
||||
moveOp.itemCount -= removeOp.itemCount;
|
||||
}
|
||||
}
|
||||
|
||||
list.set(movePos, removeOp);
|
||||
if (moveOp.positionStart != moveOp.itemCount) {
|
||||
list.set(removePos, moveOp);
|
||||
} else {
|
||||
list.remove(removePos);
|
||||
}
|
||||
if (extraRm != null) {
|
||||
list.add(movePos, extraRm);
|
||||
}
|
||||
}
|
||||
|
||||
private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
|
||||
UpdateOp addOp) {
|
||||
int offset = 0;
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < addOp.positionStart) {
|
||||
offset--;
|
||||
}
|
||||
if (moveOp.positionStart < addOp.positionStart) {
|
||||
offset++;
|
||||
}
|
||||
if (addOp.positionStart <= moveOp.positionStart) {
|
||||
moveOp.positionStart += addOp.itemCount;
|
||||
}
|
||||
if (addOp.positionStart <= moveOp.itemCount) {
|
||||
moveOp.itemCount += addOp.itemCount;
|
||||
}
|
||||
addOp.positionStart += offset;
|
||||
list.set(move, addOp);
|
||||
list.set(add, moveOp);
|
||||
}
|
||||
|
||||
void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
|
||||
UpdateOp updateOp) {
|
||||
UpdateOp extraUp1 = null;
|
||||
UpdateOp extraUp2 = null;
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < updateOp.positionStart) {
|
||||
updateOp.positionStart--;
|
||||
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
|
||||
// moved item is updated. add an update for it
|
||||
updateOp.itemCount--;
|
||||
extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1);
|
||||
}
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= updateOp.positionStart) {
|
||||
updateOp.positionStart++;
|
||||
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
|
||||
final int remaining = updateOp.positionStart + updateOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining);
|
||||
updateOp.itemCount -= remaining;
|
||||
}
|
||||
list.set(update, moveOp);
|
||||
if (updateOp.itemCount > 0) {
|
||||
list.set(move, updateOp);
|
||||
} else {
|
||||
list.remove(move);
|
||||
mCallback.recycleUpdateOp(updateOp);
|
||||
}
|
||||
if (extraUp1 != null) {
|
||||
list.add(move, extraUp1);
|
||||
}
|
||||
if (extraUp2 != null) {
|
||||
list.add(move, extraUp2);
|
||||
}
|
||||
}
|
||||
|
||||
private int getLastMoveOutOfOrder(List<UpdateOp> list) {
|
||||
boolean foundNonMove = false;
|
||||
for (int i = list.size() - 1; i >= 0; i--) {
|
||||
final UpdateOp op1 = list.get(i);
|
||||
if (op1.cmd == MOVE) {
|
||||
if (foundNonMove) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
foundNonMove = true;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
|
||||
UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount);
|
||||
|
||||
void recycleUpdateOp(UpdateOp op);
|
||||
}
|
||||
}
|
||||
@ -1,338 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
|
||||
* <p>
|
||||
* It is developed to easily support vertical and horizontal orientations in a LayoutManager but
|
||||
* can also be used to abstract calls around view bounds and child measurements with margins and
|
||||
* decorations.
|
||||
*
|
||||
* @see #createHorizontalHelper(RecyclerView.LayoutManager)
|
||||
* @see #createVerticalHelper(RecyclerView.LayoutManager)
|
||||
*/
|
||||
public abstract class OrientationHelper {
|
||||
|
||||
private static final int INVALID_SIZE = Integer.MIN_VALUE;
|
||||
|
||||
protected final RecyclerView.LayoutManager mLayoutManager;
|
||||
|
||||
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
|
||||
|
||||
public static final int VERTICAL = LinearLayout.VERTICAL;
|
||||
|
||||
private int mLastTotalSpace = INVALID_SIZE;
|
||||
|
||||
private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
mLayoutManager = layoutManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method after onLayout method is complete if state is NOT pre-layout.
|
||||
* This method records information like layout bounds that might be useful in the next layout
|
||||
* calculations.
|
||||
*/
|
||||
public void onLayoutComplete() {
|
||||
mLastTotalSpace = getTotalSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the layout space change between the previous layout pass and current layout pass.
|
||||
* <p>
|
||||
* Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's
|
||||
* {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler,
|
||||
* RecyclerView.State)} method.
|
||||
*
|
||||
* @return The difference between the current total space and previous layout's total space.
|
||||
* @see #onLayoutComplete()
|
||||
*/
|
||||
public int getTotalSpaceChange() {
|
||||
return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start of the view including its decoration and margin.
|
||||
* <p>
|
||||
* For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
|
||||
* decoration and 3px left margin, returned value will be 15px.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return The first pixel of the element
|
||||
* @see #getDecoratedEnd(View)
|
||||
*/
|
||||
public abstract int getDecoratedStart(View view);
|
||||
|
||||
/**
|
||||
* Returns the end of the view including its decoration and margin.
|
||||
* <p>
|
||||
* For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
|
||||
* decoration and 3px right margin, returned value will be 205.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return The last pixel of the element
|
||||
* @see #getDecoratedStart(View)
|
||||
*/
|
||||
public abstract int getDecoratedEnd(View view);
|
||||
|
||||
/**
|
||||
* Returns the space occupied by this View in the current orientation including decorations and
|
||||
* margins.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return Total space occupied by this view
|
||||
* @see #getDecoratedMeasurementInOther(View)
|
||||
*/
|
||||
public abstract int getDecoratedMeasurement(View view);
|
||||
|
||||
/**
|
||||
* Returns the space occupied by this View in the perpendicular orientation including
|
||||
* decorations and margins.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return Total space occupied by this view in the perpendicular orientation to current one
|
||||
* @see #getDecoratedMeasurement(View)
|
||||
*/
|
||||
public abstract int getDecoratedMeasurementInOther(View view);
|
||||
|
||||
/**
|
||||
* Returns the start position of the layout after the start padding is added.
|
||||
*
|
||||
* @return The very first pixel we can draw.
|
||||
*/
|
||||
public abstract int getStartAfterPadding();
|
||||
|
||||
/**
|
||||
* Returns the end position of the layout after the end padding is removed.
|
||||
*
|
||||
* @return The end boundary for this layout.
|
||||
*/
|
||||
public abstract int getEndAfterPadding();
|
||||
|
||||
/**
|
||||
* Returns the end position of the layout without taking padding into account.
|
||||
*
|
||||
* @return The end boundary for this layout without considering padding.
|
||||
*/
|
||||
public abstract int getEnd();
|
||||
|
||||
/**
|
||||
* Offsets all children's positions by the given amount.
|
||||
*
|
||||
* @param amount Value to add to each child's layout parameters
|
||||
*/
|
||||
public abstract void offsetChildren(int amount);
|
||||
|
||||
/**
|
||||
* Returns the total space to layout. This number is the difference between
|
||||
* {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
|
||||
*
|
||||
* @return Total space to layout children
|
||||
*/
|
||||
public abstract int getTotalSpace();
|
||||
|
||||
/**
|
||||
* Offsets the child in this orientation.
|
||||
*
|
||||
* @param view View to offset
|
||||
* @param offset offset amount
|
||||
*/
|
||||
public abstract void offsetChild(View view, int offset);
|
||||
|
||||
/**
|
||||
* Returns the padding at the end of the layout. For horizontal helper, this is the right
|
||||
* padding and for vertical helper, this is the bottom padding. This method does not check
|
||||
* whether the layout is RTL or not.
|
||||
*
|
||||
* @return The padding at the end of the layout.
|
||||
*/
|
||||
public abstract int getEndPadding();
|
||||
|
||||
/**
|
||||
* Creates an OrientationHelper for the given LayoutManager and orientation.
|
||||
*
|
||||
* @param layoutManager LayoutManager to attach to
|
||||
* @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createOrientationHelper(
|
||||
RecyclerView.LayoutManager layoutManager, int orientation) {
|
||||
switch (orientation) {
|
||||
case HORIZONTAL:
|
||||
return createHorizontalHelper(layoutManager);
|
||||
case VERTICAL:
|
||||
return createVerticalHelper(layoutManager);
|
||||
}
|
||||
throw new IllegalArgumentException("invalid orientation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a horizontal OrientationHelper for the given LayoutManager.
|
||||
*
|
||||
* @param layoutManager The LayoutManager to attach to.
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createHorizontalHelper(
|
||||
RecyclerView.LayoutManager layoutManager) {
|
||||
return new OrientationHelper(layoutManager) {
|
||||
@Override
|
||||
public int getEndAfterPadding() {
|
||||
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnd() {
|
||||
return mLayoutManager.getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChildren(int amount) {
|
||||
mLayoutManager.offsetChildrenHorizontal(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartAfterPadding() {
|
||||
return mLayoutManager.getPaddingLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurement(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
|
||||
+ params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurementInOther(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
|
||||
+ params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalSpace() {
|
||||
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
|
||||
- mLayoutManager.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChild(View view, int offset) {
|
||||
view.offsetLeftAndRight(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndPadding() {
|
||||
return mLayoutManager.getPaddingRight();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a vertical OrientationHelper for the given LayoutManager.
|
||||
*
|
||||
* @param layoutManager The LayoutManager to attach to.
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
return new OrientationHelper(layoutManager) {
|
||||
@Override
|
||||
public int getEndAfterPadding() {
|
||||
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnd() {
|
||||
return mLayoutManager.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChildren(int amount) {
|
||||
mLayoutManager.offsetChildrenVertical(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartAfterPadding() {
|
||||
return mLayoutManager.getPaddingTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurement(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
|
||||
+ params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurementInOther(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
|
||||
+ params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalSpace() {
|
||||
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
|
||||
- mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChild(View view, int offset) {
|
||||
view.offsetTopAndBottom(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndPadding() {
|
||||
return mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,460 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 android.support.v7.widget;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Like a SparseArray, but with the ability to offset key ranges for bulk insertions/deletions.
|
||||
*/
|
||||
class PositionMap<E> implements Cloneable {
|
||||
private static final Object DELETED = new Object();
|
||||
private boolean mGarbage = false;
|
||||
|
||||
private int[] mKeys;
|
||||
private Object[] mValues;
|
||||
private int mSize;
|
||||
|
||||
/**
|
||||
* Creates a new SparseArray containing no mappings.
|
||||
*/
|
||||
public PositionMap() {
|
||||
this(10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new PositionMap containing no mappings that will not
|
||||
* require any additional memory allocation to store the specified
|
||||
* number of mappings. If you supply an initial capacity of 0, the
|
||||
* sparse array will be initialized with a light-weight representation
|
||||
* not requiring any additional array allocations.
|
||||
*/
|
||||
public PositionMap(int initialCapacity) {
|
||||
if (initialCapacity == 0) {
|
||||
mKeys = ContainerHelpers.EMPTY_INTS;
|
||||
mValues = ContainerHelpers.EMPTY_OBJECTS;
|
||||
} else {
|
||||
initialCapacity = idealIntArraySize(initialCapacity);
|
||||
mKeys = new int[initialCapacity];
|
||||
mValues = new Object[initialCapacity];
|
||||
}
|
||||
mSize = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public PositionMap<E> clone() {
|
||||
PositionMap<E> clone = null;
|
||||
try {
|
||||
clone = (PositionMap<E>) super.clone();
|
||||
clone.mKeys = mKeys.clone();
|
||||
clone.mValues = mValues.clone();
|
||||
} catch (CloneNotSupportedException cnse) {
|
||||
/* ignore */
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Object mapped from the specified key, or <code>null</code>
|
||||
* if no such mapping has been made.
|
||||
*/
|
||||
public E get(int key) {
|
||||
return get(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Object mapped from the specified key, or the specified Object
|
||||
* if no such mapping has been made.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public E get(int key, E valueIfKeyNotFound) {
|
||||
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
|
||||
if (i < 0 || mValues[i] == DELETED) {
|
||||
return valueIfKeyNotFound;
|
||||
} else {
|
||||
return (E) mValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the mapping from the specified key, if there was any.
|
||||
*/
|
||||
public void delete(int key) {
|
||||
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
|
||||
if (i >= 0) {
|
||||
if (mValues[i] != DELETED) {
|
||||
mValues[i] = DELETED;
|
||||
mGarbage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for {@link #delete(int)}.
|
||||
*/
|
||||
public void remove(int key) {
|
||||
delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the mapping at the specified index.
|
||||
*/
|
||||
public void removeAt(int index) {
|
||||
if (mValues[index] != DELETED) {
|
||||
mValues[index] = DELETED;
|
||||
mGarbage = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a range of mappings as a batch.
|
||||
*
|
||||
* @param index Index to begin at
|
||||
* @param size Number of mappings to remove
|
||||
*/
|
||||
public void removeAtRange(int index, int size) {
|
||||
final int end = Math.min(mSize, index + size);
|
||||
for (int i = index; i < end; i++) {
|
||||
removeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void insertKeyRange(int keyStart, int count) {
|
||||
|
||||
}
|
||||
|
||||
public void removeKeyRange(ArrayList<E> removedItems, int keyStart, int count) {
|
||||
|
||||
}
|
||||
|
||||
private void gc() {
|
||||
// Log.e("SparseArray", "gc start with " + mSize);
|
||||
|
||||
int n = mSize;
|
||||
int o = 0;
|
||||
int[] keys = mKeys;
|
||||
Object[] values = mValues;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object val = values[i];
|
||||
|
||||
if (val != DELETED) {
|
||||
if (i != o) {
|
||||
keys[o] = keys[i];
|
||||
values[o] = val;
|
||||
values[i] = null;
|
||||
}
|
||||
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
mGarbage = false;
|
||||
mSize = o;
|
||||
|
||||
// Log.e("SparseArray", "gc end with " + mSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping from the specified key to the specified value,
|
||||
* replacing the previous mapping from the specified key if there
|
||||
* was one.
|
||||
*/
|
||||
public void put(int key, E value) {
|
||||
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
|
||||
if (i >= 0) {
|
||||
mValues[i] = value;
|
||||
} else {
|
||||
i = ~i;
|
||||
|
||||
if (i < mSize && mValues[i] == DELETED) {
|
||||
mKeys[i] = key;
|
||||
mValues[i] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mGarbage && mSize >= mKeys.length) {
|
||||
gc();
|
||||
|
||||
// Search again because indices may have changed.
|
||||
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
}
|
||||
|
||||
if (mSize >= mKeys.length) {
|
||||
int n = idealIntArraySize(mSize + 1);
|
||||
|
||||
int[] nkeys = new int[n];
|
||||
Object[] nvalues = new Object[n];
|
||||
|
||||
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
|
||||
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
|
||||
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
|
||||
|
||||
mKeys = nkeys;
|
||||
mValues = nvalues;
|
||||
}
|
||||
|
||||
if (mSize - i != 0) {
|
||||
// Log.e("SparseArray", "move " + (mSize - i));
|
||||
System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
|
||||
System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
|
||||
}
|
||||
|
||||
mKeys[i] = key;
|
||||
mValues[i] = value;
|
||||
mSize++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of key-value mappings that this SparseArray
|
||||
* currently stores.
|
||||
*/
|
||||
public int size() {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return mSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index in the range <code>0...size()-1</code>, returns
|
||||
* the key from the <code>index</code>th key-value mapping that this
|
||||
* SparseArray stores.
|
||||
*/
|
||||
public int keyAt(int index) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return mKeys[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index in the range <code>0...size()-1</code>, returns
|
||||
* the value from the <code>index</code>th key-value mapping that this
|
||||
* SparseArray stores.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public E valueAt(int index) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return (E) mValues[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an index in the range <code>0...size()-1</code>, sets a new
|
||||
* value for the <code>index</code>th key-value mapping that this
|
||||
* SparseArray stores.
|
||||
*/
|
||||
public void setValueAt(int index, E value) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
mValues[index] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index for which {@link #keyAt} would return the
|
||||
* specified key, or a negative number if the specified
|
||||
* key is not mapped.
|
||||
*/
|
||||
public int indexOfKey(int key) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
return ContainerHelpers.binarySearch(mKeys, mSize, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an index for which {@link #valueAt} would return the
|
||||
* specified key, or a negative number if no keys map to the
|
||||
* specified value.
|
||||
* <p>Beware that this is a linear search, unlike lookups by key,
|
||||
* and that multiple keys can map to the same value and this will
|
||||
* find only one of them.
|
||||
* <p>Note also that unlike most collections' {@code indexOf} methods,
|
||||
* this method compares values using {@code ==} rather than {@code equals}.
|
||||
*/
|
||||
public int indexOfValue(E value) {
|
||||
if (mGarbage) {
|
||||
gc();
|
||||
}
|
||||
|
||||
for (int i = 0; i < mSize; i++)
|
||||
if (mValues[i] == value)
|
||||
return i;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all key-value mappings from this SparseArray.
|
||||
*/
|
||||
public void clear() {
|
||||
int n = mSize;
|
||||
Object[] values = mValues;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
values[i] = null;
|
||||
}
|
||||
|
||||
mSize = 0;
|
||||
mGarbage = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts a key/value pair into the array, optimizing for the case where
|
||||
* the key is greater than all existing keys in the array.
|
||||
*/
|
||||
public void append(int key, E value) {
|
||||
if (mSize != 0 && key <= mKeys[mSize - 1]) {
|
||||
put(key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mGarbage && mSize >= mKeys.length) {
|
||||
gc();
|
||||
}
|
||||
|
||||
int pos = mSize;
|
||||
if (pos >= mKeys.length) {
|
||||
int n = idealIntArraySize(pos + 1);
|
||||
|
||||
int[] nkeys = new int[n];
|
||||
Object[] nvalues = new Object[n];
|
||||
|
||||
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
|
||||
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
|
||||
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
|
||||
|
||||
mKeys = nkeys;
|
||||
mValues = nvalues;
|
||||
}
|
||||
|
||||
mKeys[pos] = key;
|
||||
mValues[pos] = value;
|
||||
mSize = pos + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>This implementation composes a string by iterating over its mappings. If
|
||||
* this map contains itself as a value, the string "(this Map)"
|
||||
* will appear in its place.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (size() <= 0) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
StringBuilder buffer = new StringBuilder(mSize * 28);
|
||||
buffer.append('{');
|
||||
for (int i=0; i<mSize; i++) {
|
||||
if (i > 0) {
|
||||
buffer.append(", ");
|
||||
}
|
||||
int key = keyAt(i);
|
||||
buffer.append(key);
|
||||
buffer.append('=');
|
||||
Object value = valueAt(i);
|
||||
if (value != this) {
|
||||
buffer.append(value);
|
||||
} else {
|
||||
buffer.append("(this Map)");
|
||||
}
|
||||
}
|
||||
buffer.append('}');
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
static int idealByteArraySize(int need) {
|
||||
for (int i = 4; i < 32; i++)
|
||||
if (need <= (1 << i) - 12)
|
||||
return (1 << i) - 12;
|
||||
|
||||
return need;
|
||||
}
|
||||
|
||||
static int idealBooleanArraySize(int need) {
|
||||
return idealByteArraySize(need);
|
||||
}
|
||||
|
||||
static int idealShortArraySize(int need) {
|
||||
return idealByteArraySize(need * 2) / 2;
|
||||
}
|
||||
|
||||
static int idealCharArraySize(int need) {
|
||||
return idealByteArraySize(need * 2) / 2;
|
||||
}
|
||||
|
||||
static int idealIntArraySize(int need) {
|
||||
return idealByteArraySize(need * 4) / 4;
|
||||
}
|
||||
|
||||
static int idealFloatArraySize(int need) {
|
||||
return idealByteArraySize(need * 4) / 4;
|
||||
}
|
||||
|
||||
static int idealObjectArraySize(int need) {
|
||||
return idealByteArraySize(need * 4) / 4;
|
||||
}
|
||||
|
||||
static int idealLongArraySize(int need) {
|
||||
return idealByteArraySize(need * 8) / 8;
|
||||
}
|
||||
|
||||
static class ContainerHelpers {
|
||||
static final boolean[] EMPTY_BOOLEANS = new boolean[0];
|
||||
static final int[] EMPTY_INTS = new int[0];
|
||||
static final long[] EMPTY_LONGS = new long[0];
|
||||
static final Object[] EMPTY_OBJECTS = new Object[0];
|
||||
|
||||
// This is Arrays.binarySearch(), but doesn't do any argument validation.
|
||||
static int binarySearch(int[] array, int size, int value) {
|
||||
int lo = 0;
|
||||
int hi = size - 1;
|
||||
|
||||
while (lo <= hi) {
|
||||
final int mid = (lo + hi) >>> 1;
|
||||
final int midVal = array[mid];
|
||||
|
||||
if (midVal < value) {
|
||||
lo = mid + 1;
|
||||
} else if (midVal > value) {
|
||||
hi = mid - 1;
|
||||
} else {
|
||||
return mid; // value found
|
||||
}
|
||||
}
|
||||
return ~lo; // value not present
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.view.AccessibilityDelegateCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
/**
|
||||
* The AccessibilityDelegate used by RecyclerView.
|
||||
* <p>
|
||||
* This class handles basic accessibility actions and delegates them to LayoutManager.
|
||||
*/
|
||||
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
final RecyclerView mRecyclerView;
|
||||
|
||||
|
||||
public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName(RecyclerView.class.getName());
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
event.setClassName(RecyclerView.class.getName());
|
||||
if (host instanceof RecyclerView) {
|
||||
RecyclerView rv = (RecyclerView) host;
|
||||
if (rv.getLayoutManager() != null) {
|
||||
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityDelegateCompat getItemDelegate() {
|
||||
return mItemDelegate;
|
||||
}
|
||||
|
||||
final AccessibilityDelegateCompat mItemDelegate = new AccessibilityDelegateCompat() {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().
|
||||
onInitializeAccessibilityNodeInfoForItem(host, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().
|
||||
performAccessibilityActionForItem(host, action, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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 android.support.v7.widget;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A helper class to do scroll offset calculations.
|
||||
*/
|
||||
class ScrollbarHelper {
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled, boolean reverseLayout) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
final int minPosition = Math.min(lm.getPosition(startChild), lm.getPosition(endChild));
|
||||
final int maxPosition = Math.max(lm.getPosition(startChild), lm.getPosition(endChild));
|
||||
final int itemsBefore = reverseLayout
|
||||
? Math.max(0, state.getItemCount() - maxPosition - 1)
|
||||
: Math.max(0, minPosition - 1);
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return itemsBefore;
|
||||
}
|
||||
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) -
|
||||
orientation.getDecoratedStart(startChild));
|
||||
final int itemRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
final float avgSizePerRow = (float) laidOutArea / itemRange;
|
||||
|
||||
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
|
||||
- orientation.getDecoratedStart(startChild)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
}
|
||||
final int extend = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
return Math.min(orientation.getTotalSpace(), extend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return state.getItemCount();
|
||||
}
|
||||
// smooth scrollbar enabled. try to estimate better.
|
||||
final int laidOutArea = orientation.getDecoratedEnd(endChild) -
|
||||
orientation.getDecoratedStart(startChild);
|
||||
final int laidOutRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild))
|
||||
+ 1;
|
||||
// estimate a size for full list.
|
||||
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Error indicating that there was an authentication failure when performing a Request.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class AuthFailureError extends VolleyError {
|
||||
/** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
|
||||
private Intent mResolutionIntent;
|
||||
|
||||
public AuthFailureError() { }
|
||||
|
||||
public AuthFailureError(Intent intent) {
|
||||
mResolutionIntent = intent;
|
||||
}
|
||||
|
||||
public AuthFailureError(NetworkResponse response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public AuthFailureError(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthFailureError(String message, Exception reason) {
|
||||
super(message, reason);
|
||||
}
|
||||
|
||||
public Intent getResolutionIntent() {
|
||||
return mResolutionIntent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
if (mResolutionIntent != null) {
|
||||
return "User needs to (re)enter credentials.";
|
||||
}
|
||||
return super.getMessage();
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An interface for a cache keyed by a String with a byte array as data.
|
||||
*/
|
||||
public interface Cache {
|
||||
/**
|
||||
* Retrieves an entry from the cache.
|
||||
* @param key Cache key
|
||||
* @return An {@link Entry} or null in the event of a cache miss
|
||||
*/
|
||||
public Entry get(String key);
|
||||
|
||||
/**
|
||||
* Adds or replaces an entry to the cache.
|
||||
* @param key Cache key
|
||||
* @param entry Data to store and metadata for cache coherency, TTL, etc.
|
||||
*/
|
||||
public void put(String key, Entry entry);
|
||||
|
||||
/**
|
||||
* Performs any potentially long-running actions needed to initialize the cache;
|
||||
* will be called from a worker thread.
|
||||
*/
|
||||
public void initialize();
|
||||
|
||||
/**
|
||||
* Invalidates an entry in the cache.
|
||||
* @param key Cache key
|
||||
* @param fullExpire True to fully expire the entry, false to soft expire
|
||||
*/
|
||||
public void invalidate(String key, boolean fullExpire);
|
||||
|
||||
/**
|
||||
* Removes an entry from the cache.
|
||||
* @param key Cache key
|
||||
*/
|
||||
public void remove(String key);
|
||||
|
||||
/**
|
||||
* Empties the cache.
|
||||
*/
|
||||
public void clear();
|
||||
|
||||
/**
|
||||
* Data and metadata for an entry returned by the cache.
|
||||
*/
|
||||
public static class Entry {
|
||||
/** The data returned from cache. */
|
||||
public byte[] data;
|
||||
|
||||
/** ETag for cache coherency. */
|
||||
public String etag;
|
||||
|
||||
/** Date of this response as reported by the server. */
|
||||
public long serverDate;
|
||||
|
||||
/** TTL for this record. */
|
||||
public long ttl;
|
||||
|
||||
/** Soft TTL for this record. */
|
||||
public long softTtl;
|
||||
|
||||
/** Immutable response headers as received from server; must be non-null. */
|
||||
public Map<String, String> responseHeaders = Collections.emptyMap();
|
||||
|
||||
/** True if the entry is expired. */
|
||||
public boolean isExpired() {
|
||||
return this.ttl < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/** True if a refresh is needed from the original data source. */
|
||||
public boolean refreshNeeded() {
|
||||
return this.softTtl < System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* Provides a thread for performing cache triage on a queue of requests.
|
||||
*
|
||||
* Requests added to the specified cache queue are resolved from cache.
|
||||
* Any deliverable response is posted back to the caller via a
|
||||
* {@link ResponseDelivery}. Cache misses and responses that require
|
||||
* refresh are enqueued on the specified network queue for processing
|
||||
* by a {@link NetworkDispatcher}.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class CacheDispatcher extends Thread {
|
||||
|
||||
private static final boolean DEBUG = VolleyLog.DEBUG;
|
||||
|
||||
/** The queue of requests coming in for triage. */
|
||||
private final BlockingQueue<Request> mCacheQueue;
|
||||
|
||||
/** The queue of requests going out to the network. */
|
||||
private final BlockingQueue<Request> mNetworkQueue;
|
||||
|
||||
/** The cache to read from. */
|
||||
private final Cache mCache;
|
||||
|
||||
/** For posting responses. */
|
||||
private final ResponseDelivery mDelivery;
|
||||
|
||||
/** Used for telling us to die. */
|
||||
private volatile boolean mQuit = false;
|
||||
|
||||
/**
|
||||
* Creates a new cache triage dispatcher thread. You must call {@link #start()}
|
||||
* in order to begin processing.
|
||||
*
|
||||
* @param cacheQueue Queue of incoming requests for triage
|
||||
* @param networkQueue Queue to post requests that require network to
|
||||
* @param cache Cache interface to use for resolution
|
||||
* @param delivery Delivery interface to use for posting responses
|
||||
*/
|
||||
public CacheDispatcher(
|
||||
BlockingQueue<Request> cacheQueue, BlockingQueue<Request> networkQueue,
|
||||
Cache cache, ResponseDelivery delivery) {
|
||||
mCacheQueue = cacheQueue;
|
||||
mNetworkQueue = networkQueue;
|
||||
mCache = cache;
|
||||
mDelivery = delivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces this dispatcher to quit immediately. If any requests are still in
|
||||
* the queue, they are not guaranteed to be processed.
|
||||
*/
|
||||
public void quit() {
|
||||
mQuit = true;
|
||||
interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (DEBUG) VolleyLog.v("start new dispatcher");
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
|
||||
// Make a blocking call to initialize the cache.
|
||||
mCache.initialize();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
// Get a request from the cache triage queue, blocking until
|
||||
// at least one is available.
|
||||
final Request request = mCacheQueue.take();
|
||||
request.addMarker("cache-queue-take");
|
||||
|
||||
// If the request has been canceled, don't bother dispatching it.
|
||||
if (request.isCanceled()) {
|
||||
request.finish("cache-discard-canceled");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Attempt to retrieve this item from cache.
|
||||
Cache.Entry entry = mCache.get(request.getCacheKey());
|
||||
if (entry == null) {
|
||||
request.addMarker("cache-miss");
|
||||
// Cache miss; send off to the network dispatcher.
|
||||
mNetworkQueue.put(request);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it is completely expired, just send it to the network.
|
||||
if (entry.isExpired()) {
|
||||
request.addMarker("cache-hit-expired");
|
||||
request.setCacheEntry(entry);
|
||||
mNetworkQueue.put(request);
|
||||
continue;
|
||||
}
|
||||
|
||||
// We have a cache hit; parse its data for delivery back to the request.
|
||||
request.addMarker("cache-hit");
|
||||
Response<?> response = request.parseNetworkResponse(
|
||||
new NetworkResponse(entry.data, entry.responseHeaders));
|
||||
request.addMarker("cache-hit-parsed");
|
||||
|
||||
if (!entry.refreshNeeded()) {
|
||||
// Completely unexpired cache hit. Just deliver the response.
|
||||
mDelivery.postResponse(request, response);
|
||||
} else {
|
||||
// Soft-expired cache hit. We can deliver the cached response,
|
||||
// but we need to also send the request to the network for
|
||||
// refreshing.
|
||||
request.addMarker("cache-hit-refresh-needed");
|
||||
request.setCacheEntry(entry);
|
||||
|
||||
// Mark the response as intermediate.
|
||||
response.intermediate = true;
|
||||
|
||||
// Post the intermediate response back to the user and have
|
||||
// the delivery then forward the request along to the network.
|
||||
mDelivery.postResponse(request, response, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
mNetworkQueue.put(request);
|
||||
} catch (InterruptedException e) {
|
||||
// Not much we can do about this.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// We may have been interrupted because it was time to quit.
|
||||
if (mQuit) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Default retry policy for requests.
|
||||
*/
|
||||
public class DefaultRetryPolicy implements RetryPolicy {
|
||||
/** The current timeout in milliseconds. */
|
||||
private int mCurrentTimeoutMs;
|
||||
|
||||
/** The current retry count. */
|
||||
private int mCurrentRetryCount;
|
||||
|
||||
/** The maximum number of attempts. */
|
||||
private final int mMaxNumRetries;
|
||||
|
||||
/** The backoff multiplier for for the policy. */
|
||||
private final float mBackoffMultiplier;
|
||||
|
||||
/** The default socket timeout in milliseconds */
|
||||
public static final int DEFAULT_TIMEOUT_MS = 2500;
|
||||
|
||||
/** The default number of retries */
|
||||
public static final int DEFAULT_MAX_RETRIES = 1;
|
||||
|
||||
/** The default backoff multiplier */
|
||||
public static final float DEFAULT_BACKOFF_MULT = 1f;
|
||||
|
||||
/**
|
||||
* Constructs a new retry policy using the default timeouts.
|
||||
*/
|
||||
public DefaultRetryPolicy() {
|
||||
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new retry policy.
|
||||
* @param initialTimeoutMs The initial timeout for the policy.
|
||||
* @param maxNumRetries The maximum number of retries.
|
||||
* @param backoffMultiplier Backoff multiplier for the policy.
|
||||
*/
|
||||
public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
|
||||
mCurrentTimeoutMs = initialTimeoutMs;
|
||||
mMaxNumRetries = maxNumRetries;
|
||||
mBackoffMultiplier = backoffMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current timeout.
|
||||
*/
|
||||
@Override
|
||||
public int getCurrentTimeout() {
|
||||
return mCurrentTimeoutMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current retry count.
|
||||
*/
|
||||
@Override
|
||||
public int getCurrentRetryCount() {
|
||||
return mCurrentRetryCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for the next retry by applying a backoff to the timeout.
|
||||
* @param error The error code of the last attempt.
|
||||
*/
|
||||
@Override
|
||||
public void retry(VolleyError error) throws VolleyError {
|
||||
mCurrentRetryCount++;
|
||||
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
|
||||
if (!hasAttemptRemaining()) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this policy has attempts remaining, false otherwise.
|
||||
*/
|
||||
protected boolean hasAttemptRemaining() {
|
||||
return mCurrentRetryCount <= mMaxNumRetries;
|
||||
}
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Delivers responses and errors.
|
||||
*/
|
||||
public class ExecutorDelivery implements ResponseDelivery {
|
||||
/** Used for posting responses, typically to the main thread. */
|
||||
private final Executor mResponsePoster;
|
||||
|
||||
/**
|
||||
* Creates a new response delivery interface.
|
||||
* @param handler {@link Handler} to post responses on
|
||||
*/
|
||||
public ExecutorDelivery(final Handler handler) {
|
||||
// Make an Executor that just wraps the handler.
|
||||
mResponsePoster = new Executor() {
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
handler.post(command);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new response delivery interface, mockable version
|
||||
* for testing.
|
||||
* @param executor For running delivery tasks
|
||||
*/
|
||||
public ExecutorDelivery(Executor executor) {
|
||||
mResponsePoster = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postResponse(Request<?> request, Response<?> response) {
|
||||
postResponse(request, response, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
|
||||
request.markDelivered();
|
||||
request.addMarker("post-response");
|
||||
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postError(Request<?> request, VolleyError error) {
|
||||
request.addMarker("post-error");
|
||||
Response<?> response = Response.error(error);
|
||||
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* A Runnable used for delivering network responses to a listener on the
|
||||
* main thread.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
private class ResponseDeliveryRunnable implements Runnable {
|
||||
private final Request mRequest;
|
||||
private final Response mResponse;
|
||||
private final Runnable mRunnable;
|
||||
|
||||
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
|
||||
mRequest = request;
|
||||
mResponse = response;
|
||||
mRunnable = runnable;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void run() {
|
||||
// If this request has canceled, finish it and don't deliver.
|
||||
if (mRequest.isCanceled()) {
|
||||
mRequest.finish("canceled-at-delivery");
|
||||
return;
|
||||
}
|
||||
|
||||
// Deliver a normal response or error, depending.
|
||||
if (mResponse.isSuccess()) {
|
||||
mRequest.deliverResponse(mResponse.result);
|
||||
} else {
|
||||
mRequest.deliverError(mResponse.error);
|
||||
}
|
||||
|
||||
// If this is an intermediate response, add a marker, otherwise we're done
|
||||
// and the request can be finished.
|
||||
if (mResponse.intermediate) {
|
||||
mRequest.addMarker("intermediate-response");
|
||||
} else {
|
||||
mRequest.finish("done");
|
||||
}
|
||||
|
||||
// If we have been provided a post-delivery runnable, run it.
|
||||
if (mRunnable != null) {
|
||||
mRunnable.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* An interface for performing requests.
|
||||
*/
|
||||
public interface Network {
|
||||
/**
|
||||
* Performs the specified request.
|
||||
* @param request Request to process
|
||||
* @return A {@link NetworkResponse} with data and caching metadata; will never be null
|
||||
* @throws VolleyError on errors
|
||||
*/
|
||||
public NetworkResponse performRequest(Request<?> request) throws VolleyError;
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.net.TrafficStats;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* Provides a thread for performing network dispatch from a queue of requests.
|
||||
*
|
||||
* Requests added to the specified queue are processed from the network via a
|
||||
* specified {@link Network} interface. Responses are committed to cache, if
|
||||
* eligible, using a specified {@link Cache} interface. Valid responses and
|
||||
* errors are posted back to the caller via a {@link ResponseDelivery}.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class NetworkDispatcher extends Thread {
|
||||
/** The queue of requests to service. */
|
||||
private final BlockingQueue<Request> mQueue;
|
||||
/** The network interface for processing requests. */
|
||||
private final Network mNetwork;
|
||||
/** The cache to write to. */
|
||||
private final Cache mCache;
|
||||
/** For posting responses and errors. */
|
||||
private final ResponseDelivery mDelivery;
|
||||
/** Used for telling us to die. */
|
||||
private volatile boolean mQuit = false;
|
||||
|
||||
/**
|
||||
* Creates a new network dispatcher thread. You must call {@link #start()}
|
||||
* in order to begin processing.
|
||||
*
|
||||
* @param queue Queue of incoming requests for triage
|
||||
* @param network Network interface to use for performing requests
|
||||
* @param cache Cache interface to use for writing responses to cache
|
||||
* @param delivery Delivery interface to use for posting responses
|
||||
*/
|
||||
public NetworkDispatcher(BlockingQueue<Request> queue,
|
||||
Network network, Cache cache,
|
||||
ResponseDelivery delivery) {
|
||||
mQueue = queue;
|
||||
mNetwork = network;
|
||||
mCache = cache;
|
||||
mDelivery = delivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces this dispatcher to quit immediately. If any requests are still in
|
||||
* the queue, they are not guaranteed to be processed.
|
||||
*/
|
||||
public void quit() {
|
||||
mQuit = true;
|
||||
interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
Request request;
|
||||
while (true) {
|
||||
try {
|
||||
// Take a request from the queue.
|
||||
request = mQueue.take();
|
||||
} catch (InterruptedException e) {
|
||||
// We may have been interrupted because it was time to quit.
|
||||
if (mQuit) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
request.addMarker("network-queue-take");
|
||||
|
||||
// If the request was cancelled already, do not perform the
|
||||
// network request.
|
||||
if (request.isCanceled()) {
|
||||
request.finish("network-discard-cancelled");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tag the request (if API >= 14)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
|
||||
}
|
||||
|
||||
// Perform the network request.
|
||||
NetworkResponse networkResponse = mNetwork.performRequest(request);
|
||||
request.addMarker("network-http-complete");
|
||||
|
||||
// If the server returned 304 AND we delivered a response already,
|
||||
// we're done -- don't deliver a second identical response.
|
||||
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
|
||||
request.finish("not-modified");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the response here on the worker thread.
|
||||
Response<?> response = request.parseNetworkResponse(networkResponse);
|
||||
request.addMarker("network-parse-complete");
|
||||
|
||||
// Write to cache if applicable.
|
||||
// TODO: Only update cache metadata instead of entire record for 304s.
|
||||
if (request.shouldCache() && response.cacheEntry != null) {
|
||||
mCache.put(request.getCacheKey(), response.cacheEntry);
|
||||
request.addMarker("network-cache-written");
|
||||
}
|
||||
|
||||
// Post the response back.
|
||||
request.markDelivered();
|
||||
mDelivery.postResponse(request, response);
|
||||
} catch (VolleyError volleyError) {
|
||||
parseAndDeliverNetworkError(request, volleyError);
|
||||
} catch (Exception e) {
|
||||
VolleyLog.e(e, "Unhandled exception %s", e.toString());
|
||||
mDelivery.postError(request, new VolleyError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
|
||||
error = request.parseNetworkError(error);
|
||||
mDelivery.postError(request, error);
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that there was a network error when performing a Volley request.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class NetworkError extends VolleyError {
|
||||
public NetworkError() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NetworkError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public NetworkError(NetworkResponse networkResponse) {
|
||||
super(networkResponse);
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import org.apache.http.HttpStatus;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Data and headers returned from {@link Network#performRequest(Request)}.
|
||||
*/
|
||||
public class NetworkResponse {
|
||||
/**
|
||||
* Creates a new network response.
|
||||
* @param statusCode the HTTP status code
|
||||
* @param data Response body
|
||||
* @param headers Headers returned with this response, or null for none
|
||||
* @param notModified True if the server returned a 304 and the data was already in cache
|
||||
*/
|
||||
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
|
||||
boolean notModified) {
|
||||
this.statusCode = statusCode;
|
||||
this.data = data;
|
||||
this.headers = headers;
|
||||
this.notModified = notModified;
|
||||
}
|
||||
|
||||
public NetworkResponse(byte[] data) {
|
||||
this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false);
|
||||
}
|
||||
|
||||
public NetworkResponse(byte[] data, Map<String, String> headers) {
|
||||
this(HttpStatus.SC_OK, data, headers, false);
|
||||
}
|
||||
|
||||
/** The HTTP status code. */
|
||||
public final int statusCode;
|
||||
|
||||
/** Raw data from this response. */
|
||||
public final byte[] data;
|
||||
|
||||
/** Response headers. */
|
||||
public final Map<String, String> headers;
|
||||
|
||||
/** True if the server returned a 304 (Not Modified). */
|
||||
public final boolean notModified;
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Error indicating that no connection could be established when performing a Volley request.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class NoConnectionError extends NetworkError {
|
||||
public NoConnectionError() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NoConnectionError(Throwable reason) {
|
||||
super(reason);
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that the server's response could not be parsed.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ParseError extends VolleyError {
|
||||
public ParseError() { }
|
||||
|
||||
public ParseError(NetworkResponse networkResponse) {
|
||||
super(networkResponse);
|
||||
}
|
||||
|
||||
public ParseError(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@ -1,543 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.net.TrafficStats;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.volley.VolleyLog.MarkerLog;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Base class for all network requests.
|
||||
*
|
||||
* @param <T> The type of parsed response this request expects.
|
||||
*/
|
||||
public abstract class Request<T> implements Comparable<Request<T>> {
|
||||
|
||||
/**
|
||||
* Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}.
|
||||
*/
|
||||
private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
|
||||
|
||||
/**
|
||||
* Supported request methods.
|
||||
*/
|
||||
public interface Method {
|
||||
int DEPRECATED_GET_OR_POST = -1;
|
||||
int GET = 0;
|
||||
int POST = 1;
|
||||
int PUT = 2;
|
||||
int DELETE = 3;
|
||||
}
|
||||
|
||||
/** An event log tracing the lifetime of this request; for debugging. */
|
||||
private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
|
||||
|
||||
/** Request method of this request. Currently supports GET, POST, PUT, and DELETE. */
|
||||
private final int mMethod;
|
||||
|
||||
/** URL of this request. */
|
||||
private final String mUrl;
|
||||
|
||||
/** Default tag for {@link TrafficStats}. */
|
||||
private final int mDefaultTrafficStatsTag;
|
||||
|
||||
/** Listener interface for errors. */
|
||||
private final Response.ErrorListener mErrorListener;
|
||||
|
||||
/** Sequence number of this request, used to enforce FIFO ordering. */
|
||||
private Integer mSequence;
|
||||
|
||||
/** The request queue this request is associated with. */
|
||||
private RequestQueue mRequestQueue;
|
||||
|
||||
/** Whether or not responses to this request should be cached. */
|
||||
private boolean mShouldCache = true;
|
||||
|
||||
/** Whether or not this request has been canceled. */
|
||||
private boolean mCanceled = false;
|
||||
|
||||
/** Whether or not a response has been delivered for this request yet. */
|
||||
private boolean mResponseDelivered = false;
|
||||
|
||||
// A cheap variant of request tracing used to dump slow requests.
|
||||
private long mRequestBirthTime = 0;
|
||||
|
||||
/** Threshold at which we should log the request (even when debug logging is not enabled). */
|
||||
private static final long SLOW_REQUEST_THRESHOLD_MS = 3000;
|
||||
|
||||
/** The retry policy for this request. */
|
||||
private RetryPolicy mRetryPolicy;
|
||||
|
||||
/**
|
||||
* When a request can be retrieved from cache but must be refreshed from
|
||||
* the network, the cache entry will be stored here so that in the event of
|
||||
* a "Not Modified" response, we can be sure it hasn't been evicted from cache.
|
||||
*/
|
||||
private Cache.Entry mCacheEntry = null;
|
||||
|
||||
/** An opaque token tagging this request; used for bulk cancellation. */
|
||||
private Object mTag;
|
||||
|
||||
/**
|
||||
* Creates a new request with the given URL and error listener. Note that
|
||||
* the normal response listener is not provided here as delivery of responses
|
||||
* is provided by subclasses, who have a better idea of how to deliver an
|
||||
* already-parsed response.
|
||||
*
|
||||
* @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}.
|
||||
*/
|
||||
public Request(String url, Response.ErrorListener listener) {
|
||||
this(Method.DEPRECATED_GET_OR_POST, url, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new request with the given method (one of the values from {@link Method}),
|
||||
* URL, and error listener. Note that the normal response listener is not provided here as
|
||||
* delivery of responses is provided by subclasses, who have a better idea of how to deliver
|
||||
* an already-parsed response.
|
||||
*/
|
||||
public Request(int method, String url, Response.ErrorListener listener) {
|
||||
mMethod = method;
|
||||
mUrl = url;
|
||||
mErrorListener = listener;
|
||||
setRetryPolicy(new DefaultRetryPolicy());
|
||||
|
||||
mDefaultTrafficStatsTag = TextUtils.isEmpty(url) ? 0: Uri.parse(url).getHost().hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method for this request. Can be one of the values in {@link Method}.
|
||||
*/
|
||||
public int getMethod() {
|
||||
return mMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a tag on this request. Can be used to cancel all requests with this
|
||||
* tag by {@link RequestQueue#cancelAll(Object)}.
|
||||
*/
|
||||
public void setTag(Object tag) {
|
||||
mTag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this request's tag.
|
||||
* @see Request#setTag(Object)
|
||||
*/
|
||||
public Object getTag() {
|
||||
return mTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)}
|
||||
*/
|
||||
public int getTrafficStatsTag() {
|
||||
return mDefaultTrafficStatsTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the retry policy for this request.
|
||||
*/
|
||||
public void setRetryPolicy(RetryPolicy retryPolicy) {
|
||||
mRetryPolicy = retryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event to this request's event log; for debugging.
|
||||
*/
|
||||
public void addMarker(String tag) {
|
||||
if (MarkerLog.ENABLED) {
|
||||
mEventLog.add(tag, Thread.currentThread().getId());
|
||||
} else if (mRequestBirthTime == 0) {
|
||||
mRequestBirthTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the request queue that this request has finished (successfully or with error).
|
||||
*
|
||||
* <p>Also dumps all events from this request's event log; for debugging.</p>
|
||||
*/
|
||||
void finish(final String tag) {
|
||||
if (mRequestQueue != null) {
|
||||
mRequestQueue.finish(this);
|
||||
}
|
||||
if (MarkerLog.ENABLED) {
|
||||
final long threadId = Thread.currentThread().getId();
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
// If we finish marking off of the main thread, we need to
|
||||
// actually do it on the main thread to ensure correct ordering.
|
||||
Handler mainThread = new Handler(Looper.getMainLooper());
|
||||
mainThread.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mEventLog.add(tag, threadId);
|
||||
mEventLog.finish(this.toString());
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
mEventLog.add(tag, threadId);
|
||||
mEventLog.finish(this.toString());
|
||||
} else {
|
||||
long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
|
||||
if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
|
||||
VolleyLog.d("%d ms: %s", requestTime, this.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates this request with the given queue. The request queue will be notified when this
|
||||
* request has finished.
|
||||
*/
|
||||
public void setRequestQueue(RequestQueue requestQueue) {
|
||||
mRequestQueue = requestQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sequence number of this request. Used by {@link RequestQueue}.
|
||||
*/
|
||||
public final void setSequence(int sequence) {
|
||||
mSequence = sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sequence number of this request.
|
||||
*/
|
||||
public final int getSequence() {
|
||||
if (mSequence == null) {
|
||||
throw new IllegalStateException("getSequence called before setSequence");
|
||||
}
|
||||
return mSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of this request.
|
||||
*/
|
||||
public String getUrl() {
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache key for this request. By default, this is the URL.
|
||||
*/
|
||||
public String getCacheKey() {
|
||||
return getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Annotates this request with an entry retrieved for it from cache.
|
||||
* Used for cache coherency support.
|
||||
*/
|
||||
public void setCacheEntry(Cache.Entry entry) {
|
||||
mCacheEntry = entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the annotated cache entry, or null if there isn't one.
|
||||
*/
|
||||
public Cache.Entry getCacheEntry() {
|
||||
return mCacheEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this request as canceled. No callback will be delivered.
|
||||
*/
|
||||
public void cancel() {
|
||||
mCanceled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this request has been canceled.
|
||||
*/
|
||||
public boolean isCanceled() {
|
||||
return mCanceled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of extra HTTP headers to go along with this request. Can
|
||||
* throw {@link AuthFailureError} as authentication may be required to
|
||||
* provide these values.
|
||||
* @throws AuthFailureError In the event of auth failure
|
||||
*/
|
||||
public Map<String, String> getHeaders() throws AuthFailureError {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map of POST parameters to be used for this request, or null if
|
||||
* a simple GET should be used. Can throw {@link AuthFailureError} as
|
||||
* authentication may be required to provide these values.
|
||||
*
|
||||
* <p>Note that only one of getPostParams() and getPostBody() can return a non-null
|
||||
* value.</p>
|
||||
* @throws AuthFailureError In the event of auth failure
|
||||
*
|
||||
* @deprecated Use {@link #getParams()} instead.
|
||||
*/
|
||||
protected Map<String, String> getPostParams() throws AuthFailureError {
|
||||
return getParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which encoding should be used when converting POST parameters returned by
|
||||
* {@link #getPostParams()} into a raw POST body.
|
||||
*
|
||||
* <p>This controls both encodings:
|
||||
* <ol>
|
||||
* <li>The string encoding used when converting parameter names and values into bytes prior
|
||||
* to URL encoding them.</li>
|
||||
* <li>The string encoding used when converting the URL encoded parameters into a raw
|
||||
* byte array.</li>
|
||||
* </ol>
|
||||
*
|
||||
* @deprecated Use {@link #getParamsEncoding()} instead.
|
||||
*/
|
||||
protected String getPostParamsEncoding() {
|
||||
return getParamsEncoding();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getBodyContentType()} instead.
|
||||
*/
|
||||
public String getPostBodyContentType() {
|
||||
return getBodyContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw POST body to be sent.
|
||||
*
|
||||
* @throws AuthFailureError In the event of auth failure
|
||||
*
|
||||
* @deprecated Use {@link #getBody()} instead.
|
||||
*/
|
||||
public byte[] getPostBody() throws AuthFailureError {
|
||||
// Note: For compatibility with legacy clients of volley, this implementation must remain
|
||||
// here instead of simply calling the getBody() function because this function must
|
||||
// call getPostParams() and getPostParamsEncoding() since legacy clients would have
|
||||
// overridden these two member functions for POST requests.
|
||||
Map<String, String> postParams = getPostParams();
|
||||
if (postParams != null && postParams.size() > 0) {
|
||||
return encodeParameters(postParams, getPostParamsEncoding());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Map of parameters to be used for a POST or PUT request. Can throw
|
||||
* {@link AuthFailureError} as authentication may be required to provide these values.
|
||||
*
|
||||
* <p>Note that you can directly override {@link #getBody()} for custom data.</p>
|
||||
*
|
||||
* @throws AuthFailureError in the event of auth failure
|
||||
*/
|
||||
protected Map<String, String> getParams() throws AuthFailureError {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which encoding should be used when converting POST or PUT parameters returned by
|
||||
* {@link #getParams()} into a raw POST or PUT body.
|
||||
*
|
||||
* <p>This controls both encodings:
|
||||
* <ol>
|
||||
* <li>The string encoding used when converting parameter names and values into bytes prior
|
||||
* to URL encoding them.</li>
|
||||
* <li>The string encoding used when converting the URL encoded parameters into a raw
|
||||
* byte array.</li>
|
||||
* </ol>
|
||||
*/
|
||||
protected String getParamsEncoding() {
|
||||
return DEFAULT_PARAMS_ENCODING;
|
||||
}
|
||||
|
||||
public String getBodyContentType() {
|
||||
return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw POST or PUT body to be sent.
|
||||
*
|
||||
* @throws AuthFailureError in the event of auth failure
|
||||
*/
|
||||
public byte[] getBody() throws AuthFailureError {
|
||||
Map<String, String> params = getParams();
|
||||
if (params != null && params.size() > 0) {
|
||||
return encodeParameters(params, getParamsEncoding());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts <code>params</code> into an application/x-www-form-urlencoded encoded string.
|
||||
*/
|
||||
private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
|
||||
StringBuilder encodedParams = new StringBuilder();
|
||||
try {
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
|
||||
encodedParams.append('=');
|
||||
encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
|
||||
encodedParams.append('&');
|
||||
}
|
||||
return encodedParams.toString().getBytes(paramsEncoding);
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not responses to this request should be cached.
|
||||
*/
|
||||
public final void setShouldCache(boolean shouldCache) {
|
||||
mShouldCache = shouldCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if responses to this request should be cached.
|
||||
*/
|
||||
public final boolean shouldCache() {
|
||||
return mShouldCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority values. Requests will be processed from higher priorities to
|
||||
* lower priorities, in FIFO order.
|
||||
*/
|
||||
public enum Priority {
|
||||
LOW,
|
||||
NORMAL,
|
||||
HIGH,
|
||||
IMMEDIATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
|
||||
*/
|
||||
public Priority getPriority() {
|
||||
return Priority.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the socket timeout in milliseconds per retry attempt. (This value can be changed
|
||||
* per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry
|
||||
* attempts remaining, this will cause delivery of a {@link TimeoutError} error.
|
||||
*/
|
||||
public final int getTimeoutMs() {
|
||||
return mRetryPolicy.getCurrentTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the retry policy that should be used for this request.
|
||||
*/
|
||||
public RetryPolicy getRetryPolicy() {
|
||||
return mRetryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this request as having a response delivered on it. This can be used
|
||||
* later in the request's lifetime for suppressing identical responses.
|
||||
*/
|
||||
public void markDelivered() {
|
||||
mResponseDelivered = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this request has had a response delivered for it.
|
||||
*/
|
||||
public boolean hasHadResponseDelivered() {
|
||||
return mResponseDelivered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must implement this to parse the raw network response
|
||||
* and return an appropriate response type. This method will be
|
||||
* called from a worker thread. The response will not be delivered
|
||||
* if you return null.
|
||||
* @param response Response from the network
|
||||
* @return The parsed response, or null in the case of an error
|
||||
*/
|
||||
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
|
||||
|
||||
/**
|
||||
* Subclasses can override this method to parse 'networkError' and return a more specific error.
|
||||
*
|
||||
* <p>The default implementation just returns the passed 'networkError'.</p>
|
||||
*
|
||||
* @param volleyError the error retrieved from the network
|
||||
* @return an NetworkError augmented with additional information
|
||||
*/
|
||||
protected VolleyError parseNetworkError(VolleyError volleyError) {
|
||||
return volleyError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must implement this to perform delivery of the parsed
|
||||
* response to their listeners. The given response is guaranteed to
|
||||
* be non-null; responses that fail to parse are not delivered.
|
||||
* @param response The parsed response returned by
|
||||
* {@link #parseNetworkResponse(NetworkResponse)}
|
||||
*/
|
||||
abstract protected void deliverResponse(T response);
|
||||
|
||||
/**
|
||||
* Delivers error message to the ErrorListener that the Request was
|
||||
* initialized with.
|
||||
*
|
||||
* @param error Error details
|
||||
*/
|
||||
public void deliverError(VolleyError error) {
|
||||
if (mErrorListener != null) {
|
||||
mErrorListener.onErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Our comparator sorts from high to low priority, and secondarily by
|
||||
* sequence number to provide FIFO ordering.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Request<T> other) {
|
||||
Priority left = this.getPriority();
|
||||
Priority right = other.getPriority();
|
||||
|
||||
// High-priority requests are "lesser" so they are sorted to the front.
|
||||
// Equal priorities are sorted by sequence number to provide FIFO ordering.
|
||||
return left == right ?
|
||||
this.mSequence - other.mSequence :
|
||||
right.ordinal() - left.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag());
|
||||
return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + trafficStatsTag + " "
|
||||
+ getPriority() + " " + mSequence;
|
||||
}
|
||||
}
|
||||
@ -1,287 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* A request dispatch queue with a thread pool of dispatchers.
|
||||
*
|
||||
* Calling {@link #add(Request)} will enqueue the given Request for dispatch,
|
||||
* resolving from either cache or network on a worker thread, and then delivering
|
||||
* a parsed response on the main thread.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class RequestQueue {
|
||||
|
||||
/** Used for generating monotonically-increasing sequence numbers for requests. */
|
||||
private AtomicInteger mSequenceGenerator = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* Staging area for requests that already have a duplicate request in flight.
|
||||
*
|
||||
* <ul>
|
||||
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
|
||||
* key.</li>
|
||||
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
|
||||
* is <em>not</em> contained in that list. Is null if no requests are staged.</li>
|
||||
* </ul>
|
||||
*/
|
||||
private final Map<String, Queue<Request>> mWaitingRequests =
|
||||
new HashMap<String, Queue<Request>>();
|
||||
|
||||
/**
|
||||
* The set of all requests currently being processed by this RequestQueue. A Request
|
||||
* will be in this set if it is waiting in any queue or currently being processed by
|
||||
* any dispatcher.
|
||||
*/
|
||||
private final Set<Request> mCurrentRequests = new HashSet<Request>();
|
||||
|
||||
/** The cache triage queue. */
|
||||
private final PriorityBlockingQueue<Request> mCacheQueue =
|
||||
new PriorityBlockingQueue<Request>();
|
||||
|
||||
/** The queue of requests that are actually going out to the network. */
|
||||
private final PriorityBlockingQueue<Request> mNetworkQueue =
|
||||
new PriorityBlockingQueue<Request>();
|
||||
|
||||
/** Number of network request dispatcher threads to start. */
|
||||
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
|
||||
|
||||
/** Cache interface for retrieving and storing respones. */
|
||||
private final Cache mCache;
|
||||
|
||||
/** Network interface for performing requests. */
|
||||
private final Network mNetwork;
|
||||
|
||||
/** Response delivery mechanism. */
|
||||
private final ResponseDelivery mDelivery;
|
||||
|
||||
/** The network dispatchers. */
|
||||
private NetworkDispatcher[] mDispatchers;
|
||||
|
||||
/** The cache dispatcher. */
|
||||
private CacheDispatcher mCacheDispatcher;
|
||||
|
||||
/**
|
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
|
||||
*
|
||||
* @param cache A Cache to use for persisting responses to disk
|
||||
* @param network A Network interface for performing HTTP requests
|
||||
* @param threadPoolSize Number of network dispatcher threads to create
|
||||
* @param delivery A ResponseDelivery interface for posting responses and errors
|
||||
*/
|
||||
public RequestQueue(Cache cache, Network network, int threadPoolSize,
|
||||
ResponseDelivery delivery) {
|
||||
mCache = cache;
|
||||
mNetwork = network;
|
||||
mDispatchers = new NetworkDispatcher[threadPoolSize];
|
||||
mDelivery = delivery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
|
||||
*
|
||||
* @param cache A Cache to use for persisting responses to disk
|
||||
* @param network A Network interface for performing HTTP requests
|
||||
* @param threadPoolSize Number of network dispatcher threads to create
|
||||
*/
|
||||
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
|
||||
this(cache, network, threadPoolSize,
|
||||
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
|
||||
*
|
||||
* @param cache A Cache to use for persisting responses to disk
|
||||
* @param network A Network interface for performing HTTP requests
|
||||
*/
|
||||
public RequestQueue(Cache cache, Network network) {
|
||||
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the dispatchers in this queue.
|
||||
*/
|
||||
public void start() {
|
||||
stop(); // Make sure any currently running dispatchers are stopped.
|
||||
// Create the cache dispatcher and start it.
|
||||
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
|
||||
mCacheDispatcher.start();
|
||||
|
||||
// Create network dispatchers (and corresponding threads) up to the pool size.
|
||||
for (int i = 0; i < mDispatchers.length; i++) {
|
||||
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
|
||||
mCache, mDelivery);
|
||||
mDispatchers[i] = networkDispatcher;
|
||||
networkDispatcher.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the cache and network dispatchers.
|
||||
*/
|
||||
public void stop() {
|
||||
if (mCacheDispatcher != null) {
|
||||
mCacheDispatcher.quit();
|
||||
}
|
||||
for (int i = 0; i < mDispatchers.length; i++) {
|
||||
if (mDispatchers[i] != null) {
|
||||
mDispatchers[i].quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a sequence number.
|
||||
*/
|
||||
public int getSequenceNumber() {
|
||||
return mSequenceGenerator.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Cache} instance being used.
|
||||
*/
|
||||
public Cache getCache() {
|
||||
return mCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple predicate or filter interface for Requests, for use by
|
||||
* {@link RequestQueue#cancelAll(RequestFilter)}.
|
||||
*/
|
||||
public interface RequestFilter {
|
||||
public boolean apply(Request<?> request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all requests in this queue for which the given filter applies.
|
||||
* @param filter The filtering function to use
|
||||
*/
|
||||
public void cancelAll(RequestFilter filter) {
|
||||
synchronized (mCurrentRequests) {
|
||||
for (Request<?> request : mCurrentRequests) {
|
||||
if (filter.apply(request)) {
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all requests in this queue with the given tag. Tag must be non-null
|
||||
* and equality is by identity.
|
||||
*/
|
||||
public void cancelAll(final Object tag) {
|
||||
if (tag == null) {
|
||||
throw new IllegalArgumentException("Cannot cancelAll with a null tag");
|
||||
}
|
||||
cancelAll(new RequestFilter() {
|
||||
@Override
|
||||
public boolean apply(Request<?> request) {
|
||||
return request.getTag() == tag;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Request to the dispatch queue.
|
||||
* @param request The request to service
|
||||
* @return The passed-in request
|
||||
*/
|
||||
public Request add(Request request) {
|
||||
// Tag the request as belonging to this queue and add it to the set of current requests.
|
||||
request.setRequestQueue(this);
|
||||
synchronized (mCurrentRequests) {
|
||||
mCurrentRequests.add(request);
|
||||
}
|
||||
|
||||
// Process requests in the order they are added.
|
||||
request.setSequence(getSequenceNumber());
|
||||
request.addMarker("add-to-queue");
|
||||
|
||||
// If the request is uncacheable, skip the cache queue and go straight to the network.
|
||||
if (!request.shouldCache()) {
|
||||
mNetworkQueue.add(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
// Insert request into stage if there's already a request with the same cache key in flight.
|
||||
synchronized (mWaitingRequests) {
|
||||
String cacheKey = request.getCacheKey();
|
||||
if (mWaitingRequests.containsKey(cacheKey)) {
|
||||
// There is already a request in flight. Queue up.
|
||||
Queue<Request> stagedRequests = mWaitingRequests.get(cacheKey);
|
||||
if (stagedRequests == null) {
|
||||
stagedRequests = new LinkedList<Request>();
|
||||
}
|
||||
stagedRequests.add(request);
|
||||
mWaitingRequests.put(cacheKey, stagedRequests);
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
|
||||
}
|
||||
} else {
|
||||
// Insert 'null' queue for this cacheKey, indicating there is now a request in
|
||||
// flight.
|
||||
mWaitingRequests.put(cacheKey, null);
|
||||
mCacheQueue.add(request);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link Request#finish(String)}, indicating that processing of the given request
|
||||
* has finished.
|
||||
*
|
||||
* <p>Releases waiting requests for <code>request.getCacheKey()</code> if
|
||||
* <code>request.shouldCache()</code>.</p>
|
||||
*/
|
||||
void finish(Request request) {
|
||||
// Remove from the set of requests currently being processed.
|
||||
synchronized (mCurrentRequests) {
|
||||
mCurrentRequests.remove(request);
|
||||
}
|
||||
|
||||
if (request.shouldCache()) {
|
||||
synchronized (mWaitingRequests) {
|
||||
String cacheKey = request.getCacheKey();
|
||||
Queue<Request> waitingRequests = mWaitingRequests.remove(cacheKey);
|
||||
if (waitingRequests != null) {
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
|
||||
waitingRequests.size(), cacheKey);
|
||||
}
|
||||
// Process all queued up requests. They won't be considered as in flight, but
|
||||
// that's not a problem as the cache has been primed by 'request'.
|
||||
mCacheQueue.addAll(waitingRequests);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Encapsulates a parsed response for delivery.
|
||||
*
|
||||
* @param <T> Parsed type of this response
|
||||
*/
|
||||
public class Response<T> {
|
||||
|
||||
/** Callback interface for delivering parsed responses. */
|
||||
public interface Listener<T> {
|
||||
/** Called when a response is received. */
|
||||
public void onResponse(T response);
|
||||
}
|
||||
|
||||
/** Callback interface for delivering error responses. */
|
||||
public interface ErrorListener {
|
||||
/**
|
||||
* Callback method that an error has been occurred with the
|
||||
* provided error code and optional user-readable message.
|
||||
*/
|
||||
public void onErrorResponse(VolleyError error);
|
||||
}
|
||||
|
||||
/** Returns a successful response containing the parsed result. */
|
||||
public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
|
||||
return new Response<T>(result, cacheEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a failed response containing the given error code and an optional
|
||||
* localized message displayed to the user.
|
||||
*/
|
||||
public static <T> Response<T> error(VolleyError error) {
|
||||
return new Response<T>(error);
|
||||
}
|
||||
|
||||
/** Parsed response, or null in the case of error. */
|
||||
public final T result;
|
||||
|
||||
/** Cache metadata for this response, or null in the case of error. */
|
||||
public final Cache.Entry cacheEntry;
|
||||
|
||||
/** Detailed error information if <code>errorCode != OK</code>. */
|
||||
public final VolleyError error;
|
||||
|
||||
/** True if this response was a soft-expired one and a second one MAY be coming. */
|
||||
public boolean intermediate = false;
|
||||
|
||||
/**
|
||||
* Returns whether this response is considered successful.
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return error == null;
|
||||
}
|
||||
|
||||
|
||||
private Response(T result, Cache.Entry cacheEntry) {
|
||||
this.result = result;
|
||||
this.cacheEntry = cacheEntry;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
private Response(VolleyError error) {
|
||||
this.result = null;
|
||||
this.cacheEntry = null;
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
public interface ResponseDelivery {
|
||||
/**
|
||||
* Parses a response from the network or cache and delivers it.
|
||||
*/
|
||||
public void postResponse(Request<?> request, Response<?> response);
|
||||
|
||||
/**
|
||||
* Parses a response from the network or cache and delivers it. The provided
|
||||
* Runnable will be executed after delivery.
|
||||
*/
|
||||
public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
|
||||
|
||||
/**
|
||||
* Posts an error for the given request.
|
||||
*/
|
||||
public void postError(Request<?> request, VolleyError error);
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Retry policy for a request.
|
||||
*/
|
||||
public interface RetryPolicy {
|
||||
|
||||
/**
|
||||
* Returns the current timeout (used for logging).
|
||||
*/
|
||||
public int getCurrentTimeout();
|
||||
|
||||
/**
|
||||
* Returns the current retry count (used for logging).
|
||||
*/
|
||||
public int getCurrentRetryCount();
|
||||
|
||||
/**
|
||||
* Prepares for the next retry by applying a backoff to the timeout.
|
||||
* @param error The error code of the last attempt.
|
||||
* @throws VolleyError In the event that the retry could not be performed (for example if we
|
||||
* ran out of attempts), the passed in error is thrown.
|
||||
*/
|
||||
public void retry(VolleyError error) throws VolleyError;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates that the error responded with an error response.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ServerError extends VolleyError {
|
||||
public ServerError(NetworkResponse networkResponse) {
|
||||
super(networkResponse);
|
||||
}
|
||||
|
||||
public ServerError() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Indicates that the connection or the socket timed out.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class TimeoutError extends VolleyError { }
|
||||
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
/**
|
||||
* Exception style class encapsulating Volley errors
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class VolleyError extends Exception {
|
||||
public final NetworkResponse networkResponse;
|
||||
|
||||
public VolleyError() {
|
||||
networkResponse = null;
|
||||
}
|
||||
|
||||
public VolleyError(NetworkResponse response) {
|
||||
networkResponse = response;
|
||||
}
|
||||
|
||||
public VolleyError(String exceptionMessage) {
|
||||
super(exceptionMessage);
|
||||
networkResponse = null;
|
||||
}
|
||||
|
||||
public VolleyError(String exceptionMessage, Throwable reason) {
|
||||
super(exceptionMessage, reason);
|
||||
networkResponse = null;
|
||||
}
|
||||
|
||||
public VolleyError(Throwable cause) {
|
||||
super(cause);
|
||||
networkResponse = null;
|
||||
}
|
||||
}
|
||||
@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Logging helper class. */
|
||||
public class VolleyLog {
|
||||
public static String TAG = "Volley";
|
||||
|
||||
public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
|
||||
|
||||
/**
|
||||
* Customize the log tag for your application, so that other apps
|
||||
* using Volley don't mix their logs with yours.
|
||||
* <br />
|
||||
* Enable the log property for your tag before starting your app:
|
||||
* <br />
|
||||
* {@code adb shell setprop log.tag.<tag>}
|
||||
*/
|
||||
public static void setTag(String tag) {
|
||||
d("Changing log tag to %s", tag);
|
||||
TAG = tag;
|
||||
|
||||
// Reinitialize the DEBUG "constant"
|
||||
DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
|
||||
}
|
||||
|
||||
public static void v(String format, Object... args) {
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, buildMessage(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
public static void d(String format, Object... args) {
|
||||
Log.d(TAG, buildMessage(format, args));
|
||||
}
|
||||
|
||||
public static void e(String format, Object... args) {
|
||||
Log.e(TAG, buildMessage(format, args));
|
||||
}
|
||||
|
||||
public static void e(Throwable tr, String format, Object... args) {
|
||||
Log.e(TAG, buildMessage(format, args), tr);
|
||||
}
|
||||
|
||||
public static void wtf(String format, Object... args) {
|
||||
Log.wtf(TAG, buildMessage(format, args));
|
||||
}
|
||||
|
||||
public static void wtf(Throwable tr, String format, Object... args) {
|
||||
Log.wtf(TAG, buildMessage(format, args), tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the caller's provided message and prepends useful info like
|
||||
* calling thread ID and method name.
|
||||
*/
|
||||
private static String buildMessage(String format, Object... args) {
|
||||
String msg = (args == null) ? format : String.format(Locale.US, format, args);
|
||||
StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();
|
||||
|
||||
String caller = "<unknown>";
|
||||
// Walk up the stack looking for the first caller outside of VolleyLog.
|
||||
// It will be at least two frames up, so start there.
|
||||
for (int i = 2; i < trace.length; i++) {
|
||||
Class<?> clazz = trace[i].getClass();
|
||||
if (!clazz.equals(VolleyLog.class)) {
|
||||
String callingClass = trace[i].getClassName();
|
||||
callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
|
||||
callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);
|
||||
|
||||
caller = callingClass + "." + trace[i].getMethodName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return String.format(Locale.US, "[%d] %s: %s",
|
||||
Thread.currentThread().getId(), caller, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple event log with records containing a name, thread ID, and timestamp.
|
||||
*/
|
||||
static class MarkerLog {
|
||||
public static final boolean ENABLED = VolleyLog.DEBUG;
|
||||
|
||||
/** Minimum duration from first marker to last in an marker log to warrant logging. */
|
||||
private static final long MIN_DURATION_FOR_LOGGING_MS = 0;
|
||||
|
||||
private static class Marker {
|
||||
public final String name;
|
||||
public final long thread;
|
||||
public final long time;
|
||||
|
||||
public Marker(String name, long thread, long time) {
|
||||
this.name = name;
|
||||
this.thread = thread;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
private final List<Marker> mMarkers = new ArrayList<Marker>();
|
||||
private boolean mFinished = false;
|
||||
|
||||
/** Adds a marker to this log with the specified name. */
|
||||
public synchronized void add(String name, long threadId) {
|
||||
if (mFinished) {
|
||||
throw new IllegalStateException("Marker added to finished log");
|
||||
}
|
||||
|
||||
mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the log, dumping it to logcat if the time difference between
|
||||
* the first and last markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}.
|
||||
* @param header Header string to print above the marker log.
|
||||
*/
|
||||
public synchronized void finish(String header) {
|
||||
mFinished = true;
|
||||
|
||||
long duration = getTotalDuration();
|
||||
if (duration <= MIN_DURATION_FOR_LOGGING_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
long prevTime = mMarkers.get(0).time;
|
||||
d("(%-4d ms) %s", duration, header);
|
||||
for (Marker marker : mMarkers) {
|
||||
long thisTime = marker.time;
|
||||
d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name);
|
||||
prevTime = thisTime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
// Catch requests that have been collected (and hence end-of-lifed)
|
||||
// but had no debugging output printed for them.
|
||||
if (!mFinished) {
|
||||
finish("Request on the loose");
|
||||
e("Marker log finalized without finish() - uncaught exit point for request");
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the time difference between the first and last events in this log. */
|
||||
private long getTotalDuration() {
|
||||
if (mMarkers.size() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long first = mMarkers.get(0).time;
|
||||
long last = mMarkers.get(mMarkers.size() - 1).time;
|
||||
return last - first;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AccountManagerFuture;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
|
||||
/**
|
||||
* An Authenticator that uses {@link AccountManager} to get auth
|
||||
* tokens of a specified type for a specified account.
|
||||
*/
|
||||
public class AndroidAuthenticator implements Authenticator {
|
||||
private final Context mContext;
|
||||
private final Account mAccount;
|
||||
private final String mAuthTokenType;
|
||||
private final boolean mNotifyAuthFailure;
|
||||
|
||||
/**
|
||||
* Creates a new authenticator.
|
||||
* @param context Context for accessing AccountManager
|
||||
* @param account Account to authenticate as
|
||||
* @param authTokenType Auth token type passed to AccountManager
|
||||
*/
|
||||
public AndroidAuthenticator(Context context, Account account, String authTokenType) {
|
||||
this(context, account, authTokenType, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new authenticator.
|
||||
* @param context Context for accessing AccountManager
|
||||
* @param account Account to authenticate as
|
||||
* @param authTokenType Auth token type passed to AccountManager
|
||||
* @param notifyAuthFailure Whether to raise a notification upon auth failure
|
||||
*/
|
||||
public AndroidAuthenticator(Context context, Account account, String authTokenType,
|
||||
boolean notifyAuthFailure) {
|
||||
mContext = context;
|
||||
mAccount = account;
|
||||
mAuthTokenType = authTokenType;
|
||||
mNotifyAuthFailure = notifyAuthFailure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Account being used by this authenticator.
|
||||
*/
|
||||
public Account getAccount() {
|
||||
return mAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthToken() throws AuthFailureError {
|
||||
final AccountManager accountManager = AccountManager.get(mContext);
|
||||
AccountManagerFuture<Bundle> future = accountManager.getAuthToken(mAccount,
|
||||
mAuthTokenType, mNotifyAuthFailure, null, null);
|
||||
Bundle result;
|
||||
try {
|
||||
result = future.getResult();
|
||||
} catch (Exception e) {
|
||||
throw new AuthFailureError("Error while retrieving auth token", e);
|
||||
}
|
||||
String authToken = null;
|
||||
if (future.isDone() && !future.isCancelled()) {
|
||||
if (result.containsKey(AccountManager.KEY_INTENT)) {
|
||||
Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
|
||||
throw new AuthFailureError(intent);
|
||||
}
|
||||
authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
|
||||
}
|
||||
if (authToken == null) {
|
||||
throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType);
|
||||
}
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateAuthToken(String authToken) {
|
||||
AccountManager.get(mContext).invalidateAuthToken(mAccount.type, authToken);
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
|
||||
/**
|
||||
* An interface for interacting with auth tokens.
|
||||
*/
|
||||
public interface Authenticator {
|
||||
/**
|
||||
* Synchronously retrieves an auth token.
|
||||
*
|
||||
* @throws AuthFailureError If authentication did not succeed
|
||||
*/
|
||||
public String getAuthToken() throws AuthFailureError;
|
||||
|
||||
/**
|
||||
* Invalidates the provided auth token.
|
||||
*/
|
||||
public void invalidateAuthToken(String authToken);
|
||||
}
|
||||
@ -1,303 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.Network;
|
||||
import com.android.volley.NetworkError;
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.NoConnectionError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.RetryPolicy;
|
||||
import com.android.volley.ServerError;
|
||||
import com.android.volley.TimeoutError;
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.VolleyLog;
|
||||
import com.gh.common.constant.Config;
|
||||
import com.gh.common.util.GzipUtils;
|
||||
import com.gh.gamecenter.volley.extended.JsonArrayExtendedRequest;
|
||||
import com.gh.gamecenter.volley.extended.JsonObjectExtendedRequest;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.conn.ConnectTimeoutException;
|
||||
import org.apache.http.impl.cookie.DateUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A network performing Volley requests over an {@link HttpStack}.
|
||||
*/
|
||||
public class BasicNetwork implements Network {
|
||||
protected static final boolean DEBUG = VolleyLog.DEBUG;
|
||||
|
||||
private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
|
||||
|
||||
private static int DEFAULT_POOL_SIZE = 4096;
|
||||
|
||||
protected final HttpStack mHttpStack;
|
||||
|
||||
protected final ByteArrayPool mPool;
|
||||
|
||||
/**
|
||||
* @param httpStack
|
||||
* HTTP stack to be used
|
||||
*/
|
||||
public BasicNetwork(HttpStack httpStack) {
|
||||
// If a pool isn't passed in, then build a small default pool that will
|
||||
// give us a lot of
|
||||
// benefit and not use too much memory.
|
||||
this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param httpStack
|
||||
* HTTP stack to be used
|
||||
* @param pool
|
||||
* a buffer pool that improves GC performance in copy operations
|
||||
*/
|
||||
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
|
||||
mHttpStack = httpStack;
|
||||
mPool = pool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkResponse performRequest(Request<?> request)
|
||||
throws VolleyError {
|
||||
long requestStart = SystemClock.elapsedRealtime();
|
||||
while (true) {
|
||||
HttpResponse httpResponse = null;
|
||||
byte[] responseContents = null;
|
||||
Map<String, String> responseHeaders = new HashMap<String, String>();
|
||||
try {
|
||||
// Gather headers.
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
addCacheHeaders(headers, request.getCacheEntry());
|
||||
httpResponse = mHttpStack.performRequest(request, headers);
|
||||
StatusLine statusLine = httpResponse.getStatusLine();
|
||||
int statusCode = statusLine.getStatusCode();
|
||||
|
||||
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
|
||||
// Handle cache validation.
|
||||
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
|
||||
if (request.getUrl().startsWith(Config.HOST + "v2/version")) {
|
||||
if (request.getCacheEntry() != null) {
|
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
|
||||
request.getCacheEntry().data, responseHeaders, true);
|
||||
} else {
|
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
|
||||
GzipUtils.compressBytes("{}".getBytes()), responseHeaders, true);
|
||||
}
|
||||
} else {
|
||||
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
|
||||
request.getCacheEntry().data, responseHeaders, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Some responses such as 204s do not have content. We must
|
||||
// check.
|
||||
if (httpResponse.getEntity() != null) {
|
||||
responseContents = entityToBytes(httpResponse.getEntity());
|
||||
} else {
|
||||
// Add 0 byte response as a way of honestly representing a
|
||||
// no-content request.
|
||||
responseContents = new byte[0];
|
||||
}
|
||||
|
||||
// if the request is slow, log it.
|
||||
long requestLifetime = SystemClock.elapsedRealtime()
|
||||
- requestStart;
|
||||
logSlowRequests(requestLifetime, request, responseContents,
|
||||
statusLine);
|
||||
|
||||
if (statusCode != HttpStatus.SC_OK
|
||||
&& statusCode != HttpStatus.SC_NO_CONTENT) {
|
||||
throw new IOException();
|
||||
}
|
||||
return new NetworkResponse(statusCode, responseContents,
|
||||
responseHeaders, false);
|
||||
} catch (SocketTimeoutException e) {
|
||||
attemptRetryOnException("socket", request, new TimeoutError());
|
||||
} catch (ConnectTimeoutException e) {
|
||||
attemptRetryOnException("connection", request,
|
||||
new TimeoutError());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException("Bad URL " + request.getUrl(), e);
|
||||
} catch (IOException e) {
|
||||
int statusCode = 0;
|
||||
NetworkResponse networkResponse = null;
|
||||
if (httpResponse != null) {
|
||||
statusCode = httpResponse.getStatusLine().getStatusCode();
|
||||
} else {
|
||||
// If there is no network connection, judge whether the url is cached
|
||||
// If have cached, return cached
|
||||
Cache.Entry entry = request.getCacheEntry();
|
||||
if (entry != null) {
|
||||
return new NetworkResponse(HttpStatus.SC_OK,
|
||||
entry.data, entry.responseHeaders, false);
|
||||
}
|
||||
// else throw NoConnectionError
|
||||
throw new NoConnectionError(e);
|
||||
}
|
||||
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
|
||||
if (responseContents != null) {
|
||||
networkResponse = new NetworkResponse(statusCode,
|
||||
responseContents, responseHeaders, false);
|
||||
if (statusCode == HttpStatus.SC_UNAUTHORIZED
|
||||
|| statusCode == HttpStatus.SC_FORBIDDEN) {
|
||||
attemptRetryOnException("auth", request,
|
||||
new AuthFailureError(networkResponse));
|
||||
} else if (statusCode == HttpStatus.SC_NOT_FOUND) {
|
||||
if (request.getClass().equals(JsonObjectExtendedRequest.class)) {
|
||||
return new NetworkResponse(HttpStatus.SC_OK,
|
||||
GzipUtils.compressBytes("{}".getBytes()), responseHeaders, true);
|
||||
} else if (request.getClass().equals(JsonArrayExtendedRequest.class)) {
|
||||
return new NetworkResponse(HttpStatus.SC_OK,
|
||||
GzipUtils.compressBytes("[]".getBytes()), responseHeaders, true);
|
||||
} else {
|
||||
// TODO: Only throw ServerError for 5xx status codes.
|
||||
throw new ServerError(networkResponse);
|
||||
}
|
||||
} else {
|
||||
// TODO: Only throw ServerError for 5xx status codes.
|
||||
throw new ServerError(networkResponse);
|
||||
}
|
||||
} else {
|
||||
throw new NetworkError(networkResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
|
||||
*/
|
||||
private void logSlowRequests(long requestLifetime, Request<?> request,
|
||||
byte[] responseContents, StatusLine statusLine) {
|
||||
if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
|
||||
VolleyLog
|
||||
.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], "
|
||||
+ "[rc=%d], [retryCount=%s]", request,
|
||||
requestLifetime,
|
||||
responseContents != null ? responseContents.length
|
||||
: "null", statusLine.getStatusCode(),
|
||||
request.getRetryPolicy().getCurrentRetryCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to prepare the request for a retry. If there are no more
|
||||
* attempts remaining in the request's retry policy, a timeout exception is
|
||||
* thrown.
|
||||
*
|
||||
* @param request
|
||||
* The request to use.
|
||||
*/
|
||||
private static void attemptRetryOnException(String logPrefix,
|
||||
Request<?> request, VolleyError exception) throws VolleyError {
|
||||
RetryPolicy retryPolicy = request.getRetryPolicy();
|
||||
int oldTimeout = request.getTimeoutMs();
|
||||
|
||||
try {
|
||||
retryPolicy.retry(exception);
|
||||
} catch (VolleyError e) {
|
||||
request.addMarker(String.format("%s-timeout-giveup [timeout=%s]",
|
||||
logPrefix, oldTimeout));
|
||||
throw e;
|
||||
}
|
||||
request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix,
|
||||
oldTimeout));
|
||||
}
|
||||
|
||||
private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
|
||||
// If there's no cache entry, we're done.
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.etag != null) {
|
||||
headers.put("If-None-Match", entry.etag);
|
||||
}
|
||||
|
||||
if (entry.serverDate > 0) {
|
||||
Date refTime = new Date(entry.serverDate);
|
||||
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
|
||||
}
|
||||
}
|
||||
|
||||
protected void logError(String what, String url, long start) {
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start),
|
||||
url);
|
||||
}
|
||||
|
||||
/** Reads the contents of HttpEntity into a byte[]. */
|
||||
private byte[] entityToBytes(HttpEntity entity) throws IOException,
|
||||
ServerError {
|
||||
PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(
|
||||
mPool, (int) entity.getContentLength());
|
||||
byte[] buffer = null;
|
||||
try {
|
||||
InputStream in = entity.getContent();
|
||||
if (in == null) {
|
||||
throw new ServerError();
|
||||
}
|
||||
buffer = mPool.getBuf(1024);
|
||||
int count;
|
||||
while ((count = in.read(buffer)) != -1) {
|
||||
bytes.write(buffer, 0, count);
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
} finally {
|
||||
try {
|
||||
// Close the InputStream and release the resources by
|
||||
// "consuming the content".
|
||||
entity.consumeContent();
|
||||
} catch (IOException e) {
|
||||
// This can happen if there was an exception above that left the
|
||||
// entity in
|
||||
// an invalid state.
|
||||
VolleyLog.v("Error occured when calling consumingContent");
|
||||
}
|
||||
mPool.returnBuf(buffer);
|
||||
bytes.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Headers[] to Map<String, String>.
|
||||
*/
|
||||
private static Map<String, String> convertHeaders(Header[] headers) {
|
||||
Map<String, String> result = new HashMap<String, String>();
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
result.put(headers[i].getName(), headers[i].getValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -1,135 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to
|
||||
* supply those buffers to consumers who need to use them for a short period of time and then
|
||||
* dispose of them. Simply creating and disposing such buffers in the conventional manner can
|
||||
* considerable heap churn and garbage collection delays on Android, which lacks good management of
|
||||
* short-lived heap objects. It may be advantageous to trade off some memory in the form of a
|
||||
* permanently allocated pool of buffers in order to gain heap performance improvements; that is
|
||||
* what this class does.
|
||||
* <p>
|
||||
* A good candidate user for this class is something like an I/O system that uses large temporary
|
||||
* <code>byte[]</code> buffers to copy data around. In these use cases, often the consumer wants
|
||||
* the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks
|
||||
* off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into
|
||||
* account and also to maximize the odds of being able to reuse a recycled buffer, this class is
|
||||
* free to return buffers larger than the requested size. The caller needs to be able to gracefully
|
||||
* deal with getting buffers any size over the minimum.
|
||||
* <p>
|
||||
* If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this
|
||||
* class will allocate a new buffer and return it.
|
||||
* <p>
|
||||
* This class has no special ownership of buffers it creates; the caller is free to take a buffer
|
||||
* it receives from this pool, use it permanently, and never return it to the pool; additionally,
|
||||
* it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there
|
||||
* are no other lingering references to it.
|
||||
* <p>
|
||||
* This class ensures that the total size of the buffers in its recycling pool never exceeds a
|
||||
* certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,
|
||||
* least-recently-used buffers are disposed.
|
||||
*/
|
||||
public class ByteArrayPool {
|
||||
/** The buffer pool, arranged both by last use and by buffer size */
|
||||
private List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
|
||||
private List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);
|
||||
|
||||
/** The total size of the buffers in the pool */
|
||||
private int mCurrentSize = 0;
|
||||
|
||||
/**
|
||||
* The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
|
||||
* under this limit.
|
||||
*/
|
||||
private final int mSizeLimit;
|
||||
|
||||
/** Compares buffers by size */
|
||||
protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
|
||||
@Override
|
||||
public int compare(byte[] lhs, byte[] rhs) {
|
||||
return lhs.length - rhs.length;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param sizeLimit the maximum size of the pool, in bytes
|
||||
*/
|
||||
public ByteArrayPool(int sizeLimit) {
|
||||
mSizeLimit = sizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer from the pool if one is available in the requested size, or allocates a new
|
||||
* one if a pooled one is not available.
|
||||
*
|
||||
* @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
|
||||
* larger.
|
||||
* @return a byte[] buffer is always returned.
|
||||
*/
|
||||
public synchronized byte[] getBuf(int len) {
|
||||
for (int i = 0; i < mBuffersBySize.size(); i++) {
|
||||
byte[] buf = mBuffersBySize.get(i);
|
||||
if (buf.length >= len) {
|
||||
mCurrentSize -= buf.length;
|
||||
mBuffersBySize.remove(i);
|
||||
mBuffersByLastUse.remove(buf);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
return new byte[len];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted
|
||||
* size.
|
||||
*
|
||||
* @param buf the buffer to return to the pool.
|
||||
*/
|
||||
public synchronized void returnBuf(byte[] buf) {
|
||||
if (buf == null || buf.length > mSizeLimit) {
|
||||
return;
|
||||
}
|
||||
mBuffersByLastUse.add(buf);
|
||||
int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
|
||||
if (pos < 0) {
|
||||
pos = -pos - 1;
|
||||
}
|
||||
mBuffersBySize.add(pos, buf);
|
||||
mCurrentSize += buf.length;
|
||||
trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes buffers from the pool until it is under its size limit.
|
||||
*/
|
||||
private synchronized void trim() {
|
||||
while (mCurrentSize > mSizeLimit) {
|
||||
byte[] buf = mBuffersByLastUse.remove(0);
|
||||
mBuffersBySize.remove(buf);
|
||||
mCurrentSize -= buf.length;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
|
||||
/**
|
||||
* A synthetic request used for clearing the cache.
|
||||
*/
|
||||
public class ClearCacheRequest extends Request<Object> {
|
||||
private final Cache mCache;
|
||||
private final Runnable mCallback;
|
||||
|
||||
/**
|
||||
* Creates a synthetic request for clearing the cache.
|
||||
* @param cache Cache to clear
|
||||
* @param callback Callback to make on the main thread once the cache is clear,
|
||||
* or null for none
|
||||
*/
|
||||
public ClearCacheRequest(Cache cache, Runnable callback) {
|
||||
super(Method.GET, null, null);
|
||||
mCache = cache;
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCanceled() {
|
||||
// This is a little bit of a hack, but hey, why not.
|
||||
mCache.clear();
|
||||
if (mCallback != null) {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.postAtFrontOfQueue(mCallback);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Priority getPriority() {
|
||||
return Priority.IMMEDIATE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<Object> parseNetworkResponse(NetworkResponse response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(Object response) {
|
||||
}
|
||||
}
|
||||
@ -1,649 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.VolleyLog;
|
||||
import com.gh.common.util.TimestampUtils;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Cache implementation that caches files directly onto the hard disk in the
|
||||
* specified directory. The default disk usage size is 5MB, but is configurable.
|
||||
*/
|
||||
public class DiskBasedCache implements Cache {
|
||||
|
||||
/** Map of the Key, CacheHeader pairs */
|
||||
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>(
|
||||
16, .75f, true);
|
||||
|
||||
/** Total amount of space currently used by the cache in bytes. */
|
||||
private long mTotalSize = 0;
|
||||
|
||||
/** The root directory to use for the cache. */
|
||||
private final File mRootDirectory;
|
||||
|
||||
/** The maximum size of the cache in bytes. */
|
||||
private final int mMaxCacheSizeInBytes;
|
||||
|
||||
/** Default maximum disk usage in bytes. */
|
||||
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
/** High water mark percentage for the cache */
|
||||
private static final float HYSTERESIS_FACTOR = 0.9f;
|
||||
|
||||
/** Magic number for current version of cache file format. */
|
||||
private static final int CACHE_MAGIC = 0x20120504;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the DiskBasedCache at the specified directory.
|
||||
*
|
||||
* @param rootDirectory
|
||||
* The root directory of the cache.
|
||||
* @param maxCacheSizeInBytes
|
||||
* The maximum size of the cache in bytes.
|
||||
*/
|
||||
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
|
||||
mRootDirectory = rootDirectory;
|
||||
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance of the DiskBasedCache at the specified directory
|
||||
* using the default maximum cache size of 5MB.
|
||||
*
|
||||
* @param rootDirectory
|
||||
* The root directory of the cache.
|
||||
*/
|
||||
public DiskBasedCache(File rootDirectory) {
|
||||
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cache. Deletes all cached files from disk.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
File[] files = mRootDirectory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
mEntries.clear();
|
||||
mTotalSize = 0;
|
||||
VolleyLog.d("Cache cleared.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache entry with the specified key if it exists, null
|
||||
* otherwise.
|
||||
*/
|
||||
@Override
|
||||
public synchronized Entry get(String key) {
|
||||
CacheHeader entry = mEntries.get(key);
|
||||
// if the entry does not exist, return.
|
||||
if (entry == null) {
|
||||
if (key.contains("timestamp")) {
|
||||
Log.i("result", "get entrey is null");
|
||||
entry = mEntries.get(TimestampUtils.removeTimestamp(key));
|
||||
if (entry == null){
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File file = getFileForKey(key);
|
||||
if (!file.exists() && key.contains("timestamp")) {
|
||||
Log.i("result", "file = " + file.getName() + " not exists");
|
||||
file = getFileForKey(TimestampUtils.removeTimestamp(key));
|
||||
}
|
||||
Log.i("result", "key = " + key);
|
||||
Log.i("result", "name = " + file.getName());
|
||||
CountingInputStream cis = null;
|
||||
try {
|
||||
cis = new CountingInputStream(new FileInputStream(file));
|
||||
CacheHeader.readHeader(cis); // eat header
|
||||
byte[] data = streamToBytes(cis,
|
||||
(int) (file.length() - cis.bytesRead));
|
||||
return entry.toCacheEntry(data);
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
|
||||
remove(key);
|
||||
return null;
|
||||
} finally {
|
||||
if (cis != null) {
|
||||
try {
|
||||
cis.close();
|
||||
} catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized byte[] getData(String key) {
|
||||
File file = getFileForKey(key);
|
||||
CountingInputStream cis = null;
|
||||
try {
|
||||
cis = new CountingInputStream(new FileInputStream(file));
|
||||
CacheHeader.readHeader(cis); // eat header
|
||||
return streamToBytes(cis, (int) (file.length() - cis.bytesRead));
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
|
||||
return null;
|
||||
} finally {
|
||||
if (cis != null) {
|
||||
try {
|
||||
cis.close();
|
||||
} catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void modify(String key, byte[] data) {
|
||||
File file = getFileForKey(key);
|
||||
CountingInputStream cis = null;
|
||||
try {
|
||||
cis = new CountingInputStream(new FileInputStream(file));
|
||||
CacheHeader e = CacheHeader.readHeader(cis); // eat header
|
||||
Entry entry = e.toCacheEntry(data);
|
||||
cis.close();
|
||||
put(key, entry);
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the DiskBasedCache by scanning for all files currently in the
|
||||
* specified root directory. Creates the root directory if necessary.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void initialize() {
|
||||
if (!mRootDirectory.exists()) {
|
||||
if (!mRootDirectory.mkdirs()) {
|
||||
VolleyLog.e("Unable to create cache dir %s",
|
||||
mRootDirectory.getAbsolutePath());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
File[] files = mRootDirectory.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(file);
|
||||
CacheHeader entry = CacheHeader.readHeader(fis);
|
||||
entry.size = file.length();
|
||||
putEntry(entry.key, entry);
|
||||
} catch (IOException e) {
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (fis != null) {
|
||||
fis.close();
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates an entry in the cache.
|
||||
*
|
||||
* @param key
|
||||
* Cache key
|
||||
* @param fullExpire
|
||||
* True to fully expire the entry, false to soft expire
|
||||
*/
|
||||
@Override
|
||||
public synchronized void invalidate(String key, boolean fullExpire) {
|
||||
Entry entry = get(key);
|
||||
if (entry != null) {
|
||||
entry.softTtl = 0;
|
||||
if (fullExpire) {
|
||||
entry.ttl = 0;
|
||||
}
|
||||
put(key, entry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the entry with the specified key into the cache.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void put(String key, Entry entry) {
|
||||
pruneIfNeeded(entry.data.length);
|
||||
File file = getFileForKey(key);
|
||||
try {
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
CacheHeader e = new CacheHeader(key, entry);
|
||||
e.writeHeader(fos);
|
||||
fos.write(entry.data);
|
||||
fos.close();
|
||||
putEntry(key, e);
|
||||
// 如果url包含timestamp参数,则去掉该参数再存一份缓存
|
||||
if (key.contains("timestamp")) {
|
||||
put(TimestampUtils.removeTimestamp(key), entry);
|
||||
}
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
}
|
||||
boolean deleted = file.delete();
|
||||
if (!deleted) {
|
||||
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified key from the cache if it exists.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void remove(String key) {
|
||||
boolean deleted = getFileForKey(key).delete();
|
||||
removeEntry(key);
|
||||
if (!deleted) {
|
||||
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
|
||||
key, getFilenameForKey(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pseudo-unique filename for the specified cache key.
|
||||
*
|
||||
* @param key
|
||||
* The key to generate a file name for.
|
||||
* @return A pseudo-unique filename.
|
||||
*/
|
||||
private String getFilenameForKey(String key) {
|
||||
int firstHalfLength = key.length() / 2;
|
||||
String localFilename = String.valueOf(key.substring(0, firstHalfLength)
|
||||
.hashCode());
|
||||
localFilename += String.valueOf(key.substring(firstHalfLength)
|
||||
.hashCode());
|
||||
return localFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file object for the given cache key.
|
||||
*/
|
||||
public File getFileForKey(String key) {
|
||||
return new File(mRootDirectory, getFilenameForKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes the cache to fit the amount of bytes specified.
|
||||
*
|
||||
* @param neededSpace
|
||||
* The amount of bytes we are trying to fit into the cache.
|
||||
*/
|
||||
private void pruneIfNeeded(int neededSpace) {
|
||||
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
|
||||
return;
|
||||
}
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("Pruning old cache entries.");
|
||||
}
|
||||
|
||||
long before = mTotalSize;
|
||||
int prunedFiles = 0;
|
||||
long startTime = SystemClock.elapsedRealtime();
|
||||
|
||||
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet()
|
||||
.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, CacheHeader> entry = iterator.next();
|
||||
CacheHeader e = entry.getValue();
|
||||
boolean deleted = getFileForKey(e.key).delete();
|
||||
if (deleted) {
|
||||
mTotalSize -= e.size;
|
||||
} else {
|
||||
VolleyLog.d(
|
||||
"Could not delete cache entry for key=%s, filename=%s",
|
||||
e.key, getFilenameForKey(e.key));
|
||||
}
|
||||
iterator.remove();
|
||||
prunedFiles++;
|
||||
|
||||
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes
|
||||
* HYSTERESIS_FACTOR) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (VolleyLog.DEBUG) {
|
||||
VolleyLog.v("pruned %d files, %d bytes, %d ms", prunedFiles,
|
||||
(mTotalSize - before), SystemClock.elapsedRealtime()
|
||||
- startTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the entry with the specified key into the cache.
|
||||
*
|
||||
* @param key
|
||||
* The key to identify the entry by.
|
||||
* @param entry
|
||||
* The entry to cache.
|
||||
*/
|
||||
private void putEntry(String key, CacheHeader entry) {
|
||||
if (!mEntries.containsKey(key)) {
|
||||
mTotalSize += entry.size;
|
||||
} else {
|
||||
CacheHeader oldEntry = mEntries.get(key);
|
||||
mTotalSize += (entry.size - oldEntry.size);
|
||||
}
|
||||
mEntries.put(key, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the entry identified by 'key' from the cache.
|
||||
*/
|
||||
private void removeEntry(String key) {
|
||||
CacheHeader entry = mEntries.get(key);
|
||||
if (entry != null) {
|
||||
mTotalSize -= entry.size;
|
||||
mEntries.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of an InputStream into a byte[].
|
||||
* */
|
||||
private static byte[] streamToBytes(InputStream in, int length)
|
||||
throws IOException {
|
||||
byte[] bytes = new byte[length];
|
||||
int count;
|
||||
int pos = 0;
|
||||
while (pos < length
|
||||
&& ((count = in.read(bytes, pos, length - pos)) != -1)) {
|
||||
pos += count;
|
||||
}
|
||||
if (pos != length) {
|
||||
throw new IOException("Expected " + length + " bytes, read " + pos
|
||||
+ " bytes");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles holding onto the cache headers for an entry.
|
||||
*/
|
||||
// Visible for testing.
|
||||
static class CacheHeader {
|
||||
/**
|
||||
* The size of the data identified by this CacheHeader. (This is not
|
||||
* serialized to disk.
|
||||
*/
|
||||
public long size;
|
||||
|
||||
/** The key that identifies the cache entry. */
|
||||
public String key;
|
||||
|
||||
/** ETag for cache coherence. */
|
||||
public String etag;
|
||||
|
||||
/** Date of this response as reported by the server. */
|
||||
public long serverDate;
|
||||
|
||||
/** TTL for this record. */
|
||||
public long ttl;
|
||||
|
||||
/** Soft TTL for this record. */
|
||||
public long softTtl;
|
||||
|
||||
/** Headers from the response resulting in this cache entry. */
|
||||
public Map<String, String> responseHeaders;
|
||||
|
||||
private CacheHeader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new CacheHeader object
|
||||
*
|
||||
* @param key
|
||||
* The key that identifies the cache entry
|
||||
* @param entry
|
||||
* The cache entry.
|
||||
*/
|
||||
public CacheHeader(String key, Entry entry) {
|
||||
this.key = key;
|
||||
this.size = entry.data.length;
|
||||
this.etag = entry.etag;
|
||||
this.serverDate = entry.serverDate;
|
||||
this.ttl = entry.ttl;
|
||||
this.softTtl = entry.softTtl;
|
||||
this.responseHeaders = entry.responseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the header off of an InputStream and returns a CacheHeader
|
||||
* object.
|
||||
*
|
||||
* @param is
|
||||
* The InputStream to read from.
|
||||
* @throws IOException
|
||||
*/
|
||||
public static CacheHeader readHeader(InputStream is) throws IOException {
|
||||
CacheHeader entry = new CacheHeader();
|
||||
int magic = readInt(is);
|
||||
if (magic != CACHE_MAGIC) {
|
||||
// don't bother deleting, it'll get pruned eventually
|
||||
throw new IOException();
|
||||
}
|
||||
entry.key = readString(is);
|
||||
entry.etag = readString(is);
|
||||
if (entry.etag.equals("")) {
|
||||
entry.etag = null;
|
||||
}
|
||||
entry.serverDate = readLong(is);
|
||||
entry.ttl = readLong(is);
|
||||
entry.softTtl = readLong(is);
|
||||
entry.responseHeaders = readStringStringMap(is);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cache entry for the specified data.
|
||||
*/
|
||||
public Entry toCacheEntry(byte[] data) {
|
||||
Entry e = new Entry();
|
||||
e.data = data;
|
||||
e.etag = etag;
|
||||
e.serverDate = serverDate;
|
||||
e.ttl = ttl;
|
||||
e.softTtl = softTtl;
|
||||
e.responseHeaders = responseHeaders;
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the contents of this CacheHeader to the specified
|
||||
* OutputStream.
|
||||
*/
|
||||
public boolean writeHeader(OutputStream os) {
|
||||
try {
|
||||
writeInt(os, CACHE_MAGIC);
|
||||
writeString(os, key);
|
||||
writeString(os, etag == null ? "" : etag);
|
||||
writeLong(os, serverDate);
|
||||
writeLong(os, ttl);
|
||||
writeLong(os, softTtl);
|
||||
writeStringStringMap(responseHeaders, os);
|
||||
os.flush();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
VolleyLog.d("%s", e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class CountingInputStream extends FilterInputStream {
|
||||
private int bytesRead = 0;
|
||||
|
||||
private CountingInputStream(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int result = super.read();
|
||||
if (result != -1) {
|
||||
bytesRead++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] buffer, int offset, int count)
|
||||
throws IOException {
|
||||
int result = super.read(buffer, offset, count);
|
||||
if (result != -1) {
|
||||
bytesRead += result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Homebrewed simple serialization system used for reading and writing cache
|
||||
* headers on disk. Once upon a time, this used the standard Java
|
||||
* Object{Input,Output}Stream, but the default implementation relies heavily
|
||||
* on reflection (even for standard types) and generates a ton of garbage.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Simple wrapper around {@link InputStream#read()} that throws EOFException
|
||||
* instead of returning -1.
|
||||
*/
|
||||
private static int read(InputStream is) throws IOException {
|
||||
int b = is.read();
|
||||
if (b == -1) {
|
||||
throw new EOFException();
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static void writeInt(OutputStream os, int n) throws IOException {
|
||||
os.write((n >> 0) & 0xff);
|
||||
os.write((n >> 8) & 0xff);
|
||||
os.write((n >> 16) & 0xff);
|
||||
os.write((n >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static int readInt(InputStream is) throws IOException {
|
||||
int n = 0;
|
||||
n |= (read(is) << 0);
|
||||
n |= (read(is) << 8);
|
||||
n |= (read(is) << 16);
|
||||
n |= (read(is) << 24);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void writeLong(OutputStream os, long n) throws IOException {
|
||||
os.write((byte) (n >>> 0));
|
||||
os.write((byte) (n >>> 8));
|
||||
os.write((byte) (n >>> 16));
|
||||
os.write((byte) (n >>> 24));
|
||||
os.write((byte) (n >>> 32));
|
||||
os.write((byte) (n >>> 40));
|
||||
os.write((byte) (n >>> 48));
|
||||
os.write((byte) (n >>> 56));
|
||||
}
|
||||
|
||||
static long readLong(InputStream is) throws IOException {
|
||||
long n = 0;
|
||||
n |= ((read(is) & 0xFFL) << 0);
|
||||
n |= ((read(is) & 0xFFL) << 8);
|
||||
n |= ((read(is) & 0xFFL) << 16);
|
||||
n |= ((read(is) & 0xFFL) << 24);
|
||||
n |= ((read(is) & 0xFFL) << 32);
|
||||
n |= ((read(is) & 0xFFL) << 40);
|
||||
n |= ((read(is) & 0xFFL) << 48);
|
||||
n |= ((read(is) & 0xFFL) << 56);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void writeString(OutputStream os, String s) throws IOException {
|
||||
byte[] b = s.getBytes("UTF-8");
|
||||
writeLong(os, b.length);
|
||||
os.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
static String readString(InputStream is) throws IOException {
|
||||
int n = (int) readLong(is);
|
||||
byte[] b = streamToBytes(is, n);
|
||||
return new String(b, "UTF-8");
|
||||
}
|
||||
|
||||
static void writeStringStringMap(Map<String, String> map, OutputStream os)
|
||||
throws IOException {
|
||||
if (map != null) {
|
||||
writeInt(os, map.size());
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
writeString(os, entry.getKey());
|
||||
writeString(os, entry.getValue());
|
||||
}
|
||||
} else {
|
||||
writeInt(os, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, String> readStringStringMap(InputStream is)
|
||||
throws IOException {
|
||||
int size = readInt(is);
|
||||
Map<String, String> result = (size == 0) ? Collections
|
||||
.<String, String> emptyMap()
|
||||
: new HashMap<String, String>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
String key = readString(is).intern();
|
||||
String value = readString(is).intern();
|
||||
result.put(key, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,147 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Request.Method;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An HttpStack that performs request over an {@link HttpClient}.
|
||||
*/
|
||||
public class HttpClientStack implements HttpStack {
|
||||
protected final HttpClient mClient;
|
||||
|
||||
private final static String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
public HttpClientStack(HttpClient client) {
|
||||
mClient = client;
|
||||
}
|
||||
|
||||
private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
|
||||
for (String key : headers.keySet()) {
|
||||
httpRequest.setHeader(key, headers.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
|
||||
List<NameValuePair> result = new ArrayList<NameValuePair>(postParams.size());
|
||||
for (String key : postParams.keySet()) {
|
||||
result.add(new BasicNameValuePair(key, postParams.get(key)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
|
||||
throws IOException, AuthFailureError {
|
||||
HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
|
||||
addHeaders(httpRequest, additionalHeaders);
|
||||
addHeaders(httpRequest, request.getHeaders());
|
||||
onPrepareRequest(httpRequest);
|
||||
HttpParams httpParams = httpRequest.getParams();
|
||||
int timeoutMs = request.getTimeoutMs();
|
||||
// TODO: Reevaluate this connection timeout based on more wide-scale
|
||||
// data collection and possibly different for wifi vs. 3G.
|
||||
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
|
||||
HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
|
||||
return mClient.execute(httpRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the appropriate subclass of HttpUriRequest for passed in request.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
/* protected */ static HttpUriRequest createHttpRequest(Request<?> request,
|
||||
Map<String, String> additionalHeaders) throws AuthFailureError {
|
||||
switch (request.getMethod()) {
|
||||
case Method.DEPRECATED_GET_OR_POST: {
|
||||
// This is the deprecated way that needs to be handled for backwards compatibility.
|
||||
// If the request's post body is null, then the assumption is that the request is
|
||||
// GET. Otherwise, it is assumed that the request is a POST.
|
||||
byte[] postBody = request.getPostBody();
|
||||
if (postBody != null) {
|
||||
HttpPost postRequest = new HttpPost(request.getUrl());
|
||||
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
|
||||
HttpEntity entity;
|
||||
entity = new ByteArrayEntity(postBody);
|
||||
postRequest.setEntity(entity);
|
||||
return postRequest;
|
||||
} else {
|
||||
return new HttpGet(request.getUrl());
|
||||
}
|
||||
}
|
||||
case Method.GET:
|
||||
return new HttpGet(request.getUrl());
|
||||
case Method.DELETE:
|
||||
return new HttpDelete(request.getUrl());
|
||||
case Method.POST: {
|
||||
HttpPost postRequest = new HttpPost(request.getUrl());
|
||||
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
|
||||
setEntityIfNonEmptyBody(postRequest, request);
|
||||
return postRequest;
|
||||
}
|
||||
case Method.PUT: {
|
||||
HttpPut putRequest = new HttpPut(request.getUrl());
|
||||
putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
|
||||
setEntityIfNonEmptyBody(putRequest, request);
|
||||
return putRequest;
|
||||
}
|
||||
default:
|
||||
throw new IllegalStateException("Unknown request method.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest,
|
||||
Request<?> request) throws AuthFailureError {
|
||||
byte[] body = request.getBody();
|
||||
if (body != null) {
|
||||
HttpEntity entity = new ByteArrayEntity(body);
|
||||
httpRequest.setEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the request is executed using the underlying HttpClient.
|
||||
*
|
||||
* <p>Overwrite in subclasses to augment the request.</p>
|
||||
*/
|
||||
protected void onPrepareRequest(HttpUriRequest request) throws IOException {
|
||||
// Nothing.
|
||||
}
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
import com.android.volley.NetworkResponse;
|
||||
|
||||
import org.apache.http.impl.cookie.DateParseException;
|
||||
import org.apache.http.impl.cookie.DateUtils;
|
||||
import org.apache.http.protocol.HTTP;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility methods for parsing HTTP headers.
|
||||
*/
|
||||
public class HttpHeaderParser {
|
||||
|
||||
/**
|
||||
* Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
|
||||
*
|
||||
* @param response The network response to parse headers from
|
||||
* @return a cache entry for the given response, or null if the response is not cacheable.
|
||||
*/
|
||||
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
Map<String, String> headers = response.headers;
|
||||
|
||||
long serverDate = 0;
|
||||
long serverExpires = 0;
|
||||
long softExpire = 0;
|
||||
long maxAge = 0;
|
||||
boolean hasCacheControl = false;
|
||||
|
||||
String serverEtag = null;
|
||||
String headerValue;
|
||||
|
||||
headerValue = headers.get("Date");
|
||||
if (headerValue != null) {
|
||||
serverDate = parseDateAsEpoch(headerValue);
|
||||
}
|
||||
|
||||
headerValue = headers.get("Cache-Control");
|
||||
if (headerValue != null) {
|
||||
hasCacheControl = true;
|
||||
String[] tokens = headerValue.split(",");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
String token = tokens[i].trim();
|
||||
if (token.equals("no-cache") || token.equals("no-store")) {
|
||||
return null;
|
||||
} else if (token.startsWith("max-age=")) {
|
||||
try {
|
||||
maxAge = Long.parseLong(token.substring(8));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
|
||||
maxAge = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headerValue = headers.get("Expires");
|
||||
if (headerValue != null) {
|
||||
serverExpires = parseDateAsEpoch(headerValue);
|
||||
}
|
||||
|
||||
serverEtag = headers.get("ETag");
|
||||
|
||||
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
|
||||
// is more restrictive.
|
||||
if (hasCacheControl) {
|
||||
softExpire = now + maxAge * 1000;
|
||||
} else if (serverDate > 0 && serverExpires >= serverDate) {
|
||||
// Default semantic for Expire header in HTTP specification is softExpire.
|
||||
softExpire = now + (serverExpires - serverDate);
|
||||
}
|
||||
|
||||
Cache.Entry entry = new Cache.Entry();
|
||||
entry.data = response.data;
|
||||
entry.etag = serverEtag;
|
||||
entry.softTtl = softExpire;
|
||||
entry.ttl = entry.softTtl;
|
||||
entry.serverDate = serverDate;
|
||||
entry.responseHeaders = headers;
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date in RFC1123 format, and return its value as epoch
|
||||
*/
|
||||
public static long parseDateAsEpoch(String dateStr) {
|
||||
try {
|
||||
// Parse date in RFC1123 format if this header contains one
|
||||
return DateUtils.parseDate(dateStr).getTime();
|
||||
} catch (DateParseException e) {
|
||||
// Date in invalid format, fallback to 0
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the charset specified in the Content-Type of this header,
|
||||
* or the HTTP default (ISO-8859-1) if none can be found.
|
||||
*/
|
||||
public static String parseCharset(Map<String, String> headers) {
|
||||
String contentType = headers.get(HTTP.CONTENT_TYPE);
|
||||
if (contentType != null) {
|
||||
String[] params = contentType.split(";");
|
||||
for (int i = 1; i < params.length; i++) {
|
||||
String[] pair = params[i].trim().split("=");
|
||||
if (pair.length == 2) {
|
||||
if (pair[0].equals("charset")) {
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return HTTP.UTF_8;
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An HTTP stack abstraction.
|
||||
*/
|
||||
public interface HttpStack {
|
||||
/**
|
||||
* Performs an HTTP request with the given parameters.
|
||||
*
|
||||
* <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
|
||||
* and the Content-Type header is set to request.getPostBodyContentType().</p>
|
||||
*
|
||||
* @param request the request to perform
|
||||
* @param additionalHeaders additional headers to be sent together with
|
||||
* {@link Request#getHeaders()}
|
||||
* @return the HTTP response
|
||||
*/
|
||||
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
|
||||
throws IOException, AuthFailureError;
|
||||
|
||||
}
|
||||
@ -1,265 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.AuthFailureError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Request.Method;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.ProtocolVersion;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.entity.BasicHttpEntity;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.apache.http.message.BasicHttpResponse;
|
||||
import org.apache.http.message.BasicStatusLine;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/**
|
||||
* An {@link HttpStack} based on {@link HttpURLConnection}.
|
||||
*/
|
||||
public class HurlStack implements HttpStack {
|
||||
|
||||
private static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
|
||||
/**
|
||||
* An interface for transforming URLs before use.
|
||||
*/
|
||||
public interface UrlRewriter {
|
||||
/**
|
||||
* Returns a URL to use instead of the provided one, or null to indicate
|
||||
* this URL should not be used at all.
|
||||
*/
|
||||
public String rewriteUrl(String originalUrl);
|
||||
}
|
||||
|
||||
private final UrlRewriter mUrlRewriter;
|
||||
private final SSLSocketFactory mSslSocketFactory;
|
||||
|
||||
public HurlStack() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param urlRewriter
|
||||
* Rewriter to use for request URLs
|
||||
*/
|
||||
public HurlStack(UrlRewriter urlRewriter) {
|
||||
this(urlRewriter, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param urlRewriter
|
||||
* Rewriter to use for request URLs
|
||||
* @param sslSocketFactory
|
||||
* SSL factory to use for HTTPS connections
|
||||
*/
|
||||
public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
|
||||
mUrlRewriter = urlRewriter;
|
||||
mSslSocketFactory = sslSocketFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpResponse performRequest(Request<?> request,
|
||||
Map<String, String> additionalHeaders) throws IOException,
|
||||
AuthFailureError {
|
||||
String url = request.getUrl();
|
||||
HashMap<String, String> map = new HashMap<String, String>();
|
||||
map.putAll(request.getHeaders());
|
||||
map.putAll(additionalHeaders);
|
||||
if (mUrlRewriter != null) {
|
||||
String rewritten = mUrlRewriter.rewriteUrl(url);
|
||||
if (rewritten == null) {
|
||||
throw new IOException("URL blocked by rewriter: " + url);
|
||||
}
|
||||
url = rewritten;
|
||||
}
|
||||
URL parsedUrl = new URL(url);
|
||||
HttpURLConnection connection = openConnection(parsedUrl, request);
|
||||
|
||||
// if have etag, don't use lastmodified
|
||||
// because the aliyun cnd will use lastmodified do something we don't know
|
||||
if (map.containsKey("If-None-Match")) {
|
||||
map.remove("If-Modified-Since");
|
||||
}
|
||||
|
||||
for (String headerName : map.keySet()) {
|
||||
connection.addRequestProperty(headerName, map.get(headerName));
|
||||
}
|
||||
|
||||
setConnectionParametersForRequest(connection, request);
|
||||
// Initialize HttpResponse with data from the HttpURLConnection.
|
||||
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
|
||||
if (responseCode == -1) {
|
||||
// -1 is returned by getResponseCode() if the response code could
|
||||
// not be retrieved.
|
||||
// Signal to the caller that something was wrong with the
|
||||
// connection.
|
||||
throw new IOException(
|
||||
"Could not retrieve response code from HttpUrlConnection.");
|
||||
}
|
||||
StatusLine responseStatus = new BasicStatusLine(protocolVersion,
|
||||
connection.getResponseCode(), connection.getResponseMessage());
|
||||
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
|
||||
response.setEntity(entityFromConnection(connection));
|
||||
for (Entry<String, List<String>> header : connection.getHeaderFields()
|
||||
.entrySet()) {
|
||||
if (header.getKey() != null) {
|
||||
Header h = new BasicHeader(header.getKey(), header.getValue()
|
||||
.get(0));
|
||||
response.addHeader(h);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an {@link HttpEntity} from the given
|
||||
* {@link HttpURLConnection}.
|
||||
*
|
||||
* @param connection
|
||||
* @return an HttpEntity populated with data from <code>connection</code>.
|
||||
*/
|
||||
private static HttpEntity entityFromConnection(HttpURLConnection connection) {
|
||||
BasicHttpEntity entity = new BasicHttpEntity();
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = connection.getInputStream();
|
||||
} catch (IOException ioe) {
|
||||
inputStream = connection.getErrorStream();
|
||||
}
|
||||
entity.setContent(inputStream);
|
||||
entity.setContentLength(connection.getContentLength());
|
||||
entity.setContentEncoding(connection.getContentEncoding());
|
||||
entity.setContentType(connection.getContentType());
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link HttpURLConnection} for the specified {@code url}.
|
||||
*/
|
||||
protected HttpURLConnection createConnection(URL url) throws IOException {
|
||||
return (HttpURLConnection) url.openConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an {@link HttpURLConnection} with parameters.
|
||||
*
|
||||
* @param url
|
||||
* @return an open connection
|
||||
* @throws IOException
|
||||
*/
|
||||
private HttpURLConnection openConnection(URL url, Request<?> request)
|
||||
throws IOException {
|
||||
HttpURLConnection connection = createConnection(url);
|
||||
|
||||
int timeoutMs = request.getTimeoutMs();
|
||||
connection.setConnectTimeout(timeoutMs);
|
||||
connection.setReadTimeout(timeoutMs);
|
||||
connection.setUseCaches(false);
|
||||
connection.setDoInput(true);
|
||||
|
||||
// use caller-provided custom SslSocketFactory, if any, for HTTPS
|
||||
if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
|
||||
((HttpsURLConnection) connection)
|
||||
.setSSLSocketFactory(mSslSocketFactory);
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
/* package */static void setConnectionParametersForRequest(
|
||||
HttpURLConnection connection, Request<?> request)
|
||||
throws IOException, AuthFailureError {
|
||||
switch (request.getMethod()) {
|
||||
case Method.DEPRECATED_GET_OR_POST:
|
||||
// This is the deprecated way that needs to be handled for backwards
|
||||
// compatibility.
|
||||
// If the request's post body is null, then the assumption is that
|
||||
// the request is
|
||||
// GET. Otherwise, it is assumed that the request is a POST.
|
||||
byte[] postBody = request.getPostBody();
|
||||
if (postBody != null) {
|
||||
// Prepare output. There is no need to set Content-Length
|
||||
// explicitly,
|
||||
// since this is handled by HttpURLConnection using the size of
|
||||
// the prepared
|
||||
// output stream.
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.addRequestProperty(HEADER_CONTENT_TYPE,
|
||||
request.getPostBodyContentType());
|
||||
DataOutputStream out = new DataOutputStream(
|
||||
connection.getOutputStream());
|
||||
out.write(postBody);
|
||||
out.close();
|
||||
}
|
||||
break;
|
||||
case Method.GET:
|
||||
// Not necessary to set the request method because connection
|
||||
// defaults to GET but
|
||||
// being explicit here.
|
||||
connection.setRequestMethod("GET");
|
||||
break;
|
||||
case Method.DELETE:
|
||||
connection.setRequestMethod("DELETE");
|
||||
break;
|
||||
case Method.POST:
|
||||
connection.setRequestMethod("POST");
|
||||
addBodyIfExists(connection, request);
|
||||
break;
|
||||
case Method.PUT:
|
||||
connection.setRequestMethod("PUT");
|
||||
addBodyIfExists(connection, request);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown method type.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void addBodyIfExists(HttpURLConnection connection,
|
||||
Request<?> request) throws IOException, AuthFailureError {
|
||||
byte[] body = request.getBody();
|
||||
if (body != null) {
|
||||
connection.setDoOutput(true);
|
||||
connection.addRequestProperty(HEADER_CONTENT_TYPE,
|
||||
request.getBodyContentType());
|
||||
DataOutputStream out = new DataOutputStream(
|
||||
connection.getOutputStream());
|
||||
out.write(body);
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,479 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.RequestQueue;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
import com.android.volley.VolleyError;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Helper that handles loading and caching images from remote URLs.
|
||||
*
|
||||
* The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
|
||||
* and to pass in the default image listener provided by
|
||||
* {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
|
||||
* this class must be made from the main thead, and all responses will be delivered to the main
|
||||
* thread as well.
|
||||
*/
|
||||
public class ImageLoader {
|
||||
/** RequestQueue for dispatching ImageRequests onto. */
|
||||
private final RequestQueue mRequestQueue;
|
||||
|
||||
/** Amount of time to wait after first response arrives before delivering all responses. */
|
||||
private int mBatchResponseDelayMs = 100;
|
||||
|
||||
/** The cache implementation to be used as an L1 cache before calling into volley. */
|
||||
private final ImageCache mCache;
|
||||
|
||||
/**
|
||||
* HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
|
||||
* that we can coalesce multiple requests to the same URL into a single network request.
|
||||
*/
|
||||
private final HashMap<String, BatchedImageRequest> mInFlightRequests =
|
||||
new HashMap<String, BatchedImageRequest>();
|
||||
|
||||
/** HashMap of the currently pending responses (waiting to be delivered). */
|
||||
private final HashMap<String, BatchedImageRequest> mBatchedResponses =
|
||||
new HashMap<String, BatchedImageRequest>();
|
||||
|
||||
/** Handler to the main thread. */
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
/** Runnable for in-flight response delivery. */
|
||||
private Runnable mRunnable;
|
||||
|
||||
/**
|
||||
* Simple cache adapter interface. If provided to the ImageLoader, it
|
||||
* will be used as an L1 cache before dispatch to Volley. Implementations
|
||||
* must not block. Implementation with an LruCache is recommended.
|
||||
*/
|
||||
public interface ImageCache {
|
||||
public Bitmap getBitmap(String url);
|
||||
public void putBitmap(String url, Bitmap bitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new ImageLoader.
|
||||
* @param queue The RequestQueue to use for making image requests.
|
||||
* @param imageCache The cache to use as an L1 cache.
|
||||
*/
|
||||
public ImageLoader(RequestQueue queue, ImageCache imageCache) {
|
||||
mRequestQueue = queue;
|
||||
mCache = imageCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of ImageListener which handles basic functionality
|
||||
* of showing a default image until the network response is received, at which point
|
||||
* it will switch to either the actual image or the error image.
|
||||
* @param imageView The imageView that the listener is associated with.
|
||||
* @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
|
||||
* @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
|
||||
*/
|
||||
public static ImageListener getImageListener(final ImageView view,
|
||||
final int defaultImageResId, final int errorImageResId) {
|
||||
return new ImageListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
if (errorImageResId != 0) {
|
||||
view.setImageResource(errorImageResId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(ImageContainer response, boolean isImmediate) {
|
||||
if (response.getBitmap() != null) {
|
||||
view.setImageBitmap(response.getBitmap());
|
||||
} else if (defaultImageResId != 0) {
|
||||
view.setImageResource(defaultImageResId);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the response handlers on image requests.
|
||||
*
|
||||
* The call flow is this:
|
||||
* 1. Upon being attached to a request, onResponse(response, true) will
|
||||
* be invoked to reflect any cached data that was already available. If the
|
||||
* data was available, response.getBitmap() will be non-null.
|
||||
*
|
||||
* 2. After a network response returns, only one of the following cases will happen:
|
||||
* - onResponse(response, false) will be called if the image was loaded.
|
||||
* or
|
||||
* - onErrorResponse will be called if there was an error loading the image.
|
||||
*/
|
||||
public interface ImageListener extends ErrorListener {
|
||||
/**
|
||||
* Listens for non-error changes to the loading of the image request.
|
||||
*
|
||||
* @param response Holds all information pertaining to the request, as well
|
||||
* as the bitmap (if it is loaded).
|
||||
* @param isImmediate True if this was called during ImageLoader.get() variants.
|
||||
* This can be used to differentiate between a cached image loading and a network
|
||||
* image loading in order to, for example, run an animation to fade in network loaded
|
||||
* images.
|
||||
*/
|
||||
public void onResponse(ImageContainer response, boolean isImmediate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is available in the cache.
|
||||
* @param requestUrl The url of the remote image
|
||||
* @param maxWidth The maximum width of the returned image.
|
||||
* @param maxHeight The maximum height of the returned image.
|
||||
* @return True if the item exists in cache, false otherwise.
|
||||
*/
|
||||
public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
|
||||
throwIfNotOnMainThread();
|
||||
|
||||
String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
|
||||
return mCache.getBitmap(cacheKey) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ImageContainer for the requested URL.
|
||||
*
|
||||
* The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
|
||||
* If the default was returned, the {@link ImageLoader} will be invoked when the
|
||||
* request is fulfilled.
|
||||
*
|
||||
* @param requestUrl The URL of the image to be loaded.
|
||||
* @param defaultImage Optional default image to return until the actual image is loaded.
|
||||
*/
|
||||
public ImageContainer get(String requestUrl, final ImageListener listener) {
|
||||
return get(requestUrl, listener, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a bitmap request with the given URL if that image is not available
|
||||
* in the cache, and returns a bitmap container that contains all of the data
|
||||
* relating to the request (as well as the default image if the requested
|
||||
* image is not available).
|
||||
* @param requestUrl The url of the remote image
|
||||
* @param imageListener The listener to call when the remote image is loaded
|
||||
* @param maxWidth The maximum width of the returned image.
|
||||
* @param maxHeight The maximum height of the returned image.
|
||||
* @return A container object that contains all of the properties of the request, as well as
|
||||
* the currently available image (default if remote is not loaded).
|
||||
*/
|
||||
public ImageContainer get(String requestUrl, ImageListener imageListener,
|
||||
int maxWidth, int maxHeight) {
|
||||
// only fulfill requests that were initiated from the main thread.
|
||||
throwIfNotOnMainThread();
|
||||
|
||||
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
|
||||
|
||||
// Try to look up the request in the cache of remote images.
|
||||
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
|
||||
if (cachedBitmap != null) {
|
||||
// Return the cached bitmap.
|
||||
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
|
||||
imageListener.onResponse(container, true);
|
||||
return container;
|
||||
}
|
||||
|
||||
// The bitmap did not exist in the cache, fetch it!
|
||||
ImageContainer imageContainer =
|
||||
new ImageContainer(null, requestUrl, cacheKey, imageListener);
|
||||
|
||||
// Update the caller to let them know that they should use the default bitmap.
|
||||
imageListener.onResponse(imageContainer, true);
|
||||
|
||||
// Check to see if a request is already in-flight.
|
||||
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
|
||||
if (request != null) {
|
||||
// If it is, add this request to the list of listeners.
|
||||
request.addContainer(imageContainer);
|
||||
return imageContainer;
|
||||
}
|
||||
|
||||
// The request is not already in flight. Send the new request to the network and
|
||||
// track it.
|
||||
Request<?> newRequest =
|
||||
new ImageRequest(requestUrl, new Listener<Bitmap>() {
|
||||
@Override
|
||||
public void onResponse(Bitmap response) {
|
||||
onGetImageSuccess(cacheKey, response);
|
||||
}
|
||||
}, maxWidth, maxHeight,
|
||||
Config.RGB_565, new ErrorListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
onGetImageError(cacheKey, error);
|
||||
}
|
||||
});
|
||||
|
||||
mRequestQueue.add(newRequest);
|
||||
mInFlightRequests.put(cacheKey,
|
||||
new BatchedImageRequest(newRequest, imageContainer));
|
||||
return imageContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the amount of time to wait after the first response arrives before delivering all
|
||||
* responses. Batching can be disabled entirely by passing in 0.
|
||||
* @param newBatchedResponseDelayMs The time in milliseconds to wait.
|
||||
*/
|
||||
public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
|
||||
mBatchResponseDelayMs = newBatchedResponseDelayMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when an image was successfully loaded.
|
||||
* @param cacheKey The cache key that is associated with the image request.
|
||||
* @param response The bitmap that was returned from the network.
|
||||
*/
|
||||
private void onGetImageSuccess(String cacheKey, Bitmap response) {
|
||||
// cache the image that was fetched.
|
||||
mCache.putBitmap(cacheKey, response);
|
||||
|
||||
// remove the request from the list of in-flight requests.
|
||||
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
|
||||
|
||||
if (request != null) {
|
||||
// Update the response bitmap.
|
||||
request.mResponseBitmap = response;
|
||||
|
||||
// Send the batched response
|
||||
batchResponse(cacheKey, request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when an image failed to load.
|
||||
* @param cacheKey The cache key that is associated with the image request.
|
||||
*/
|
||||
private void onGetImageError(String cacheKey, VolleyError error) {
|
||||
// Notify the requesters that something failed via a null result.
|
||||
// Remove this request from the list of in-flight requests.
|
||||
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
|
||||
|
||||
// Set the error for this request
|
||||
request.setError(error);
|
||||
|
||||
if (request != null) {
|
||||
// Send the batched response
|
||||
batchResponse(cacheKey, request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Container object for all of the data surrounding an image request.
|
||||
*/
|
||||
public class ImageContainer {
|
||||
/**
|
||||
* The most relevant bitmap for the container. If the image was in cache, the
|
||||
* Holder to use for the final bitmap (the one that pairs to the requested URL).
|
||||
*/
|
||||
private Bitmap mBitmap;
|
||||
|
||||
private final ImageListener mListener;
|
||||
|
||||
/** The cache key that was associated with the request */
|
||||
private final String mCacheKey;
|
||||
|
||||
/** The request URL that was specified */
|
||||
private final String mRequestUrl;
|
||||
|
||||
/**
|
||||
* Constructs a BitmapContainer object.
|
||||
* @param bitmap The final bitmap (if it exists).
|
||||
* @param requestUrl The requested URL for this container.
|
||||
* @param cacheKey The cache key that identifies the requested URL for this container.
|
||||
*/
|
||||
public ImageContainer(Bitmap bitmap, String requestUrl,
|
||||
String cacheKey, ImageListener listener) {
|
||||
mBitmap = bitmap;
|
||||
mRequestUrl = requestUrl;
|
||||
mCacheKey = cacheKey;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases interest in the in-flight request (and cancels it if no one else is listening).
|
||||
*/
|
||||
public void cancelRequest() {
|
||||
if (mListener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
|
||||
if (request != null) {
|
||||
boolean canceled = request.removeContainerAndCancelIfNecessary(this);
|
||||
if (canceled) {
|
||||
mInFlightRequests.remove(mCacheKey);
|
||||
}
|
||||
} else {
|
||||
// check to see if it is already batched for delivery.
|
||||
request = mBatchedResponses.get(mCacheKey);
|
||||
if (request != null) {
|
||||
request.removeContainerAndCancelIfNecessary(this);
|
||||
if (request.mContainers.size() == 0) {
|
||||
mBatchedResponses.remove(mCacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
|
||||
*/
|
||||
public Bitmap getBitmap() {
|
||||
return mBitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested URL for this container.
|
||||
*/
|
||||
public String getRequestUrl() {
|
||||
return mRequestUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper class used to map a Request to the set of active ImageContainer objects that are
|
||||
* interested in its results.
|
||||
*/
|
||||
private class BatchedImageRequest {
|
||||
/** The request being tracked */
|
||||
private final Request<?> mRequest;
|
||||
|
||||
/** The result of the request being tracked by this item */
|
||||
private Bitmap mResponseBitmap;
|
||||
|
||||
/** Error if one occurred for this response */
|
||||
private VolleyError mError;
|
||||
|
||||
/** List of all of the active ImageContainers that are interested in the request */
|
||||
private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
|
||||
|
||||
/**
|
||||
* Constructs a new BatchedImageRequest object
|
||||
* @param request The request being tracked
|
||||
* @param container The ImageContainer of the person who initiated the request.
|
||||
*/
|
||||
public BatchedImageRequest(Request<?> request, ImageContainer container) {
|
||||
mRequest = request;
|
||||
mContainers.add(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error for this response
|
||||
*/
|
||||
public void setError(VolleyError error) {
|
||||
mError = error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error for this response
|
||||
*/
|
||||
public VolleyError getError() {
|
||||
return mError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another ImageContainer to the list of those interested in the results of
|
||||
* the request.
|
||||
*/
|
||||
public void addContainer(ImageContainer container) {
|
||||
mContainers.add(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detatches the bitmap container from the request and cancels the request if no one is
|
||||
* left listening.
|
||||
* @param container The container to remove from the list
|
||||
* @return True if the request was canceled, false otherwise.
|
||||
*/
|
||||
public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
|
||||
mContainers.remove(container);
|
||||
if (mContainers.size() == 0) {
|
||||
mRequest.cancel();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the runnable for batched delivery of responses if it is not already started.
|
||||
* @param cacheKey The cacheKey of the response being delivered.
|
||||
* @param request The BatchedImageRequest to be delivered.
|
||||
* @param error The volley error associated with the request (if applicable).
|
||||
*/
|
||||
private void batchResponse(String cacheKey, BatchedImageRequest request) {
|
||||
mBatchedResponses.put(cacheKey, request);
|
||||
// If we don't already have a batch delivery runnable in flight, make a new one.
|
||||
// Note that this will be used to deliver responses to all callers in mBatchedResponses.
|
||||
if (mRunnable == null) {
|
||||
mRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (BatchedImageRequest bir : mBatchedResponses.values()) {
|
||||
for (ImageContainer container : bir.mContainers) {
|
||||
// If one of the callers in the batched request canceled the request
|
||||
// after the response was received but before it was delivered,
|
||||
// skip them.
|
||||
if (container.mListener == null) {
|
||||
continue;
|
||||
}
|
||||
if (bir.getError() == null) {
|
||||
container.mBitmap = bir.mResponseBitmap;
|
||||
container.mListener.onResponse(container, false);
|
||||
} else {
|
||||
container.mListener.onErrorResponse(bir.getError());
|
||||
}
|
||||
}
|
||||
}
|
||||
mBatchedResponses.clear();
|
||||
mRunnable = null;
|
||||
}
|
||||
|
||||
};
|
||||
// Post the runnable.
|
||||
mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
private void throwIfNotOnMainThread() {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates a cache key for use with the L1 cache.
|
||||
* @param url The URL of the request.
|
||||
* @param maxWidth The max-width of the output.
|
||||
* @param maxHeight The max-height of the output.
|
||||
*/
|
||||
private static String getCacheKey(String url, int maxWidth, int maxHeight) {
|
||||
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
|
||||
.append("#H").append(maxHeight).append(url).toString();
|
||||
}
|
||||
}
|
||||
@ -1,211 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Bitmap.Config;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import com.android.volley.DefaultRetryPolicy;
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.VolleyLog;
|
||||
|
||||
/**
|
||||
* A canned request for getting an image at a given URL and calling
|
||||
* back with a decoded Bitmap.
|
||||
*/
|
||||
public class ImageRequest extends Request<Bitmap> {
|
||||
/** Socket timeout in milliseconds for image requests */
|
||||
private static final int IMAGE_TIMEOUT_MS = 1000;
|
||||
|
||||
/** Default number of retries for image requests */
|
||||
private static final int IMAGE_MAX_RETRIES = 2;
|
||||
|
||||
/** Default backoff multiplier for image requests */
|
||||
private static final float IMAGE_BACKOFF_MULT = 2f;
|
||||
|
||||
private final Response.Listener<Bitmap> mListener;
|
||||
private final Config mDecodeConfig;
|
||||
private final int mMaxWidth;
|
||||
private final int mMaxHeight;
|
||||
|
||||
/** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
|
||||
private static final Object sDecodeLock = new Object();
|
||||
|
||||
/**
|
||||
* Creates a new image request, decoding to a maximum specified width and
|
||||
* height. If both width and height are zero, the image will be decoded to
|
||||
* its natural size. If one of the two is nonzero, that dimension will be
|
||||
* clamped and the other one will be set to preserve the image's aspect
|
||||
* ratio. If both width and height are nonzero, the image will be decoded to
|
||||
* be fit in the rectangle of dimensions width x height while keeping its
|
||||
* aspect ratio.
|
||||
*
|
||||
* @param url URL of the image
|
||||
* @param listener Listener to receive the decoded bitmap
|
||||
* @param maxWidth Maximum width to decode this bitmap to, or zero for none
|
||||
* @param maxHeight Maximum height to decode this bitmap to, or zero for
|
||||
* none
|
||||
* @param decodeConfig Format to decode the bitmap to
|
||||
* @param errorListener Error listener, or null to ignore errors
|
||||
*/
|
||||
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
|
||||
Config decodeConfig, Response.ErrorListener errorListener) {
|
||||
super(Method.GET, url, errorListener);
|
||||
setRetryPolicy(
|
||||
new DefaultRetryPolicy(IMAGE_TIMEOUT_MS, IMAGE_MAX_RETRIES, IMAGE_BACKOFF_MULT));
|
||||
mListener = listener;
|
||||
mDecodeConfig = decodeConfig;
|
||||
mMaxWidth = maxWidth;
|
||||
mMaxHeight = maxHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Priority getPriority() {
|
||||
return Priority.LOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales one side of a rectangle to fit aspect ratio.
|
||||
*
|
||||
* @param maxPrimary Maximum size of the primary dimension (i.e. width for
|
||||
* max width), or zero to maintain aspect ratio with secondary
|
||||
* dimension
|
||||
* @param maxSecondary Maximum size of the secondary dimension, or zero to
|
||||
* maintain aspect ratio with primary dimension
|
||||
* @param actualPrimary Actual size of the primary dimension
|
||||
* @param actualSecondary Actual size of the secondary dimension
|
||||
*/
|
||||
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
|
||||
int actualSecondary) {
|
||||
// If no dominant value at all, just return the actual.
|
||||
if (maxPrimary == 0 && maxSecondary == 0) {
|
||||
return actualPrimary;
|
||||
}
|
||||
|
||||
// If primary is unspecified, scale primary to match secondary's scaling ratio.
|
||||
if (maxPrimary == 0) {
|
||||
double ratio = (double) maxSecondary / (double) actualSecondary;
|
||||
return (int) (actualPrimary * ratio);
|
||||
}
|
||||
|
||||
if (maxSecondary == 0) {
|
||||
return maxPrimary;
|
||||
}
|
||||
|
||||
double ratio = (double) actualSecondary / (double) actualPrimary;
|
||||
int resized = maxPrimary;
|
||||
if (resized * ratio > maxSecondary) {
|
||||
resized = (int) (maxSecondary / ratio);
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
|
||||
// Serialize all decode on a global lock to reduce concurrent heap usage.
|
||||
synchronized (sDecodeLock) {
|
||||
try {
|
||||
return doParse(response);
|
||||
} catch (OutOfMemoryError e) {
|
||||
VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
|
||||
return Response.error(new ParseError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The real guts of parseNetworkResponse. Broken out for readability.
|
||||
*/
|
||||
private Response<Bitmap> doParse(NetworkResponse response) {
|
||||
byte[] data = response.data;
|
||||
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
|
||||
Bitmap bitmap = null;
|
||||
if (mMaxWidth == 0 && mMaxHeight == 0) {
|
||||
decodeOptions.inPreferredConfig = mDecodeConfig;
|
||||
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
|
||||
} else {
|
||||
// If we have to resize this image, first get the natural bounds.
|
||||
decodeOptions.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
|
||||
int actualWidth = decodeOptions.outWidth;
|
||||
int actualHeight = decodeOptions.outHeight;
|
||||
|
||||
// Then compute the dimensions we would ideally like to decode to.
|
||||
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
|
||||
actualWidth, actualHeight);
|
||||
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
|
||||
actualHeight, actualWidth);
|
||||
|
||||
// Decode to the nearest power of two scaling factor.
|
||||
decodeOptions.inJustDecodeBounds = false;
|
||||
// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
|
||||
// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
|
||||
decodeOptions.inSampleSize =
|
||||
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
|
||||
Bitmap tempBitmap =
|
||||
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
|
||||
|
||||
// If necessary, scale down to the maximal acceptable size.
|
||||
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
|
||||
tempBitmap.getHeight() > desiredHeight)) {
|
||||
bitmap = Bitmap.createScaledBitmap(tempBitmap,
|
||||
desiredWidth, desiredHeight, true);
|
||||
tempBitmap.recycle();
|
||||
} else {
|
||||
bitmap = tempBitmap;
|
||||
}
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
return Response.error(new ParseError(response));
|
||||
} else {
|
||||
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(Bitmap response) {
|
||||
mListener.onResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the largest power-of-two divisor for use in downscaling a bitmap
|
||||
* that will not result in the scaling past the desired dimensions.
|
||||
*
|
||||
* @param actualWidth Actual width of the bitmap
|
||||
* @param actualHeight Actual height of the bitmap
|
||||
* @param desiredWidth Desired width of the bitmap
|
||||
* @param desiredHeight Desired height of the bitmap
|
||||
*/
|
||||
// Visible for testing.
|
||||
static int findBestSampleSize(
|
||||
int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
|
||||
double wr = (double) actualWidth / desiredWidth;
|
||||
double hr = (double) actualHeight / desiredHeight;
|
||||
double ratio = Math.min(wr, hr);
|
||||
float n = 1.0f;
|
||||
while ((n * 2) <= ratio) {
|
||||
n *= 2;
|
||||
}
|
||||
|
||||
return (int) n;
|
||||
}
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* A request for retrieving a {@link JSONArray} response body at a given URL.
|
||||
*/
|
||||
public class JsonArrayRequest extends JsonRequest<JSONArray> {
|
||||
|
||||
/**
|
||||
* Creates a new request.
|
||||
* @param url URL to fetch the JSON from
|
||||
* @param listener Listener to receive the JSON response
|
||||
* @param errorListener Error listener, or null to ignore errors.
|
||||
*/
|
||||
public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) {
|
||||
super(Method.GET, url, null, listener, errorListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
|
||||
try {
|
||||
String jsonString =
|
||||
new String(response.data, HttpHeaderParser.parseCharset(response.headers));
|
||||
return Response.success(new JSONArray(jsonString),
|
||||
HttpHeaderParser.parseCacheHeaders(response));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return Response.error(new ParseError(e));
|
||||
} catch (JSONException je) {
|
||||
return Response.error(new ParseError(je));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.ParseError;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
|
||||
* optional {@link JSONObject} to be passed in as part of the request body.
|
||||
*/
|
||||
public class JsonObjectRequest extends JsonRequest<JSONObject> {
|
||||
|
||||
/**
|
||||
* Creates a new request.
|
||||
* @param method the HTTP method to use
|
||||
* @param url URL to fetch the JSON from
|
||||
* @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and
|
||||
* indicates no parameters will be posted along with request.
|
||||
* @param listener Listener to receive the JSON response
|
||||
* @param errorListener Error listener, or null to ignore errors.
|
||||
*/
|
||||
public JsonObjectRequest(int method, String url, JSONObject jsonRequest,
|
||||
Listener<JSONObject> listener, ErrorListener errorListener) {
|
||||
super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener,
|
||||
errorListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is
|
||||
* <code>null</code>, <code>POST</code> otherwise.
|
||||
*
|
||||
* @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
|
||||
*/
|
||||
public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener,
|
||||
ErrorListener errorListener) {
|
||||
this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest,
|
||||
listener, errorListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
|
||||
try {
|
||||
String jsonString =
|
||||
new String(response.data, HttpHeaderParser.parseCharset(response.headers));
|
||||
return Response.success(new JSONObject(jsonString),
|
||||
HttpHeaderParser.parseCacheHeaders(response));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return Response.error(new ParseError(e));
|
||||
} catch (JSONException je) {
|
||||
return Response.error(new ParseError(je));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.NetworkResponse;
|
||||
import com.android.volley.Request;
|
||||
import com.android.volley.Response;
|
||||
import com.android.volley.Response.ErrorListener;
|
||||
import com.android.volley.Response.Listener;
|
||||
import com.android.volley.VolleyLog;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* A request for retrieving a T type response body at a given URL that also
|
||||
* optionally sends along a JSON body in the request specified.
|
||||
*
|
||||
* @param <T> JSON type of response expected
|
||||
*/
|
||||
public abstract class JsonRequest<T> extends Request<T> {
|
||||
/** Charset for request. */
|
||||
private static final String PROTOCOL_CHARSET = "utf-8";
|
||||
|
||||
/** Content type for request. */
|
||||
private static final String PROTOCOL_CONTENT_TYPE =
|
||||
String.format("application/json; charset=%s", PROTOCOL_CHARSET);
|
||||
|
||||
private final Listener<T> mListener;
|
||||
private final String mRequestBody;
|
||||
|
||||
/**
|
||||
* Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
|
||||
* or {@link #getPostParams()} is overridden (which defaults to POST).
|
||||
*
|
||||
* @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
|
||||
*/
|
||||
public JsonRequest(String url, String requestBody, Listener<T> listener,
|
||||
ErrorListener errorListener) {
|
||||
this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
|
||||
}
|
||||
|
||||
public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
|
||||
ErrorListener errorListener) {
|
||||
super(method, url, errorListener);
|
||||
mListener = listener;
|
||||
mRequestBody = requestBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deliverResponse(T response) {
|
||||
mListener.onResponse(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getBodyContentType()}.
|
||||
*/
|
||||
@Override
|
||||
public String getPostBodyContentType() {
|
||||
return getBodyContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getBody()}.
|
||||
*/
|
||||
@Override
|
||||
public byte[] getPostBody() {
|
||||
return getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBodyContentType() {
|
||||
return PROTOCOL_CONTENT_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBody() {
|
||||
try {
|
||||
return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
|
||||
mRequestBody, PROTOCOL_CHARSET);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,202 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.volley.VolleyError;
|
||||
import com.android.volley.toolbox.ImageLoader.ImageContainer;
|
||||
import com.android.volley.toolbox.ImageLoader.ImageListener;
|
||||
|
||||
/**
|
||||
* Handles fetching an image from a URL as well as the life-cycle of the
|
||||
* associated request.
|
||||
*/
|
||||
public class NetworkImageView extends ImageView {
|
||||
/** The URL of the network image to load */
|
||||
private String mUrl;
|
||||
|
||||
/**
|
||||
* Resource ID of the image to be used as a placeholder until the network image is loaded.
|
||||
*/
|
||||
private int mDefaultImageId;
|
||||
|
||||
/**
|
||||
* Resource ID of the image to be used if the network response fails.
|
||||
*/
|
||||
private int mErrorImageId;
|
||||
|
||||
/** Local copy of the ImageLoader. */
|
||||
private ImageLoader mImageLoader;
|
||||
|
||||
/** Current ImageContainer. (either in-flight or finished) */
|
||||
private ImageContainer mImageContainer;
|
||||
|
||||
public NetworkImageView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public NetworkImageView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets URL of the image that should be loaded into this view. Note that calling this will
|
||||
* immediately either set the cached image (if available) or the default image specified by
|
||||
* {@link NetworkImageView#setDefaultImageResId(int)} on the view.
|
||||
*
|
||||
* NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
|
||||
* {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
|
||||
* this function.
|
||||
*
|
||||
* @param url The URL that should be loaded into this ImageView.
|
||||
* @param imageLoader ImageLoader that will be used to make the request.
|
||||
*/
|
||||
public void setImageUrl(String url, ImageLoader imageLoader) {
|
||||
mUrl = url;
|
||||
mImageLoader = imageLoader;
|
||||
// The URL has potentially changed. See if we need to load it.
|
||||
loadImageIfNecessary(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default image resource ID to be used for this view until the attempt to load it
|
||||
* completes.
|
||||
*/
|
||||
public void setDefaultImageResId(int defaultImage) {
|
||||
mDefaultImageId = defaultImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the error image resource ID to be used for this view in the event that the image
|
||||
* requested fails to load.
|
||||
*/
|
||||
public void setErrorImageResId(int errorImage) {
|
||||
mErrorImageId = errorImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the image for the view if it isn't already loaded.
|
||||
* @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
|
||||
*/
|
||||
private void loadImageIfNecessary(final boolean isInLayoutPass) {
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
boolean isFullyWrapContent = getLayoutParams() != null
|
||||
&& getLayoutParams().height == LayoutParams.WRAP_CONTENT
|
||||
&& getLayoutParams().width == LayoutParams.WRAP_CONTENT;
|
||||
// if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
|
||||
// view, hold off on loading the image.
|
||||
if (width == 0 && height == 0 && !isFullyWrapContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the URL to be loaded in this view is empty, cancel any old requests and clear the
|
||||
// currently loaded image.
|
||||
if (TextUtils.isEmpty(mUrl)) {
|
||||
if (mImageContainer != null) {
|
||||
mImageContainer.cancelRequest();
|
||||
mImageContainer = null;
|
||||
}
|
||||
setImageBitmap(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// if there was an old request in this view, check if it needs to be canceled.
|
||||
if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
|
||||
if (mImageContainer.getRequestUrl().equals(mUrl)) {
|
||||
// if the request is from the same URL, return.
|
||||
return;
|
||||
} else {
|
||||
// if there is a pre-existing request, cancel it if it's fetching a different URL.
|
||||
mImageContainer.cancelRequest();
|
||||
setImageBitmap(null);
|
||||
}
|
||||
}
|
||||
|
||||
// The pre-existing content of this view didn't match the current URL. Load the new image
|
||||
// from the network.
|
||||
ImageContainer newContainer = mImageLoader.get(mUrl,
|
||||
new ImageListener() {
|
||||
@Override
|
||||
public void onErrorResponse(VolleyError error) {
|
||||
if (mErrorImageId != 0) {
|
||||
setImageResource(mErrorImageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(final ImageContainer response, boolean isImmediate) {
|
||||
// If this was an immediate response that was delivered inside of a layout
|
||||
// pass do not set the image immediately as it will trigger a requestLayout
|
||||
// inside of a layout. Instead, defer setting the image by posting back to
|
||||
// the main thread.
|
||||
if (isImmediate && isInLayoutPass) {
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onResponse(response, false);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.getBitmap() != null) {
|
||||
setImageBitmap(response.getBitmap());
|
||||
} else if (mDefaultImageId != 0) {
|
||||
setImageResource(mDefaultImageId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update the ImageContainer to be the new bitmap container.
|
||||
mImageContainer = newContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
loadImageIfNecessary(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
if (mImageContainer != null) {
|
||||
// If the view was bound to an image request, cancel it and clear
|
||||
// out the image from the view.
|
||||
mImageContainer.cancelRequest();
|
||||
setImageBitmap(null);
|
||||
// also clear out the container so we can reload the image if necessary.
|
||||
mImageContainer = null;
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawableStateChanged() {
|
||||
super.drawableStateChanged();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import com.android.volley.Cache;
|
||||
|
||||
/**
|
||||
* A cache that doesn't.
|
||||
*/
|
||||
public class NoCache implements Cache {
|
||||
@Override
|
||||
public void clear() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry get(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String key, Entry entry) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(String key, boolean fullExpire) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(String key) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.volley.toolbox;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A variation of {@link ByteArrayOutputStream} that uses a pool of byte[] buffers instead
|
||||
* of always allocating them fresh, saving on heap churn.
|
||||
*/
|
||||
public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
|
||||
/**
|
||||
* If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is
|
||||
* the default size to which the underlying byte array is initialized.
|
||||
*/
|
||||
private static final int DEFAULT_SIZE = 256;
|
||||
|
||||
private final ByteArrayPool mPool;
|
||||
|
||||
/**
|
||||
* Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written
|
||||
* to this instance, the underlying byte array will expand.
|
||||
*/
|
||||
public PoolingByteArrayOutputStream(ByteArrayPool pool) {
|
||||
this(pool, DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If
|
||||
* more than {@code size} bytes are written to this instance, the underlying byte array will
|
||||
* expand.
|
||||
*
|
||||
* @param size initial size for the underlying byte array. The value will be pinned to a default
|
||||
* minimum size.
|
||||
*/
|
||||
public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
|
||||
mPool = pool;
|
||||
buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mPool.returnBuf(buf);
|
||||
buf = null;
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalize() {
|
||||
mPool.returnBuf(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures there is enough space in the buffer for the given number of additional bytes.
|
||||
*/
|
||||
private void expand(int i) {
|
||||
/* Can the buffer handle @i more bytes, if not expand it */
|
||||
if (count + i <= buf.length) {
|
||||
return;
|
||||
}
|
||||
byte[] newbuf = mPool.getBuf((count + i) * 2);
|
||||
System.arraycopy(buf, 0, newbuf, 0, count);
|
||||
mPool.returnBuf(buf);
|
||||
buf = newbuf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(byte[] buffer, int offset, int len) {
|
||||
expand(len);
|
||||
super.write(buffer, offset, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void write(int oneByte) {
|
||||
expand(1);
|
||||
super.write(oneByte);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user