Compare commits
2701 Commits
Author | SHA1 | Date |
---|---|---|
Nicolò Balzarotti | b6ae04383d | |
Nicolò Balzarotti | 6c313e2e9b | |
Nicolò Balzarotti | 6bdc4bbcab | |
Nicolò Balzarotti | ac07ddc932 | |
protomors | 4b4e95bfe0 | |
protomors | 895c19cc1c | |
protomors | 440d1e6a81 | |
protomors | 23b66459f5 | |
protomors | bf34586a22 | |
Andreas Shimokawa | 32e1dec0f8 | |
José Rebelo | 5701707e87 | |
Andreas Shimokawa | 58c7691142 | |
Andreas Shimokawa | b982d27c9a | |
protomors | b819be7db6 | |
protomors | 7b78003ba1 | |
Andreas Shimokawa | 6f358ff722 | |
Andreas Shimokawa | 486596b1a8 | |
Andreas Shimokawa | 6d8ffad55c | |
naofum | 8e0688ba66 | |
LL | 33d49dbdcc | |
Andreas Shimokawa | 9f05aff11b | |
Andreas Shimokawa | 0ffa2ce45a | |
cpfeiffer | 7137387b08 | |
cpfeiffer | ffb2f37af5 | |
cpfeiffer | 0687564bbb | |
Andreas Shimokawa | 7d05682d8e | |
Andreas Shimokawa | 7e29234a7e | |
Andreas Shimokawa | 3d09b9dc97 | |
cpfeiffer | 770c8a482d | |
cpfeiffer | 32d5ceb78f | |
cpfeiffer | 976942757f | |
Andreas Shimokawa | 47bdeea257 | |
Andreas Shimokawa | 56269c5a37 | |
Daniele Gobbetti | c74ea64b70 | |
Daniele Gobbetti | 4d2cc8cfcb | |
Jonas | ad5dcb0836 | |
Hadrián Candela | a19c3093fc | |
Hadrián Candela | a1ed51edde | |
masakoodaa | 71f10ee30e | |
masakoodaa | d0725782c9 | |
Jasper | 8d27c9baab | |
masakoodaa | 98250855bc | |
Jasper | e86673fd1f | |
c4ndel4 | ac8524f503 | |
c4ndel4 | f230750a5c | |
c4ndel4 | 178445e98c | |
Minori Hiraoka (미노리) | d533f92748 | |
Jonas | 5150ec11a3 | |
Jonas | dc29bb7fca | |
Jonas | 36cd9ba2b5 | |
Jonas | a59e9d8877 | |
cpfeiffer | 3c9fab0471 | |
Andreas Shimokawa | e28085e6af | |
Andreas Shimokawa | 717eb6ba58 | |
Andreas Shimokawa | 46b50515f3 | |
Andreas Shimokawa | 2be84435ce | |
Frank Slezak | 6ec1555178 | |
Andreas Shimokawa | 67584be314 | |
Andreas Shimokawa | 622a6e7679 | |
Andreas Shimokawa | 081426d2ef | |
Andreas Shimokawa | f5b8bdb1c2 | |
Andreas Shimokawa | 412153364e | |
cpfeiffer | 35e59d0add | |
Andreas Shimokawa | 37da178365 | |
Andreas Shimokawa | 9471131490 | |
Andreas Shimokawa | e5a8ca5374 | |
Andreas Shimokawa | d49db12a0d | |
Andreas Shimokawa | 3301194e75 | |
Kaz Wolfe | 2bc0c27b90 | |
mueller-ma | 92d79055e0 | |
mueller-ma | a845f21493 | |
mueller-ma | d7152f568f | |
mueller-ma | 37b5cd626e | |
Jonas | 06c4a91eee | |
Jonas | 13b141b22f | |
Minori Hiraoka (미노리) | 3e48e97ab3 | |
naofum | 0e0310efa8 | |
Jonas | 33aa1eff90 | |
Jonas | b190354ba1 | |
mueller-ma | 78494f3155 | |
License Bot | a10fc8ad4e | |
Andreas Shimokawa | acf779a8e4 | |
Andreas Shimokawa | b87d9d649d | |
Andreas Shimokawa | 0b8494faee | |
Michal Novotny | 05d0625b68 | |
Quallenauge | 851e47f550 | |
Daniele Gobbetti | 6def9dc07e | |
Gabe Schrecker | 6f702778f4 | |
protomors | b66b33239d | |
protomors | 273c2ddbfd | |
protomors | 918cc75f6c | |
cpfeiffer | e9a68e70b5 | |
Andreas Shimokawa | 5cd00ccbb5 | |
Andreas Shimokawa | 1efd73af5e | |
Michal Novotny | fcf9be877a | |
Yaron Shahrabani | 25fdf50525 | |
Yaron Shahrabani | 4ea93a2471 | |
Jonas | 0f6a86ef8f | |
Yaron Shahrabani | 0e6b73502b | |
Jonas | 345338b16a | |
Jonas | a727b859e0 | |
Jonas | 3f800e5fd3 | |
Jonas | fac2778d05 | |
naofum | 057cf7a0d8 | |
Jan Lolek | 62e1b1fc84 | |
Andreas Shimokawa | 1f3530c22d | |
Daniele | a21c93359b | |
mueller-ma | ea3dd08d0e | |
Michal Novak | d6c2623ef2 | |
mueller-ma | 29ecac3557 | |
mueller-ma | 03a8535078 | |
cpfeiffer | a93ace255b | |
cpfeiffer | e44eb03698 | |
cpfeiffer | 1ddea9268d | |
Sami Alaoui | f6ce0c1a0e | |
protomors | b8c5a44709 | |
protomors | 22a15c631c | |
protomors | b7c1c28e76 | |
protomors | ecd9964c5b | |
cpfeiffer | f5934dfb3b | |
cpfeiffer | 55bf9ef784 | |
Andreas Shimokawa | cfc310692f | |
Andreas Shimokawa | 45263d08d5 | |
Andreas Shimokawa | a63dc4a018 | |
cpfeiffer | 24797c7dd7 | |
cpfeiffer | 962720145e | |
Andreas Shimokawa | f39d8cd2e2 | |
cpfeiffer | c468e7f521 | |
cpfeiffer | c91e14f644 | |
Andreas Shimokawa | eaf7b76715 | |
protomors | e7fff32fb8 | |
protomors | c97136e4fe | |
protomors | f8473ac42d | |
protomors | 259fc87b68 | |
protomors | f5b8fada75 | |
Andreas Shimokawa | e839a2c6a3 | |
Andreas Shimokawa | be147913c3 | |
Andreas Shimokawa | 32c03013ce | |
Andreas Shimokawa | c946ef5201 | |
Andreas Shimokawa | 144491ea4b | |
Andreas Shimokawa | 23fa37d99d | |
AnthonyDiGirolamo | f855dc5d45 | |
AnthonyDiGirolamo | 24c9ef339b | |
Translation Bot | d4b29418ca | |
Andreas Shimokawa | 4bc6e2f71d | |
Andreas Shimokawa | a3108a4958 | |
Andreas Shimokawa | 455dfde63d | |
Andreas Shimokawa | 1ff8fbac55 | |
Daniele Gobbetti | 612592516b | |
Daniele Gobbetti | f7e814431e | |
Daniele Gobbetti | 44d2384aec | |
Daniele Gobbetti | 41feb008a7 | |
mueller-ma | fe07adb041 | |
Andreas Shimokawa | 18eb39853b | |
Translation Bot | 4e751454b4 | |
Translation Bot | 63e846dbf3 | |
Andreas Shimokawa | fdcdd76b22 | |
Andreas Shimokawa | 107a03b0db | |
Andreas Shimokawa | 2eb25e7c4e | |
Andreas Shimokawa | b4639b9062 | |
Andreas Shimokawa | 6fb0a977fc | |
Vebryn | 1a1eec3008 | |
Vebryn | ea6457c359 | |
Vebryn | a61cbddb5d | |
mueller-ma | bde8e4c0e6 | |
Andreas Shimokawa | 88520a018c | |
Andreas Shimokawa | 3fd6590a9a | |
Andreas Shimokawa | dea968edf6 | |
Andreas Shimokawa | 32578d3c46 | |
Translation Bot | 1cec43bfe4 | |
Andreas Shimokawa | 9ac4b923c4 | |
Andreas Shimokawa | 165dcf897b | |
Andreas Shimokawa | a5886cbb49 | |
Andreas Shimokawa | 6d28b8232b | |
Daniele Gobbetti | 8aebf2d9d5 | |
License Bot | 15f4ce2869 | |
Andreas Shimokawa | 013e270a9c | |
Andreas Shimokawa | 4bb18b9795 | |
Andreas Shimokawa | 5f1014f553 | |
Andreas Shimokawa | 12d9b7812f | |
Andreas Shimokawa | ebc1cedf55 | |
Andreas Shimokawa | a398f33cb8 | |
Andreas Shimokawa | f9e43919ae | |
Andreas Shimokawa | 460c5c9a24 | |
Andreas Shimokawa | fd952aa5ae | |
Andreas Shimokawa | b9eedce13b | |
Andreas Shimokawa | 4c8728c78f | |
Andreas Shimokawa | fdcc51cb98 | |
Andreas Shimokawa | a969d4b7dd | |
Andreas Shimokawa | 1eff950bde | |
Andreas Shimokawa | 628efe88e5 | |
Daniele Gobbetti | 1017561fb6 | |
Daniele Gobbetti | 8c23d6ec29 | |
Andreas Shimokawa | 0f8638f2fd | |
Andreas Shimokawa | 1f6634374d | |
Daniele Gobbetti | c05e5f15ab | |
Andreas Shimokawa | b19cf85a12 | |
Andreas Shimokawa | 6f522ec5f2 | |
Daniele Gobbetti | c1834ec4ea | |
Daniele Gobbetti | 8cce2d1362 | |
Daniele Gobbetti | 6c38c6bb79 | |
Andreas Shimokawa | a6059a5ce2 | |
Daniele Gobbetti | 91b1464824 | |
Andreas Shimokawa | cd5af1e66a | |
Translation Bot | 7108dd7b88 | |
cpfeiffer | 6b1ba4d161 | |
Translation Bot | 5cfb108d52 | |
Andreas Shimokawa | 0009ed8729 | |
Andreas Shimokawa | f0c9728775 | |
Translation Bot | 3c9964f265 | |
Translation Bot | 98f01e8b23 | |
Andreas Shimokawa | 80dce95372 | |
Andreas Shimokawa | 4dc53a4390 | |
Andreas Shimokawa | 7302832d84 | |
Andreas Shimokawa | b25febf0e5 | |
Andreas Shimokawa | 9ea4b8ae43 | |
mueller-ma | 2f375e9a41 | |
mueller-ma | 3c8706cc22 | |
cpfeiffer | 95ce3d333e | |
cpfeiffer | 12f9386fac | |
cpfeiffer | c1925a4e64 | |
Gergely Peidl | 6cb400a63c | |
Andreas Shimokawa | c2af2dd15c | |
Daniele Gobbetti | 8353026c08 | |
lazarosfs | 7f5aeb6ab1 | |
Andreas Shimokawa | 23d12f7289 | |
Andreas Shimokawa | 8269b363b0 | |
Daniele Gobbetti | fe3448f6e4 | |
Andreas Shimokawa | 7806a9a6cf | |
Andreas Shimokawa | aa58bf6815 | |
Andreas Shimokawa | 3de35a6f6a | |
Andreas Shimokawa | e5edd334c1 | |
Andreas Shimokawa | 30eee7ccd5 | |
Andreas Shimokawa | f98131ccd5 | |
Andreas Shimokawa | 0916769096 | |
Daniele Gobbetti | caf79bb5e6 | |
Andreas Shimokawa | 166501e221 | |
José Rebelo | f7abe2d4a3 | |
Andreas Shimokawa | 42f4200209 | |
José Rebelo | 34bd2ed9cc | |
cpfeiffer | 01d3a3a7be | |
José Rebelo | ceb82f3474 | |
cpfeiffer | a43a940f0c | |
cpfeiffer | 18926e6bbd | |
Carsten Pfeiffer | 377e999067 | |
José Rebelo | 6c95a9fcb9 | |
Daniele Gobbetti | 7e6a41a773 | |
José Rebelo | 2c0b105aa6 | |
Andreas Shimokawa | 23c6219cef | |
cpfeiffer | 7ee3deef38 | |
Andreas Shimokawa | a4e35b49b2 | |
Translation Bot | fb8f866031 | |
Translation Bot | f9131f1c5e | |
Andreas Shimokawa | 683a074f7a | |
João Paulo Barraca | e97f4d3909 | |
freezed-or-frozen | 9b5c1b91c0 | |
Andreas Shimokawa | 05a4486277 | |
Daniele Gobbetti | 737578debc | |
cpfeiffer | 4e9b85999e | |
Pavel Motyrev | 3a55c67b9e | |
João Paulo Barraca | 4c7d6d4a10 | |
João Paulo Barraca | bd754b4130 | |
João Paulo Barraca | 013cbf139a | |
João Paulo Barraca | 759b9c81a3 | |
cpfeiffer | e279cd736f | |
cpfeiffer | c79eda5507 | |
cpfeiffer | 5e079bb480 | |
João Paulo Barraca | 845869e25e | |
cpfeiffer | 1d79c9d93d | |
cpfeiffer | db935c650d | |
cpfeiffer | 03d8667827 | |
cpfeiffer | 07f4d3148a | |
Andreas Shimokawa | b1d1e701f9 | |
Andreas Shimokawa | 7cce2aeb8b | |
cpfeiffer | e4faabeca3 | |
cpfeiffer | 0e4b9a4eb8 | |
Vebryn | 7dc9c28c74 | |
Andreas Shimokawa | b31a6a5db9 | |
Andreas Shimokawa | a1690700f4 | |
Andreas Shimokawa | 4591f07bcd | |
Tomer Rosenfeld | c3df48a25b | |
Andreas Shimokawa | bbbb9dd448 | |
Andreas Shimokawa | 805a38ae3c | |
Andreas Shimokawa | eea1fbcca4 | |
Translation Bot | 6be1a4b7e7 | |
João Paulo Barraca | d73d4b3a13 | |
Andreas Shimokawa | 9f309df84d | |
João Paulo Barraca | 8a39d8b2eb | |
João Paulo Barraca | 497f9a6658 | |
Andreas Shimokawa | b475fd2dc7 | |
Andreas Shimokawa | bdeb215fe0 | |
Daniele Gobbetti | bc28990a96 | |
Daniele Gobbetti | ccfe8d5777 | |
Andreas Shimokawa | 15102d525b | |
Daniele Gobbetti | cede8a0826 | |
cpfeiffer | 07c61e6bcb | |
cpfeiffer | c3c5e0415d | |
cpfeiffer | d8cbb18587 | |
cpfeiffer | f1fbab7dd9 | |
Andreas Shimokawa | c4f8f86c00 | |
Andreas Shimokawa | 43fc3873bb | |
Andreas Shimokawa | 62efd90e17 | |
Andreas Shimokawa | b0384e90d5 | |
Andreas Shimokawa | 6627371f92 | |
João Paulo Barraca | 69d215cb99 | |
João Paulo Barraca | 166695f00a | |
João Paulo Barraca | c9da7548ed | |
João Paulo Barraca | 58cb73a756 | |
Daniele Gobbetti | 41ecd4e7d6 | |
License Bot | 8af9054f2d | |
cpfeiffer | 739b5e9c50 | |
cpfeiffer | 85511fb97f | |
cpfeiffer | b2a1805e4f | |
cpfeiffer | d9b0d639b8 | |
Daniele Gobbetti | 534eb385f7 | |
Daniele Gobbetti | fae116d1bd | |
Andreas Shimokawa | bc4503c8bf | |
Andreas Shimokawa | 839e350d1e | |
Translation Bot | b5255f2e2a | |
cpfeiffer | 523055189f | |
Daniele Gobbetti | d570d188a2 | |
cpfeiffer | ed02a9781a | |
Daniele Gobbetti | f06298a3c8 | |
cpfeiffer | 9ed8004a69 | |
cpfeiffer | 3f8620e026 | |
cpfeiffer | 2003d56190 | |
cpfeiffer | 36c1b5a6f2 | |
cpfeiffer | 63c640f471 | |
cpfeiffer | 1e4351b03a | |
João Paulo Barraca | 70663af35a | |
Andreas Shimokawa | 33c9752fde | |
Andreas Shimokawa | edcfae3645 | |
Andreas Shimokawa | 6dc1d76592 | |
Andreas Shimokawa | 965ba190a6 | |
cpfeiffer | 696dc1f08d | |
Andreas Shimokawa | bc368a788b | |
Andreas Shimokawa | 8385d8079a | |
Andreas Shimokawa | 15fb71337b | |
Andreas Shimokawa | 5717379aec | |
Andreas Shimokawa | beb173f162 | |
Andreas Shimokawa | 7ee20348db | |
Andreas Shimokawa | a28d27839f | |
Daniele Gobbetti | 3ef5f5b811 | |
Andreas Shimokawa | 546b68ad2d | |
Daniele Gobbetti | 18157daf46 | |
João Paulo Barraca | 9decb7788b | |
João Paulo Barraca | 9f0d260e7a | |
cpfeiffer | b142add631 | |
Andreas Shimokawa | 087f5879b0 | |
Andreas Shimokawa | 35efa30c4b | |
Daniele Gobbetti | 7b50ba9572 | |
Daniele Gobbetti | 1e231e6129 | |
Daniele Gobbetti | 61690eb2cc | |
Daniele Gobbetti | d9769be78d | |
Andreas Shimokawa | 60b7a73558 | |
Andreas Shimokawa | a936ff0616 | |
Daniele Gobbetti | 0cf3625304 | |
Andreas Shimokawa | df0e77f368 | |
Andreas Shimokawa | f7ca1fc76c | |
Andreas Shimokawa | 67f035accf | |
Andreas Shimokawa | 9970f8017f | |
Daniel Hauck | ccb58f0f3c | |
cpfeiffer | 16af0724dd | |
cpfeiffer | 589945f234 | |
João Paulo Barraca | 6ed40a21c6 | |
cpfeiffer | c93e97f10f | |
Andreas Shimokawa | 3860c2f9c4 | |
Translation Bot | a5fdc90b6e | |
Andreas Shimokawa | 810ba5419b | |
Daniele Gobbetti | 5bf6251dc5 | |
Daniele Gobbetti | fe626eb11e | |
cpfeiffer | 1a88858c6f | |
Daniele Gobbetti | a77ff03ca5 | |
cpfeiffer | bb98910e1c | |
cpfeiffer | 39c7c1aae3 | |
cpfeiffer | 4519f35ff1 | |
Andreas Shimokawa | f658059d20 | |
cpfeiffer | 4a4a1e25df | |
Alberto | 155ce5be02 | |
cpfeiffer | 2feb3bed47 | |
cpfeiffer | c2f83fa857 | |
Daniele Gobbetti | 48728cbb50 | |
Daniele Gobbetti | 16cff936d3 | |
Daniele Gobbetti | 018c2a971e | |
Daniele Gobbetti | dd5ee03932 | |
Daniele Gobbetti | 2e98b1396f | |
Daniele Gobbetti | bd833a37d4 | |
Daniele Gobbetti | 5a019c238a | |
Alberto | de6ce1a3d7 | |
Daniele Gobbetti | 3004177f44 | |
Daniele Gobbetti | cad777e4ce | |
Daniele Gobbetti | df71d695c3 | |
Daniele Gobbetti | 4dbc255ad5 | |
cpfeiffer | 60ed9ca373 | |
cpfeiffer | 202ae53d71 | |
cpfeiffer | e1797fc9f7 | |
cpfeiffer | 26ff7d67e3 | |
cpfeiffer | d2053b32bf | |
cpfeiffer | 94edaa0cc1 | |
Alberto | f3edf7559d | |
cpfeiffer | 5b8624de71 | |
Andreas Shimokawa | 3675386c13 | |
Alberto | 45eb14684b | |
cpfeiffer | f48729cc64 | |
Daniele Gobbetti | 8e780fa122 | |
Daniele Gobbetti | 4ab39e2c00 | |
Daniele Gobbetti | e556a65ff5 | |
Daniele Gobbetti | 0573939c9e | |
Daniele Gobbetti | 96a49f0b7a | |
Daniele Gobbetti | d8894d315a | |
walkjivefly | f0d81818b3 | |
Andreas Shimokawa | e4c7a921ea | |
Andreas Shimokawa | 742615c6f4 | |
cpfeiffer | f2dca649a3 | |
walkjivefly | f321a4bac5 | |
Daniele Gobbetti | e89ba529c3 | |
Daniele Gobbetti | 9a0439c6e0 | |
Daniele Gobbetti | db4e37d08b | |
Andreas Shimokawa | 173b4fbbe6 | |
Daniele Gobbetti | 8fccbe3b69 | |
Daniele Gobbetti | f80215b37a | |
Andreas Shimokawa | 562049296c | |
João Paulo Barraca | ab3f6c0bbf | |
Daniele Gobbetti | 447885033b | |
Daniele Gobbetti | eae119f9df | |
Andreas Shimokawa | b25bc66485 | |
Andreas Shimokawa | 07272e5a68 | |
Daniele Gobbetti | 46501be249 | |
Daniele Gobbetti | 1813ec9378 | |
Daniele Gobbetti | d550defcb3 | |
João Paulo Barraca | 11b48e7a1a | |
João Paulo Barraca | 75089d7cb4 | |
cpfeiffer | da9742fd67 | |
cpfeiffer | 14552a1a80 | |
cpfeiffer | b97674ba85 | |
Andreas Shimokawa | 7c63f92aaa | |
Translation Bot | ad82a75312 | |
Andreas Shimokawa | abd79ef5c9 | |
Andreas Shimokawa | 5d042cf3c8 | |
cpfeiffer | cc159cf80f | |
cpfeiffer | f1d07c83f6 | |
Andreas Shimokawa | b2886b81c9 | |
Andreas Shimokawa | fe07e09d41 | |
Andreas Shimokawa | 9eade33d72 | |
License Bot | 6a842c52fa | |
Andreas Shimokawa | 7a6b0ed2b0 | |
Andreas Shimokawa | 408dd9c26f | |
cpfeiffer | f4e955dbe0 | |
Translation Bot | c1af01a155 | |
cpfeiffer | d408be5ec8 | |
cpfeiffer | cf35e84feb | |
cpfeiffer | 5d96df3508 | |
cpfeiffer | 2d60beea1f | |
cpfeiffer | e62a860ee6 | |
Alberto | 6989ca9db3 | |
cpfeiffer | 17ecee0cab | |
cpfeiffer | 8fc6dfeca7 | |
cpfeiffer | 4b230412b6 | |
Andreas Shimokawa | a6bba1b094 | |
Daniele Gobbetti | 5008f08272 | |
Andreas Shimokawa | 2c1923dd96 | |
Translation Bot | 6020b591d7 | |
Andreas Shimokawa | 99bc922d13 | |
Daniele Gobbetti | 0ac77fc0a4 | |
Daniele Gobbetti | 183d89dc47 | |
Daniele Gobbetti | ecd2c166c2 | |
Daniele Gobbetti | 2c152e8447 | |
Daniele Gobbetti | 68608f8582 | |
Daniele Gobbetti | 8117caf73c | |
Andreas Shimokawa | fe870ebc77 | |
Andreas Shimokawa | e6928202c5 | |
Translation Bot | aa60acbf07 | |
Daniele Gobbetti | c23b938a9a | |
Daniele Gobbetti | a1af4a4599 | |
Translation Bot | 05a28cc580 | |
License Bot | e392fbfd80 | |
Daniele Gobbetti | a566a6656c | |
cpfeiffer | 6282597790 | |
Translation Bot | 3abbe12b53 | |
Andreas Shimokawa | f070ce5ce7 | |
Andreas Shimokawa | 410fc0e8dc | |
cpfeiffer | 9411f80440 | |
cpfeiffer | 2b17d7fb14 | |
cpfeiffer | 31e0e9a5f7 | |
cpfeiffer | f6bee00582 | |
cpfeiffer | 0b45fe63f0 | |
cpfeiffer | 09d4f81ce8 | |
cpfeiffer | 4ecd4b6896 | |
cpfeiffer | 94744677c9 | |
cpfeiffer | c56b655b48 | |
cpfeiffer | 88b35c6eec | |
Andreas Shimokawa | 4a3eb6e8de | |
Yaron Shahrabani | 858eaa6690 | |
cpfeiffer | 58e2538c4e | |
cpfeiffer | 09967b2006 | |
cpfeiffer | 82ea5702c5 | |
cpfeiffer | 72801af0e7 | |
cpfeiffer | 4419200624 | |
Andreas Shimokawa | aa6e9608bd | |
Andreas Shimokawa | a90e0074fc | |
Andreas Shimokawa | a0bb0c973c | |
Andreas Shimokawa | 216d35aeee | |
Andreas Shimokawa | 9b2f47d10a | |
Andreas Shimokawa | 1efdfb757c | |
Andreas Shimokawa | f97a53a89f | |
Andreas Shimokawa | 03181202d1 | |
Andreas Shimokawa | 55019579ef | |
Andreas Shimokawa | 914143960d | |
Andreas Shimokawa | 7a3b2899e7 | |
Andreas Shimokawa | cbebd845cf | |
cpfeiffer | 7fea11d752 | |
cpfeiffer | c619f17637 | |
cpfeiffer | 21f2fed7e8 | |
Daniele Gobbetti | ca73d0c2d4 | |
cpfeiffer | 19b0e5e801 | |
Andreas Shimokawa | ac1875eea0 | |
Daniele Gobbetti | 176cf79cc1 | |
Andreas Shimokawa | 8b39ef3a52 | |
Andreas Shimokawa | aac9827e63 | |
Daniele Gobbetti | e7846f4754 | |
Daniele Gobbetti | 2eb43fa740 | |
Andreas Shimokawa | 08080b02bb | |
cpfeiffer | 49e1b55ad8 | |
Andreas Shimokawa | 437ec6c9b7 | |
cpfeiffer | 6834f36ec7 | |
cpfeiffer | 337bfa1938 | |
Andreas Shimokawa | e9cb5fd374 | |
Andreas Shimokawa | db58b32b6f | |
Andreas Shimokawa | 24794c46b1 | |
Andreas Shimokawa | c23e496db6 | |
Andreas Shimokawa | 2dbda6138b | |
Andreas Shimokawa | ad9cfae6f9 | |
Andreas Shimokawa | 946ed5f000 | |
cpfeiffer | e5d09b9fa2 | |
Avamander | 23f2dd35d4 | |
Andreas Shimokawa | a26563d6c7 | |
Andreas Shimokawa | 1d1edd41d7 | |
Andreas Shimokawa | b31dd9b2fa | |
Andreas Shimokawa | c851f73265 | |
Andreas Shimokawa | 3936a7d8a0 | |
Avamander | fea31924ba | |
Andreas Shimokawa | 5dfd40062f | |
Andreas Shimokawa | f956d94181 | |
Andreas Shimokawa | ee28ccd4fe | |
cpfeiffer | 0042ffc514 | |
cpfeiffer | 89bf63d540 | |
Andreas Shimokawa | f35e3e460d | |
Andreas Shimokawa | c0076b20d3 | |
Andreas Shimokawa | 083b8db1ec | |
Andreas Shimokawa | 2b7162055d | |
Andreas Shimokawa | 5bb1995eb9 | |
Andreas Shimokawa | 436a7ddb24 | |
cpfeiffer | 4f0674d038 | |
Andreas Shimokawa | e2b3394900 | |
Andreas Shimokawa | b6852308b7 | |
Andreas Shimokawa | 32a326c24b | |
João Paulo Barraca | 475426c0ed | |
João Paulo Barraca | a3cc84c01d | |
João Paulo Barraca | bf8ae5d5af | |
João Paulo Barraca | 644c06df68 | |
Andreas Shimokawa | 030edef033 | |
Andreas Shimokawa | b3cddebdbb | |
Daniele Gobbetti | b7bad268c2 | |
Daniele Gobbetti | dccd6c1b06 | |
Daniele Gobbetti | b894c01822 | |
ivanovlev | fd61dc602f | |
Andreas Shimokawa | cc917e97a6 | |
João Paulo Barraca | 006a23dfe8 | |
Andreas Shimokawa | 22cf74bbd1 | |
Daniele Gobbetti | 3fcf4938b9 | |
Daniele Gobbetti | e08a900978 | |
João Paulo Barraca | f79e8f8833 | |
cpfeiffer | d030ad9400 | |
cpfeiffer | b157f84b83 | |
cpfeiffer | 2ae4497261 | |
cpfeiffer | ec6a8b6743 | |
cpfeiffer | 6c16b4fb15 | |
cpfeiffer | 4c48b473ac | |
Carsten Pfeiffer | 42031cb50f | |
ivanovlev | 2d3907b0f0 | |
cpfeiffer | 13af1c1e11 | |
cpfeiffer | f9779d9695 | |
cpfeiffer | ba7d13fa5d | |
Andreas Shimokawa | 298e2a9955 | |
Andreas Shimokawa | f81ff8591b | |
João Paulo Barraca | d7db6559d8 | |
Andreas Shimokawa | e19ea26478 | |
Andreas Shimokawa | 896eb19b3e | |
Andreas Shimokawa | b0ac785066 | |
Andreas Shimokawa | cfa08d4fc4 | |
João Paulo Barraca | b3e1cbf55e | |
cpfeiffer | 5d3028c123 | |
cpfeiffer | ac68bfe351 | |
cpfeiffer | b8b2d8830f | |
cpfeiffer | 4c26c2933b | |
cpfeiffer | d103d09fcf | |
Andreas Shimokawa | 083cbdbfbe | |
Andreas Shimokawa | 2b632d8b39 | |
Andreas Shimokawa | 25433ef6bc | |
Andreas Shimokawa | 4f45ad660d | |
ivanovlev | 09539fd9bf | |
ivanovlev | 06295abcb6 | |
Andreas Shimokawa | a451b5367b | |
Andreas Shimokawa | 712ce1aa8b | |
Andreas Shimokawa | 3233432ee1 | |
Andreas Shimokawa | 3dd058cf81 | |
Andreas Shimokawa | fb7db523c7 | |
João Paulo Barraca | b4a4b3916a | |
Andreas Shimokawa | 746eeda777 | |
Andreas Shimokawa | 8027b8ac96 | |
Andreas Shimokawa | 378d285b1a | |
João Paulo Barraca | 1f083041b9 | |
João Paulo Barraca | c4a0c60b8c | |
Andreas Shimokawa | 019da98dfa | |
Andreas Shimokawa | c39318af05 | |
Daniele Gobbetti | 373e96ca30 | |
Carsten Pfeiffer | 5b116aae93 | |
Daniele Gobbetti | a7a37fd9c8 | |
6arms1leg | befedaf7d2 | |
Carsten Pfeiffer | 31ccaf361b | |
ivanovlev | c13725911f | |
Andreas Shimokawa | cf45c665a5 | |
Andreas Shimokawa | 26a751977e | |
Andreas Shimokawa | ed020c2a97 | |
ivanovlev | 0094805359 | |
ivanovlev | cbc91e7fef | |
Carsten Pfeiffer | e226a97c73 | |
João Paulo Barraca | 5222cf99a2 | |
Andreas Shimokawa | f19541c654 | |
ivanovlev | bfe24dd9f0 | |
ivanovlev | 2de9580dea | |
Daniele Gobbetti | 26a349210e | |
Andreas Shimokawa | d9d153c463 | |
ivanovlev | d08972e82a | |
ivanovlev | 01d9a63e8b | |
ivanovlev | 074394cba4 | |
Andreas Shimokawa | b0991d3869 | |
Andreas Shimokawa | ce67bf2c52 | |
ivanovlev | b9249065eb | |
Daniele Gobbetti | 4dfef382a9 | |
Daniele Gobbetti | 0152e7ce02 | |
Daniele Gobbetti | cb3460912f | |
Andreas Shimokawa | 23a1663384 | |
ivanovlev | c873312831 | |
cpfeiffer | 1e24fa7ad8 | |
Andreas Shimokawa | 38e234552d | |
Andreas Shimokawa | 0218cee0e1 | |
Andreas Shimokawa | 50cb3c9db3 | |
Andreas Shimokawa | 5e74338efe | |
Andreas Shimokawa | 453eeb0bae | |
Andreas Shimokawa | bcc4fa8e9c | |
Andreas Shimokawa | 9132736428 | |
Andreas Shimokawa | 185605211d | |
Carsten Pfeiffer | d646b6773e | |
Andreas Shimokawa | f2e6ce6380 | |
João Paulo Barraca | b92b1c08bf | |
João Paulo Barraca | 13ec497127 | |
João Paulo Barraca | 4cf872664c | |
Andreas Shimokawa | 8f988de49d | |
Andreas Shimokawa | fda317671a | |
Andreas Shimokawa | 8c0f5599a1 | |
Andreas Shimokawa | 3644d5e7a6 | |
Andreas Shimokawa | c6999713d2 | |
Andreas Shimokawa | f05b51fd83 | |
Andreas Shimokawa | a2c052090b | |
Andreas Shimokawa | f56a4d878e | |
Andreas Shimokawa | 6619a16b63 | |
Andreas Shimokawa | 98dc1e127e | |
cpfeiffer | 7b1ea68b62 | |
cpfeiffer | 96203f574f | |
cpfeiffer | 6c269aa089 | |
Andreas Shimokawa | 855a141ec4 | |
Daniele Gobbetti | 1fda1ba1b2 | |
Daniele Gobbetti | c1abaaa4e0 | |
Yar | 5c8f02d054 | |
Daniele Gobbetti | 09cc0134db | |
Daniele Gobbetti | 7f50e0d2b7 | |
Andreas Shimokawa | 380e3b3640 | |
João Paulo Barraca | 970c6960ea | |
João Paulo Barraca | ade7161c4d | |
João Paulo Barraca | d491921b1c | |
João Paulo Barraca | 93ae57bd60 | |
João Paulo Barraca | 051d1e7390 | |
João Paulo Barraca | ae0718c398 | |
João Paulo Barraca | 510427e30b | |
João Paulo Barraca | 91b346b23d | |
João Paulo Barraca | 9d67394720 | |
Andreas Shimokawa | fd51f32765 | |
Daniele Gobbetti | e70d6a1260 | |
Hasan Ammar | 0ba377bb42 | |
João Paulo Barraca | 547736f8f7 | |
João Paulo Barraca | 1fb4ee8a8f | |
Andreas Shimokawa | 507e58922f | |
Andreas Shimokawa | f25605f5a1 | |
Andreas Shimokawa | 8b55110679 | |
Andreas Shimokawa | 1722a6dc47 | |
Andreas Shimokawa | 7930b7da75 | |
Andreas Shimokawa | 5a83cb1c48 | |
Andreas Shimokawa | b811247704 | |
Andreas Shimokawa | 984e639e97 | |
Andreas Shimokawa | 4e543d4b34 | |
Andreas Shimokawa | 82c0f35c58 | |
Andreas Shimokawa | 19c5cbfbb9 | |
Andreas Shimokawa | 266c6b8817 | |
Andreas Shimokawa | f12e786837 | |
Andreas Shimokawa | 95e6d2c740 | |
Andreas Shimokawa | 4631e5bbaf | |
Daniele Gobbetti | 3280607cc9 | |
Daniele Gobbetti | e477d22c88 | |
Daniele Gobbetti | 0e9ce5d186 | |
Andreas Shimokawa | 240c81ecb4 | |
Andreas Shimokawa | b045d5ac26 | |
cpfeiffer | b2d9c357e7 | |
cpfeiffer | cde3b36968 | |
cpfeiffer | bf777800d2 | |
cpfeiffer | 5d3c45d2c0 | |
cpfeiffer | e77c4e7bdb | |
Daniele Gobbetti | b1914a140c | |
Daniele Gobbetti | 5f48b89dc5 | |
cpfeiffer | df1fe7c5b8 | |
cpfeiffer | aadde7d1ca | |
cpfeiffer | a96a747119 | |
cpfeiffer | 0646eda646 | |
cpfeiffer | 9cea2fc3bd | |
João Paulo Barraca | a135f51d31 | |
João Paulo Barraca | fed5638782 | |
Andreas Shimokawa | bcb522d2d0 | |
Daniele Gobbetti | 4ce890b5ce | |
Daniele Gobbetti | 353bd4651b | |
Andreas Shimokawa | 440a5e071f | |
Andreas Shimokawa | 16d9279728 | |
cpfeiffer | bb8aff8c99 | |
cpfeiffer | da494cde7b | |
cpfeiffer | 8719cadc43 | |
cpfeiffer | 305bd7600c | |
cpfeiffer | 999d3e3252 | |
Andreas Shimokawa | e8d4575261 | |
Andreas Shimokawa | 4925dec9f6 | |
Andreas Shimokawa | 3441192d19 | |
Andreas Shimokawa | 6c5b51cd6d | |
Carsten Pfeiffer | a84bc16503 | |
Andreas Shimokawa | 5fb05e8546 | |
Andreas Shimokawa | 467e90bccb | |
Andreas Shimokawa | 6af95d99be | |
Andreas Shimokawa | 0bdcdbae54 | |
Andreas Shimokawa | b5225145d4 | |
6arms1leg | f027dc2005 | |
João Paulo Barraca | 649e20ad04 | |
João Paulo Barraca | 88f2d2ee4f | |
João Paulo Barraca | cd915598b0 | |
João Paulo Barraca | 9dd5967f4e | |
João Paulo Barraca | 9a338c9bae | |
João Paulo Barraca | 2b78f2708f | |
João Paulo Barraca | b7cd908fbe | |
João Paulo Barraca | 6c186329df | |
João Paulo Barraca | ae9ebc1be8 | |
Andreas Shimokawa | 8c80146e16 | |
Andreas Shimokawa | 119028827d | |
João Paulo Barraca | 5b3ef8999f | |
Andreas Shimokawa | 2b777ecba9 | |
Andreas Shimokawa | aa62fa8f8e | |
Andreas Shimokawa | 7cb2425ffd | |
Andreas Shimokawa | 2148b431ea | |
Andreas Shimokawa | bd5dc6bfbc | |
Daniele Gobbetti | 771ca948a4 | |
Daniele Gobbetti | 846c74aa86 | |
Daniele Gobbetti | f1965c7b00 | |
Daniele Gobbetti | 861c655b5d | |
Daniele Gobbetti | 1f1a34cf25 | |
Andreas Shimokawa | 8990e7e3da | |
Andreas Shimokawa | 97aed43518 | |
Andreas Shimokawa | 9588535004 | |
Andreas Shimokawa | 321298e08a | |
Andreas Shimokawa | 2b3592f354 | |
cpfeiffer | 5f4254e39d | |
cpfeiffer | 375aa491d4 | |
cpfeiffer | 321c288e27 | |
cpfeiffer | d12103e95d | |
cpfeiffer | caaa38ed04 | |
cpfeiffer | dd48869fa5 | |
cpfeiffer | ed36410500 | |
Andreas Shimokawa | f74bb4e3f3 | |
cpfeiffer | efe3f2773b | |
cpfeiffer | 6e633e948a | |
cpfeiffer | c69889d177 | |
cpfeiffer | eb8129f62e | |
cpfeiffer | bcfc8bc110 | |
Daniele Gobbetti | bb5791485c | |
cpfeiffer | 40354f8f5a | |
cpfeiffer | 94c0d6af9d | |
Andreas Shimokawa | 825f2bf2e8 | |
Andreas Shimokawa | 31122f0b09 | |
cpfeiffer | daf6d12c62 | |
cpfeiffer | 6dfc895303 | |
cpfeiffer | 4a19046301 | |
cpfeiffer | fd2c182714 | |
cpfeiffer | 83ad2a9bd9 | |
Andreas Shimokawa | f63a7db5f9 | |
Daniele Gobbetti | a6a2c6d6d6 | |
Andreas Shimokawa | 779699cd95 | |
Andreas Shimokawa | a0e21d7c6d | |
Andreas Shimokawa | 0e1287e382 | |
Andreas Shimokawa | efb1cd389b | |
Daniele Gobbetti | 388c47ea29 | |
Andreas Shimokawa | 6d713a9be5 | |
Andreas Shimokawa | 17b581022b | |
Kevin Richter | 34296c021f | |
Daniele Gobbetti | 3e9898d86c | |
Andreas Shimokawa | 313499abd4 | |
Andreas Shimokawa | f735739396 | |
Daniele Gobbetti | 2d0489960e | |
Daniele Gobbetti | 13b761c073 | |
Andreas Shimokawa | ed38e524bf | |
Daniele Gobbetti | 259eb51784 | |
Daniele Gobbetti | 6a09023c24 | |
cpfeiffer | 04c3bc8203 | |
Carsten Pfeiffer | 82a05e29df | |
Andreas Shimokawa | 9e8aae3b2c | |
Daniele Gobbetti | 4eb56eb9ca | |
Carlos Ferreira | 4f08e18073 | |
Daniele Gobbetti | e53b8b6b32 | |
Daniele Gobbetti | e773b71194 | |
Daniele Gobbetti | 219cc7bff1 | |
cpfeiffer | 928bdd5d36 | |
Daniele Gobbetti | 8c01123a48 | |
Andreas Shimokawa | 17e9c7e331 | |
Andreas Shimokawa | 013029443b | |
Andreas Shimokawa | a691cd0ff7 | |
cpfeiffer | c1c6e37066 | |
Daniele Gobbetti | e0a844b60a | |
Andreas Shimokawa | 4763731c4e | |
Andreas Shimokawa | 3db009e77d | |
Andreas Shimokawa | ae2c107ed1 | |
Andreas Shimokawa | bc9041a4c9 | |
Andreas Shimokawa | 3eda2d4b81 | |
cpfeiffer | 44f74270df | |
cpfeiffer | da297ecd8b | |
Carsten Pfeiffer | dbe90d7ae3 | |
Uwe Hermann | 0746aaa579 | |
Uwe Hermann | 6dd74d04ac | |
Uwe Hermann | 1352575f12 | |
Uwe Hermann | 82bc89f042 | |
Uwe Hermann | 2846b9cc15 | |
Andreas Shimokawa | f0789cc147 | |
Andreas Shimokawa | 2993bb6b5c | |
Andreas Shimokawa | 74c20f3a82 | |
Andreas Shimokawa | b878fa5eda | |
Andreas Shimokawa | 95ec1fb44c | |
cpfeiffer | 092012ab31 | |
cpfeiffer | 09ff95eb34 | |
cpfeiffer | 49acde118d | |
Andreas Shimokawa | 1862b59dad | |
cpfeiffer | 011646b097 | |
Andreas Shimokawa | 2677dad873 | |
Andreas Shimokawa | 109a032f1e | |
Carsten Pfeiffer | d9e20b161a | |
Daniele Gobbetti | 84327a5b86 | |
Andreas Shimokawa | fa8df9f552 | |
Andreas Shimokawa | 16b4bfd0e7 | |
Andreas Shimokawa | 723ad53588 | |
Andreas Shimokawa | 24752d3455 | |
Andreas Shimokawa | 34ad088b88 | |
cpfeiffer | 2f7eb9ef23 | |
cpfeiffer | b9ff2cd468 | |
Gilles Émilien MOREL | c84003c1c0 | |
Andreas Shimokawa | b2e86ca061 | |
Andreas Shimokawa | 352fc1a030 | |
Andreas Shimokawa | 6106dda2a3 | |
Andreas Shimokawa | a5263141d7 | |
cpfeiffer | 2d4645f6cc | |
Andreas Shimokawa | 79eb4f32df | |
Andreas Shimokawa | 84caf22479 | |
cpfeiffer | 7da328d5db | |
Andreas Shimokawa | df4293108a | |
Andreas Shimokawa | 9d083e2330 | |
cpfeiffer | 02e6ce02b2 | |
cpfeiffer | 3fdfb7d172 | |
cpfeiffer | 9bebf1d32f | |
cpfeiffer | 60cb67c3c8 | |
cpfeiffer | cc0fbff297 | |
Andreas Shimokawa | 6520b46238 | |
Andreas Shimokawa | 381323011e | |
Daniele Gobbetti | 5b804effa4 | |
Gilles MOREL | a5a5e66c62 | |
cpfeiffer | 67d89ce1b9 | |
cpfeiffer | dfbaba4cb6 | |
cpfeiffer | 8d6888a13a | |
cpfeiffer | a8a7d8db31 | |
cpfeiffer | 0c51f86afc | |
cpfeiffer | 82cd06f4c1 | |
cpfeiffer | dbe96582a7 | |
Andreas Shimokawa | 4ed7906a9e | |
Andreas Shimokawa | 9dd61031f0 | |
Andreas Shimokawa | 8cb2030478 | |
Andreas Shimokawa | eb052cead3 | |
Andreas Shimokawa | 647b67cfca | |
Andreas Shimokawa | fce86482b9 | |
Andreas Shimokawa | e8da301da3 | |
Andreas Shimokawa | 4f3c46f704 | |
Andreas Shimokawa | 3b250a4568 | |
Andreas Shimokawa | c95587c915 | |
Andreas Shimokawa | 029cc02a29 | |
Andreas Shimokawa | ddfab1cdae | |
Andreas Shimokawa | 4dc085de57 | |
cpfeiffer | 51fa31aa66 | |
Andreas Shimokawa | 66e3de9168 | |
cpfeiffer | 96a16245df | |
Daniele Gobbetti | 42901a295d | |
Andreas Shimokawa | d41848014b | |
Andreas Shimokawa | 485cda52a8 | |
Andreas Shimokawa | d7256d172e | |
Andreas Shimokawa | 163a7bdf15 | |
Andreas Shimokawa | 1012236989 | |
Andreas Shimokawa | 4a243ff361 | |
Andreas Shimokawa | 82a47022fa | |
Andreas Shimokawa | 4b7f47ba6c | |
Daniele Gobbetti | 1a22752b98 | |
cpfeiffer | d8145a52f9 | |
Andreas Shimokawa | 00a71f53b3 | |
cpfeiffer | d89899557c | |
cpfeiffer | ddaf51768d | |
cpfeiffer | 3cc8d887ca | |
Carsten Pfeiffer | 1751c86479 | |
6arms1leg | 37eb60b60c | |
Andreas Shimokawa | f68bbe453b | |
Andreas Shimokawa | 1fcd7d8144 | |
Andreas Shimokawa | eb7646d26a | |
Andreas Shimokawa | 837dfd5917 | |
cpfeiffer | 3b474bb5a9 | |
cpfeiffer | 4843f7fc2b | |
cpfeiffer | d0beee3df6 | |
cpfeiffer | 705912172d | |
Daniele Gobbetti | 16c4f1a5ca | |
cpfeiffer | 119c225ec4 | |
cpfeiffer | 4c1b7e0328 | |
cpfeiffer | 55f036c104 | |
Andreas Shimokawa | 14ef5202e1 | |
cpfeiffer | 0076bbf572 | |
Daniele Gobbetti | 879e47760b | |
Daniele Gobbetti | 8c769b15c3 | |
Daniele Gobbetti | a45f76d3bf | |
Daniele Gobbetti | 46824b7235 | |
Daniele Gobbetti | d087e2142d | |
Daniele Gobbetti | b9bfb8c93a | |
Andreas Shimokawa | e50574d23c | |
Andreas Shimokawa | 2b834f96c9 | |
Daniele Gobbetti | 8fdb233ef0 | |
Daniele Gobbetti | a4b7b87b24 | |
Daniele Gobbetti | e2a9574406 | |
cpfeiffer | a4f615ce71 | |
cpfeiffer | d442030c2a | |
Andreas Shimokawa | e0d78e8208 | |
Andreas Shimokawa | 5b73690972 | |
Andreas Shimokawa | f755d99023 | |
cpfeiffer | c2a9348c6e | |
cpfeiffer | 8187c6d207 | |
cpfeiffer | cea5f5fa36 | |
cpfeiffer | 1cadb692fe | |
cpfeiffer | a941a6cd5f | |
cpfeiffer | e75f4f84e1 | |
cpfeiffer | 3db9748136 | |
cpfeiffer | e5ade5c0ef | |
cpfeiffer | 1352403089 | |
cpfeiffer | 544ec4958b | |
cpfeiffer | afe5f17e5a | |
Carsten Pfeiffer | 247023a57f | |
xzovy | d73b3137cb | |
Andreas Shimokawa | 300d0466af | |
Andreas Shimokawa | 55daaf247c | |
Julien Pivotto | 67937dd6ee | |
Andreas Shimokawa | 8603c3ffa0 | |
Andreas Shimokawa | 9a41d4d7a2 | |
Andreas Shimokawa | d6b9e6d64b | |
Andreas Shimokawa | bdf403210e | |
atkyritsis | 45cf4e5396 | |
Andreas Shimokawa | 4edfc44d64 | |
Andreas Shimokawa | d3571d53b2 | |
Daniele Gobbetti | ee1cf74a7b | |
Daniele Gobbetti | d467b37493 | |
Daniele Gobbetti | d93a5be57a | |
Daniele Gobbetti | 1f77e3e84f | |
cpfeiffer | 59212b54c8 | |
cpfeiffer | d302a0a5c3 | |
cpfeiffer | a39e3a035c | |
Daniele Gobbetti | cde09d71bc | |
cpfeiffer | 84f36b528a | |
cpfeiffer | de46555e37 | |
cpfeiffer | b20a9c9ccc | |
cpfeiffer | 069abe17b7 | |
cpfeiffer | 17b70a1b82 | |
cpfeiffer | 3a12ffd42d | |
Daniele Gobbetti | c6ce516f6a | |
Daniele Gobbetti | c20747226f | |
Daniele Gobbetti | 00938baf7d | |
Andreas Shimokawa | 68f83d3f33 | |
Andreas Shimokawa | 192b8e52ed | |
Andreas Shimokawa | d08012709f | |
Daniele Gobbetti | 503bcee7b4 | |
Daniele Gobbetti | 371f0ecdd0 | |
cpfeiffer | ee24443b6a | |
cpfeiffer | 15954d4561 | |
cpfeiffer | 839da4f06a | |
cpfeiffer | c87d08bf4b | |
Andreas Shimokawa | 04673923b6 | |
Andreas Shimokawa | 858714d73d | |
Andreas Shimokawa | cc2b22cfc7 | |
Gergely Peidl | bca408f366 | |
Andreas Shimokawa | 336ffd5bf7 | |
cpfeiffer | 9dc9ad6ce4 | |
cpfeiffer | 4122e0c20c | |
Daniele Gobbetti | 21fc5c7498 | |
cpfeiffer | 713989ef38 | |
cpfeiffer | b1dcb997bb | |
cpfeiffer | 344f6bcaa0 | |
cpfeiffer | 363b7cbf28 | |
cpfeiffer | 7c3dc741d2 | |
cpfeiffer | a559140f67 | |
cpfeiffer | 1fc44034f0 | |
cpfeiffer | f877a4a485 | |
cpfeiffer | e7c0afa603 | |
cpfeiffer | f1243f52c1 | |
Andreas Shimokawa | 24220ee5d3 | |
Andreas Shimokawa | 0fbc4d85ef | |
Andreas Shimokawa | c65a0a16de | |
Andreas Shimokawa | 09a5c7cceb | |
Andreas Shimokawa | a094f0cc76 | |
Andreas Shimokawa | cd195a5969 | |
Andreas Shimokawa | 18bcfe78b9 | |
cpfeiffer | 1100790456 | |
cpfeiffer | 92c629c351 | |
cpfeiffer | 17c152596b | |
cpfeiffer | 62828e5158 | |
cpfeiffer | b2d36dfb54 | |
cpfeiffer | 5e9c45e8b0 | |
cpfeiffer | 5c8525c5d0 | |
cpfeiffer | f57fec25f8 | |
cpfeiffer | db034a246c | |
cpfeiffer | 9e32e7d0d3 | |
cpfeiffer | c2ff05e849 | |
cpfeiffer | 125c0092cb | |
cpfeiffer | 5a2ddaaec0 | |
cpfeiffer | 558c9e4664 | |
cpfeiffer | 7479c3d420 | |
cpfeiffer | 713e9426b9 | |
cpfeiffer | e5d178b315 | |
cpfeiffer | 478782998e | |
cpfeiffer | ac9008aa02 | |
cpfeiffer | 75bca1b924 | |
cpfeiffer | f35f76a42b | |
cpfeiffer | eccf9164f6 | |
cpfeiffer | dd217c83af | |
cpfeiffer | dee492bc4f | |
Andreas Shimokawa | 6a5c3fb945 | |
Andreas Shimokawa | b8b8a05181 | |
cpfeiffer | 827c99f620 | |
Daniele Gobbetti | bbecfbeace | |
cpfeiffer | dd590528dc | |
cpfeiffer | f23ed5ce69 | |
Andreas Shimokawa | ed343778ee | |
cpfeiffer | 5bdc7933b3 | |
cpfeiffer | 2a0d97b39a | |
cpfeiffer | 09502f96c9 | |
cpfeiffer | 2e7fb57172 | |
cpfeiffer | b890242c4f | |
Andreas Shimokawa | 5e63b7ce04 | |
cpfeiffer | f44974c215 | |
cpfeiffer | 1fd6b59bf8 | |
cpfeiffer | 27c83604d3 | |
Andreas Shimokawa | 56d8a49d5b | |
Andreas Shimokawa | 456fcfdd98 | |
Andreas Shimokawa | bce28fd8ac | |
Andreas Shimokawa | 5c0618d43d | |
Daniele Gobbetti | 42f622af85 | |
Andreas Shimokawa | 30d686fa50 | |
cpfeiffer | e3f15f7bd8 | |
cpfeiffer | fbfc9ed97f | |
cpfeiffer | f58b1f33c6 | |
cpfeiffer | b2065fd91f | |
cpfeiffer | 1b5bc23981 | |
cpfeiffer | 0a4eefcf11 | |
cpfeiffer | 8f36712342 | |
Daniele Gobbetti | fabc52fdad | |
Andreas Shimokawa | b5373d9593 | |
cpfeiffer | dbdd7366ed | |
cpfeiffer | c2f8037f07 | |
Andreas Shimokawa | ea76e568cc | |
Andreas Shimokawa | cb232638d4 | |
Andreas Shimokawa | 5364bf6246 | |
Andreas Shimokawa | 9cccb085c4 | |
Andreas Shimokawa | 55a1248e8f | |
cpfeiffer | d4b134a490 | |
cpfeiffer | 0341c7f61f | |
JohnnySun | 3259efbd10 | |
Andreas Shimokawa | fd03dac5cd | |
Andreas Shimokawa | 8080734470 | |
Andreas Shimokawa | 28a1768f32 | |
Andreas Shimokawa | 09c68cebad | |
Andreas Shimokawa | 5bba58cf21 | |
Andreas Shimokawa | c8fb7c5d10 | |
Andreas Shimokawa | e1992f43e5 | |
Andreas Shimokawa | 88b3a69747 | |
cpfeiffer | 1bd919ccaa | |
cpfeiffer | ccdb843b6e | |
Andreas Shimokawa | 696611d392 | |
Andreas Shimokawa | 1f8cfa5a68 | |
Andreas Shimokawa | 6a18d90fee | |
Andreas Shimokawa | da01a76594 | |
Andreas Shimokawa | b2669d6fd7 | |
Andreas Shimokawa | 8ba7bc7353 | |
Carsten Pfeiffer | 2ee253965b | |
JohnnySun | 23428c3a16 | |
JohnnySun | 53d4681763 | |
JohnnySun | 19fbe5719c | |
JohnnySun | 08f2b0eb7c | |
Carsten Pfeiffer | 539bc73806 | |
JohnnySun | 90d730bdc8 | |
cpfeiffer | 15e3d6565b | |
Andreas Shimokawa | abd298d8aa | |
cpfeiffer | e555066ffc | |
Andreas Shimokawa | 40e079ad5d | |
Andreas Shimokawa | 3dea675987 | |
Andreas Shimokawa | 56c7b6b1cb | |
Andreas Shimokawa | 0cc95bd297 | |
Andreas Shimokawa | d0f8e308a4 | |
Andreas Shimokawa | 67bfe2b81e | |
Andreas Shimokawa | ec1f539267 | |
Andreas Shimokawa | 053b9553bc | |
Andreas Shimokawa | 57a9a7ab0b | |
Andreas Shimokawa | 9c2e40ecc0 | |
Andreas Shimokawa | e1927733ba | |
Andreas Shimokawa | dcff1f840c | |
cpfeiffer | bfffd64b65 | |
cpfeiffer | c31049839a | |
Andreas Shimokawa | ee0f1f23c0 | |
Andreas Shimokawa | 0bab0b1cfa | |
Andreas Shimokawa | d5e31451b4 | |
Daniele Gobbetti | ea39bb9c09 | |
Daniele Gobbetti | b9d3028d3f | |
Andreas Shimokawa | 9fbd8688c8 | |
cpfeiffer | ec0a0db4f6 | |
Andreas Shimokawa | 8c1577a478 | |
cpfeiffer | da18f661fe | |
cpfeiffer | d011c437a2 | |
cpfeiffer | f2b344349f | |
cpfeiffer | afef50dfab | |
cpfeiffer | 9928ad80c4 | |
Andreas Shimokawa | 051c617f75 | |
Andreas Shimokawa | c901fa2a5b | |
Andreas Shimokawa | 3c6bc9051a | |
cpfeiffer | cd84b891d9 | |
cpfeiffer | ce175c2952 | |
cpfeiffer | 2e91246a45 | |
cpfeiffer | 411a90326e | |
cpfeiffer | 2c27f30575 | |
cpfeiffer | 549c05cd0d | |
cpfeiffer | 7c1ddc7f82 | |
Andreas Shimokawa | f030a1bdea | |
Daniele Gobbetti | aa2d37c76b | |
Daniele Gobbetti | 5cbedc782d | |
Andreas Shimokawa | 41b20b8c57 | |
cpfeiffer | 8e154ca67d | |
Daniele Gobbetti | 044c9ed101 | |
Andreas Shimokawa | 36b03e92b3 | |
Andreas Shimokawa | 5a49f1215e | |
cpfeiffer | 6f02f9e350 | |
Daniele Gobbetti | 507338d034 | |
Daniele Gobbetti | 1e6cb67edd | |
Daniele Gobbetti | e230bd1d07 | |
Daniele Gobbetti | 6a2043eeb7 | |
Daniele Gobbetti | 84e644fa1a | |
cpfeiffer | cd535a0a45 | |
cpfeiffer | 6340bcff15 | |
cpfeiffer | d9283d0f22 | |
cpfeiffer | b96f2ed301 | |
cpfeiffer | 29dc5daa43 | |
Andreas Shimokawa | 031a683215 | |
Andreas Shimokawa | 854b925c17 | |
cpfeiffer | 7c2bc3804c | |
Andreas Shimokawa | 93b165ee96 | |
cpfeiffer | bcb07ccacd | |
Andreas Shimokawa | c93186cc56 | |
Andreas Shimokawa | 07ee860b1c | |
Andreas Shimokawa | c55369747d | |
cpfeiffer | eb7771c1a9 | |
cpfeiffer | ca6b51b435 | |
cpfeiffer | 39c7762416 | |
cpfeiffer | 1a22259b4e | |
cpfeiffer | 840a125c81 | |
cpfeiffer | 8d6e6c8675 | |
cpfeiffer | ae2df2580c | |
cpfeiffer | e139840fee | |
cpfeiffer | c879e1c063 | |
cpfeiffer | bfaaed7e5c | |
cpfeiffer | fb30321cca | |
cpfeiffer | 083d752011 | |
cpfeiffer | c2a509be74 | |
cpfeiffer | ec9e999be1 | |
cpfeiffer | ec0db033b1 | |
cpfeiffer | 350e72d534 | |
Andreas Shimokawa | 5ab40918c0 | |
Andreas Shimokawa | 34aead6c63 | |
cpfeiffer | e81c1bdc28 | |
cpfeiffer | 20be49b717 | |
cpfeiffer | 770fa952d0 | |
cpfeiffer | b5221eb276 | |
cpfeiffer | 69f73467ea | |
cpfeiffer | c59553c9c9 | |
Andreas Shimokawa | 4363f110fb | |
Andreas Shimokawa | 063d00cc51 | |
cpfeiffer | 49b8b9ebca | |
cpfeiffer | 38c4be4379 | |
cpfeiffer | bfc0b4faaf | |
cpfeiffer | 02ac70e2a7 | |
cpfeiffer | 24d342565b | |
Andreas Shimokawa | ec4469a87b | |
Daniele Gobbetti | 2a2ad20aa3 | |
Daniele Gobbetti | b617ba7264 | |
Daniele Gobbetti | 5a3a0495c9 | |
Andreas Shimokawa | 0ae9955a6f | |
Daniele Gobbetti | 6119f3501a | |
Andreas Shimokawa | 3fb558c536 | |
Daniele Gobbetti | 0126b90f20 | |
cpfeiffer | 7a16834482 | |
cpfeiffer | deeaa87df7 | |
Andreas Shimokawa | ce8af615d1 | |
cpfeiffer | 6e98defe94 | |
cpfeiffer | fbf06c1fe3 | |
cpfeiffer | 26d490ffd6 | |
cpfeiffer | e0c52c7da5 | |
cpfeiffer | 9b7e8e06d6 | |
cpfeiffer | 6843b5aa8f | |
cpfeiffer | 8766fc5269 | |
cpfeiffer | a38bea892a | |
Andreas Shimokawa | 4ddbbfdfb0 | |
Andreas Shimokawa | 69933c5e92 | |
Andreas Shimokawa | eb962c65f0 | |
cpfeiffer | b9df746ea6 | |
cpfeiffer | 7c060506cf | |
cpfeiffer | b3984a409c | |
cpfeiffer | 65d973401a | |
Andreas Shimokawa | f6629ad8e4 | |
Andreas Shimokawa | 4280e9612d | |
Andreas Shimokawa | 68b303246d | |
Andreas Shimokawa | 359ed46b06 | |
Daniele Gobbetti | 23c289ce1a | |
Daniele Gobbetti | 22d0387f76 | |
Daniele Gobbetti | 4a7a34f461 | |
Daniele Gobbetti | 5cfddbb7e9 | |
Andreas Shimokawa | fe5ec74ca1 | |
Andreas Shimokawa | 5072d6b959 | |
Andreas Shimokawa | b708ad942e | |
Andreas Shimokawa | af58b4600d | |
Andreas Shimokawa | c4f83d68cd | |
Andreas Shimokawa | 6b2565e4c9 | |
Andreas Shimokawa | e05d40dc7e | |
cpfeiffer | a7b9ae5596 | |
Andreas Shimokawa | 43f3913669 | |
Andreas Shimokawa | 9520e23439 | |
cpfeiffer | 43d7566c0b | |
Andreas Shimokawa | 4fe498efc2 | |
Andreas Shimokawa | 8ba1ae3f3e | |
Andreas Shimokawa | eabe625c47 | |
cpfeiffer | b43b7948b0 | |
cpfeiffer | c9a9566dad | |
cpfeiffer | 493444a2a0 | |
cpfeiffer | b22111df9d | |
Andreas Shimokawa | 8ea29e6e1d | |
Carsten Pfeiffer | bce7a6c406 | |
Andreas Shimokawa | dd5c80c2e7 | |
Andreas Shimokawa | 726f767576 | |
cpfeiffer | f5ba09ebe0 | |
Ivan | fd1e0e5648 | |
cpfeiffer | df59ce7b96 | |
cpfeiffer | 1997a9b7fa | |
Carsten Pfeiffer | c3d7b4a7cf | |
cpfeiffer | 802314fc13 | |
cpfeiffer | 7b26986ab0 | |
Roman Plevka | e8a4c28510 | |
cpfeiffer | ebda3e1535 | |
cpfeiffer | 367091587f | |
cpfeiffer | aa00d2f93a | |
cpfeiffer | 76895aa2b1 | |
cpfeiffer | 80930ce42a | |
cpfeiffer | eb7b4be986 | |
Andreas Shimokawa | 340a0f4a66 | |
cpfeiffer | f54163faeb | |
cpfeiffer | 9215233344 | |
cpfeiffer | 8154a887cb | |
cpfeiffer | ce47f62c5b | |
cpfeiffer | 31c9d7ed3b | |
Andreas Shimokawa | 8ea0fa46fb | |
Andreas Shimokawa | 26bab26917 | |
Andreas Shimokawa | 4de45787c3 | |
cpfeiffer | 20d8732d10 | |
cpfeiffer | 154b7d28bb | |
Andreas Shimokawa | 903890067d | |
cpfeiffer | 94cc1a883a | |
cpfeiffer | 3bb1a228ec | |
Andreas Shimokawa | 43f95aee9c | |
cpfeiffer | 9ae69eac55 | |
cpfeiffer | 9881b6c281 | |
cpfeiffer | abeb642972 | |
cpfeiffer | 8549031c6f | |
cpfeiffer | 91d1cea51f | |
Andreas Shimokawa | 73b2fc357e | |
Andreas Shimokawa | 966b9abb87 | |
Andreas Shimokawa | a2c2e48719 | |
cpfeiffer | 8b24e098ea | |
Andreas Shimokawa | 9eb768ace0 | |
Andreas Shimokawa | 09c1717e68 | |
cpfeiffer | f65afa64d9 | |
cpfeiffer | f0da25c49b | |
cpfeiffer | 76dcb8f828 | |
cpfeiffer | 7613b62dab | |
cpfeiffer | 76a44ad3a4 | |
cpfeiffer | 56615de1f0 | |
cpfeiffer | e70a2290c3 | |
Andreas Shimokawa | 358cd6df5e | |
Andreas Shimokawa | 07283d4a75 | |
Daniele Gobbetti | 69be5dbbc7 | |
Daniele Gobbetti | 1430619c30 | |
Andreas Shimokawa | 248e38b5ef | |
Andreas Shimokawa | 339eaf05aa | |
cpfeiffer | 2fa166e381 | |
Andreas Shimokawa | 0209b1b403 | |
Andreas Shimokawa | b5cf2b20be | |
Daniele Gobbetti | 20e2846d00 | |
Daniele Gobbetti | 0f0a7ea925 | |
Andreas Shimokawa | 0a1ef37c14 | |
Andreas Shimokawa | 181df7311a | |
Andreas Shimokawa | 659165fa4c | |
Andreas Shimokawa | 1de6ee019f | |
Andreas Shimokawa | b77f3ad3bf | |
Andreas Shimokawa | 7ed867a88d | |
Gergely Peidl | 5131d50617 | |
Andreas Shimokawa | 67e5bc0434 | |
Andreas Shimokawa | 72dff2abd2 | |
Andreas Shimokawa | 1a9c40e790 | |
cpfeiffer | 45fa930ac3 | |
cpfeiffer | 8772631087 | |
Andreas Shimokawa | 4347f134d6 | |
cpfeiffer | 9772d8af06 | |
Andreas Shimokawa | 7597ce337d | |
Andreas Shimokawa | 24e840e03b | |
Andreas Shimokawa | 4b5969ef96 | |
Andreas Shimokawa | f42899d910 | |
Andreas Shimokawa | b2bae26d7d | |
Andreas Shimokawa | 64182941d0 | |
cpfeiffer | 7aa900ce82 | |
cpfeiffer | 0596c80381 | |
cpfeiffer | 04c8a17d6e | |
cpfeiffer | 5607b1c892 | |
cpfeiffer | dc932355b5 | |
cpfeiffer | 233a6155cc | |
Daniele Gobbetti | 988f5ef1b2 | |
Daniele Gobbetti | ad3f7e53b3 | |
Daniele Gobbetti | 245b8655e7 | |
Daniele Gobbetti | 6749c493b1 | |
Daniele Gobbetti | 7263307409 | |
Daniele Gobbetti | 966c3d4811 | |
Daniele Gobbetti | fffeb87607 | |
cpfeiffer | 2890fd6737 | |
cpfeiffer | 41e6833b2d | |
Andreas Shimokawa | 22b4e15988 | |
Andreas Shimokawa | e8f2a0bc9f | |
Andreas Shimokawa | 79b439da28 | |
Daniele Gobbetti | d5586478f3 | |
Gergely Peidl | 33d8ea2f56 | |
cpfeiffer | 13959677af | |
cpfeiffer | d544509b60 | |
cpfeiffer | 687beee501 | |
Andreas Shimokawa | 65ac4b364f | |
Andreas Shimokawa | 9f61458790 | |
Andreas Shimokawa | b79b94809a | |
Andreas Shimokawa | 1c6c78507c | |
Andreas Shimokawa | d225743d64 | |
Andreas Shimokawa | 7937fd6ea7 | |
Andreas Shimokawa | 7690ad3af6 | |
Daniele Gobbetti | 4120d686b8 | |
Andreas Shimokawa | b5693bcb45 | |
cpfeiffer | 71d99384c1 | |
Andreas Shimokawa | 4895704f99 | |
cpfeiffer | a01507a924 | |
cpfeiffer | 61957d6cb0 | |
Andreas Shimokawa | 3418543c31 | |
Andreas Shimokawa | 1d6a697000 | |
Andreas Shimokawa | 98999993e5 | |
Andreas Shimokawa | f20b659b86 | |
Andreas Shimokawa | f812fb1b1f | |
Andreas Shimokawa | 2d080cabb2 | |
Andreas Shimokawa | d1a62968f6 | |
Andreas Shimokawa | 8d3bd494b4 | |
Andreas Shimokawa | 771ff7b2be | |
Andreas Shimokawa | 26ca526fdd | |
Andreas Shimokawa | 6de002c88b | |
Andreas Shimokawa | 243250f41f | |
Andreas Shimokawa | 66b5a21cf2 | |
Andreas Shimokawa | b0fe4b1519 | |
Andreas Shimokawa | 9623449b6e | |
Andreas Shimokawa | b76619bb5b | |
Andreas Shimokawa | fd31bfe56b | |
Steffen Liebergeld | c5262869d9 | |
Steffen Liebergeld | 91f374edec | |
Andreas Shimokawa | 088dfda5f4 | |
Steffen Liebergeld | 204748c518 | |
Steffen Liebergeld | fb71cdf55b | |
Steffen Liebergeld | 73fbaf0a54 | |
Steffen Liebergeld | e386d6da43 | |
Steffen Liebergeld | 1d5c8bae9d | |
Steffen Liebergeld | 0470731e4b | |
Andreas Shimokawa | 98a0774fc2 | |
Andreas Shimokawa | 32429df7bc | |
Steffen Liebergeld | 389a143bdb | |
cpfeiffer | ae548d0806 | |
cpfeiffer | 3b87966fe9 | |
cpfeiffer | 2b6ee41970 | |
cpfeiffer | cb4dcf9fa6 | |
cpfeiffer | ca26e27c60 | |
Carsten Pfeiffer | 1ed0dc59b2 | |
Andreas Shimokawa | 0fb664c141 | |
Andreas Shimokawa | 9d3f3c57cd | |
Andreas Shimokawa | 321707af8f | |
Daniele Gobbetti | 968d15c8d8 | |
Daniele Gobbetti | edb7471e0c | |
Daniele Gobbetti | 409097bc00 | |
Natanael Arndt | 8096cad626 | |
Szymon Tomasz Stefanek | 60fc29cc4d | |
Andreas Shimokawa | df4ae49b72 | |
cpfeiffer | 2e6536555b | |
cpfeiffer | 9a106667d2 | |
Andreas Shimokawa | 19d7c03545 | |
Andreas Shimokawa | a9d74b52f8 | |
Andreas Shimokawa | 1dd0965ae1 | |
Andreas Shimokawa | 9da050c51d | |
Andreas Shimokawa | a15d07858e | |
Daniele Gobbetti | 42acb8915a | |
Andreas Shimokawa | 0231e83ea3 | |
Andreas Shimokawa | b71597800a | |
Andreas Shimokawa | f2cbee39f1 | |
Andreas Shimokawa | 33da6c2925 | |
Andreas Shimokawa | 4533c80c95 | |
Andreas Shimokawa | af14fb4f90 | |
Andreas Shimokawa | 2e8d96e995 | |
Andreas Shimokawa | c9aad271da | |
Andreas Shimokawa | 2b88720f83 | |
Andreas Shimokawa | a13cd9d951 | |
Andreas Shimokawa | 8970bbe044 | |
cpfeiffer | 2d49ce505a | |
cpfeiffer | 50b7a02ef2 | |
cpfeiffer | 6e33c7364a | |
cpfeiffer | c360eb3392 | |
Andreas Shimokawa | b0e0aec465 | |
Andreas Shimokawa | 88f338b0b9 | |
Andreas Shimokawa | 7ef005f6a3 | |
Andreas Shimokawa | fa6100fcec | |
Andreas Shimokawa | 31c15bb8b8 | |
Carsten Pfeiffer | 145dbeedfa | |
andre | bf66c25c7f | |
Andreas Shimokawa | 55a40f7b06 | |
Andreas Shimokawa | e3bee37b81 | |
Andreas Shimokawa | cb1ec5dccb | |
Andreas Shimokawa | c9c9b420dc | |
Andreas Shimokawa | ec154c9041 | |
Andreas Shimokawa | af3cfefec0 | |
Andreas Shimokawa | 30c37d3172 | |
Andreas Shimokawa | 884c4262cf | |
cpfeiffer | 4504c5b5a4 | |
Andreas Shimokawa | 24c51deaf9 | |
Andreas Shimokawa | f697906572 | |
Daniele Gobbetti | bef59ae9c0 | |
Andreas Shimokawa | 437de7f660 | |
Andreas Shimokawa | efe5e546fd | |
Andreas Shimokawa | 80cf9fa8fe | |
Andreas Shimokawa | 30883ab244 | |
Andreas Shimokawa | 5a20d7ec81 | |
Andreas Shimokawa | ca714417ac | |
Andreas Shimokawa | 6370fdbebe | |
Andreas Shimokawa | 0d7986a5ab | |
cpfeiffer | d5cca84780 | |
cpfeiffer | 0267ddb356 | |
cpfeiffer | 400ae2bc3b | |
cpfeiffer | fa34cf9a17 | |
cpfeiffer | a97efe1513 | |
Carsten Pfeiffer | f933eb8fcd | |
Normano64 | 31eabe9605 | |
cpfeiffer | cfed531ad0 | |
cpfeiffer | 2e2030f67b | |
Normano64 | 8a91628322 | |
cpfeiffer | 4370be28b6 | |
cpfeiffer | 75703b0dea | |
cpfeiffer | 2d2df64003 | |
Andreas Shimokawa | 907ad8f27a | |
cpfeiffer | 4b374e3f7e | |
Andreas Shimokawa | 4bd578ebea | |
cpfeiffer | 876bdac918 | |
cpfeiffer | 40a376bbd0 | |
cpfeiffer | 3e0bc16741 | |
Andreas Shimokawa | 017f650b3f | |
Andreas Shimokawa | dafdb1008d | |
Andreas Shimokawa | 8c88223f26 | |
Andreas Shimokawa | d66f842e9b | |
cpfeiffer | 3a1f91b5a8 | |
cpfeiffer | 5963843216 | |
cpfeiffer | 6e44ddaee6 | |
Andreas Shimokawa | 5efe9a5eb8 | |
Andreas Shimokawa | e2def7b467 | |
cpfeiffer | 8ca821ab8a | |
cpfeiffer | d0c8483d92 | |
cpfeiffer | 9532fc879f | |
cpfeiffer | 21cafa83d8 | |
cpfeiffer | b805612ae5 | |
cpfeiffer | 40837996f8 | |
cpfeiffer | 1a353239c4 | |
Andreas Shimokawa | 9b7f2c1e91 | |
Andreas Shimokawa | 5b21895283 | |
cpfeiffer | cc5941f7eb | |
cpfeiffer | 808e12d680 | |
Andreas Shimokawa | 65a95366f4 | |
Andreas Shimokawa | 045d5119ff | |
Andreas Shimokawa | 619a17425f | |
cpfeiffer | 70eaca8883 | |
cpfeiffer | 827e10f49e | |
cpfeiffer | 4744d8b59e | |
cpfeiffer | fc89194396 | |
cpfeiffer | b15ffcbf15 | |
cpfeiffer | 7d15d4ff42 | |
cpfeiffer | b363d08efb | |
cpfeiffer | 403a14ce8d | |
cpfeiffer | 64a6b9a936 | |
cpfeiffer | 6863fababe | |
cpfeiffer | 10d7274aa1 | |
cpfeiffer | 5e02724c4c | |
cpfeiffer | eca5d40efe | |
Andreas Shimokawa | e1551226f6 | |
cpfeiffer | 47984dba0a | |
cpfeiffer | e35ce978bd | |
cpfeiffer | 0704915a88 | |
cpfeiffer | 0c715a2669 | |
cpfeiffer | b89eb14be7 | |
cpfeiffer | 65bd1581bc | |
Andreas Shimokawa | 36a34bd17c | |
Andreas Shimokawa | abe1c9070f | |
Andreas Shimokawa | 18fe09bb7c | |
Andreas Shimokawa | 3fefb57fdd | |
cpfeiffer | d5639a0520 | |
cpfeiffer | c573f989d0 | |
cpfeiffer | 0427294227 | |
cpfeiffer | a45eacf9b8 | |
cpfeiffer | 97cac282c8 | |
cpfeiffer | 98d7237ec3 | |
Andreas Shimokawa | fe310a9df8 | |
Andreas Shimokawa | 7c21f2872a | |
cpfeiffer | e451e8155c | |
cpfeiffer | a8279faa5b | |
cpfeiffer | a460049a1b | |
Andreas Shimokawa | a9e7cdcaa7 | |
Andreas Shimokawa | a9b75a63b3 | |
Andreas Shimokawa | 46086f0408 | |
Andreas Shimokawa | faa6a9d906 | |
Andreas Shimokawa | f76a1ba16f | |
Andreas Shimokawa | 367aced03d | |
Andreas Shimokawa | 4bcebca744 | |
Andreas Shimokawa | a9b4ea8eda | |
Andreas Shimokawa | 24cc3725d2 | |
cpfeiffer | b25a47c398 | |
cpfeiffer | e87a357bed | |
cpfeiffer | f52126ed36 | |
cpfeiffer | ae5d9089d8 | |
cpfeiffer | 78bf516897 | |
cpfeiffer | f15a97d994 | |
Andreas Shimokawa | 58d90c2a66 | |
cpfeiffer | 7ab31514dc | |
cpfeiffer | f334131119 | |
cpfeiffer | 82b4394b40 | |
Lem Dulfo | 206b0670a4 | |
cpfeiffer | 3fb252f74d | |
cpfeiffer | 290d695fec | |
Lem Dulfo | 39cba84ab1 | |
Lem Dulfo | e5726075a4 | |
Lem Dulfo | eba1ee6dc6 | |
Lem Dulfo | 70ed14243f | |
Lem Dulfo | 83e6e6b85f | |
Lem Dulfo | 80a21f2ec2 | |
Lem Dulfo | 5a3004cbce | |
Lem Dulfo | 3ef942b5d3 | |
cpfeiffer | 5ea107b439 | |
cpfeiffer | 57ecba16f3 | |
cpfeiffer | 802e9a8235 | |
cpfeiffer | 22a5aef7d7 | |
cpfeiffer | 42dda911e4 | |
cpfeiffer | 059c7d0b15 | |
cpfeiffer | 3953e4232d | |
cpfeiffer | 1e5dbb6a23 | |
Daniele Gobbetti | a49335fa67 | |
Andreas Shimokawa | b1a93c430d | |
Andreas Shimokawa | 6895c5b776 | |
Andreas Shimokawa | c7b64b6da7 | |
Andreas Shimokawa | 10be21e07b | |
Andreas Shimokawa | e91b5a07bd | |
Andreas Shimokawa | 4055cc1173 | |
Andreas Shimokawa | 94cec55a20 | |
danielegobbetti | d2af3468f0 | |
Julien Pivotto | e42a041448 | |
Daniele Gobbetti | 51def0d497 | |
Daniele Gobbetti | 34600e085e | |
cpfeiffer | 403f74e59b | |
Andreas Shimokawa | a15b327ff1 | |
cpfeiffer | 3e3cf462a6 | |
cpfeiffer | 59c3970008 | |
cpfeiffer | b129844169 | |
cpfeiffer | 7cda9f1923 | |
cpfeiffer | 804a85d31f | |
cpfeiffer | b54fe53cd5 | |
Andreas Shimokawa | 4389c1cca3 | |
Andreas Shimokawa | 7ddfd35c35 | |
cpfeiffer | a4919789ca | |
cpfeiffer | 7a224243a3 | |
cpfeiffer | 2d10c11005 | |
cpfeiffer | 0e49535966 | |
Christian Fischer | f2de21a664 | |
Christian Fischer | 20aa7d9ad9 | |
Christian Fischer | 72258c178c | |
cpfeiffer | ea5c6a0848 | |
cpfeiffer | 6f97b8c1e5 | |
cpfeiffer | 66c1b3f178 | |
cpfeiffer | 4631df67ac | |
cpfeiffer | 776a743285 | |
cpfeiffer | ffc006c21c | |
cpfeiffer | cc7f5406ef | |
cpfeiffer | 5f72daa43a | |
cpfeiffer | f8c761068e | |
cpfeiffer | e931cf47d7 | |
Andreas Shimokawa | 834a727a39 | |
Andreas Shimokawa | b3590fed35 | |
Andreas Shimokawa | cbc57b4407 | |
cpfeiffer | 3513a902ae | |
cpfeiffer | 8815f0d134 | |
Andreas Shimokawa | 6ce63276a3 | |
Andreas Shimokawa | adfef3db42 | |
cpfeiffer | bfcfe82f17 | |
Andreas Shimokawa | 9d29e4db3f | |
cpfeiffer | a70c31f965 | |
cpfeiffer | 298b7542a4 | |
cpfeiffer | bff5837930 | |
cpfeiffer | 8165751e57 | |
Andreas Shimokawa | a208907ba7 | |
Andreas Shimokawa | 98949f3b54 | |
cpfeiffer | 3714ec82da | |
cpfeiffer | ce382198d1 | |
cpfeiffer | 89eddb13b0 | |
cpfeiffer | e5b0afb916 | |
cpfeiffer | 0e435d6d94 | |
cpfeiffer | 11ac01f0e8 | |
cpfeiffer | 1348bad4d3 | |
cpfeiffer | 9d9ef8a6f8 | |
cpfeiffer | 71461642f7 | |
Andreas Shimokawa | df3a06ac9b | |
cpfeiffer | b0ec74696d | |
Andreas Shimokawa | 767f359319 | |
Andreas Shimokawa | 5eb2d04513 | |
cpfeiffer | b419c93254 | |
cpfeiffer | 424d9cd142 | |
danielegobbetti | 1933e2bf10 | |
Andreas Shimokawa | 1aadcb958b | |
cpfeiffer | 275839a7f4 | |
cpfeiffer | f7b71c1f96 | |
Andreas Shimokawa | 76fc7a2aec | |
Andreas Shimokawa | f046e66bf1 | |
danielegobbetti | 4a3547228e | |
Andreas Shimokawa | dbeded8d04 | |
Andreas Shimokawa | c5a7ca4b5b | |
Andreas Shimokawa | 4fe9489909 | |
Andreas Shimokawa | b5f71febdc | |
Andreas Shimokawa | 4be1926459 | |
cpfeiffer | b3410dcebe | |
cpfeiffer | e59c012553 | |
cpfeiffer | 4f956000c5 | |
cpfeiffer | 6d8d6d5bc8 | |
Andreas Shimokawa | c2ae9ec530 | |
Daniele Gobbetti | 538961fd2c | |
Daniele Gobbetti | e69fac9704 | |
Andreas Shimokawa | 1603d60144 | |
Andreas Shimokawa | 89591fd5fe | |
Andreas Shimokawa | 61e3cf4348 | |
Andreas Shimokawa | 238e394d21 | |
Andreas Shimokawa | c224a40d0e | |
Andreas Shimokawa | 5906c02330 | |
cpfeiffer | c5a887192d | |
cpfeiffer | e26e6d7b24 | |
cpfeiffer | 4aaf3dd162 | |
cpfeiffer | 91f02ae920 | |
danielegobbetti | ea855a4cc2 | |
cpfeiffer | 3f39928df5 | |
cpfeiffer | 10975feb49 | |
cpfeiffer | 9643fa6062 | |
danielegobbetti | d378b4eb7b | |
cpfeiffer | 7e8281e8d4 | |
cpfeiffer | 87023ebdb3 | |
Daniele Gobbetti | 2da50e27c2 | |
Andreas Shimokawa | a89fea9c7d | |
Andreas Shimokawa | 4362f78028 | |
Andreas Shimokawa | a3ee3c15fc | |
Andreas Shimokawa | 88982a6174 | |
cpfeiffer | a96120f91d | |
cpfeiffer | 5eb8f57b4c | |
cpfeiffer | 5ae680cab5 | |
cpfeiffer | 25e58eb414 | |
cpfeiffer | be012eca8d | |
cpfeiffer | 50dd7f5eba | |
Andreas Shimokawa | f1ba50b62a | |
cpfeiffer | 2d9673afe7 | |
cpfeiffer | dda6cb34e1 | |
Andreas Shimokawa | 2902e60d51 | |
cpfeiffer | 619ea04a63 | |
danielegobbetti | 459f6baf08 | |
Andreas Shimokawa | fa6b572172 | |
cpfeiffer | 97faf61c5a | |
cpfeiffer | dc162a9ac8 | |
cpfeiffer | 3b3458e196 | |
Daniele Gobbetti | 6d4b98719a | |
Daniele Gobbetti | 3920b3f977 | |
Andreas Shimokawa | f616e4f571 | |
Andreas Shimokawa | aba21d3ab7 | |
Andreas Shimokawa | a96d042dce | |
Andreas Shimokawa | 3786e0b7f2 | |
Andreas Shimokawa | 1e44bb03fb | |
Andreas Shimokawa | bd7b34985b | |
Andreas Shimokawa | 864e0953d9 | |
Andreas Shimokawa | 902ff39c0b | |
Andreas Shimokawa | 2a7f9226a0 | |
Andreas Shimokawa | fa924ff9d8 | |
Andreas Shimokawa | 860ded1022 | |
Andreas Shimokawa | 63d938559e | |
Daniele Gobbetti | 089a59168e | |
Andreas Shimokawa | 652c5575b3 | |
Andreas Shimokawa | 5eb525ee44 | |
0nse | ba35679690 | |
0nse | 7651c080c2 | |
0nse | 6e7abecb17 | |
Andreas Shimokawa | 7756968a8f | |
cpfeiffer | 2cdeecb39c | |
Andreas Shimokawa | fc464d112d | |
Andreas Shimokawa | 9adae3b538 | |
cpfeiffer | 9dd0a8bf2b | |
cpfeiffer | cbe73f71a1 | |
cpfeiffer | 1b9f297ebe | |
0nse | 3babedf936 | |
Andreas Shimokawa | ed85fd5011 | |
cpfeiffer | c4dc972804 | |
cpfeiffer | badf384619 | |
cpfeiffer | 1c9be79c67 | |
cpfeiffer | ddde25e5df | |
cpfeiffer | d7822d07a6 | |
cpfeiffer | 540e008548 | |
cpfeiffer | 4898dab652 | |
cpfeiffer | 3ff31cd73b | |
cpfeiffer | d6dfc3b6ec | |
Andreas Shimokawa | c449181083 | |
cpfeiffer | ac8d7bee5f | |
cpfeiffer | de6f898fef | |
Kasha | 9e636d66f6 | |
cpfeiffer | 0ef738067d | |
Daniele Gobbetti | df741e9571 | |
cpfeiffer | a10c6f3b9f | |
cpfeiffer | 0b568df8de | |
cpfeiffer | 095ef56c14 | |
cpfeiffer | defa97b882 | |
Andreas Shimokawa | 8de836efb8 | |
cpfeiffer | fee04a05ae | |
danielegobbetti | b5a726b777 | |
danielegobbetti | 6eb35b955e | |
Andreas Shimokawa | db6f26fcd5 | |
Andreas Shimokawa | 1a96bd31e5 | |
danielegobbetti | b858e50804 | |
Andreas Shimokawa | c436c4c055 | |
Andreas Shimokawa | 7626667a0a | |
cpfeiffer | 109146c8c1 | |
Andreas Shimokawa | 70ae5a2a3a | |
Daniele Gobbetti | cc42583885 | |
cpfeiffer | c86365ee2e | |
Daniele Gobbetti | 8294921de7 | |
Andreas Shimokawa | 7436778700 | |
Andreas Shimokawa | 823cb12035 | |
Daniele Gobbetti | 20c4e49fe1 | |
Andreas Shimokawa | d62946df63 | |
Andreas Shimokawa | 93db073538 | |
Andreas Shimokawa | 12a5b53f00 | |
Andreas Shimokawa | b01a517813 | |
Julien Pivotto | 5b539d5252 | |
Andreas Shimokawa | cdb25f3183 | |
Julien Pivotto | dd9864015d | |
Andreas Shimokawa | 0c4e606e74 | |
danielegobbetti | 10b5c571bb | |
danielegobbetti | 2f8207abf9 | |
Andreas Shimokawa | 299b850a67 | |
danielegobbetti | 03ad7f5a24 | |
danielegobbetti | ba9e00d2e4 | |
danielegobbetti | a451b37ceb | |
Andreas Shimokawa | 3db88574fa | |
Andreas Shimokawa | 59d6553c54 | |
Andreas Shimokawa | 85bad9abf5 | |
Andreas Shimokawa | 493fcfc853 | |
Daniele Gobbetti | baf5eee72f | |
Daniele Gobbetti | 94c8633bad | |
Andreas Shimokawa | 703cbd1745 | |
Chris Perelstein | 60c7e9f6f6 | |
Andreas Shimokawa | cc64bcf03c | |
Daniele Gobbetti | 5b016e2577 | |
Daniele Gobbetti | 8e7dc18fa8 | |
Daniele Gobbetti | 9f2a7f5448 | |
Daniele Gobbetti | f9d2fddb7a | |
Daniele Gobbetti | e7c93ca1c3 | |
Andreas Shimokawa | 33cf76172b | |
cpfeiffer | b9cb89ab8b | |
Andreas Shimokawa | 666c53a1e4 | |
danielegobbetti | 7ba3a874a2 | |
Daniele Gobbetti | c08d49d28e | |
Daniele Gobbetti | 1ac7e08289 | |
Andreas Shimokawa | 857e282bdc | |
Andreas Shimokawa | 9af976657b | |
Andreas Shimokawa | e6f68f445a | |
danielegobbetti | 3d3643ece3 | |
danielegobbetti | 11297cd855 | |
Andreas Shimokawa | a72373c17c | |
Andreas Shimokawa | b9c1332442 | |
Daniele Gobbetti | ea1ce4f43f | |
Andreas Shimokawa | a6ce10d306 | |
Andreas Shimokawa | cf1c245e58 | |
Andreas Shimokawa | 2b78b0a67f | |
Andreas Shimokawa | 31724b3ef2 | |
Marc Schlaich | 887b478ec6 | |
Andreas Shimokawa | 818c31eb2b | |
Andreas Shimokawa | de4ffe8fb0 | |
Andreas Shimokawa | 7ba156da62 | |
danielegobbetti | 4bb78722b5 | |
Andreas Shimokawa | 0b53f60b0d | |
Andreas Shimokawa | 46bbab7df0 | |
Andreas Shimokawa | 803e58743a | |
cpfeiffer | ae5417b9cc | |
cpfeiffer | a6d3c50f94 | |
cpfeiffer | 824382b385 | |
cpfeiffer | 779d8ee930 | |
cpfeiffer | 41dde9a9c2 | |
danielegobbetti | 3d389f31a3 | |
danielegobbetti | d7f4769f57 | |
danielegobbetti | 092af9f38d | |
danielegobbetti | c7c723134e | |
danielegobbetti | 1f1ac8cf37 | |
danielegobbetti | abb7ed0189 | |
Andreas Shimokawa | c425fd24ea | |
Andreas Shimokawa | e41a9c208b | |
Andreas Shimokawa | d7f74851e2 | |
Andreas Shimokawa | 50cd5b2629 | |
Andreas Shimokawa | d358ed81d2 | |
Andreas Shimokawa | 890016d652 | |
Andreas Shimokawa | 3655c833a9 | |
Andreas Shimokawa | ba446b7b17 | |
Andreas Shimokawa | ae269e51e7 | |
Andreas Shimokawa | e533fdbaa6 | |
Andreas Shimokawa | eb50040320 | |
Andreas Shimokawa | 790e4d5d80 | |
Andreas Shimokawa | c962dbbac2 | |
Andreas Shimokawa | a7ea2141d6 | |
Daniele Gobbetti | 1cbe965802 | |
Daniele Gobbetti | f9122bc674 | |
Daniele Gobbetti | 1d9e1d7caf | |
Andreas Shimokawa | af9ee90383 | |
danielegobbetti | aa1014f51c | |
Andreas Shimokawa | 7a1a6dbb2b | |
cpfeiffer | 9ea2977143 | |
Andreas Shimokawa | e3d0c63676 | |
Andreas Shimokawa | 483c435aa5 | |
Andreas Shimokawa | 55989c426c | |
Andreas Shimokawa | 2caef02309 | |
Andreas Shimokawa | 11e02fbf5f | |
Andreas Shimokawa | 9f60bf3561 | |
Andreas Shimokawa | 15436c59e5 | |
Andreas Shimokawa | cf5a0f19ed | |
Daniele Gobbetti | 3ee418a45b | |
Daniele Gobbetti | 5f189aedbd | |
Daniele Gobbetti | 26646af974 | |
Andreas Shimokawa | 0c805809a5 | |
Andreas Shimokawa | 87739d94db | |
danielegobbetti | a71c27d25e | |
Andreas Shimokawa | 96e21dbf21 | |
Andreas Shimokawa | 35c7ab6dde | |
danielegobbetti | 5026cf269f | |
Andreas Shimokawa | 4b29d63d4e | |
danielegobbetti | 070f3fa66f | |
danielegobbetti | 9fb2e1620e | |
danielegobbetti | 9acdefd5c1 | |
danielegobbetti | 6582ead01c | |
Andreas Shimokawa | 7eabf1e603 | |
danielegobbetti | 89ef950c62 | |
danielegobbetti | 5fb8c7bed8 | |
Andreas Shimokawa | 47a34bb7bf | |
Andreas Shimokawa | c9dcf06529 | |
Daniele Gobbetti | 036e92ee64 | |
Daniele Gobbetti | 78cd11ad93 | |
Daniele Gobbetti | 0dda5c214b | |
Andreas Shimokawa | 7b12a3b50c | |
Andreas Shimokawa | f387f7c96b | |
Andreas Shimokawa | 87674db5f9 | |
Andreas Shimokawa | c6e67a9059 | |
Daniele Gobbetti | 19afe23703 | |
Andreas Shimokawa | ddd196fab4 | |
Ⲇⲁⲛⲓ Φi | 53f8221f98 | |
Andreas Shimokawa | dfa85745e8 | |
Andreas Shimokawa | 3961e32c2b | |
Andreas Shimokawa | de5f30ae97 | |
Andreas Shimokawa | 14f8929439 | |
Andreas Shimokawa | e5cf22bda6 | |
Andreas Shimokawa | 53fb63781e | |
cpfeiffer | f258e62633 | |
Andreas Shimokawa | 7cf1e0e004 | |
Andreas Shimokawa | b1954eec3e | |
Andreas Shimokawa | c9d1b9dd4a | |
Andreas Shimokawa | 4528aaf22f | |
cpfeiffer | 854a7ee1ac | |
cpfeiffer | 794ae6d800 | |
cpfeiffer | 6d7428ad29 | |
cpfeiffer | 66ed672ad6 | |
cpfeiffer | a1fe16bbcb | |
cpfeiffer | e642971b4c | |
cpfeiffer | fe60d6aaf0 | |
cpfeiffer | 0d63e5b770 | |
cpfeiffer | 159c187e5e | |
Andreas Shimokawa | 8b87e97f51 | |
Andreas Shimokawa | 804621aa14 | |
Andreas Shimokawa | f59382e3c8 | |
Andreas Shimokawa | 18726eca33 | |
Andreas Shimokawa | 62c196eb1d | |
Andreas Shimokawa | 3123f30e5a | |
Andreas Shimokawa | 3ac00a004f | |
cpfeiffer | 7f8ba83aab | |
Andreas Shimokawa | 1c3e0b628b | |
cpfeiffer | 44667a60d1 | |
Andreas Shimokawa | f20e11d517 | |
Andreas Shimokawa | 803a3bea90 | |
cpfeiffer | 265dcd25eb | |
cpfeiffer | accb055307 | |
cpfeiffer | 134eeacaea | |
cpfeiffer | 365ce61cb6 | |
cpfeiffer | 6b053c4240 | |
cpfeiffer | 5a479c9175 | |
cpfeiffer | aa60ce4b56 | |
cpfeiffer | 579546c9f8 | |
cpfeiffer | b056e1b2a0 | |
cpfeiffer | 8cd6bf09a4 | |
Andreas Shimokawa | 31599f2776 | |
Andreas Shimokawa | b05cfc6aac | |
Andreas Shimokawa | 79f92b8495 | |
Andreas Shimokawa | 9ebb320e10 | |
Andreas Shimokawa | 05a8436f7c | |
Andreas Shimokawa | 39e09946cd | |
Andreas Shimokawa | 112dfa184a | |
Andreas Shimokawa | a8db606240 | |
Andreas Shimokawa | bf61147224 | |
Andreas Shimokawa | 0cf6e61ca6 | |
Andreas Shimokawa | bc108ba095 | |
Andreas Shimokawa | f3a33cb620 | |
Andreas Shimokawa | aca0149b45 | |
Andreas Shimokawa | 729555b045 | |
cpfeiffer | 40d7f3b19f | |
cpfeiffer | 4b42a9a4f6 | |
cpfeiffer | 8585572197 | |
Andreas Shimokawa | 17ba49374c | |
Andreas Shimokawa | c768107db8 | |
cpfeiffer | bd0716ba58 | |
cpfeiffer | 95dc67c98d | |
cpfeiffer | 81c2f657bd | |
cpfeiffer | a7a061e298 | |
cpfeiffer | 4616dcc965 | |
cpfeiffer | 394a0905dc | |
cpfeiffer | 4622b384f2 | |
cpfeiffer | c1d9777047 | |
cpfeiffer | 52047b615c | |
cpfeiffer | a53f1c21eb | |
Carsten Pfeiffer | be644befb4 | |
Nicolò Balzarotti | 99c97ccda7 | |
cpfeiffer | 34e08f6de8 | |
cpfeiffer | 1e6db708d2 | |
Andreas Shimokawa | ea98e207d9 | |
Andreas Shimokawa | 1734e58f70 | |
cpfeiffer | ebbb71ae9d | |
cpfeiffer | f349846f4a | |
cpfeiffer | 5864189b91 | |
cpfeiffer | 2e267a4c2b | |
cpfeiffer | d9722c6db2 | |
Carsten Pfeiffer | 193d74879a | |
Sergey Trofimov | fc2cc3adb4 | |
Sergey Trofimov | b70fbeccc9 | |
cpfeiffer | 753286a341 | |
cpfeiffer | da5acf748a | |
cpfeiffer | 58cfd0fef9 | |
cpfeiffer | d8960c4e16 | |
cpfeiffer | c27459b6cc | |
cpfeiffer | 952a383856 | |
cpfeiffer | d4f070f0aa | |
cpfeiffer | 8920f5e95b | |
cpfeiffer | 1f599c660f | |
cpfeiffer | 694b3d897f | |
cpfeiffer | a7ebed94f7 | |
cpfeiffer | 39c7a853c8 | |
Carsten Pfeiffer | 695f83e19e | |
Andreas Shimokawa | b99b9bbb75 | |
Alexey Afanasev | dde32f5a3f | |
cpfeiffer | 54c316778b | |
cpfeiffer | dd0ba8a230 | |
cpfeiffer | 4200e77016 | |
Andreas Shimokawa | f287c04ad9 | |
Andreas Shimokawa | 4b9304b897 | |
Andreas Shimokawa | 59f4766bc5 | |
Andreas Shimokawa | e809c490dc | |
cpfeiffer | 0cd9b0419c | |
Andreas Shimokawa | 4aff3c8e8e | |
cpfeiffer | c350f04fa9 | |
cpfeiffer | 88fb81f921 | |
cpfeiffer | ac120dc7d6 | |
cpfeiffer | 3b94a96060 | |
Andreas Shimokawa | 44a36a5f1d | |
cpfeiffer | aa5749cd40 | |
cpfeiffer | 52f3ca5253 | |
cpfeiffer | 5a3990b9d2 | |
Carsten Pfeiffer | 4096e50681 | |
Daniele Gobbetti | cee03debbb | |
Daniele Gobbetti | 6460b391d9 | |
Sergey Trofimov | 94cbf2f301 | |
cpfeiffer | 4e0fed8857 | |
cpfeiffer | 86d17c7792 | |
cpfeiffer | ef15bf8ce8 | |
cpfeiffer | 8d62a16176 | |
cpfeiffer | dcd776e09a | |
cpfeiffer | 99470c67ff | |
cpfeiffer | 4cdfea25f9 | |
cpfeiffer | 7a44ea9596 | |
Carsten Pfeiffer | e0289f63ce | |
Sergey Trofimov | d57c6166b9 | |
Sergey Trofimov | 7591d4a8af | |
Carsten Pfeiffer | 1073303849 | |
Sergey Trofimov | a1fd31c260 | |
Sergey Trofimov | 1c1f8e8535 | |
Andreas Shimokawa | f0a1d5f8a0 | |
cpfeiffer | e755e9f51f | |
cpfeiffer | b43e96318a | |
cpfeiffer | 1e56e540fa | |
cpfeiffer | 2c29384ee8 | |
cpfeiffer | 45fc2c181c | |
Andreas Shimokawa | a9186791dc | |
Andreas Shimokawa | 85777f99e4 | |
Andreas Shimokawa | 3410e90fb0 | |
cpfeiffer | 4533ae22ee | |
Andreas Shimokawa | 600e7d59b5 | |
Andreas Shimokawa | 262271dbd0 | |
Andreas Shimokawa | cd7acf6572 | |
Andreas Shimokawa | 5860c4f4f9 | |
cpfeiffer | 7dce1d62b0 | |
Andreas Shimokawa | d21b5e68b5 | |
Andreas Shimokawa | a4a59f5df4 | |
cpfeiffer | 0d27245dd1 | |
cpfeiffer | 27aa660ca4 | |
cpfeiffer | 9db7d13a94 | |
cpfeiffer | 5b8bf468f5 | |
cpfeiffer | 321c0ff125 | |
cpfeiffer | 38c3a41279 | |
cpfeiffer | 2231dcbce3 | |
cpfeiffer | 0a7366e458 | |
Andreas Shimokawa | 4d6f8ec742 | |
Andreas Shimokawa | f33a5fd3a9 | |
Andreas Shimokawa | a3e531155f | |
cpfeiffer | 0ff0c6d176 | |
cpfeiffer | 93523d7831 | |
cpfeiffer | d6f9eac711 | |
cpfeiffer | 586d959055 | |
cpfeiffer | baa2d0b27a | |
Daniele Gobbetti | 2f0d4815b8 | |
Daniele Gobbetti | ff2e8d1ce7 | |
cpfeiffer | c9e91bd708 | |
cpfeiffer | 2149b18ae3 | |
Daniele Gobbetti | d14ccf1c5c | |
Daniele Gobbetti | f8edf5c525 | |
Daniele Gobbetti | 18f952250a | |
Daniele Gobbetti | deea721090 | |
cpfeiffer | 4250a002b4 | |
cpfeiffer | 0395977fde | |
Andreas Shimokawa | 8fca35f94f | |
Andreas Shimokawa | 4f5edb7231 | |
Andreas Shimokawa | 949c49f2c9 | |
Andreas Shimokawa | e7e583a5ba | |
Andreas Shimokawa | c8a08510ce | |
Daniele Gobbetti | 720eaa111d | |
Andreas Shimokawa | e3533a2b18 | |
Andreas Shimokawa | 94ad7f2eb9 | |
Andreas Shimokawa | 1d41f2f8e4 | |
Daniele Gobbetti | e1ea8270ca | |
Daniele Gobbetti | 5578691321 | |
cpfeiffer | 8ba307657a | |
Andreas Shimokawa | ab78d167d9 | |
Andreas Shimokawa | 3bb673d33a | |
cpfeiffer | d9b4bbe550 | |
cpfeiffer | 3852fcd756 | |
Andreas Shimokawa | 77b4bb9cf1 | |
Andreas Shimokawa | 98b1abedac | |
Andreas Shimokawa | f6ef72e9fb | |
Andreas Shimokawa | 60b24e004a | |
Andreas Shimokawa | 0c4dbf75e0 | |
Andreas Shimokawa | a3ef85d243 | |
Andreas Shimokawa | 4b690ad641 | |
Andreas Shimokawa | 5fb6090be1 | |
Andreas Shimokawa | e1b02e1be4 | |
Daniele Gobbetti | 8bef384855 | |
Daniele Gobbetti | 94abac05d1 | |
Andreas Shimokawa | e6a8a1a36c | |
Andreas Shimokawa | b02c286818 | |
Andreas Shimokawa | 041bd1a7f4 | |
Andreas Shimokawa | a5ef952e37 | |
Daniele Gobbetti | 502c005a0e | |
Daniele Gobbetti | 55341678b3 | |
Daniele Gobbetti | b73ff49681 | |
Andreas Shimokawa | 4e83742d6c | |
Andreas Shimokawa | 58bbcb0035 | |
cpfeiffer | 7dedff3ce1 | |
Carsten Pfeiffer | d5087a9daa | |
Andreas Shimokawa | d8c096d931 | |
Andreas Shimokawa | 6e3c839608 | |
kevlarcade | 78dafd6abc | |
Andreas Shimokawa | de74a033f6 | |
Andreas Shimokawa | aa524cc5ea | |
Julien Pivotto | 21d59b23c1 | |
Andreas Shimokawa | 95e22a4e32 | |
Andreas Shimokawa | 6fff4fb7ba | |
Andreas Shimokawa | 9a32be97cb | |
Andreas Shimokawa | d3dbde6917 | |
Andreas Shimokawa | dfe86d681d | |
Andreas Shimokawa | 04086a3b4c | |
Julien Pivotto | 1027336591 | |
Andreas Shimokawa | ca6c940aa6 | |
Julien Pivotto | eafb795874 | |
Andreas Shimokawa | c69ae753ac | |
Andreas Shimokawa | a73beceb44 | |
Andreas Shimokawa | 4f80844016 | |
Andreas Shimokawa | 44d8294f8c | |
Andreas Shimokawa | a85c04c02a | |
Andreas Shimokawa | c046add965 | |
Julien Pivotto | d50a82d495 | |
Andreas Shimokawa | a2242d971a | |
Julien Pivotto | bfd7908f56 | |
Andreas Shimokawa | 7ba255080b | |
Andreas Shimokawa | b886ba4ac7 | |
Andreas Shimokawa | 637539861d | |
Julien Pivotto | 986e7e0450 | |
Andreas Shimokawa | 70fcbbbe17 | |
Andreas Shimokawa | 914d1b9625 | |
Andreas Shimokawa | 03b9f02b2c | |
Andreas Shimokawa | 0ad758fbca | |
cpfeiffer | 80d15573af | |
cpfeiffer | c23905070c | |
cpfeiffer | 22a9ff1819 | |
cpfeiffer | e80a3cc591 | |
cpfeiffer | e0ccb6bf84 | |
cpfeiffer | d9d222ca9b | |
cpfeiffer | bddf6c8909 | |
cpfeiffer | 2dec5574cc | |
cpfeiffer | e34c5614d7 | |
Andreas Shimokawa | 7f331a1bb1 | |
cpfeiffer | 518b1ee6f4 | |
Daniele Gobbetti | 6c28b50f52 | |
cpfeiffer | 020d758f69 | |
cpfeiffer | 1711a7a731 | |
cpfeiffer | b979a1feff | |
cpfeiffer | 9ffa9ca870 | |
cpfeiffer | 6f7a3f7d8d | |
cpfeiffer | fbd23c2d4c | |
cpfeiffer | ed6629a9c7 | |
cpfeiffer | 78321e28bf | |
cpfeiffer | bc3c0760d0 | |
Andreas Shimokawa | a62647fa46 | |
Andreas Shimokawa | 3fb92e2e79 | |
Andreas Shimokawa | 02cc8ba455 | |
Andreas Shimokawa | a839f07496 | |
Andreas Shimokawa | 1a1277fa3d | |
cpfeiffer | 5f993c0049 | |
cpfeiffer | 69b64ed4b6 | |
cpfeiffer | 8f4e933e30 | |
cpfeiffer | 9a1f4875fc | |
cpfeiffer | 05ee6e46c6 | |
cpfeiffer | 42420e676b | |
Andreas Shimokawa | bc98805809 | |
Andreas Shimokawa | e4a72a83ee | |
Andreas Shimokawa | ab29736a50 | |
Daniele Gobbetti | b6cbb5d6be | |
cpfeiffer | ab8982e7f2 | |
cpfeiffer | 536b2bd8a0 | |
cpfeiffer | da5df5621e | |
cpfeiffer | 4b4c6d1a6b | |
cpfeiffer | c5db816cd1 | |
cpfeiffer | c49c795b1d | |
cpfeiffer | d791054e42 | |
cpfeiffer | a6d18e599b | |
cpfeiffer | 917801f223 | |
Andreas Shimokawa | fd789c445e | |
cpfeiffer | 5c2bd1e8df | |
Andreas Shimokawa | 46171e4ab8 | |
Andreas Shimokawa | 2da717ea4c | |
Daniele Gobbetti | ee3ca5998e | |
Daniele Gobbetti | 9360b81ef3 | |
Daniele Gobbetti | 249ff5bf94 | |
Andreas Shimokawa | face7cceea | |
Andreas Shimokawa | 95b65265b4 | |
cpfeiffer | 50960277dd | |
cpfeiffer | 41d8bcf634 | |
cpfeiffer | 87a5b09e43 | |
cpfeiffer | ad4d4fb0da | |
Andreas Shimokawa | 128a1d7d7a | |
cpfeiffer | 277e5821a5 | |
cpfeiffer | 530116976c | |
cpfeiffer | 184f81fc7a | |
cpfeiffer | 25ddc20f89 | |
Daniele Gobbetti | 49c9b92020 | |
Andreas Shimokawa | 13300fcb5d | |
Andreas Shimokawa | 15fc5a02ae | |
Andreas Shimokawa | b4632f1292 | |
Andreas Shimokawa | 6e448c14db | |
Andreas Shimokawa | 38f4526817 | |
Andreas Shimokawa | e30379e77d | |
Daniele Gobbetti | 26792717d4 | |
Daniele Gobbetti | fb5ebeacb6 | |
cpfeiffer | 086bb8aa4a | |
cpfeiffer | ff989390f9 | |
Daniele Gobbetti | d27bf567cf | |
Daniele Gobbetti | 2f1aa45445 | |
Daniele Gobbetti | cbea0feb9e | |
cpfeiffer | 75a1068a69 | |
Andreas Shimokawa | 2ca8e149ee | |
Andreas Shimokawa | 44c7f99c58 | |
Daniele Gobbetti | 3a6e433fb3 | |
Daniele Gobbetti | 677e0808bf | |
Daniele Gobbetti | 7923e153e6 | |
Andreas Shimokawa | 027e6fe8c3 | |
Daniele Gobbetti | 3356a4b066 | |
cpfeiffer | d0fbc57cf1 | |
cpfeiffer | e8a12f92be | |
cpfeiffer | 8128651bcc | |
cpfeiffer | e1c02cc373 | |
cpfeiffer | b4e34db1d2 | |
cpfeiffer | 0bb3188bc8 | |
cpfeiffer | e47ebb8f09 | |
Daniele Gobbetti | 6ebc727f97 | |
cpfeiffer | 070876db06 | |
cpfeiffer | 9a3769aeba | |
Carsten Pfeiffer | 93c2f40cd8 | |
cpfeiffer | ba670bbb50 | |
Andreas Shimokawa | ba6bdad057 | |
Andreas Shimokawa | 8b9406996c | |
cpfeiffer | 6f7de96461 | |
cpfeiffer | 77cad5c47f | |
Andreas Shimokawa | cedd95186f | |
Andreas Shimokawa | 1150ad2b8d | |
Daniele Gobbetti | dcc988139f | |
Andreas Shimokawa | 12337836bc | |
Daniele Gobbetti | df417e5c6c | |
Andreas Shimokawa | 6e80978998 | |
Andreas Shimokawa | 2b0acd649b | |
Daniele Gobbetti | eb39ce9367 | |
cpfeiffer | a1cb246e27 | |
Andreas Shimokawa | 6869fc85ee | |
Andreas Shimokawa | ff6d28cdc8 | |
Andreas Shimokawa | faaa04b670 | |
Daniele Gobbetti | a6b28a804c | |
Daniele Gobbetti | 0d8adeb7f9 | |
Daniele Gobbetti | 57a85e63b0 | |
Andreas Shimokawa | d2173d37ce | |
Andreas Shimokawa | e8e631fb49 | |
cpfeiffer | 5a4f8fb56f | |
Andreas Shimokawa | a7796ecbc6 | |
Daniele Gobbetti | c3e395818f | |
cpfeiffer | 121baa19ec | |
cpfeiffer | 99293d4ee5 | |
cpfeiffer | dbb92b55bc | |
Andreas Shimokawa | c0323339e8 | |
cpfeiffer | 33b598ce5c | |
cpfeiffer | 585a888ecb | |
Andreas Shimokawa | 0c872a920e | |
cpfeiffer | e6a0c35f73 | |
cpfeiffer | 9dc945a406 | |
Andreas Shimokawa | 6fede31bdf | |
cpfeiffer | 9dd2f039f2 | |
Andreas Shimokawa | 913f37246f | |
Andreas Shimokawa | 8fee88a1ba | |
Andreas Shimokawa | e704357728 | |
Andreas Shimokawa | c8c882c3d1 | |
Andreas Shimokawa | f0924716fc | |
Andreas Shimokawa | 0d0b3a87e1 | |
cpfeiffer | bcf42f8022 | |
cpfeiffer | 964994972b | |
cpfeiffer | 27a9eb8a07 | |
cpfeiffer | 0bd65e050c | |
Andreas Shimokawa | 48f5931043 | |
Andreas Shimokawa | 6af0bb2754 | |
Andreas Shimokawa | a4f5524f6e | |
cpfeiffer | 567f27b0f4 | |
cpfeiffer | 929831e4f0 | |
cpfeiffer | 5e233f6eb9 | |
cpfeiffer | 3486b5ab69 | |
Andreas Shimokawa | a5e009b371 | |
Andreas Shimokawa | a2e2600469 | |
Andreas Shimokawa | c69bf1b0af | |
cpfeiffer | 4ba5a7804a | |
cpfeiffer | b6f66eb57c | |
cpfeiffer | a66a3a15c2 | |
cpfeiffer | ecc483f027 | |
cpfeiffer | d0229847e7 | |
Andreas Shimokawa | f659e34efc | |
Andreas Shimokawa | f5fbb08696 | |
Andreas Shimokawa | e28d6fa7cb | |
cpfeiffer | 2e3de0cd0f | |
cpfeiffer | eec7fae288 | |
cpfeiffer | eb2332c8be | |
Andreas Shimokawa | 749fbe5ecc | |
Andreas Shimokawa | 6ed54484a6 | |
Andreas Shimokawa | e43fed2e7e | |
Andreas Shimokawa | 13260416f3 | |
Andreas Shimokawa | 151f5b8e12 | |
Andreas Shimokawa | 5a8c9a9180 | |
Andreas Shimokawa | 0be251e83d | |
Andreas Shimokawa | 5884684cad | |
Daniele Gobbetti | 7bf45d9b9f | |
Daniele Gobbetti | b6d3317b2d | |
Daniele Gobbetti | fd5a620091 | |
Andreas Shimokawa | d983d7a5c4 | |
Andreas Shimokawa | 27f88e484d | |
Daniele Gobbetti | fe11e6d306 | |
Andreas Shimokawa | 6125594703 | |
Andreas Shimokawa | ce7b42d9d4 | |
cpfeiffer | 9004a8b0c1 | |
cpfeiffer | f4cb798977 | |
cpfeiffer | 64298fc9af | |
cpfeiffer | 2a2eae068a | |
cpfeiffer | 8dee55198e | |
cpfeiffer | b7223c7e86 | |
Daniele Gobbetti | 495a8cc650 | |
Daniele Gobbetti | 65fc6f6866 | |
cpfeiffer | e92c9dbbb5 | |
cpfeiffer | 910d9ef398 | |
cpfeiffer | bdc9e70e6e | |
cpfeiffer | c407ed1a76 | |
cpfeiffer | db4261e02b | |
cpfeiffer | 7c597b325a | |
Daniele Gobbetti | afc4c9efaa | |
Daniele Gobbetti | c798cd00fe | |
Andreas Shimokawa | 94b9736a5d | |
cpfeiffer | 1b1d60faa8 | |
cpfeiffer | e9f693942a | |
cpfeiffer | 802f48011d | |
cpfeiffer | 9ca595a5cb | |
Daniele Gobbetti | 216cdad591 | |
Daniele Gobbetti | 46ee5a5499 | |
Daniele Gobbetti | 69ddead8fb | |
Daniele Gobbetti | 5b3510fade | |
Daniele Gobbetti | 2208d5088b | |
Daniele Gobbetti | fbbc2afda4 | |
Daniele Gobbetti | 0a6dc8f7a0 | |
Daniele Gobbetti | e4ddbf4aaa | |
Andreas Shimokawa | 0eeb5a6479 | |
Daniele Gobbetti | 878afd79df | |
Daniele Gobbetti | 27669761bf | |
Daniele Gobbetti | 3ed4856bf6 | |
Daniele Gobbetti | c05cfc775e | |
Andreas Shimokawa | f51ffcf16f | |
Daniele Gobbetti | 1bef702485 | |
Daniele Gobbetti | 23f752dc20 | |
Daniele Gobbetti | 14f754306d | |
Andreas Shimokawa | 732f26823b | |
Andreas Shimokawa | 9f591ef8b5 | |
Andreas Shimokawa | c3853c7735 | |
cpfeiffer | 858c962dd0 | |
Andreas Shimokawa | 91b8d7789d | |
Andreas Shimokawa | dbbcf20bbc | |
Daniele Gobbetti | 47cbf68c37 | |
Daniele Gobbetti | c4590d3989 | |
Daniele Gobbetti | 0dd29807b2 | |
Daniele Gobbetti | 8f840df5f6 | |
cpfeiffer | 716bbc7b78 | |
cpfeiffer | a1f60aab91 | |
Andreas Shimokawa | 49cc4ec9d6 | |
Andreas Shimokawa | 91cc19befe | |
Daniele Gobbetti | f16a96e9b2 | |
Andreas Shimokawa | 844d929748 | |
Andreas Shimokawa | 69f4b11594 | |
Andreas Shimokawa | 268e658e6f | |
Andreas Shimokawa | 5c26b2281f | |
Andreas Shimokawa | 80e93aeaf7 | |
cpfeiffer | 34dde2eb64 | |
cpfeiffer | 383a300566 | |
cpfeiffer | 76fcbfcb52 | |
Andreas Shimokawa | b3a04f7afb | |
Andreas Shimokawa | 6c1e41b4ec | |
cpfeiffer | 3662f32dbf | |
cpfeiffer | 3590c7c853 | |
Andreas Shimokawa | 3c35f94a7e | |
cpfeiffer | 1f2b0329c5 | |
Andreas Shimokawa | c9ab10e7e8 | |
Andreas Shimokawa | 74e1598bf7 | |
cpfeiffer | 0f6491a11d | |
cpfeiffer | 13d2c2166c | |
cpfeiffer | 184e0f2dea | |
cpfeiffer | 4aa80c149c | |
cpfeiffer | 5d121e6e8f | |
Daniele Gobbetti | d967411d70 | |
Daniele Gobbetti | 6953086c99 | |
Daniele Gobbetti | 7f7cea75c1 | |
Andreas Shimokawa | f847d834af | |
Andreas Shimokawa | e6bc406d98 | |
Andreas Shimokawa | 095c70d469 | |
cpfeiffer | d498bd976a | |
cpfeiffer | b1e2671bec | |
cpfeiffer | 04e628b2d1 | |
cpfeiffer | 8b44f90bb6 | |
cpfeiffer | f53a037d7f | |
cpfeiffer | 623e4724c2 | |
Andreas Shimokawa | 47c43e9c28 | |
cpfeiffer | 6553558947 | |
cpfeiffer | b516ceda01 | |
cpfeiffer | be45f7fe0c | |
cpfeiffer | a6e26e5ddc | |
cpfeiffer | 7c61bbb2be | |
cpfeiffer | 40be935abd | |
cpfeiffer | 8b54c6e5c4 | |
cpfeiffer | cc549a6c49 | |
Andreas Shimokawa | b9d5fc6572 | |
Andreas Shimokawa | c01423e79d | |
Andreas Shimokawa | cdd26a43d2 | |
Andreas Shimokawa | 0ca375b87b | |
Andreas Shimokawa | 33d785f67c | |
cpfeiffer | f36caafc54 | |
cpfeiffer | 580b86f41b | |
Daniele Gobbetti | 78c0f2797d | |
cpfeiffer | 410b29dd6d | |
Andreas Shimokawa | dfea2cbcc1 | |
Andreas Shimokawa | b25d771ee9 | |
Andreas Shimokawa | 594bf8c45b | |
Daniele Gobbetti | 3068d687bf | |
Daniele Gobbetti | 371a7bb4af | |
Daniele Gobbetti | 900511760c | |
Daniele Gobbetti | 109b2bef4d | |
Daniele Gobbetti | dc3ed1659c | |
Daniele Gobbetti | 764dd70e45 | |
Daniele Gobbetti | 1caca1439a | |
cpfeiffer | ef2bbf13c7 | |
Andreas Shimokawa | bf29814294 | |
cpfeiffer | 5cde8181c9 | |
Andreas Shimokawa | 27e611c583 | |
Andreas Shimokawa | c16510003c | |
Andreas Shimokawa | 44636748e7 | |
Andreas Shimokawa | 388e72d029 | |
Andreas Shimokawa | 51595aad7a | |
Andreas Shimokawa | a73d8b7f0a | |
Andreas Shimokawa | d0178686d8 | |
Andreas Shimokawa | 07d59322bd | |
cpfeiffer | 4b241ca9eb | |
Andreas Shimokawa | 5ae812c854 | |
Andreas Shimokawa | 73da7fff0a | |
Andreas Shimokawa | c2582e1e1f | |
Andreas Shimokawa | 1bd32b713a | |
cpfeiffer | 39db968e34 | |
cpfeiffer | 23d91ac79e | |
cpfeiffer | c7b4f295a1 | |
cpfeiffer | f105bbbde3 | |
cpfeiffer | 1fb20926b3 | |
Andreas Shimokawa | b1973994f0 | |
Andreas Shimokawa | da3bff0fd4 | |
cpfeiffer | e6086d6f79 | |
Andreas Shimokawa | 2458e55865 | |
Andreas Shimokawa | a8e2646fd9 | |
Andreas Shimokawa | 7e1700250f | |
cpfeiffer | e562fa9870 | |
cpfeiffer | f9e5ab5fc1 | |
cpfeiffer | 03fa05756e | |
cpfeiffer | 83079b0456 | |
Daniele Gobbetti | cb3be26349 | |
Andreas Shimokawa | ea43d76705 | |
Andreas Shimokawa | 1801a679c5 | |
rober | c1769fb4b4 | |
cpfeiffer | 8aa53fbb2c | |
Andreas Shimokawa | bffd7f332f | |
cpfeiffer | a5ae7acc63 | |
cpfeiffer | 8709b95a95 | |
Andreas Shimokawa | a07aed62ad | |
cpfeiffer | 4908fe7a53 | |
Andreas Shimokawa | 90302c83ef | |
Andreas Shimokawa | 70c021e92c | |
Andreas Shimokawa | 73187431b2 | |
Andreas Shimokawa | d2f7169de4 | |
Andreas Shimokawa | cfedf4acde | |
Andreas Shimokawa | 3a6e8789c1 | |
Andreas Shimokawa | 56d314d054 | |
Andreas Shimokawa | ad4f708140 | |
Andreas Shimokawa | 818399b157 | |
Daniele Gobbetti | ac7e21be48 | |
Andreas Shimokawa | 17c514a860 | |
Daniele Gobbetti | 376f9c53a0 | |
Andreas Shimokawa | e37491ab56 | |
Daniele Gobbetti | 75de3b21e1 | |
cpfeiffer | 8aef92026c | |
Andreas Shimokawa | 7e70341b26 | |
Andreas Shimokawa | 9051e10aad | |
Andreas Shimokawa | 0fa87b9eed | |
Andreas Shimokawa | dfcad94c2c | |
Andreas Shimokawa | 824a88c55f | |
cpfeiffer | d0b9914770 | |
cpfeiffer | e78e79a9a9 | |
Andreas Shimokawa | efac912929 | |
Andreas Shimokawa | 4f8a7ea64d | |
Andreas Shimokawa | c98716d469 | |
Andreas Shimokawa | 042963f2e2 | |
Carsten Pfeiffer | 1c8173c218 | |
cpfeiffer | 3d49426a4c | |
Tarik Sekmen | 56a0935d62 | |
cpfeiffer | ba76f64bf6 | |
cpfeiffer | dc8d1e961d | |
cpfeiffer | ab97b544f0 | |
cpfeiffer | f5a569610f | |
cpfeiffer | f6d5767276 | |
cpfeiffer | 3fe9195d0d | |
cpfeiffer | 020d8d74d6 | |
cpfeiffer | 9e4e50be47 | |
cpfeiffer | 2f0d00d645 | |
cpfeiffer | 1e89b12b15 | |
cpfeiffer | dea4ee82a1 | |
cpfeiffer | c4096e6d3c | |
Andreas Shimokawa | b12a3e74cd | |
Andreas Shimokawa | baecc20742 | |
Andreas Shimokawa | 813a02d5c7 | |
cpfeiffer | 2f1908e480 | |
Daniele Gobbetti | fc374881c5 | |
Carsten Pfeiffer | 7ad38c5e1a | |
Daniele Gobbetti | a1ff9aab21 | |
Andreas Shimokawa | 7d86396e30 | |
Carsten Pfeiffer | 32b2500d6b | |
Daniele Gobbetti | 406f9ab90d | |
Daniele Gobbetti | 9e2d32c33f | |
Daniele Gobbetti | ea97a902d1 | |
Daniele Gobbetti | 8b268a676c | |
cpfeiffer | 5d950dc407 | |
cpfeiffer | 7f89f4bb57 | |
Daniele Gobbetti | f60903699e | |
cpfeiffer | 11884d8073 | |
cpfeiffer | 75b9fe4c4d | |
Daniele Gobbetti | 228e922ce7 | |
Daniele Gobbetti | e4076dc725 | |
cpfeiffer | 92caed5af4 | |
cpfeiffer | a7792f6b72 | |
cpfeiffer | 637b43e892 | |
cpfeiffer | f004b7b11c | |
Andreas Shimokawa | 6ea9537d38 | |
Andreas Shimokawa | 81b1d1d28d | |
Andreas Shimokawa | 603d31a59e | |
Andreas Shimokawa | 41207516b1 | |
Andreas Shimokawa | 900b3f3833 | |
Andreas Shimokawa | e79f4523c3 | |
Andreas Shimokawa | 08fbbb9152 | |
Andreas Shimokawa | 06ee7efe79 | |
Andreas Shimokawa | 8366af736c | |
Carsten Pfeiffer | 6fb6b5c164 | |
cpfeiffer | 8a26ce9d67 | |
cpfeiffer | 4518e8819d | |
cpfeiffer | c469248de1 | |
cpfeiffer | 880dc7b3a4 | |
Andreas Shimokawa | f54927624b | |
Andreas Shimokawa | 8309234784 | |
Daniele Gobbetti | 7e2545f9b4 | |
Andreas Shimokawa | 68b76aa5c5 | |
Andreas Shimokawa | cb2a95398b | |
Andreas Shimokawa | 1c5d6de3ad | |
Andreas Shimokawa | 50c7206cf6 | |
Andreas Shimokawa | c4f7fc1531 | |
Andreas Shimokawa | c37cacf43d | |
cpfeiffer | 6fa2017dda | |
cpfeiffer | e3c42ace2d | |
cpfeiffer | d1d3e758d9 | |
cpfeiffer | 4be50b3a82 | |
cpfeiffer | 301c7622ef | |
cpfeiffer | 14a05c3383 | |
cpfeiffer | 9819819b92 | |
Andreas Shimokawa | 55400817b4 | |
Andreas Shimokawa | 2b98620ee0 | |
cpfeiffer | 562840a7c5 | |
Andreas Shimokawa | 80eb386dd8 | |
cpfeiffer | d2bcccaeef | |
cpfeiffer | 27d725853f | |
Andreas Shimokawa | c81e28c030 | |
Andreas Shimokawa | 1b2f20160a | |
Andreas Shimokawa | f8341918ee | |
Andreas Shimokawa | 7540a3955b | |
cpfeiffer | 68383b6c05 | |
cpfeiffer | 9a26769c3e | |
Andreas Shimokawa | 6fab01a3c2 | |
Andreas Shimokawa | 05d22b8dd1 | |
cpfeiffer | 87512149a5 | |
cpfeiffer | 9195652f11 | |
cpfeiffer | 84d1e95767 | |
cpfeiffer | b25da80656 | |
cpfeiffer | 095ada8e5d | |
cpfeiffer | be52724fdd | |
cpfeiffer | c89bba0cba | |
cpfeiffer | 61e8d88de4 | |
cpfeiffer | bd2d5fd608 | |
Andreas Shimokawa | 2b84ffdc1a | |
Andreas Shimokawa | b1cb5f3f21 | |
Andreas Shimokawa | c8feea9f37 | |
Andreas Shimokawa | 0d77a5ac05 | |
Andreas Shimokawa | f101926186 | |
Andreas Shimokawa | a70426d84d | |
Andreas Shimokawa | 1443c8088c | |
Andreas Shimokawa | 22daa507ce | |
Andreas Shimokawa | 7f5b495480 | |
Andreas Shimokawa | e78c912be3 | |
Andreas Shimokawa | f9efa36322 | |
cpfeiffer | fa9bed81ce | |
cpfeiffer | 86119a877a | |
cpfeiffer | 39d84831ed | |
cpfeiffer | 1a7c3c42e4 | |
cpfeiffer | 60210e069c | |
cpfeiffer | 29cc364f8a | |
cpfeiffer | ab5d5f6c6f | |
Andreas Shimokawa | 8112d4afd8 | |
Andreas Shimokawa | 6f162c593b | |
Andreas Shimokawa | c999c52501 | |
cpfeiffer | ddc2f116aa | |
cpfeiffer | d036f0539d | |
cpfeiffer | 43ae05673b | |
cpfeiffer | 9801a94704 | |
cpfeiffer | 45fde87df6 | |
cpfeiffer | b2518ff927 | |
Andreas Shimokawa | 157deff237 | |
Andreas Shimokawa | 70889e5326 | |
cpfeiffer | 1604ae2c22 | |
cpfeiffer | cf360455a0 | |
cpfeiffer | 84a89c7c9b | |
cpfeiffer | a23690c8e1 | |
Andreas Shimokawa | 44333c4244 | |
Andreas Shimokawa | 9277874983 | |
Andreas Shimokawa | 8b75440867 | |
cpfeiffer | fa57bf11a2 | |
cpfeiffer | 637cc3be51 | |
cpfeiffer | 0c039b8a46 | |
cpfeiffer | 0629d6aa5d | |
cpfeiffer | dbffd5a42f | |
Andreas Shimokawa | 33db0bf890 | |
cpfeiffer | 7b02548427 | |
cpfeiffer | bd0df2c527 | |
cpfeiffer | dc6fd8cb61 | |
cpfeiffer | 50034e0bfe | |
Andreas Shimokawa | 3e85efa898 | |
Andreas Shimokawa | 9efcd8974d | |
Andreas Shimokawa | d09b5442cf | |
cpfeiffer | cf12c78a64 | |
cpfeiffer | 9df661bd96 | |
Andreas Shimokawa | e859ece7c6 | |
Andreas Shimokawa | 6d1a4312ef | |
cpfeiffer | fb48086b77 | |
cpfeiffer | 1a627ad1a1 | |
Andreas Shimokawa | e71181f962 | |
xphnx | cac3bc01a9 | |
Andreas Shimokawa | 93b463c47e | |
cpfeiffer | 3e79269d43 | |
Andreas Shimokawa | f143c9ec54 | |
cpfeiffer | 8b3b4d0882 | |
Andreas Shimokawa | 75b9d0e833 | |
Andreas Shimokawa | b256c9ed15 | |
Andreas Shimokawa | 959626049e | |
Andreas Shimokawa | f8bbbdb47c | |
Andreas Shimokawa | b7c3578e5b | |
Andreas Shimokawa | 93cc35bda5 | |
Andreas Shimokawa | e65c492792 | |
Andreas Shimokawa | b5cf81eedf | |
Andreas Shimokawa | 01e96f9c8d | |
Andreas Shimokawa | 31b01d860f | |
cpfeiffer | 9b676b5354 | |
cpfeiffer | 5a458611e2 | |
cpfeiffer | 8e25c03350 | |
Andreas Shimokawa | 462f9f028f | |
Andreas Shimokawa | 2eb62ebff3 | |
cpfeiffer | 29d4f7d615 | |
cpfeiffer | 5d5bbc0068 | |
cpfeiffer | 3b249b0d2a | |
Andreas Shimokawa | 2fdaa383c4 | |
Andreas Shimokawa | c94108f2d1 | |
Andreas Shimokawa | c06ef3d260 | |
Andreas Shimokawa | 613ff1fc91 | |
cpfeiffer | 7a4330c324 | |
cpfeiffer | 0377a751b0 | |
cpfeiffer | 1772076b62 | |
Andreas Shimokawa | 442ae6499a | |
cpfeiffer | c773181da3 | |
cpfeiffer | 274e8591dc | |
Carsten Pfeiffer | 454f9a6d07 | |
Daniele Gobbetti | 98696ce9e2 | |
cpfeiffer | cf681a089a | |
cpfeiffer | 1ff1c20056 | |
Andreas Shimokawa | ab2f5a73a8 |
|
@ -0,0 +1,19 @@
|
|||
#### Before opening an issue please confirm the following:
|
||||
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
|
||||
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
|
||||
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
|
||||
|
||||
#### Your issue is:
|
||||
*In case of a bug, do not forget to attach logs!*
|
||||
|
||||
#### Your wearable device is:
|
||||
|
||||
*Please specify model and firmware version if possible*
|
||||
|
||||
#### Your android version is:
|
||||
|
||||
#### Your Gadgetbridge version is:
|
||||
|
||||
|
||||
|
||||
*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.*
|
|
@ -27,3 +27,7 @@ proguard/
|
|||
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
MPChartLib
|
||||
|
||||
fw.dirs
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
language: android
|
||||
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
# disabled -- we now set sourceCompatibility and targetCompatibility appropriately
|
||||
# - oraclejdk7
|
||||
|
||||
env:
|
||||
- GRADLE_OPTS="-XX:MaxPermSize=256m"
|
||||
|
||||
android:
|
||||
components:
|
||||
# Uncomment the lines below if you want to
|
||||
# use the latest revision of Android SDK Tools
|
||||
# - platform-tools
|
||||
- tools
|
||||
|
||||
# The BuildTools version used by your project
|
||||
- build-tools-25.0.2
|
||||
|
||||
# The SDK version used to compile your project
|
||||
- android-25
|
||||
|
||||
# Additional components
|
||||
- extra-android-m2repository
|
||||
- addon-google_apis-google-19
|
||||
|
||||
# Specify at least one system image,
|
||||
# if you need to run emulator(s) during your tests
|
||||
#- sys-img-armeabi-v7a-android-19
|
||||
#- sys-img-x86-android-17
|
||||
|
||||
script: ./gradlew build connectedCheck --stacktrace
|
734
CHANGELOG.md
734
CHANGELOG.md
|
@ -1,55 +1,743 @@
|
|||
###Changelog
|
||||
### Changelog
|
||||
|
||||
####Version 0.3.2
|
||||
* Miband: Fix for notifications only working after manual connection
|
||||
* Miband: Display firmware version
|
||||
#### Version 0.21.5
|
||||
* Mi2/Bip: Support setting distance units (metric/imperial)
|
||||
|
||||
#### Version 0.21.4
|
||||
* Mi2/Bip: Fix sleep detection for newer firmwares
|
||||
* Mi2/Bip: Fix ancient bug resulting in wrong activity data at the beginning in diagrams and aggregate data
|
||||
* No.1 F1: Support setting time format and distance units (metric/imperial)
|
||||
* Pebble: Support setting distance units to miles for Health (need to reactivate Health in App Manager after toggling)
|
||||
* HPlus: Make changing distance unit system effective immediately on toggling
|
||||
|
||||
#### Version 0.21.3
|
||||
* Amazfit Bip: Auto-switch language on connect (English, Simplified Chinese, Traditional Chinese), requires FW 0.0.9.14+
|
||||
|
||||
#### Version 0.21.2
|
||||
* Amazfit Bip: Support flashing CEP and ALM files for AGPS
|
||||
* Amazfit Bip: Initial experimental support for fetching logs from the watch
|
||||
* Mi2/Bip: Send user info to the device (fixes calories and distance display)
|
||||
* Mi2/Bip: Fix firmware update progressbar being stuck at the end
|
||||
* Pebble/Bip: Support more notification icons
|
||||
* Pebble: Automatically determine color for unknown notifications on Pebble Time
|
||||
|
||||
#### Version 0.21.1
|
||||
* Initial support for EXRIZU K8 (HPLus variant)
|
||||
* Amazfit Bip: fix long messages not being displayed at all
|
||||
* Mi Band 2: Support multiple button actions
|
||||
* NO.1 F1: Fetch sleep data
|
||||
* NO.1 F1: Heart rate support
|
||||
* Pebble: Support controlling the current active media playback application
|
||||
* Fix suspended activities coming to front when rotating the screen
|
||||
|
||||
#### Version 0.21.0
|
||||
* Initial NO.1 F1 support
|
||||
* Initial Teclast H30 support
|
||||
* Amazfit Bip: Display GPS firmware version
|
||||
* Amazfit Bip: Fix E-Mail notifications
|
||||
* Amazfit Bip: Fix call notification with unknown caller
|
||||
* Amazfit Bip: Fix crash when weather is updated and device reconnecting
|
||||
* Mi2/Bip: Fix crash when synchronizing calendar to alarms
|
||||
* Pebble: Fix crash when takeing screenshots on Android 8.0 (Oreo)
|
||||
* Pebble: Support some google app icons
|
||||
* Pebble: try to support spotify
|
||||
* Mi Band 2: Support configurable button actions
|
||||
* Fix language being reset to system default
|
||||
|
||||
#### Version 0.20.2
|
||||
* Amazfit Bip: Various fixes regarding weather, add condition string support for FW 0.0.8.74
|
||||
* Amazfit Bip: enable caller display in later firmwares
|
||||
* Amazfit Bip: initial firmware update support (EXPERIMENTAL, AT YOUR OWN RISK)
|
||||
* Re-enable improved speed zones tab
|
||||
* Probably fix crash with certain music players
|
||||
* Improve theme and add changelog icon
|
||||
|
||||
#### Version 0.20.1
|
||||
* Amazfit Bip: Support icons and text body for notifications
|
||||
* Mi Band: Fix setting smart alarms
|
||||
|
||||
#### Version 0.20.0
|
||||
* Inital Amazfit Bip support (WIP)
|
||||
* Various theming fixes
|
||||
* Add workaround for blacklist not properly persisting
|
||||
* Handle resetting language to default properly
|
||||
* Pebble: Pass booleans from Javascript Appmessage correctly
|
||||
* Pebble: Make local configuration pages work on most recent webview implementation
|
||||
* Pebble: Allow to blacklist calendars
|
||||
* Add Greek and German transliteration support
|
||||
* Various visual improvements to charts
|
||||
|
||||
#### Version 0.19.4
|
||||
* Replace or relicense CC-NC licensed icons to satisfy F-Droid
|
||||
* Mi Band 2: Make infos to display on the Band configurable
|
||||
* Mi Band 2: Support wrist rotation to switch info setting
|
||||
* Mi Band 2: Support goal notification setting
|
||||
* Mi Band 2: Support do not disturb setting
|
||||
* Mi Band 2: Support inactivity warning setting
|
||||
|
||||
#### Version 0.19.3
|
||||
* Pebble: Fix crash when calendar access permission has been denied
|
||||
* Pebble: Fix wrong timestamps with Morpheuz running on Firmware >=3
|
||||
* Mi Band 2: Improve reliability when fetching activity data
|
||||
* HPlus: Fix intensity calculation without continuous connectivity
|
||||
* HPlus: Fix Unicode handling
|
||||
* HPlus: Initial not work detection
|
||||
* Fix memory leak
|
||||
* Only show Realtime Chart on devices supporting it
|
||||
|
||||
#### Version 0.19.2
|
||||
* Pebble: Fix recurring calendar events only appearing once per week
|
||||
* HPlus: Fix crash when receiving calls without phone number
|
||||
* HPlus: Detect unicode support on Zeband Plus
|
||||
* No longer quit Gadgetbridge when bluetooth gets turned off
|
||||
|
||||
#### Version 0.19.1
|
||||
* Fix crash at startup
|
||||
* HPlus: Improve reconnection to device
|
||||
* Improve transliteration
|
||||
|
||||
#### Version 0.19.0
|
||||
* Pebble: allow calendar sync with Timeline (Title, Location, Description)
|
||||
* Pebble: display calendar icon for reminders from AOSP Calendar
|
||||
* HPlus: try to fix latin characters showing as random Chinese text
|
||||
* Improve reconnection with BLE devices
|
||||
* Improve generic notification reliability by trying to restart the notification listener when stale/crashed
|
||||
* Other small bugfixes
|
||||
|
||||
#### Version 0.18.5
|
||||
* Applied some material design guidelines to Charts and (pebble) app management
|
||||
* Changed colours: deep sleep is now dark blue, light sleep is now light blue
|
||||
* Support for exporting and importing of preferences in addition to the database
|
||||
* Visual improvements of the pie charts
|
||||
* Add filter by name in the App blacklist activity
|
||||
* Pebble: improve compatibility with watch app configuration pages
|
||||
* Pebble: display battery percentage (will only update once an hour)
|
||||
* HPlus: users can now decide whether they want to pair the device or not, hopefully fixing some connection problems (#642)
|
||||
* HPlus: display battery state and warn on low battery
|
||||
|
||||
#### Version 0.18.4
|
||||
* Mi Band 2: Display realtime steps in Live Activity
|
||||
* Mi Band: Attempt to recognize Mi Band model with hwVersion = 8
|
||||
* Alarms activity improvements and fixes
|
||||
* Make Buttons in the main activity easier to hit
|
||||
|
||||
#### Version 0.18.3
|
||||
* Fix bug that caused the same value in weekly charts for every day on Android 6 and older
|
||||
|
||||
#### Version 0.18.2
|
||||
* Mi Band 2: Fix crash on "chat" or "social network" text notification (#603)
|
||||
|
||||
#### Version 0.18.1
|
||||
* Pebble: Fix Firmware insstallation on Pebble Time Round (broken since 0.16.0)
|
||||
* Start VibrationActivity when using "find device" button with Vibratissimo
|
||||
* Support material fork of K9
|
||||
|
||||
#### Version 0.18.0
|
||||
* All new GUI for the control center
|
||||
* Add Portuguese pt_PT and pt_BR translations
|
||||
* Add Czech translation
|
||||
* Add Hebrew translation and transliteration
|
||||
* Consistently display device specific icons already during discovery
|
||||
* Add sleep chart displaying the last week of sleep
|
||||
* Huge speedup for weekly charts when changing days
|
||||
* Drop support for importing pre Gadgetbridge 0.12.0 database
|
||||
* Pebble: allow configuration web pages (clay) to access device location
|
||||
* Mi Band 2: Initial support for text notifications, caller ID, and icons (requires font installation) (#560)
|
||||
* Mi Band 2: Support for flashing Mili_pro.ft* font files
|
||||
* Mi Band 2: Improved firmware/font updated
|
||||
* Mi Band 2: Set 12h/24h time format, following the Android configuration (#573)
|
||||
* Improved BLE discovery and connectivity
|
||||
|
||||
#### Version 0.17.5
|
||||
* Automatically start the service on boot (can be turned off)
|
||||
* Pebble: PebbleKit compatibility improvements (Datalogging)
|
||||
* Pebble: Display music shuffle and repeat states for some players
|
||||
* Pebble 2/LE: Speed up data transfer
|
||||
|
||||
#### Version 0.17.4
|
||||
* Better integration with android music players
|
||||
* Privacy options for calls (hide caller name/number)
|
||||
* Send a notification to the connected if the Android Alarm Clock rings (com.android.deskclock)
|
||||
* Fixes for cyrillic transliteration
|
||||
* Pebble: Implement notification privacy modes
|
||||
* Pebble: Support weather for Obisdian watchface
|
||||
* Pebble: add a dev option to always and immediately ACK PebbleKit messages to the watch
|
||||
* HPlus: Support alarms
|
||||
* HPlus: Fix time and date sync and time format (12/24)
|
||||
* HPlus: Add device specific preferences and icon
|
||||
* HPlus: Support for Makibes F68
|
||||
|
||||
#### Version 0.17.3
|
||||
* HPlus: Improve display of new messages and phone calls
|
||||
* HPlus: Fix bug related to steps and heart rate
|
||||
* Pebble: Support dynamic keys for natively supported watchfaces and watchapps (more stability accross versions)
|
||||
* Pebble: Fix error Toast being displayed when TimeStyle watchface is not installed
|
||||
* Mi Band 1+2: Support for connecting wihout BT pairing (workaround for certain connection problems)
|
||||
|
||||
#### Version 0.17.2
|
||||
* Pebble: Fix temperature unit in Timestyle Pebble watchface
|
||||
* Add optional Cyrillic transliteration (for devices lacking the font)
|
||||
|
||||
#### Version 0.17.1
|
||||
* Pebble: Fix installation of some watchapps
|
||||
* Pebble: Try to improve PebbleKit compatibility
|
||||
* HPlus: Fix bug setting current date
|
||||
|
||||
#### Version 0.17.0
|
||||
* Add weather support through "Weather Notification" app
|
||||
* Various fixes for K9 mail when using the generic notification receiver
|
||||
* Add a preference to hide the persistent notification icon of Gadgetbridge
|
||||
* Pebble: Support for build-in weather system app (FW 4.x)
|
||||
* Pebble: Add weather support for various watchfaces
|
||||
* Pebble: Add option to disable call display
|
||||
* Pebble: Add option to automatically delete notifications that got dismissed on the phone
|
||||
* Pebble: Bugfix for some PebbleKit enabled 3rd party apps (TCW and maybe other)
|
||||
* Pebble 2/LE: Improve reliablitly and transfer speed
|
||||
* HPlus: Improved discovery and pairing
|
||||
* HPlus: Improved notifications (display + vibration)
|
||||
* HPlus: Synchronize time and date
|
||||
* HPlus: Display firmware version and battery charge
|
||||
* HPlus: Near real time Heart rate measurement
|
||||
* HPlus: Experimental synchronization of activity data (only sleep, steps and intensity)
|
||||
* HPlus: Fix some disconnection issues
|
||||
|
||||
#### Version 0.16.0
|
||||
* New devices: HPlus (e.g. Zeblaze ZeBand), contributed by João Paulo Barraca
|
||||
* ZeBand: Initial support: notifications, heart rate, sleep monitoring, user configuration, date+time
|
||||
* Pebble 2: Fix Pebble Classic FW 3.x app variant being prioritized over native Pebble 2 app variant
|
||||
* Charts (Live Activity): Fix axis labels color in dark theme
|
||||
* Mi Band: Fix ginormous step count when using Live Activity
|
||||
* Mi Band: Improved performance during activity sync
|
||||
* Mi Band 2: Fix activity data missing after doing manual hr measurements or live activity
|
||||
* Support sharing firmwares/watchapps/watchfaces to Gadgetbridge
|
||||
* Support for the "Subsonic" music player (#474)
|
||||
|
||||
#### Version 0.15.2
|
||||
* Mi Band: Fix crash with unknown notification sources
|
||||
|
||||
#### Version 0.15.1
|
||||
* Improved handling of notifications for some apps
|
||||
* Pebble 2/LE: Add setting to limit GATT MTU for debugging broken BLE stacks
|
||||
* Mi Band 2: Display battery status
|
||||
|
||||
#### Version 0.15.0
|
||||
* New device: Liveview
|
||||
* Liveview: initial support (set the time and receive notifications)
|
||||
* Pebble: log pebble app logs if option is enabled in pebble development settings
|
||||
* Pebble: notification icons for more apps
|
||||
* Pebble: Further improve compatibility for watchface configuration
|
||||
* Mi Band 2: Initial support for firmware update (tested so far: 1.0.0.39)
|
||||
|
||||
#### Version 0.14.4
|
||||
* Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings
|
||||
* Mi Band 2: Experimental support for activity recognition
|
||||
* Mi Band 2: Fix time setting code
|
||||
|
||||
#### Version 0.14.3
|
||||
* Pebble: Experimental support for pairing and using all Pebble models via BLE
|
||||
* Mi Band 1: Fix regression causing display of wrong activity data (#440)
|
||||
* Mi Band 2: Support for continuous heart rate measurements in live activity view
|
||||
|
||||
#### Version 0.14.2
|
||||
* Pebble 2: Fix a bug where the Pebble got disconnected by other unrelated LE devices
|
||||
|
||||
#### Version 0.14.1
|
||||
* Mi Band 2: Initial experimental support for activity data
|
||||
* Mi Band 2: Send the fitness goal (steps) to the band
|
||||
* Pebble 2: Work around firmware installation issues (tested with upgrading 4.2 to 4.3)
|
||||
* Pebble: Further improve compatibility for watchface configuration
|
||||
* Pebble: add Kickstart watch face to app manager on FW 4.x
|
||||
* Charts: display the total time range, not just the range with available data
|
||||
|
||||
#### Version 0.14.0
|
||||
* Pebble 2: Initial experimental support for P2/PT2 using BLE
|
||||
* Pebble: Special support in device discovery activity (MUST be used to get Pebble 2 working)
|
||||
* Pebble: Improve compatibility for watchface configuration
|
||||
* Mi Band 2: support for heart rate measurement during sleep
|
||||
* Mi Band 2: configuration option to activate the display on lift
|
||||
* Mi Band 2: configuration option to display the time + date or just the time
|
||||
* Mi Band 2: honor the wear location configuration option
|
||||
|
||||
#### Version 0.13.9
|
||||
* Pebble: use the last known location for setting sunrise and sunset
|
||||
* Pebble: fix Health disappearing forever when deactivating through app manager (and get it back for affected users)
|
||||
* Mi Band 2: More fixes for connection issues (#408)
|
||||
|
||||
#### Version 0.13.8
|
||||
* Mi Band 2: fix connection issues for users of Mi Fit (#408, #425)
|
||||
* Mi Band 1A: fix firmware update for certain 1A models
|
||||
|
||||
#### Version 0.13.7
|
||||
* Pebble: Fix configuration of certain pebble apps (eg. QR Generator, Squared 4.0)
|
||||
* Pebble: Add context menu option in app manager to search a watchapp in the pebble appstore
|
||||
* Mi Band: allow to delete Mi Band address from development settings
|
||||
* Mi Band 2: Initial support for heart rate readings (Debug activity only)
|
||||
* Mi Band 2: Support disabled alarms
|
||||
* Attempt to fix spurious device discovery problems
|
||||
* Correctly recognize Toffeed, Slimsocial and MaterialFBook as facebook notification sources
|
||||
|
||||
#### Version 0.13.6
|
||||
* Mi Band 2: Support for multiple alarms (3 at the moment)
|
||||
* Mi Band 2: Fix for alarms not working when just one is enabled
|
||||
|
||||
#### Version 0.13.5
|
||||
* Mi Band 2: Support setting one alarm
|
||||
* Pebble: Health compatibility for Firmware 4.2
|
||||
* Improve support for K9 when generic notifications are used (K9 notifications set to never)
|
||||
|
||||
#### Version 0.13.4
|
||||
* Mi Band: Initial support for recording heart and displaying rate values
|
||||
* Mi Band: Support for testing vibration patterns directly from the preferences
|
||||
* Mi Band: Clean up vibration preferences
|
||||
* Possibly fix logging to file on certain devices (#406)
|
||||
* Mi Band 2: Possibly fix weird connection interdependency between Mi 1 and 2 (#323)
|
||||
* Mi Band 1S: Whitelist firmware 4.16.4.22
|
||||
* Mi Band: try application level pairing again, in order to support data sharing with Mi Fit (#250)
|
||||
* Pebble: new icons and colours for certain apps
|
||||
* Debug-screen: added button to test "new functionality", currently live sensor data for Mi Band 1
|
||||
|
||||
#### Version 0.13.3
|
||||
* Fix regressions with missing bars and labels in charts
|
||||
* Allow to set notification type in Debug activity
|
||||
* Move "Disconnect" back to the bottom of the context menu
|
||||
* Mi Band 2: Display Message and Phone icons
|
||||
|
||||
#### Version 0.13.2
|
||||
* Support deleting devices (and their data) in control center
|
||||
* Sort devices lexicographically in control center
|
||||
* Do not forward group summary notifications (could fix some duplicate notifications)
|
||||
* Pebble: Support for health on FW 4.1
|
||||
* Mi Band: Fix offline charts not displaying heartrate for Mi 1S
|
||||
|
||||
#### Version 0.13.1
|
||||
* Improved BLE scanning for Android 5.0+
|
||||
* Pebble: try to work around duplicate Telegram messages and support Telegram icon
|
||||
* Pebble: fix some incompatibilities with certain PebbleKit Android apps
|
||||
|
||||
#### Version 0.13.0
|
||||
* Initial working Mi Band 2 support (only notifications, no activity and heart rate support)
|
||||
* Experimental support for Vibratissimo devices
|
||||
|
||||
#### Version 0.12.2
|
||||
* Fix for user attribute database table getting spammed and store sleep and steps goals properly
|
||||
|
||||
#### Version 0.12.1 (release withdrawn)
|
||||
* Pebble: Fix activity data being associated with the wrong device and/or user in some cases causing them to invisible in charts
|
||||
* Remove special handling for Conversations notifications since upstream dropped special pebble support
|
||||
|
||||
#### Version 0.12.0 (release withdrawn)
|
||||
* NB: User action needed to migrate existing data!
|
||||
* Store activity data per device and provider to allow multiple devices of the same kind with separate data. Migration is available, except for Pebble Misfit data. Existing data from multiple devices of the same kind (eg. multiple Mi Bands) will get merged while importing.
|
||||
* In Control Center, display known devices even when Bluetooth is off
|
||||
* In Control center, new menu point to launch the new "Database management" activity
|
||||
* Pebble: Support for Pebble Health on Firmware 4.0
|
||||
* Pebble: Optionally allow raw Pebble Health data to be stored in database completely (for later interpretation, when we are able to decode it)
|
||||
* Mi Band: fix displaying of deep sleep vs. light sleep (was inverted)
|
||||
|
||||
#### Version 0.11.2
|
||||
* Mi Band: support for devices that cannot pair with the band (#349)
|
||||
|
||||
#### Version 0.11.1
|
||||
* Various fixes (including crashes) for location settings
|
||||
* Pebble: Support Pebble Time 2 emulator (needs recompilation of Gadgetbridge)
|
||||
* Fix a rare crash when, due to Bluetooth problems, when a device has no name
|
||||
* Fix activity fetching getting stuck when double tapping (#333)
|
||||
* Mi Band: in the Device Discovery activity, do not display devices that are already paired
|
||||
* Mi Band: only allow automatic reconnection on disconnect when the device was previously fully connected
|
||||
* Mi Band: fix a rare crash when reading data fails due to Bluetooth problems
|
||||
* Mi Band: log full activity sample to help deciphering activity kinds (#341)
|
||||
* Mi Band 2: improved discovery mechanism to not rely on MAC addresses (#323)
|
||||
* Charts: only display heart rate samples on devices that support that
|
||||
* Add more logging to detect problems with external directories (#343)
|
||||
|
||||
#### Version 0.11.0
|
||||
* Pebble: new App Manager (keeps track of installed apps and allows app sorting on FW 3.x)
|
||||
* Pebble: call dismissal with canned SMS (FW 3.x)
|
||||
* Pebble: watchapp configuration presets
|
||||
* Pebble: fix regression with FW 2.x (almost everything was broken in 0.10.2)
|
||||
|
||||
#### Version 0.10.2
|
||||
* Pebble: allow to manually paste configuration data for legacy configuration pages
|
||||
* Pebble: various improvements to the configuration page
|
||||
* Pebble: Support FW 4.0-dp1 and Pebble2 emulator (needs recompilation of Gadgetbridge)
|
||||
* Pebble: Fix a problem with key events when using the Pebble music player
|
||||
|
||||
#### Version 0.10.1
|
||||
* Pebble: set extended music info by dissecting notifications on Android 5.0+
|
||||
* Pebble: various other improvements to music playback
|
||||
* Pebble: allow ignoring activity trackers individually (to keep the data on the pebble)
|
||||
* Mi Band: support for shifting the device time by N hours (for people who sleep at daytime)
|
||||
* Mi Band: initial and untested support for Mi Band 2
|
||||
* Allow setting the application language
|
||||
|
||||
#### Version 0.10.0
|
||||
* Pebble: option to send sunrise and sunset events to timeline
|
||||
* Pebble: fix problems with unknown app keys while configuring watchfaces
|
||||
* Mi Band: BLE connection fixes
|
||||
* Fixes for enabling logging at without restarting Gadgetbridge
|
||||
* Re-enable device paring activity on Android 6 (BLE scanning needs the location preference)
|
||||
* Display device address in device info
|
||||
|
||||
#### Version 0.9.8
|
||||
* Pebble: fix more reconnect issues
|
||||
* Pebble: fix deep sleep not being detected with Firmware 3.12 when using Pebble Health
|
||||
* Pebble: option in AppManager to delete files from cache
|
||||
* Pebble: enable pbw cache and watchface configuration for Firmware 2.x
|
||||
* Pebble: allow enabling of Pebble Health without "untested features" being enabled
|
||||
* Pebble: fix music information being messed up
|
||||
* Honour "Do Not Disturb" for phone calls and SMS
|
||||
|
||||
#### Version 0.9.7
|
||||
* Pebble: hopefully fix some reconnect issues
|
||||
* Mi Band: fix live activity monitoring running forever if back button pressed
|
||||
* Mi Band: allow low latency firmware updates, fixes update with some phones
|
||||
* Mi Band: initial experimental and probably broken support for Amazfit
|
||||
* Show aliases for BT Devices if they had been renamed in BT Settings
|
||||
* Do not show a hint about App Manager when a Mi Band is connected
|
||||
|
||||
#### Version 0.9.6
|
||||
* Again some UI/theme improvements
|
||||
* New preference to reconnect after connection loss (defaults to true)
|
||||
* Fix crash when dealing with certain old preference values
|
||||
* Mi Band: automatically reconnect when back in range after connection loss
|
||||
* Mi Band 1S: display heart rate value again when invoked via the Debug view
|
||||
|
||||
#### Version 0.9.5
|
||||
* Several UI Improvements
|
||||
* Easier First-time setup by using a FAB
|
||||
* Optional Dark Theme
|
||||
* Notification App Blacklist is now sorted
|
||||
* Gadgetbridge Icon in the notification bar displays connection state
|
||||
* Logging is now configurable without restart
|
||||
* Mi Band 1S: Initial live heartrate tracking
|
||||
* Fix certain crash in charts activity on slower devices (#277)
|
||||
|
||||
#### Version 0.9.4
|
||||
* Pebble: support pebble health datalog messages of firmware 3.11 (this adds support for deep sleep!)
|
||||
* Pebble: try to reconnect on new notifications and phone calls when connection was lost unexpectedly
|
||||
* Pebble: delay between reconnection attempts (from 1 up to 64 seconds)
|
||||
* Fix crash in charts activities when changing the date, quickly (#277)
|
||||
* Mi Band: preference to enable heart rate measurement during sleep (#232, thanks computerlyrik!)
|
||||
* Mi Band: display measured heart rate in charts (#232)
|
||||
* Mi Band 1S: full support for firmware upgrade/downgrade (both for Mi Band and heart rate sensor) (#234)
|
||||
* Mi Band 1S: fix device detection for certain versions
|
||||
|
||||
#### Version 0.9.3
|
||||
* Pebble: Fix Pebble Health activation (was not available in the App Manager)
|
||||
* Simplify connection state display (only connecting->connected)
|
||||
* Small improvements to the pairing activity
|
||||
* Mi Band 1S: Fix for mi band firmware update
|
||||
|
||||
#### Version 0.9.2
|
||||
* Mi Band: Fix update of second (HR) firmware on Mi1S (#234)
|
||||
* Fix ordering issue of device infos being displayed
|
||||
|
||||
#### Version 0.9.1
|
||||
* Mi Band: fix sporadic connection problems (stuck on "Initializing" #249)
|
||||
* Mi Band: enable low latency connection (faster) during initialization and activity sync
|
||||
* Mi Band: better feedback for firmware update
|
||||
* Device Item is now clickable also when the information entries are visible
|
||||
* Fix enabling log file writing #261
|
||||
|
||||
#### Version 0.9.0
|
||||
* Pebble: Support for configuring watchfaces/apps locally (clay) or though webbrowser (some do not work)
|
||||
* Pebble: hide the alarm management activity as it's unsupported
|
||||
* Mi Band: Improve firmware detection and updates, including 1S support
|
||||
* Mi Band: Display HR FW for 1S
|
||||
* FW and HW versions are only displayed after tapping on the "info" button in Control Center
|
||||
* Do not display activity samples when navigating too far in the past
|
||||
* Fix auto connect which was broken under some circumstances
|
||||
|
||||
#### Version 0.8.2
|
||||
* Fix database creation and updates (thanks @feclare)
|
||||
* Add experimental widget to set the alarm time to a configurable number of hours in the future (thanks @0nse)
|
||||
* Use ckChangeLog to display the Changelog within Gadgetbridge
|
||||
* Workaround to fix logfile rotation (bug in logback-android)
|
||||
|
||||
#### Version 0.8.1
|
||||
* Pebble: install (and start) freshly-installed apps on the watch instead of showing a Toast that tells the user to do so. (only applies to firmware 3.x)
|
||||
* Pebble: fix crash while receiving Health data
|
||||
* Mi Band 1S: support for synchronizing activity data (#205)
|
||||
* Mi Band 1S: support for reading the heart rate via the "Debug Screen" #178
|
||||
|
||||
#### Version 0.8.0
|
||||
* Pebble: Support Pebble Health: steps/activity data are stored correctly. Sleep time is considered as light sleep. Deep sleep is discarded. The pebble will send data where it seems appropriate, there is no action to perform on the watch for this to happen.
|
||||
* Pebble: Fix support for newer version of morpheuz (>=3.3?)
|
||||
* Pebble: Allow to select the preferred activity tracker via settings activity (Health, Misfit, Morpheuz)
|
||||
* Pebble: Fix wrong(previous) contact being displayed on the pebble
|
||||
* Mi Band: improvements to pairing and connecting
|
||||
* Fix a problem related to shared preferences storage of activity settings
|
||||
* Very basic support Android 6 runtime permission
|
||||
* Fix layout of the alarms activity
|
||||
|
||||
#### Version 0.7.4
|
||||
* Refactored the settings activity: User details are now generic instead of miband specific. Old settings are preserved.
|
||||
* Pebble: Fix regression with broken active reconnect since 0.7.0
|
||||
* Pebble: Support activation and deactivation of Pebble Health. Activation uses the User details as seen above. Insights are NOT activated.
|
||||
Please be aware that deactivation does NOT delete the data stored on the watch (but it seems to stop the tracking), and we do not know how to switch to metric length units.
|
||||
|
||||
#### Version 0.7.3
|
||||
* Pebble: Report connection state to PebbleKit companion apps via content provider. NOTE: Makes Gadgetbridge mutual exclusive with the original Pebble app.
|
||||
* Ignore generic notification when from SMSSecure when SMS Notifications are on
|
||||
|
||||
#### Version 0.7.2
|
||||
* Pebble: Allow replying to generic notifications that contain a wearable reply action (tested with Signal)
|
||||
* Pebble: Support setting up a common suffix for canned replies (defaults to " (canned reply)")
|
||||
* Mi Band: Avoid NPEs when aborting an erroneous sync #205
|
||||
* Mi Band: Fix discovery of Mi Band 1S
|
||||
* Add a confirmation dialog when performing a db import
|
||||
* Sort blacklist by package names
|
||||
|
||||
#### Version 0.7.1
|
||||
* Pebble: allow reinstallation of apps in pbw-cache from App Manager (long press menu)
|
||||
* Pebble: Fix regression which freezes Gadgetbridge when disconnecting via long-press menu
|
||||
|
||||
#### Version 0.7.0
|
||||
* Read upcoming events (up to 7 days in the future). Requires READ_CALENDAR permission
|
||||
* Fix double SMS on Sony Android and Android 6.0
|
||||
* Pebble: Support replying to SMS form the watch (canned replies)
|
||||
* Pebble: Allow installing apps compiled with SDK 2.x also on the basalt platform (Time, Time Steel)
|
||||
* Pebble: Fix decoding strings in appmessages from the pebble (fixes sending SMS from "Dialer for Pebble")
|
||||
* Pebble: Support incoming reconnections when device returns from "Airplane Mode" or "Stand-By Mode"
|
||||
* Pebble: Fix crash when turning off Bluetooth when connected on Android 6.0
|
||||
* Mi Band: reserve some alarm slots for alerting when upcoming events begin. NB: the band will vibrate at the start time of the event, android reminders are ignored
|
||||
* Mi Band: Display unique devices Names, not just "MI"
|
||||
* Some new and updated icons
|
||||
|
||||
#### Version 0.6.9
|
||||
* Pebble: Store app details in pbw-cache and display them in app manager on firmware 3.x
|
||||
* Pebble: Increase maximum notification body length from 255 to 512 bytes on firmware 3.x
|
||||
* Pebble: Support installing .pbl (language files) on firmware 3.x
|
||||
* Pebble: Correct setting the timezone on firmware 3.x (pebble expects the "ID" eg. Europe/Berlin)
|
||||
* Pebble: Show correct icon for activity tracker and watchfaces in app installer (language and fw icons still missing)
|
||||
* Pebble: Fix crash when trying to install files though a file manager which are located inside the pbw-cache on firmware 3.x
|
||||
* Support for deleting all activity data (in the 'Debug' screen)
|
||||
* Don't pop up the virtual keyboard when entering the Debug screen
|
||||
* Remove all pending notifications on quit
|
||||
* Mi Band: KitKat: hopefully fixed showing the progress bar during activity data synchronization (#155)
|
||||
* Mi Band 1S: hopefully fixed connection errors (#178) Notifications probably do not work yet, though
|
||||
|
||||
#### Version 0.6.8
|
||||
* Mi Band: support for Firmware upgrade/downgrade on Mi Band 1A (white LEDs, no heartrate sensor)
|
||||
* Pebble: fix regression in 0.6.7 when installing pbw/pbz files from content providers (eg. download manager)
|
||||
* Pebble: fix installation of pbw files on firmware 3.x when using content providers (eg. download manager)
|
||||
* Pebble: fix crash on firmware 3.x when pebble requests a pbw that is not in Gadgetbridge's cache
|
||||
+ Treat Signal notifications as chat notifications
|
||||
* Fix crash when contacts cannot be read on Android 6.0 (non-granted permissions)
|
||||
|
||||
#### Version 0.6.7
|
||||
* Pebble: Allow installation of 3.x apps on OG Pebble (FW will be released soon)
|
||||
* Fix crashes on startup when logging is enabled or when entering the app manager on some phones
|
||||
+ Fix Pebble being detected as MI when unpaired and autoconnect is enabled
|
||||
* Fix Crash when not having K9 Mail permissions (happens when installing K9 after Gadgetbridge) (#175)
|
||||
|
||||
#### Version 0.6.6
|
||||
* Mi Band: Huge performance improvement fetching activity data
|
||||
* Mi Band: attempt at fixing connection problems (#156)
|
||||
* Pebble: Try to interpret sleep data from Misfit data
|
||||
* Fix exporting the activity database on devices with read-only external storage (#153)
|
||||
* Fix totally wrong sleep time in the sleep chart
|
||||
|
||||
#### Version 0.6.5
|
||||
* Mi Band: Support "Locate Device" with Mi Band 1A (and Mi Band 1 with new firmware)
|
||||
* Pebble: Support syncing steps from Misfit (untested features must be turned on to see them), intensity=steps, no sleep support yet
|
||||
* Disable activity fetching when not supported
|
||||
* Small improvements to live activity charts
|
||||
|
||||
#### Version 0.6.4
|
||||
* Support pull down to synchronize activity data (#138)
|
||||
* Display tabs in the Charts activity (#139)
|
||||
* Mi Band: initial support for Mi Band 1a (the one with white LEDs) (thanks @sarg) (#136)
|
||||
* Mi Band: Attempt at fixing problem with never finishing activity data fetching (#141, #142)
|
||||
* Register/unregister BroadcastReceivers instead of enabling/disabling them with PackageManager (#134)
|
||||
(should fix disconnection because the service is being killed)
|
||||
|
||||
#### Version 0.6.3
|
||||
* Pebble: support installation of language files (.pbl) on FW 2.x
|
||||
* Try to prevent service being killed by disallowing backups
|
||||
|
||||
#### Version 0.6.2
|
||||
* Mi Band: support firmware version 1.0.10.14 (and onwards?) vibration
|
||||
* Mi Band: get device name from official BT SIG endpoint
|
||||
* Mi Band: initial support for displaying live activity data, screen stays on
|
||||
|
||||
#### Version 0.6.1
|
||||
* Pebble: Allow muting (blacklisting) Apps from within generic notifications on the watch
|
||||
* Pebble: Detect all known Pebble Versions including new "chalk" platform (Pebble Time Round)
|
||||
* Option to ignore phone calls (useful for Pebble Dialer)
|
||||
* Mi Band: Added progressbar for activity data transfer and fixes for firmware transfer progressbar
|
||||
* Bugfix for app blacklist (some checkboxes where wrongly drawn as checked)
|
||||
|
||||
#### Version 0.6.0
|
||||
* Pebble: WIP implementation of PebbleKit Intents to make some 3rd party Android apps work with the Pebble (eg. Ventoo)
|
||||
* Pebble: Option to set reconnection attempts in settings (one attempt usually takes about 5 seconds)
|
||||
* Support controlling all audio players that react to media buttons (can be chosen in settings)
|
||||
* Treat SMS as generic notification if set to "never" (can be blacklisted there also if desired)
|
||||
* Treat Conversations messages as chat messages, even if arrived via Pebble Intents (nice icon for Pebble FW 3.x)
|
||||
* Allow opening firmware / app files from the download manager "app" (technically a content provider)
|
||||
* Mi Band: whitelisted a few firmware versions
|
||||
|
||||
#### Version 0.5.4
|
||||
* Mi Band: allow the transfer of activity data without clearing MiBand's memory
|
||||
* Pebble: for generic notifications use generic icon instead of SMS icons on FW 3.x (thanks @roidelapluie)
|
||||
* Pebble: use different icons and background colors for specific groups of applications (chat, mail, etc) (thanks @roidelapluie)
|
||||
* In settings, support blacklisting apps for generic notifications
|
||||
|
||||
#### Version 0.5.3
|
||||
* Pebble: For generic notifications, support dismissing individual notifications and "Open on Phone" feature (OG & PT)
|
||||
* Pebble: Allow to treat K9 notifications as generic notifications (if notification mode is set to never)
|
||||
* Ignore QKSMS notifications to avoid double notification for incoming SMS
|
||||
* Improved UI of Firmware/App installer
|
||||
* Device state again visible on lockscreen
|
||||
* Date display and navigation now working properly for all charts
|
||||
|
||||
#### Version 0.5.2
|
||||
* Pebble: support "dismiss all" action also on Pebble Time/FW 3.x notifications
|
||||
* Mi Band: show a notification when the battery is below 10%
|
||||
* Graphs are now using the same theme as the rest of the application
|
||||
* Graphs now show when the device was not worn by the user (for devices that send this information)
|
||||
* Remove unused settings option in charts view
|
||||
* Build target is now Android SDK 23 (Marshmallow)
|
||||
|
||||
#### Version 0.5.1
|
||||
* Pebble: support taking screenshot from Pebble Time
|
||||
* Fix broken "find lost device" which was broken in 0.5.0
|
||||
|
||||
#### Version 0.5.0
|
||||
* Mi Band: fix setting wear location
|
||||
* Pebble: experimental watchapp installation support for FW 3.x/Pebble Time
|
||||
* Pebble: support Pebble emulator via TCP connection (needs rebuild with INTERNET permission)
|
||||
* Pebble: use SMS/EMAIL icons for FW 3.x/Pebble Time
|
||||
* Pebble: do not throttle notifications
|
||||
* Support going forward/backwards in time in the activity charts
|
||||
* Various small bugfixes to the App/FW Installation Activity
|
||||
|
||||
#### Version 0.4.6
|
||||
* Mi Band: Fixed negative number of steps displayed (#91)
|
||||
* Mi Band: fixed (re-) connection problems after band getting disconnected
|
||||
* Pebble: new option to enable untested code (enable only if you like bad surprises)
|
||||
* Pebble: always enable 2.x notifications with "dismiss all" action on FW 2.x (except for K9)
|
||||
* Fixed slight steps graph distortion through black text labels
|
||||
* Fixed control center activity and notification showing different device connection state
|
||||
* Small firmware installation improvements
|
||||
* Various refactorings and code cleanups
|
||||
|
||||
#### Version 0.4.5
|
||||
* Enhancement to activity graphs: new graph showing the number of steps done today and in the last week
|
||||
* New preference to set the desired fitness goal (number of steps to walk in one day)
|
||||
* Mi Band: support for setting the fitness goal (the band will show the progress to the goal with the LEDs and vibrates when the goal is reached)
|
||||
* Mi Band: send the wear location (left / right hand) to the device
|
||||
* Mi Band: support for flashing firmware from .fw files (upgrades and downgrades are possible)
|
||||
* Fixed crash when synchronizing activity data in the graphs activity and changing device orientation
|
||||
|
||||
#### Version 0.4.4
|
||||
* Set Gadgetbridge notification visibility to public, to show the connection status on the lockscreen
|
||||
* Support for backup up and restoring of the activity database (via Debug activity)
|
||||
* Support for graceful upgrades and downgrades, keeping your activity database intact
|
||||
* Enhancement to activity graphs: new graphs for sleep data (only last night) accessible swiping right from the main graph
|
||||
* Enhancement to graphs activity: it is now possible to fetch the activity data directly from this activity
|
||||
* Pebble: experimental support for dismissing (all) notifications via actionable notifications (disabled by default)
|
||||
* Pebble: make FW 3.x notifications available by default
|
||||
* Mi Band: Set the graphs activity as the default action available with a single tap on the connected device
|
||||
|
||||
#### Version 0.4.3
|
||||
* Mi Band: Support for setting alarms
|
||||
* Mi Band: Bugfix for activity data synchronization
|
||||
|
||||
#### Version 0.4.2
|
||||
* Material style for Lollipop
|
||||
* Support for finding a lost device (vibrate until cancelled)
|
||||
* Mi Band: Support for vibration profiles, configurable for notifications
|
||||
* Pebble: Support taking screenshots from the device context menu (Pebble Time not supported yet)
|
||||
|
||||
#### Version 0.4.1
|
||||
* New icons, thanks xphnx!
|
||||
* Improvements to Sleep Monitor charts
|
||||
* Pebble: use new Sleep Monitor for Morpheuz (previously Mi Band only)
|
||||
* Pebble: experimental support for FW 3.x notification protocol
|
||||
* Pebble: dev option to force latest notification protocol
|
||||
|
||||
#### Version 0.4.0
|
||||
* Pebble: Initial Morpheuz protocol support for getting sleep data
|
||||
* Pebble: Support launching of watchapps though the AppManager Activity
|
||||
* Pebble: Support CM 12.1 default music app (Eleven)
|
||||
* Pebble: Fix firmware installation when all 8 app slots are in use
|
||||
* Pebble: Fix firmware installation when Pebble is in recovery mode
|
||||
* Pebble: Fix error when reinstalling apps, useful for upgrading/downgrading
|
||||
* Mi Band: Make vibration count configurable for different kinds of Notifications
|
||||
* Mi Band: Initial support for fetching activity data
|
||||
* Support rebooting Mi Band/Pebble through the Debug Activity
|
||||
* Add highly experimental sleep monitor (accessible via long press on a device)
|
||||
* Fix Debug activity (SMS and E-Mail buttons were broken)
|
||||
* Add Turkish translation contributed by Tarik Sekmen
|
||||
|
||||
#### Version 0.3.5
|
||||
* Add discovery and pairing Activity for Pebble and Mi Band
|
||||
* Listen for Pebble Message Intents and forward notifications (used by Conversations)
|
||||
* Make strings translatable and add German, Italian, Russian, Spanish and Korean translations
|
||||
* Mi Band: Display battery status
|
||||
|
||||
#### Version 0.3.4
|
||||
* Pebble: Huge speedup for app/firmware installation.
|
||||
* Pebble: Use a separate notification with progress bar for installation procedure
|
||||
* Pebble: Bugfix for being stuck while waiting for a slot, when none is available
|
||||
* Mi Band: Display connection status in notification (previously Pebble only)
|
||||
|
||||
#### Version 0.3.3
|
||||
* Pebble: Try to reduce battery usage by acknowledging datalog packets
|
||||
* Mi Band: Set current time on the device (thanks to PR by @danielegobbetti)
|
||||
* More robust connection state handling and display
|
||||
|
||||
#### Version 0.3.2
|
||||
* Mi Band: Fix for notifications only working after manual connection
|
||||
* Mi Band: Display firmware version
|
||||
* Pebble: Display hardware revision
|
||||
* Pebble: Check if firmware is compatible before allowing installation
|
||||
|
||||
####Version 0.3.1
|
||||
* Miband: Fix for notifications only woking in Debug
|
||||
#### Version 0.3.1
|
||||
* Mi Band: Fix for notifications only working in Debug
|
||||
|
||||
####Version 0.3.0
|
||||
* Miband: Initial support (see README.md)
|
||||
#### Version 0.3.0
|
||||
* Mi Band: Initial support (see README.md)
|
||||
* Pebble: Firmware installation (USE AT YOUR OWN RISK)
|
||||
* Pebble: Fix installation problems with certain .pbw files
|
||||
* Pebble: Volume control
|
||||
* Add icon for activity tracker apps (icon by xphnx)
|
||||
* Let the application quit when in reconnecting state
|
||||
|
||||
####Version 0.2.0
|
||||
#### Version 0.2.0
|
||||
* Experimental pbw installation support (watchfaces/apps)
|
||||
* New icons for device and app lists
|
||||
* Fix for device list not refreshing when bluetooth gets turned on
|
||||
* Filter out annyoing low battery notifications
|
||||
* Fix for device list not refreshing when Bluetooth gets turned on
|
||||
* Filter out annoying low battery notifications
|
||||
* Fix for crash on some devices when creating a debug notification
|
||||
* Lots of internal changes preparing multi device support
|
||||
|
||||
####Version 0.1.5
|
||||
#### Version 0.1.5
|
||||
* Fix for DST (summer time)
|
||||
* Option to sync time on connect (enabled by default)
|
||||
* Opening .pbw files with Gadgetbridge prints some package information
|
||||
(This was not meant to be released yet, but the DST fix made a new release neccessary)
|
||||
(This was not meant to be released yet, but the DST fix made a new release necessary)
|
||||
|
||||
####Version 0.1.4
|
||||
#### Version 0.1.4
|
||||
* New AppManager shows installed Apps/Watchfaces (removal possible via context menu)
|
||||
* Allow back navigation in ActionBar (Debug and AppMananger Activities)
|
||||
* Make sure Intent broadcasts do not leave Gadgetbridge
|
||||
* Show hint in the Main Activiy (tap to connect etc)
|
||||
* Show hint in the Main Activity (tap to connect etc)
|
||||
|
||||
####Version 0.1.3
|
||||
* Remove the connect button, list all suported devices and connect on tap instead
|
||||
#### Version 0.1.3
|
||||
* Remove the connect button, list all supported devices and connect on tap instead
|
||||
* Display connection status and firmware of connected devices in the device list
|
||||
* Remove quit button from the service notification, put a quit item in the context menu instead
|
||||
|
||||
####Version 0.1.2
|
||||
* Added option to start Gadgetbridge and connect automatically when bluetooth is turned on
|
||||
* stop service if bluetooth is turned off
|
||||
#### Version 0.1.2
|
||||
* Added option to start Gadgetbridge and connect automatically when Bluetooth is turned on
|
||||
* stop service if Bluetooth is turned off
|
||||
* try to reconnect if connection was lost
|
||||
|
||||
####Version 0.1.1
|
||||
#### Version 0.1.1
|
||||
* Fixed various bugs regarding K-9 Mail notifications.
|
||||
* "Generic notification support" in Setting now opens Androids "Notifcaion access" dialog.
|
||||
* "Generic notification support" in Setting now opens Androids "Notification access" dialog.
|
||||
|
||||
####Version 0.1.0
|
||||
#### Version 0.1.0
|
||||
* Initial release
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
.. 2>/dev/null
|
||||
names ()
|
||||
{
|
||||
echo -e "\n exit;\n**Contributors (sorted by number of commits):**\n";
|
||||
git log --format='%aN:%ae' origin/master | grep -Ev "FYG_.*_bot_ignore_me" | sed 's/@users.github.com/@users.noreply.github.com/g' | awk 'BEGIN{FS=":"}{ct[$1]+=1;if (length($2) > length(e[$1])) {e[$1]=$2}}END{for (i in e) { n[i]=e[i];c[i]+=ct[i] }; for (a in e) print c[a]"\t* "a" <"n[a]">";}' | sort -n -r | cut -f 2-
|
||||
}
|
||||
quine ()
|
||||
{
|
||||
{
|
||||
echo ".. 2>/dev/null";
|
||||
declare -f names | sed -e 's/^[[:space:]]*/ /';
|
||||
declare -f quine | sed -e 's/^[[:space:]]*/ /';
|
||||
echo -e " quine\n";
|
||||
names;
|
||||
echo -e "\nAnd all the Transifex translators, which I cannot automatically list, at the moment.\n\n*To update the contributors list just run this file with bash*"
|
||||
} > CONTRIBUTORS.rst;
|
||||
exit
|
||||
}
|
||||
quine
|
||||
|
||||
|
||||
exit;
|
||||
**Contributors (sorted by number of commits):**
|
||||
|
||||
* Andreas Shimokawa <shimokawa@fsfe.org>
|
||||
* Carsten Pfeiffer <cpfeiffer@users.noreply.github.com>
|
||||
* Daniele Gobbetti <daniele+github@gobbetti.name>
|
||||
* João Paulo Barraca <jpbarraca@gmail.com>
|
||||
* ivanovlev <lion.ivanov@gmal.com>
|
||||
* Julien Pivotto <roidelapluie@inuits.eu>
|
||||
* Steffen Liebergeld <perl@gmx.org>
|
||||
* Lem Dulfo <lemuel.dulfo@gmail.com>
|
||||
* Sergey Trofimov <sarg@sarg.org.ru>
|
||||
* JohnnySun <bmy001@gmail.com>
|
||||
* Uwe Hermann <uwe@hermann-uwe.de>
|
||||
* Alberto <albertsal83@gmail.com>
|
||||
* 0nse <0nse@users.noreply.github.com>
|
||||
* Gergely Peidl <gergely@peidl.net>
|
||||
* Christian Fischer <sw-dev@computerlyrik.de>
|
||||
* 6arms1leg <m.brnsfld@googlemail.com>
|
||||
* walkjivefly <mark@walkjivefly.com>
|
||||
* Normano64 <per.bergqwist@gmail.com>
|
||||
* Avamander <Avamander@users.noreply.github.com>
|
||||
* Ⲇⲁⲛⲓ Φi <daniphii@outlook.com>
|
||||
* Yar <yaroslav.isakov@gmail.com>
|
||||
* Yaron Shahrabani <sh.yaron@gmail.com>
|
||||
* xzovy <caleb@caleb-cooper.net>
|
||||
* xphnx <xphnx@users.noreply.github.com>
|
||||
* Tarik Sekmen <tarik@ilixi.org>
|
||||
* Szymon Tomasz Stefanek <s.stefanek@gmail.com>
|
||||
* Roman Plevka <rplevka@redhat.com>
|
||||
* rober <rober@prtl.nodomain.net>
|
||||
* Nicolò Balzarotti <anothersms@gmail.com>
|
||||
* Natanael Arndt <arndtn@gmail.com>
|
||||
* Marc Schlaich <marc.schlaich@googlemail.com>
|
||||
* kevlarcade <kevlarcade@gmail.com>
|
||||
* Kevin Richter <me@kevinrichter.nl>
|
||||
* Kasha <kasha_malaga@hotmail.com>
|
||||
* Ivan <ivan_tizhanin@mail.ru>
|
||||
* Hasan Ammar <ammarh@gmail.com>
|
||||
* Gilles MOREL <contact@gilles-morel.fr>
|
||||
* Gilles Émilien MOREL <Almtesh@users.noreply.github.com>
|
||||
* Daniel Hauck <maill@dhauck.eu>
|
||||
* Chris Perelstein <chris.perelstein@gmail.com>
|
||||
* Carlos Ferreira <calbertoferreira@gmail.com>
|
||||
* atkyritsis <at.kyritsis@gmail.com>
|
||||
* andre <andre.buesgen@yahoo.de>
|
||||
* Alexey Afanasev <avafanasiev@gmail.com>
|
||||
|
||||
And all the Transifex translators, which I cannot automatically list, at the moment.
|
||||
|
||||
*To update the contributors list just run this file with bash*
|
|
@ -0,0 +1,34 @@
|
|||
## Feature Matrix
|
||||
|
||||
| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Amazfit Bip |
|
||||
|-----------------------------------| ----------|---------------|---------|-----------|-------------|
|
||||
|Calls Notification | YES | YES | YES | YES | YES |
|
||||
|Reject Calls | YES | YES | NO | NO | YES |
|
||||
|Accept Calls | NO(2) | NO(2) | NO | NO | NO(3) |
|
||||
|Generic Notification | YES | YES | YES | YES | YES |
|
||||
|Dismiss Notifications on Phone | YES | YES | NO | NO | NO |
|
||||
|Predefined Replies | YES | YES | NO | NO | NO |
|
||||
|Voice Replies | N/A | NO(3) | N/A | N/A | N/A |
|
||||
|Calendar Sync | YES | YES | NO | NO | NO |
|
||||
|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES |
|
||||
|Smart alarms | NO(1) | YES | YES | NO | NO |
|
||||
|Weather | NO(1) | YES | NO | NO | YES |
|
||||
|Activity Tracking | NO(1) | YES | YES | YES | YES |
|
||||
|Sleep Tracking | NO(1) | YES | YES | YES | YES |
|
||||
|HR Tracking | N/A | YES | YES | YES | YES |
|
||||
|Realtime Activity Tracking | NO | NO | YES | YES | YES |
|
||||
|Music Control | YES | YES | NO | NO | NO |
|
||||
|Watchapp/face Installation | YES | YES | NO | NO | NO |
|
||||
|Firmware Installaton | YES | YES | YES | YES | YES |
|
||||
|Taking Screenshots | YES | YES | NO | NO | NO |
|
||||
|Support Android Companion Apps | YES | YES | NO | NO | NO |
|
||||
|
||||
(1) Possible via 3rd Party Watchapp
|
||||
(2) Theoretically possible (works on iOS, would need lot of work)
|
||||
(3) Possible but not implemented yet
|
||||
|
||||
|
||||
### Notes about Pebble Firmware >=3.0
|
||||
|
||||
* Gadgetbridge will keep track of installed watchfaces, but if the Pebble is used with another phone or another app, the information displayed in the app manager can get out of sync since it is impossible to query Firmware >= 3.x for installed apps/watchfaces.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/bin
|
||||
/build
|
|
@ -0,0 +1,32 @@
|
|||
apply plugin: 'java'
|
||||
//apply plugin: 'maven'
|
||||
apply plugin:'application'
|
||||
|
||||
archivesBaseName = 'gadgetbridge-daogenerator'
|
||||
//version = '0.9.2-SNAPSHOT'
|
||||
|
||||
dependencies {
|
||||
// compile 'org.greenrobot:greendao-generator:2.2.0'
|
||||
// compile project(":DaoGenerator")
|
||||
compile 'com.github.freeyourgadget:greendao:1998d7cd2d21f662c6044f6ccf3b3a251bbad341'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
srcDir 'src'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainClassName = "nodomain.freeyourgadget.gadgetbridge.daogen.GBDaoGenerator"
|
||||
|
||||
task genSources(type: JavaExec) {
|
||||
main = mainClassName
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
workingDir = '../'
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives jar
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
/*
|
||||
* Copyright (C) 2011 Markus Junginger, greenrobot (http://greenrobot.de)
|
||||
*
|
||||
* 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 nodomain.freeyourgadget.gadgetbridge.daogen;
|
||||
|
||||
import de.greenrobot.daogenerator.DaoGenerator;
|
||||
import de.greenrobot.daogenerator.Entity;
|
||||
import de.greenrobot.daogenerator.Index;
|
||||
import de.greenrobot.daogenerator.Property;
|
||||
import de.greenrobot.daogenerator.Schema;
|
||||
|
||||
/**
|
||||
* Generates entities and DAOs for the example project DaoExample.
|
||||
* Automatically run during build.
|
||||
*/
|
||||
public class GBDaoGenerator {
|
||||
|
||||
private static final String VALID_FROM_UTC = "validFromUTC";
|
||||
private static final String VALID_TO_UTC = "validToUTC";
|
||||
private static final String MAIN_PACKAGE = "nodomain.freeyourgadget.gadgetbridge";
|
||||
private static final String MODEL_PACKAGE = MAIN_PACKAGE + ".model";
|
||||
private static final String VALID_BY_DATE = MODEL_PACKAGE + ".ValidByDate";
|
||||
private static final String OVERRIDE = "@Override";
|
||||
private static final String SAMPLE_RAW_INTENSITY = "rawIntensity";
|
||||
private static final String SAMPLE_STEPS = "steps";
|
||||
private static final String SAMPLE_RAW_KIND = "rawKind";
|
||||
private static final String SAMPLE_HEART_RATE = "heartRate";
|
||||
private static final String TIMESTAMP_FROM = "timestampFrom";
|
||||
private static final String TIMESTAMP_TO = "timestampTo";
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Schema schema = new Schema(17, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
|
||||
Entity deviceAttributes = addDeviceAttributes(schema);
|
||||
Entity device = addDevice(schema, deviceAttributes);
|
||||
|
||||
// yeah deep shit, has to be here (after device) for db upgrade and column order
|
||||
// because addDevice adds a property to deviceAttributes also....
|
||||
deviceAttributes.addStringProperty("volatileIdentifier");
|
||||
|
||||
Entity tag = addTag(schema);
|
||||
Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user);
|
||||
|
||||
addMiBandActivitySample(schema, user, device);
|
||||
addPebbleHealthActivitySample(schema, user, device);
|
||||
addPebbleHealthActivityKindOverlay(schema, user, device);
|
||||
addPebbleMisfitActivitySample(schema, user, device);
|
||||
addPebbleMorpheuzActivitySample(schema, user, device);
|
||||
addHPlusHealthActivityKindOverlay(schema, user, device);
|
||||
addHPlusHealthActivitySample(schema, user, device);
|
||||
addNo1F1ActivitySample(schema, user, device);
|
||||
|
||||
addCalendarSyncState(schema, device);
|
||||
|
||||
new DaoGenerator().generateAll(schema, "app/src/main/java");
|
||||
}
|
||||
|
||||
private static Entity addTag(Schema schema) {
|
||||
Entity tag = addEntity(schema, "Tag");
|
||||
tag.addIdProperty();
|
||||
tag.addStringProperty("name").notNull();
|
||||
tag.addStringProperty("description").javaDocGetterAndSetter("An optional description of this tag.");
|
||||
tag.addLongProperty("userId").notNull();
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static Entity addActivityDescription(Schema schema, Entity tag, Entity user) {
|
||||
Entity activityDesc = addEntity(schema, "ActivityDescription");
|
||||
activityDesc.setJavaDoc("A user may further specify his activity with a detailed description and the help of tags.\nOne or more tags can be added to a given activity range.");
|
||||
activityDesc.addIdProperty();
|
||||
activityDesc.addIntProperty(TIMESTAMP_FROM).notNull();
|
||||
activityDesc.addIntProperty(TIMESTAMP_TO).notNull();
|
||||
activityDesc.addStringProperty("details").javaDocGetterAndSetter("An optional detailed description, specific to this very activity occurrence.");
|
||||
|
||||
Property userId = activityDesc.addLongProperty("userId").notNull().getProperty();
|
||||
activityDesc.addToOne(user, userId);
|
||||
|
||||
Entity activityDescTagLink = addEntity(schema, "ActivityDescTagLink");
|
||||
activityDescTagLink.addIdProperty();
|
||||
Property sourceId = activityDescTagLink.addLongProperty("activityDescriptionId").notNull().getProperty();
|
||||
Property targetId = activityDescTagLink.addLongProperty("tagId").notNull().getProperty();
|
||||
|
||||
activityDesc.addToMany(tag, activityDescTagLink, sourceId, targetId);
|
||||
|
||||
return activityDesc;
|
||||
}
|
||||
|
||||
private static Entity addUserInfo(Schema schema, Entity userAttributes) {
|
||||
Entity user = addEntity(schema, "User");
|
||||
user.addIdProperty();
|
||||
user.addStringProperty("name").notNull();
|
||||
user.addDateProperty("birthday").notNull();
|
||||
user.addIntProperty("gender").notNull();
|
||||
Property userId = userAttributes.addLongProperty("userId").notNull().getProperty();
|
||||
|
||||
// sorted by the from-date, newest first
|
||||
Property userAttributesSortProperty = getPropertyByName(userAttributes, VALID_FROM_UTC);
|
||||
user.addToMany(userAttributes, userId).orderDesc(userAttributesSortProperty);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private static Property getPropertyByName(Entity entity, String propertyName) {
|
||||
for (Property prop : entity.getProperties()) {
|
||||
if (propertyName.equals(prop.getPropertyName())) {
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Could not find property " + propertyName + " in entity " + entity.getClassName());
|
||||
}
|
||||
|
||||
private static Entity addUserAttributes(Schema schema) {
|
||||
// additional properties of a user, which may change during the lifetime of a user
|
||||
// this allows changing attributes while preserving user identity
|
||||
Entity userAttributes = addEntity(schema, "UserAttributes");
|
||||
userAttributes.addIdProperty();
|
||||
userAttributes.addIntProperty("heightCM").notNull();
|
||||
userAttributes.addIntProperty("weightKG").notNull();
|
||||
userAttributes.addIntProperty("sleepGoalHPD").javaDocGetterAndSetter("Desired number of hours of sleep per day.");
|
||||
userAttributes.addIntProperty("stepsGoalSPD").javaDocGetterAndSetter("Desired number of steps per day.");
|
||||
addDateValidityTo(userAttributes);
|
||||
|
||||
return userAttributes;
|
||||
}
|
||||
|
||||
private static void addDateValidityTo(Entity entity) {
|
||||
entity.addDateProperty(VALID_FROM_UTC).codeBeforeGetter(OVERRIDE);
|
||||
entity.addDateProperty(VALID_TO_UTC).codeBeforeGetter(OVERRIDE);
|
||||
|
||||
entity.implementsInterface(VALID_BY_DATE);
|
||||
}
|
||||
|
||||
private static Entity addDevice(Schema schema, Entity deviceAttributes) {
|
||||
Entity device = addEntity(schema, "Device");
|
||||
device.addIdProperty();
|
||||
device.addStringProperty("name").notNull();
|
||||
device.addStringProperty("manufacturer").notNull();
|
||||
device.addStringProperty("identifier").notNull().unique().javaDocGetterAndSetter("The fixed identifier, i.e. MAC address of the device.");
|
||||
device.addIntProperty("type").notNull().javaDocGetterAndSetter("The DeviceType key, i.e. the GBDevice's type.");
|
||||
device.addStringProperty("model").javaDocGetterAndSetter("An optional model, further specifying the kind of device-");
|
||||
Property deviceId = deviceAttributes.addLongProperty("deviceId").notNull().getProperty();
|
||||
// sorted by the from-date, newest first
|
||||
Property deviceAttributesSortProperty = getPropertyByName(deviceAttributes, VALID_FROM_UTC);
|
||||
device.addToMany(deviceAttributes, deviceId).orderDesc(deviceAttributesSortProperty);
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
private static Entity addDeviceAttributes(Schema schema) {
|
||||
Entity deviceAttributes = addEntity(schema, "DeviceAttributes");
|
||||
deviceAttributes.addIdProperty();
|
||||
deviceAttributes.addStringProperty("firmwareVersion1").notNull();
|
||||
deviceAttributes.addStringProperty("firmwareVersion2");
|
||||
addDateValidityTo(deviceAttributes);
|
||||
|
||||
return deviceAttributes;
|
||||
}
|
||||
|
||||
private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "MiBandActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static void addHeartRateProperties(Entity activitySample) {
|
||||
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
}
|
||||
|
||||
private static Entity addPebbleHealthActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "PebbleHealthActivitySample");
|
||||
addCommonActivitySampleProperties("AbstractPebbleHealthActivitySample", activitySample, user, device);
|
||||
activitySample.addByteArrayProperty("rawPebbleHealthData").codeBeforeGetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addPebbleHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
|
||||
Entity activityOverlay = addEntity(schema, "PebbleHealthActivityOverlay");
|
||||
|
||||
activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
|
||||
activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
|
||||
activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
|
||||
Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
|
||||
activityOverlay.addToOne(device, deviceId);
|
||||
|
||||
Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
|
||||
activityOverlay.addToOne(user, userId);
|
||||
activityOverlay.addByteArrayProperty("rawPebbleHealthData");
|
||||
|
||||
return activityOverlay;
|
||||
}
|
||||
|
||||
private static Entity addPebbleMisfitActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "PebbleMisfitSample");
|
||||
addCommonActivitySampleProperties("AbstractPebbleMisfitActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty("rawPebbleMisfitSample").notNull().codeBeforeGetter(OVERRIDE);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addPebbleMorpheuzActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "PebbleMorpheuzSample");
|
||||
addCommonActivitySampleProperties("AbstractPebbleMorpheuzActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addHPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "HPlusHealthActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addByteArrayProperty("rawHPlusHealthData");
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
activitySample.addIntProperty("distance");
|
||||
activitySample.addIntProperty("calories");
|
||||
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addHPlusHealthActivityKindOverlay(Schema schema, Entity user, Entity device) {
|
||||
Entity activityOverlay = addEntity(schema, "HPlusHealthActivityOverlay");
|
||||
|
||||
activityOverlay.addIntProperty(TIMESTAMP_FROM).notNull().primaryKey();
|
||||
activityOverlay.addIntProperty(TIMESTAMP_TO).notNull().primaryKey();
|
||||
activityOverlay.addIntProperty(SAMPLE_RAW_KIND).notNull().primaryKey();
|
||||
Property deviceId = activityOverlay.addLongProperty("deviceId").primaryKey().notNull().getProperty();
|
||||
activityOverlay.addToOne(device, deviceId);
|
||||
|
||||
Property userId = activityOverlay.addLongProperty("userId").notNull().getProperty();
|
||||
activityOverlay.addToOne(user, userId);
|
||||
activityOverlay.addByteArrayProperty("rawHPlusHealthData");
|
||||
return activityOverlay;
|
||||
}
|
||||
|
||||
private static Entity addNo1F1ActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "No1F1ActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
|
||||
activitySample.setSuperclass(superClass);
|
||||
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
|
||||
activitySample.setJavaDoc(
|
||||
"This class represents a sample specific to the device. Values like activity kind or\n" +
|
||||
"intensity, are device specific. Normalized values can be retrieved through the\n" +
|
||||
"corresponding {@link SampleProvider}.");
|
||||
activitySample.addIntProperty("timestamp").notNull().codeBeforeGetterAndSetter(OVERRIDE).primaryKey();
|
||||
Property deviceId = activitySample.addLongProperty("deviceId").primaryKey().notNull().codeBeforeGetterAndSetter(OVERRIDE).getProperty();
|
||||
activitySample.addToOne(device, deviceId);
|
||||
Property userId = activitySample.addLongProperty("userId").notNull().codeBeforeGetterAndSetter(OVERRIDE).getProperty();
|
||||
activitySample.addToOne(user, userId);
|
||||
}
|
||||
|
||||
private static void addCalendarSyncState(Schema schema, Entity device) {
|
||||
Entity calendarSyncState = addEntity(schema, "CalendarSyncState");
|
||||
calendarSyncState.addIdProperty();
|
||||
Property deviceId = calendarSyncState.addLongProperty("deviceId").notNull().getProperty();
|
||||
Property calendarEntryId = calendarSyncState.addLongProperty("calendarEntryId").notNull().getProperty();
|
||||
Index indexUnique = new Index();
|
||||
indexUnique.addProperty(deviceId);
|
||||
indexUnique.addProperty(calendarEntryId);
|
||||
indexUnique.makeUnique();
|
||||
calendarSyncState.addIndex(indexUnique);
|
||||
calendarSyncState.addToOne(device, deviceId);
|
||||
calendarSyncState.addIntProperty("hash").notNull();
|
||||
}
|
||||
|
||||
private static Property findProperty(Entity entity, String propertyName) {
|
||||
for (Property prop : entity.getProperties()) {
|
||||
if (propertyName.equals(prop.getPropertyName())) {
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Property " + propertyName + " not found in Entity " + entity.getClassName());
|
||||
}
|
||||
|
||||
private static Entity addEntity(Schema schema, String className) {
|
||||
Entity entity = schema.addEntity(className);
|
||||
entity.addImport("de.greenrobot.dao.AbstractDao");
|
||||
return entity;
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
@ -1,10 +1,27 @@
|
|||
The following artwork is licensed under the following licenses
|
||||
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0):
|
||||
ic_device_pebble.png (pebble.svg from https://gitlab.com/xphnx/twelf_cm12_theme/issues/13)
|
||||
ic_device_miband.png (miband.svg from https://gitlab.com/xphnx/twelf_cm12_theme/issues/13)
|
||||
ic_activitytracker.png (androidrun.png from https://gitlab.com/xphnx/twelf_cm12_theme/)
|
||||
ic_watchface.png (clock.png from https://gitlab.com/xphnx/twelf_cm12_theme/)
|
||||
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
|
||||
ic_launcher.png
|
||||
ic_device_pebble.png
|
||||
ic_device_miband.png
|
||||
ic_device_lovetoy.png
|
||||
ic_device_hplus.png
|
||||
ic_device_default.png
|
||||
ic_activitytracker.png
|
||||
ic_watchface.png
|
||||
ic_languagepack.png
|
||||
ic_firmware.png
|
||||
ic_watchapp.png
|
||||
ic_systemapp.png
|
||||
icon.png (fastlane metadata directories)
|
||||
featureGraphic.png (fastlane metadata directories)
|
||||
(All of the above by xphnx)
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
|
||||
"GET IT ON F-Droid" button by Laura Kalbag. Source: https://ind.ie/about/blog/f-droid-button/
|
||||
|
||||
Creative Commons Attribution 3.0 Unported license (CC BY-3.0):
|
||||
ic_notification_battery_low.png by Picol.org. Source: https://commons.wikimedia.org/wiki/File:Battery_1_Picol_icon.svg
|
||||
|
||||
Creative Commons Attribution 3.0 United States (CC BY-3.0 US):
|
||||
ic_donate by Peter van Driel https://thenounproject.com/term/donate/239009/
|
||||
|
|
140
README.md
140
README.md
|
@ -1,45 +1,117 @@
|
|||
Gadgetbridge
|
||||
============
|
||||
|
||||
Gadgetbridge is a Android (4.4+) Application which will allow you to use your
|
||||
Pebble or Miband without the vendors closed source application and without the
|
||||
need to create an account and transmit any of your data to the vendors servers.
|
||||
Gadgetbridge is an Android (4.4+) application which will allow you to use your
|
||||
Pebble, Mi Band, Amazfit Bit and HPlus device (and more) without the vendor's closed source application
|
||||
and without the need to create an account and transmit any of your data to the
|
||||
vendor's servers.
|
||||
|
||||
Features (Pebble):
|
||||
|
||||
* Incoming calls notification and display (caller, phone number)
|
||||
* Outgoing call display
|
||||
* Reject/hangup calls
|
||||
* SMS notification (sender, body)
|
||||
* K-9 Mail notification support (sender, subject, preview)
|
||||
* Support for generic notificaions (above filtered out)
|
||||
* Apollo playback info (artist, album, track)
|
||||
* Music control: play/pause, next track, previous track, volume up, volume down
|
||||
* List and remove installed apps/watchfaces
|
||||
* Install .pbw files
|
||||
* Install firmware from .pbz files (EXPERIMENTAL)
|
||||
[Homepage](https://gadgetbridge.org)
|
||||
|
||||
How to use (Pebble):
|
||||
[Blog](https://blog.gadgetbridge.org)
|
||||
|
||||
1. Pair your Pebble though the Android Bluetooth Settings
|
||||
2. Start Gadgetbridge, tap on the device you want to connect to
|
||||
3. To test, chose "Debug" from the menu and play around
|
||||
|
||||
How to use (Miband):
|
||||
|
||||
1. Add your Mibands MAC address manually for now (Settings -> Debug)
|
||||
2. Configure other notifications as desired
|
||||
3. Go back to the "Gadgetbridge" Activity
|
||||
4. Tap the "MI" device to connect
|
||||
5. To test, chose "Debug" from the menu and play around
|
||||
|
||||
Known Issues:
|
||||
|
||||
* Android 4.4+ only, we can only change this by not handling generic
|
||||
notifications or by using AccessibiltyService. Don't know if it is worth the
|
||||
hassle.
|
||||
[![Donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Gadgetbridge/donate)
|
||||
|
||||
[![Build](https://travis-ci.org/Freeyourgadget/Gadgetbridge.svg?branch=master)](https://travis-ci.org/Freeyourgadget/Gadgetbridge)
|
||||
|
||||
## Download
|
||||
|
||||
[![Gadgetbridge on F-Droid](/Get_it_on_F-Droid.svg.png?raw=true "Download from F-Droid")](https://f-droid.org/repository/browse/?fdid=nodomain.freeyourgadget.gadgetbridge)
|
||||
[![Gadgetbridge on F-Droid](/Get_it_on_F-Droid.svg.png?raw=true "Download from F-Droid")](https://f-droid.org/repository/browse/?fdid=nodomain.freeyourgadget.gadgetbridge)
|
||||
|
||||
[List of changes](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/CHANGELOG.md)
|
||||
|
||||
## Supported Devices
|
||||
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble)
|
||||
* Pebble 2 (add the device from within Gadgetbridge!) [Wiki section about pebble](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble), most parts apply to Pebble 2 as well
|
||||
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
|
||||
* Mi Band 2 [Wiki section about mi band](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), some parts apply to mi band 2 as well
|
||||
* Amazfit Bip (WIP) [Wiki section about the Amazfit Bip](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
|
||||
* HPlus Devices (e.g. ZeBand) [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
|
||||
* Teclast H30 (WIP)
|
||||
* NO.1 F1 (WIP)
|
||||
* Liveview
|
||||
* Vibratissimo (experimental)
|
||||
|
||||
## Features
|
||||
|
||||
Please see [FEATURES.md](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/FEATURES.md)
|
||||
|
||||
## Getting Started (Pebble)
|
||||
|
||||
1. Pair your Pebble through the Android's Bluetooth Settings or Gadgetbridge. Pebble 2 MUST be paired though Gadgetbridge (tap on the + in Control Center)
|
||||
2. Start Gadgetbridge, tap on the device you want to connect to
|
||||
3. To test, choose "Debug" from the menu and play around
|
||||
|
||||
For more information read [this wiki article](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
|
||||
|
||||
## How to use (Mi Band 1+2)
|
||||
|
||||
* When starting Gadgetbridge the first time, it will automatically
|
||||
attempt to discover and pair your Mi Band. Alternatively you can invoke discovery
|
||||
manually via the "+" button. It will ask you for some personal info that appears
|
||||
to be needed for proper steps calculation on the band. If you do not provide these,
|
||||
some hardcoded default "dummy" values will be used instead.
|
||||
|
||||
When your Mi Band starts to vibrate and blink during the pairing process,
|
||||
tap it quickly a few times in a row to confirm the pairing with the band.
|
||||
|
||||
1. Configure other notifications as desired
|
||||
2. Go back to the "Gadgetbridge" activity
|
||||
3. Tap the Mi Band item to connect if you're not connected yet
|
||||
4. To test, chose "Debug" from the menu and play around
|
||||
|
||||
**Known Issues:**
|
||||
|
||||
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
|
||||
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
|
||||
knows your MAC address. This behavior may also only occur with older firmware versions.
|
||||
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
|
||||
try to unpair the band in the other app and try again with Gadgetbridge.
|
||||
* While all Mi Band devices are supported, some firmware versions might work better than others.
|
||||
You can consult the [projects wiki pages](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
|
||||
to check if your firmware version is fully supported or if an upgrade/downgrade might be beneficial.
|
||||
|
||||
## Features (Liveview)
|
||||
|
||||
* set time (automatically upon connection)
|
||||
* display notifications and vibrate
|
||||
|
||||
## Authors
|
||||
### Core Team (in order of first code contribution)
|
||||
|
||||
* Andreas Shimokawa
|
||||
* Carsten Pfeiffer
|
||||
* Daniele Gobbetti
|
||||
|
||||
### Additional device support
|
||||
|
||||
* João Paulo Barraca (HPlus)
|
||||
* Vitaly Svyastyn (NO.1 F1)
|
||||
* Sami Alaoui (Teclast H30)
|
||||
|
||||
## Contribute
|
||||
|
||||
Contributions are welcome, be it feedback, bugreports, documentation, translation, research or code. Feel free to work
|
||||
on any of the open [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues?q=is%3Aopen+is%3Aissue);
|
||||
just leave a comment that you're working on one to avoid duplicated work.
|
||||
|
||||
Translations can be contributed via https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/
|
||||
|
||||
## Do you have further questions or feedback?
|
||||
|
||||
Feel free to open an issue on our issue tracker, but please:
|
||||
- do not use the issue tracker as a forum, do not ask for ETAs and read the issue conversation before posting
|
||||
- use the search functionality to ensure that your question wasn't already answered. Don't forget to check the **closed** issues as well!
|
||||
- remember that this is a community project, people are contributing in their free time because they like doing so: don't take the fun away! Be kind and constructive.
|
||||
|
||||
## Having problems?
|
||||
|
||||
0. Phone crashing during device discovery? Disable Privacy Guard (or similarly named functionality) during discovery.
|
||||
1. Open Gadgetbridge's settings and check the option to write log files
|
||||
2. Reproduce the problem you encountered
|
||||
3. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
|
||||
4. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
|
||||
|
||||
Alternatively you may use the standard logcat functionality to access the log.
|
||||
|
||||
|
|
142
app/build.gradle
142
app/build.gradle
|
@ -1,15 +1,34 @@
|
|||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'findbugs'
|
||||
apply plugin: 'pmd'
|
||||
|
||||
def ABORT_ON_CHECK_FAILURE=false
|
||||
|
||||
tasks.withType(Test) {
|
||||
systemProperty 'MiFirmwareDir', System.getProperty('MiFirmwareDir', null)
|
||||
systemProperty 'logback.configurationFile', System.getProperty('user.dir', null) + '/app/src/main/assets/logback.xml'
|
||||
systemProperty 'GB_LOGFILES_DIR', java.nio.file.Files.createTempDirectory('gblog').toString();
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 21
|
||||
buildToolsVersion "21.1.2"
|
||||
compileOptions {
|
||||
// for KitKat
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion '25.0.2'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "nodomain.freeyourgadget.gadgetbridge"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 21
|
||||
versionCode 10
|
||||
versionName "0.3.2"
|
||||
targetSdkVersion 25
|
||||
|
||||
// note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.21.5"
|
||||
versionCode 106
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -17,10 +36,119 @@ android {
|
|||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError ABORT_ON_CHECK_FAILURE
|
||||
lintConfig file("${project.rootDir}/config/lint/lint.xml")
|
||||
|
||||
// if true, generate an HTML report (with issue explanations, sourcecode, etc)
|
||||
htmlReport true
|
||||
// optional path to report (default will be lint-results.html in the builddir)
|
||||
htmlOutput file("$project.buildDir/reports/lint/lint.html")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
}
|
||||
|
||||
pmd {
|
||||
toolVersion = '5.5.5'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// testCompile 'ch.qos.logback:logback-classic:1.1.3'
|
||||
// testCompile 'ch.qos.logback:logback-core:1.1.3'
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "org.mockito:mockito-core:1.9.5"
|
||||
testCompile "org.robolectric:robolectric:3.3.2"
|
||||
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
compile 'com.android.support:appcompat-v7:21.0.3'
|
||||
compile 'com.android.support:support-v4:21.0.3'
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
compile 'com.android.support:cardview-v7:25.3.1'
|
||||
compile 'com.android.support:recyclerview-v7:25.3.1'
|
||||
compile 'com.android.support:support-v4:25.3.1'
|
||||
compile 'com.android.support:gridlayout-v7:25.3.1'
|
||||
compile 'com.android.support:design:25.3.1'
|
||||
compile 'com.android.support:palette-v7:25.3.1'
|
||||
compile 'com.github.tony19:logback-android-classic:1.1.1-6'
|
||||
compile 'org.slf4j:slf4j-api:1.7.7'
|
||||
compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'
|
||||
compile 'com.github.pfichtner:durationformatter:0.1.1'
|
||||
compile 'de.cketti.library.changelog:ckchangelog:1.2.2'
|
||||
compile 'net.e175.klaus:solarpositioning:0.0.9'
|
||||
// use pristine greendao instead of our custom version, since our custom jitpack-packaged
|
||||
// version contains way too much and our custom patches are in the generator only.
|
||||
compile 'org.greenrobot:greendao:2.2.1'
|
||||
compile 'org.apache.commons:commons-lang3:3.5'
|
||||
|
||||
// compile project(":DaoCore")
|
||||
}
|
||||
|
||||
preBuild.dependsOn(":GBDaoGenerator:genSources")
|
||||
gradle.beforeProject {
|
||||
preBuild.dependsOn(":GBDaoGenerator:genSources")
|
||||
}
|
||||
|
||||
check.dependsOn 'findbugs', 'pmd', 'lint'
|
||||
|
||||
task pmd(type: Pmd) {
|
||||
ruleSetFiles = files("${project.rootDir}/config/pmd/pmd-ruleset.xml")
|
||||
ignoreFailures = !ABORT_ON_CHECK_FAILURE
|
||||
ruleSets = [
|
||||
'java-android',
|
||||
'java-basic',
|
||||
'java-braces',
|
||||
'java-clone',
|
||||
'java-codesize',
|
||||
'java-controversial',
|
||||
'java-coupling',
|
||||
'java-design',
|
||||
'java-empty',
|
||||
'java-finalizers',
|
||||
'java-imports',
|
||||
'java-junit',
|
||||
'java-optimizations',
|
||||
'java-strictexception',
|
||||
'java-strings',
|
||||
'java-sunsecure',
|
||||
'java-typeresolution',
|
||||
'java-unnecessary',
|
||||
'java-unusedcode'
|
||||
]
|
||||
|
||||
source 'src'
|
||||
include '**/*.java'
|
||||
exclude '**/gen/**'
|
||||
|
||||
reports {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
xml {
|
||||
destination "$project.buildDir/reports/pmd/pmd.xml"
|
||||
}
|
||||
html {
|
||||
destination "$project.buildDir/reports/pmd/pmd.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task findbugs(type: FindBugs) {
|
||||
ignoreFailures = !ABORT_ON_CHECK_FAILURE
|
||||
effort = "default"
|
||||
reportLevel = "medium"
|
||||
excludeFilter = new File("${project.rootDir}/config/findbugs/findbugs-filter.xml")
|
||||
classes = files("${project.rootDir}/app/build/intermediates/classes")
|
||||
source = fileTree('src/main/java/')
|
||||
classpath = files()
|
||||
reports {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
xml {
|
||||
destination "$project.buildDir/reports/findbugs/findbugs-output.xml"
|
||||
}
|
||||
html {
|
||||
destination "$project.buildDir/reports/findbugs/findbugs-output.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -2,58 +2,288 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="nodomain.freeyourgadget.gadgetbridge">
|
||||
|
||||
<!--
|
||||
Comment in for testing Pebble Emulator
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
-->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="com.fsck.k9.permission.READ_MESSAGES" />
|
||||
<uses-sdk android:targetSdkVersion="21" android:minSdkVersion="19"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name=".GBApplication"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/GadgetbridgeTheme">
|
||||
<activity
|
||||
android:name=".ControlCenter"
|
||||
android:label="@string/title_activity_controlcenter">
|
||||
android:name=".activities.ControlCenterv2"
|
||||
android:label="@string/title_activity_controlcenter"
|
||||
android:theme="@style/GadgetbridgeTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/title_activity_settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ControlCenter" />
|
||||
</activity>
|
||||
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".AppManagerActivity"
|
||||
android:label="@string/title_activity_appmanager">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ControlCenter" />
|
||||
</activity>
|
||||
android:name=".devices.miband.MiBandPreferencesActivity"
|
||||
android:label="@string/preferences_miband_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".pebble.PebbleAppInstallerActivity"
|
||||
android:label="@string/title_activity_appinstaller">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".AppManagerActivity" />
|
||||
android:launchMode="singleTop"
|
||||
android:name=".activities.appmanager.AppManagerActivity"
|
||||
android:label="@string/title_activity_appmanager"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.AppBlacklistActivity"
|
||||
android:label="@string/title_activity_appblacklist"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.CalBlacklistActivity"
|
||||
android:label="@string/title_activity_calblacklist"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.FwAppInstallerActivity"
|
||||
android:label="@string/title_activity_fw_app_insaller"
|
||||
android:parentActivityName=".activities.ControlCenterv2">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*" />
|
||||
<!-- needed for aosp-derived ROMs? -->
|
||||
|
||||
<data android:host="*" />
|
||||
<data android:scheme="file" />
|
||||
<data android:pathPattern=".*\\.pbw" />
|
||||
<data android:pathPattern=".*\\.pbz" />
|
||||
|
||||
<!-- as seen on openkeychain repo: https://github.com/open-keychain/open-keychain/blob/master/OpenKeychain/src/main/AndroidManifest.xml -->
|
||||
|
||||
<data android:pathPattern="/.*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<!-- no mimeType filter, needed for CM-derived ROMs? -->
|
||||
|
||||
<data android:host="*" />
|
||||
<data android:scheme="file" />
|
||||
|
||||
<!-- as seen on openkeychain repo: https://github.com/open-keychain/open-keychain/blob/master/OpenKeychain/src/main/AndroidManifest.xml -->
|
||||
|
||||
<data android:pathPattern="/.*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.fw" />
|
||||
<data android:pathPattern="/.*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft.en" />
|
||||
<data android:pathPattern="/.*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
|
||||
<data android:pathPattern="/.*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
|
||||
<data android:pathPattern="/.*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
|
||||
<data android:pathPattern="/.*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbl" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- to receive the firmwares from the download content provider -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
</intent-filter>
|
||||
<!-- to receive firmwares from the download content provider if recognized as zip-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/x-zip-compressed" />
|
||||
</intent-filter>
|
||||
<!-- to receive files from the "share" intent -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*" />
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
@ -65,69 +295,147 @@
|
|||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name=".BluetoothCommunicationService"></service>
|
||||
<service android:name=".service.NotificationCollectorMonitorService" />
|
||||
<service android:name=".service.DeviceCommunicationService" />
|
||||
|
||||
<receiver
|
||||
android:name=".externalevents.PhoneCallReceiver"
|
||||
android:enabled="false">
|
||||
android:name=".externalevents.WeatherNotificationReceiver"
|
||||
android:enabled="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PHONE_STATE" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
|
||||
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_UPDATE_2" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".externalevents.SMSReceiver"
|
||||
android:enabled="false">
|
||||
|
||||
<activity android:name=".externalevents.WeatherNotificationConfig">
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_SKIN_PREFERENCES"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".externalevents.AutoStartReceiver"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".externalevents.K9Receiver"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<data android:scheme="email" />
|
||||
<action android:name="com.fsck.k9.intent.action.EMAIL_RECEIVED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".externalevents.MusicPlaybackReceiver"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.andrew.apollo.metachanged" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".BluetoothStateChangeReceiver"
|
||||
android:name=".externalevents.BluetoothStateChangeReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".GBMusicControlReceiver"
|
||||
android:name=".service.receivers.GBMusicControlReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="nodomain.freeyourgadget.gadgetbridge.musiccontrol" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".GBCallControlReceiver"
|
||||
android:name=".service.receivers.GBCallControlReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="nodomain.freeyourgadget.gadgetbridge.callcontrol" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!--
|
||||
forcing the DebugActivity to portrait mode avoids crashes with the progress
|
||||
dialog when changing orientation
|
||||
-->
|
||||
<activity
|
||||
android:name=".DebugActivity"
|
||||
android:label="@string/title_activity_debug">
|
||||
android:name=".activities.DebugActivity"
|
||||
android:label="@string/title_activity_debug"
|
||||
android:parentActivityName=".activities.ControlCenterv2"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".activities.DbManagementActivity"
|
||||
android:label="@string/title_activity_db_management"
|
||||
android:parentActivityName=".activities.ControlCenterv2"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".activities.DiscoveryActivity"
|
||||
android:label="@string/title_activity_discovery"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.AndroidPairingActivity"
|
||||
android:label="@string/title_activity_android_pairing" />
|
||||
<activity
|
||||
android:name=".devices.miband.MiBandPairingActivity"
|
||||
android:label="@string/title_activity_mi_band_pairing" />
|
||||
<activity
|
||||
android:name=".devices.pebble.PebblePairingActivity"
|
||||
android:label="@string/title_activity_pebble_pairing" />
|
||||
<activity
|
||||
android:name=".activities.charts.ChartsActivity"
|
||||
android:label="@string/title_activity_charts"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.ConfigureAlarms"
|
||||
android:label="@string/title_activity_set_alarm"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AlarmDetails"
|
||||
android:label="@string/title_activity_alarm_details"
|
||||
android:screenOrientation="portrait"
|
||||
android:parentActivityName=".activities.ConfigureAlarms" />
|
||||
<activity
|
||||
android:name=".activities.VibrationActivity"
|
||||
android:label="@string/title_activity_vibration"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.AudioSettingsActivity"
|
||||
android:label="@string/title_audio_activity"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<provider
|
||||
android:name=".contentprovider.PebbleContentProvider"
|
||||
android:authorities="com.getpebble.android.provider"
|
||||
android:exported="true" />
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.screenshot_provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/screenshot_provider_paths"/>
|
||||
</provider>
|
||||
|
||||
<receiver android:name=".SleepAlarmWidget">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
<action android:name="nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/sleep_alarm_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:launchMode="singleTask"
|
||||
android:allowTaskReparenting="true"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:name=".activities.ExternalPebbleJSActivity"
|
||||
android:label="@string/app_configure"
|
||||
android:parentActivityName=".activities.appmanager.AppManagerActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ControlCenter" />
|
||||
android:value="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="gadgetbridge" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name='viewport' content='initial-scale=1.0, maximum-scale=1.0'>
|
||||
<script type="text/javascript" src="js/Uri.js">
|
||||
</script>
|
||||
<script type="text/javascript" src="js/gadgetbridge_boilerplate.js">
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
body {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
#config_url,#jsondata {
|
||||
word-wrap: break-word;
|
||||
margin: 20px 0;
|
||||
width: 90%;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 2px;
|
||||
font-size: 0.9em;
|
||||
background-color: #eee;
|
||||
color: #646464;
|
||||
text-align: center;
|
||||
border-style: none;
|
||||
}
|
||||
.btn:active {
|
||||
border-style: none;
|
||||
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2)inset;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
p {
|
||||
width: 90%;
|
||||
}
|
||||
#pastereturn {
|
||||
width: 90%;
|
||||
min-height: 3em;
|
||||
}
|
||||
#step1compat, #step2 {
|
||||
display: none;
|
||||
}
|
||||
<!-- TODO -->
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="step1" class="step">
|
||||
<h2>URL of the configuration:</h2>
|
||||
<div id="config_url"></div>
|
||||
<!--<button class="btn" name="show config" value="show config" onclick="Pebble.showConfiguration()" >Show config / URL</button>-->
|
||||
<button class="btn" name="open config" value="open config" onclick="Pebble.actuallyOpenURL()">
|
||||
Open configuration website
|
||||
</button>
|
||||
<h2 class="load_presets">App presets:</h2>
|
||||
<button class="btn load_presets" name="read config" value="read config"
|
||||
onclick="Pebble.loadPreset()">
|
||||
Load saved configuration
|
||||
</button>
|
||||
</div>
|
||||
<div id="step1compat" class="step">
|
||||
<p>In case of "network error" after saving settings in the watchapp, copy the "network error"
|
||||
URL and paste it here:</p>
|
||||
<textarea id="pastereturn"></textarea><br/>
|
||||
<button class="btn" name="parse" onclick="Pebble.parseReturnedPebbleJS()">Parse legacy app
|
||||
configuration
|
||||
</button>
|
||||
</div>
|
||||
<div id="step2" class="step">
|
||||
<h2>Incoming configuration data:</h2>
|
||||
<div id="jsondata"></div>
|
||||
<button class="btn" name="send config" value="send config" onclick="Pebble.actuallySendData()">
|
||||
Send data to pebble
|
||||
</button>
|
||||
<h2 class="store_presets">App Presets:</h2>
|
||||
<button class="btn store_presets" name="store config" value="store config"
|
||||
onclick="Pebble.savePreset()">
|
||||
Store incoming configuration
|
||||
</button>
|
||||
<p class="store_presets">Existing presets will be deleted.</p>
|
||||
</div>
|
||||
</body>
|
|
@ -0,0 +1,458 @@
|
|||
/*!
|
||||
* jsUri
|
||||
* https://github.com/derek-watson/jsUri
|
||||
*
|
||||
* Copyright 2013, Derek Watson
|
||||
* Released under the MIT license.
|
||||
*
|
||||
* Includes parseUri regular expressions
|
||||
* http://blog.stevenlevithan.com/archives/parseuri
|
||||
* Copyright 2007, Steven Levithan
|
||||
* Released under the MIT license.
|
||||
*/
|
||||
|
||||
/*globals define, module */
|
||||
|
||||
(function(global) {
|
||||
|
||||
var re = {
|
||||
starts_with_slashes: /^\/+/,
|
||||
ends_with_slashes: /\/+$/,
|
||||
pluses: /\+/g,
|
||||
query_separator: /[&;]/,
|
||||
uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@\/]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
|
||||
};
|
||||
|
||||
/**
|
||||
* Define forEach for older js environments
|
||||
* @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach#Compatibility
|
||||
*/
|
||||
if (!Array.prototype.forEach) {
|
||||
Array.prototype.forEach = function(callback, thisArg) {
|
||||
var T, k;
|
||||
|
||||
if (this == null) {
|
||||
throw new TypeError(' this is null or not defined');
|
||||
}
|
||||
|
||||
var O = Object(this);
|
||||
var len = O.length >>> 0;
|
||||
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError(callback + ' is not a function');
|
||||
}
|
||||
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
k = 0;
|
||||
|
||||
while (k < len) {
|
||||
var kValue;
|
||||
if (k in O) {
|
||||
kValue = O[k];
|
||||
callback.call(T, kValue, k, O);
|
||||
}
|
||||
k++;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* unescape a query param value
|
||||
* @param {string} s encoded value
|
||||
* @return {string} decoded value
|
||||
*/
|
||||
function decode(s) {
|
||||
if (s) {
|
||||
s = s.toString().replace(re.pluses, '%20');
|
||||
s = decodeURIComponent(s);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a uri string down into its individual parts
|
||||
* @param {string} str uri
|
||||
* @return {object} parts
|
||||
*/
|
||||
function parseUri(str) {
|
||||
var parser = re.uri_parser;
|
||||
var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"];
|
||||
var m = parser.exec(str || '');
|
||||
var parts = {};
|
||||
|
||||
parserKeys.forEach(function(key, i) {
|
||||
parts[key] = m[i] || '';
|
||||
});
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a query string down into an array of key/value pairs
|
||||
* @param {string} str query
|
||||
* @return {array} array of arrays (key/value pairs)
|
||||
*/
|
||||
function parseQuery(str) {
|
||||
var i, ps, p, n, k, v, l;
|
||||
var pairs = [];
|
||||
|
||||
if (typeof(str) === 'undefined' || str === null || str === '') {
|
||||
return pairs;
|
||||
}
|
||||
|
||||
if (str.indexOf('?') === 0) {
|
||||
str = str.substring(1);
|
||||
}
|
||||
|
||||
ps = str.toString().split(re.query_separator);
|
||||
|
||||
for (i = 0, l = ps.length; i < l; i++) {
|
||||
p = ps[i];
|
||||
n = p.indexOf('=');
|
||||
|
||||
if (n !== 0) {
|
||||
k = decode(p.substring(0, n));
|
||||
v = decode(p.substring(n + 1));
|
||||
pairs.push(n === -1 ? [p, null] : [k, v]);
|
||||
}
|
||||
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Uri object
|
||||
* @constructor
|
||||
* @param {string} str
|
||||
*/
|
||||
function Uri(str) {
|
||||
this.uriParts = parseUri(str);
|
||||
this.queryPairs = parseQuery(this.uriParts.query);
|
||||
this.hasAuthorityPrefixUserPref = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define getter/setter methods
|
||||
*/
|
||||
['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function(key) {
|
||||
Uri.prototype[key] = function(val) {
|
||||
if (typeof val !== 'undefined') {
|
||||
this.uriParts[key] = val;
|
||||
}
|
||||
return this.uriParts[key];
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* if there is no protocol, the leading // can be enabled or disabled
|
||||
* @param {Boolean} val
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Uri.prototype.hasAuthorityPrefix = function(val) {
|
||||
if (typeof val !== 'undefined') {
|
||||
this.hasAuthorityPrefixUserPref = val;
|
||||
}
|
||||
|
||||
if (this.hasAuthorityPrefixUserPref === null) {
|
||||
return (this.uriParts.source.indexOf('//') !== -1);
|
||||
} else {
|
||||
return this.hasAuthorityPrefixUserPref;
|
||||
}
|
||||
};
|
||||
|
||||
Uri.prototype.isColonUri = function (val) {
|
||||
if (typeof val !== 'undefined') {
|
||||
this.uriParts.isColonUri = !!val;
|
||||
} else {
|
||||
return !!this.uriParts.isColonUri;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes the internal state of the query pairs
|
||||
* @param {string} [val] set a new query string
|
||||
* @return {string} query string
|
||||
*/
|
||||
Uri.prototype.query = function(val) {
|
||||
var s = '', i, param, l;
|
||||
|
||||
if (typeof val !== 'undefined') {
|
||||
this.queryPairs = parseQuery(val);
|
||||
}
|
||||
|
||||
for (i = 0, l = this.queryPairs.length; i < l; i++) {
|
||||
param = this.queryPairs[i];
|
||||
if (s.length > 0) {
|
||||
s += '&';
|
||||
}
|
||||
if (param[1] === null) {
|
||||
s += param[0];
|
||||
} else {
|
||||
s += param[0];
|
||||
s += '=';
|
||||
if (typeof param[1] !== 'undefined') {
|
||||
s += encodeURIComponent(param[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.length > 0 ? '?' + s : s;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the first query param value found for the key
|
||||
* @param {string} key query key
|
||||
* @return {string} first value found for key
|
||||
*/
|
||||
Uri.prototype.getQueryParamValue = function (key) {
|
||||
var param, i, l;
|
||||
for (i = 0, l = this.queryPairs.length; i < l; i++) {
|
||||
param = this.queryPairs[i];
|
||||
if (key === param[0]) {
|
||||
return param[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns an array of query param values for the key
|
||||
* @param {string} key query key
|
||||
* @return {array} array of values
|
||||
*/
|
||||
Uri.prototype.getQueryParamValues = function (key) {
|
||||
var arr = [], i, param, l;
|
||||
for (i = 0, l = this.queryPairs.length; i < l; i++) {
|
||||
param = this.queryPairs[i];
|
||||
if (key === param[0]) {
|
||||
arr.push(param[1]);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
/**
|
||||
* removes query parameters
|
||||
* @param {string} key remove values for key
|
||||
* @param {val} [val] remove a specific value, otherwise removes all
|
||||
* @return {Uri} returns self for fluent chaining
|
||||
*/
|
||||
Uri.prototype.deleteQueryParam = function (key, val) {
|
||||
var arr = [], i, param, keyMatchesFilter, valMatchesFilter, l;
|
||||
|
||||
for (i = 0, l = this.queryPairs.length; i < l; i++) {
|
||||
|
||||
param = this.queryPairs[i];
|
||||
keyMatchesFilter = decode(param[0]) === decode(key);
|
||||
valMatchesFilter = param[1] === val;
|
||||
|
||||
if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
|
||||
arr.push(param);
|
||||
}
|
||||
}
|
||||
|
||||
this.queryPairs = arr;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* adds a query parameter
|
||||
* @param {string} key add values for key
|
||||
* @param {string} val value to add
|
||||
* @param {integer} [index] specific index to add the value at
|
||||
* @return {Uri} returns self for fluent chaining
|
||||
*/
|
||||
Uri.prototype.addQueryParam = function (key, val, index) {
|
||||
if (arguments.length === 3 && index !== -1) {
|
||||
index = Math.min(index, this.queryPairs.length);
|
||||
this.queryPairs.splice(index, 0, [key, val]);
|
||||
} else if (arguments.length > 0) {
|
||||
this.queryPairs.push([key, val]);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* test for the existence of a query parameter
|
||||
* @param {string} key check values for key
|
||||
* @return {Boolean} true if key exists, otherwise false
|
||||
*/
|
||||
Uri.prototype.hasQueryParam = function (key) {
|
||||
var i, len = this.queryPairs.length;
|
||||
for (i = 0; i < len; i++) {
|
||||
if (this.queryPairs[i][0] == key)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* replaces query param values
|
||||
* @param {string} key key to replace value for
|
||||
* @param {string} newVal new value
|
||||
* @param {string} [oldVal] replace only one specific value (otherwise replaces all)
|
||||
* @return {Uri} returns self for fluent chaining
|
||||
*/
|
||||
Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) {
|
||||
var index = -1, len = this.queryPairs.length, i, param;
|
||||
|
||||
if (arguments.length === 3) {
|
||||
for (i = 0; i < len; i++) {
|
||||
param = this.queryPairs[i];
|
||||
if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= 0) {
|
||||
this.deleteQueryParam(key, decode(oldVal)).addQueryParam(key, newVal, index);
|
||||
}
|
||||
} else {
|
||||
for (i = 0; i < len; i++) {
|
||||
param = this.queryPairs[i];
|
||||
if (decode(param[0]) === decode(key)) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.deleteQueryParam(key);
|
||||
this.addQueryParam(key, newVal, index);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Define fluent setter methods (setProtocol, setHasAuthorityPrefix, etc)
|
||||
*/
|
||||
['protocol', 'hasAuthorityPrefix', 'isColonUri', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function(key) {
|
||||
var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
|
||||
Uri.prototype[method] = function(val) {
|
||||
this[key](val);
|
||||
return this;
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Scheme name, colon and doubleslash, as required
|
||||
* @return {string} http:// or possibly just //
|
||||
*/
|
||||
Uri.prototype.scheme = function() {
|
||||
var s = '';
|
||||
|
||||
if (this.protocol()) {
|
||||
s += this.protocol();
|
||||
if (this.protocol().indexOf(':') !== this.protocol().length - 1) {
|
||||
s += ':';
|
||||
}
|
||||
s += '//';
|
||||
} else {
|
||||
if (this.hasAuthorityPrefix() && this.host()) {
|
||||
s += '//';
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as Mozilla nsIURI.prePath
|
||||
* @return {string} scheme://user:password@host:port
|
||||
* @see https://developer.mozilla.org/en/nsIURI
|
||||
*/
|
||||
Uri.prototype.origin = function() {
|
||||
var s = this.scheme();
|
||||
|
||||
if (this.userInfo() && this.host()) {
|
||||
s += this.userInfo();
|
||||
if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) {
|
||||
s += '@';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.host()) {
|
||||
s += this.host();
|
||||
if (this.port() || (this.path() && this.path().substr(0, 1).match(/[0-9]/))) {
|
||||
s += ':' + this.port();
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a trailing slash to the path
|
||||
*/
|
||||
Uri.prototype.addTrailingSlash = function() {
|
||||
var path = this.path() || '';
|
||||
|
||||
if (path.substr(-1) !== '/') {
|
||||
this.path(path + '/');
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes the internal state of the Uri object
|
||||
* @return {string}
|
||||
*/
|
||||
Uri.prototype.toString = function() {
|
||||
var path, s = this.origin();
|
||||
|
||||
if (this.isColonUri()) {
|
||||
if (this.path()) {
|
||||
s += ':'+this.path();
|
||||
}
|
||||
} else if (this.path()) {
|
||||
path = this.path();
|
||||
if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
|
||||
s += '/';
|
||||
} else {
|
||||
if (s) {
|
||||
s.replace(re.ends_with_slashes, '/');
|
||||
}
|
||||
path = path.replace(re.starts_with_slashes, '/');
|
||||
}
|
||||
s += path;
|
||||
} else {
|
||||
if (this.host() && (this.query().toString() || this.anchor())) {
|
||||
s += '/';
|
||||
}
|
||||
}
|
||||
if (this.query().toString()) {
|
||||
s += this.query().toString();
|
||||
}
|
||||
|
||||
if (this.anchor()) {
|
||||
if (this.anchor().indexOf('#') !== 0) {
|
||||
s += '#';
|
||||
}
|
||||
s += this.anchor();
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clone a Uri object
|
||||
* @return {Uri} duplicate copy of the Uri
|
||||
*/
|
||||
Uri.prototype.clone = function() {
|
||||
return new Uri(this.toString());
|
||||
};
|
||||
|
||||
/**
|
||||
* export via AMD or CommonJS, otherwise leak a global
|
||||
*/
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define(function() {
|
||||
return Uri;
|
||||
});
|
||||
} else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = Uri;
|
||||
} else {
|
||||
global.Uri = Uri;
|
||||
}
|
||||
}(this));
|
|
@ -0,0 +1,254 @@
|
|||
var reportedPositionFailures = 0;
|
||||
navigator.geolocation.getCurrentPosition = function(success, failure, options) { //override because default implementation requires GPS permission
|
||||
geoposition = JSON.parse(GBjs.getCurrentPosition());
|
||||
|
||||
if(options && options.maximumAge && (geoposition.timestamp < Date.now() - options.maximumAge) && reportedPositionFailures <= 10 ) {
|
||||
reportedPositionFailures++;
|
||||
failure({ code: 2, message: "POSITION_UNAVAILABLE"});
|
||||
} else {
|
||||
reportedPositionFailures = 0;
|
||||
success(geoposition);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.Storage){
|
||||
var prefix = GBjs.getAppLocalstoragePrefix();
|
||||
GBjs.gbLog("redefining local storage with prefix: " + prefix);
|
||||
|
||||
Storage.prototype.setItem = (function(key, value) {
|
||||
this.call(localStorage,prefix + key, value);
|
||||
}).bind(Storage.prototype.setItem);
|
||||
|
||||
Storage.prototype.getItem = (function(key) {
|
||||
// console.log("I am about to return " + prefix + key);
|
||||
var def = null;
|
||||
if(key == 'clay-settings') {
|
||||
def = '{}';
|
||||
}
|
||||
return this.call(localStorage,prefix + key) || def;
|
||||
}).bind(Storage.prototype.getItem);
|
||||
}
|
||||
|
||||
function loadScript(url, callback) {
|
||||
// Adding the script tag to the head as suggested before
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
|
||||
// Then bind the event to the callback function.
|
||||
// There are several events for cross browser compatibility.
|
||||
script.onreadystatechange = callback;
|
||||
script.onload = callback;
|
||||
|
||||
// Fire the loading
|
||||
head.appendChild(script);
|
||||
}
|
||||
|
||||
function getURLVariable(variable, defaultValue) {
|
||||
// Find all URL parameters
|
||||
var query = location.search.substring(1);
|
||||
var vars = query.split('&');
|
||||
for (var i = 0; i < vars.length; i++) {
|
||||
var pair = vars[i].split('=');
|
||||
|
||||
// If the query variable parameter is found, decode it to use and return it for use
|
||||
if (pair[0] === variable) {
|
||||
return decodeURIComponent(pair[1]);
|
||||
}
|
||||
}
|
||||
return defaultValue || false;
|
||||
}
|
||||
|
||||
function showStep(desiredStep) {
|
||||
var steps = document.getElementsByClassName("step");
|
||||
var testStep = null;
|
||||
for (var i = 0; i < steps.length; i ++) {
|
||||
if (steps[i].id == desiredStep)
|
||||
testStep = steps[i].id;
|
||||
}
|
||||
if (testStep !== null) {
|
||||
for (var i = 0; i < steps.length; i ++) {
|
||||
steps[i].style.display = 'none';
|
||||
}
|
||||
document.getElementById(desiredStep).style.display="block";
|
||||
}
|
||||
}
|
||||
|
||||
function hideSteps() {
|
||||
var steps = document.getElementsByClassName("step");
|
||||
for (var i = 0; i < steps.length; i ++) {
|
||||
steps[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function gbPebble() {
|
||||
this.configurationURL = null;
|
||||
this.configurationValues = null;
|
||||
var self = this;
|
||||
self.events = {};
|
||||
//events processing: see http://stackoverflow.com/questions/10978311/implementing-events-in-my-own-object
|
||||
self.addEventListener = function(name, handler) {
|
||||
if (self.events.hasOwnProperty(name))
|
||||
self.events[name].push(handler);
|
||||
else
|
||||
self.events[name] = [handler];
|
||||
}
|
||||
|
||||
self.removeEventListener = function(name, handler) {
|
||||
if (!self.events.hasOwnProperty(name))
|
||||
return;
|
||||
|
||||
var index = self.events[name].indexOf(handler);
|
||||
if (index != -1)
|
||||
self.events[name].splice(index, 1);
|
||||
}
|
||||
|
||||
self.evaluate = function(name, args) {
|
||||
if (!self.events.hasOwnProperty(name))
|
||||
return;
|
||||
|
||||
if (!args || !args.length)
|
||||
args = [];
|
||||
|
||||
var evs = self.events[name], l = evs.length;
|
||||
for (var i = 0; i < l; i++) {
|
||||
evs[i].apply(null, args);
|
||||
}
|
||||
}
|
||||
|
||||
this.actuallyOpenURL = function() {
|
||||
showStep("step1compat");
|
||||
window.open(self.configurationURL.toString(), "config");
|
||||
}
|
||||
|
||||
this.actuallySendData = function() {
|
||||
GBjs.sendAppMessage(self.configurationValues);
|
||||
GBjs.closeActivity();
|
||||
}
|
||||
|
||||
this.savePreset = function() {
|
||||
GBjs.saveAppStoredPreset(self.configurationValues);
|
||||
}
|
||||
|
||||
this.loadPreset = function() {
|
||||
showStep("step2");
|
||||
var presetElements = document.getElementsByClassName("store_presets");
|
||||
for (var i = 0; i < presetElements.length; i ++) {
|
||||
presetElements[i].style.display = 'none';
|
||||
}
|
||||
self.configurationValues = GBjs.getAppStoredPreset();
|
||||
document.getElementById("jsondata").innerHTML=self.configurationValues;
|
||||
}
|
||||
|
||||
//needs to be called like this because of original Pebble function name
|
||||
this.openURL = function(url) {
|
||||
if (url.lastIndexOf("http", 0) === 0) {
|
||||
document.getElementById("config_url").innerHTML=url;
|
||||
var UUID = GBjs.getAppUUID();
|
||||
self.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
|
||||
} else {
|
||||
//TODO: add custom return_to
|
||||
var iframe = document.getElementsByTagName('iframe')[0];
|
||||
var oldbody = document.getElementsByTagName("body")[0];
|
||||
if (iframe === undefined && oldbody !== undefined) {
|
||||
iframe = document.createElement("iframe");
|
||||
oldbody.parentNode.replaceChild(iframe,oldbody);
|
||||
} else {
|
||||
hideSteps();
|
||||
document.documentElement.appendChild(iframe);
|
||||
}
|
||||
|
||||
iframe.src = url;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.getActiveWatchInfo = function() {
|
||||
return JSON.parse(GBjs.getActiveWatchInfo());
|
||||
}
|
||||
|
||||
this.sendAppMessage = function (dict, callbackAck, callbackNack){
|
||||
try {
|
||||
self.configurationValues = JSON.stringify(dict);
|
||||
document.getElementById("jsondata").innerHTML=self.configurationValues;
|
||||
if (callbackAck != undefined) {
|
||||
callbackAck();
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
GBjs.gbLog("sendAppMessage failed");
|
||||
if (callbackNack != undefined) {
|
||||
callbackNack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.getAccountToken = function() {
|
||||
return '';
|
||||
}
|
||||
|
||||
this.getWatchToken = function() {
|
||||
return GBjs.getWatchToken();
|
||||
}
|
||||
|
||||
this.getTimelineToken = function() {
|
||||
return '';
|
||||
}
|
||||
|
||||
this.showSimpleNotificationOnPebble = function(title, body) {
|
||||
GBjs.gbLog("app wanted to show: " + title + " body: "+ body);
|
||||
}
|
||||
|
||||
|
||||
this.showConfiguration = function() {
|
||||
console.error("This watchapp doesn't support configuration");
|
||||
GBjs.closeActivity();
|
||||
}
|
||||
|
||||
this.parseReturnedPebbleJS = function() {
|
||||
var str = document.getElementById('pastereturn').value;
|
||||
var needle = "pebblejs://close#";
|
||||
|
||||
if (str.split(needle)[1] !== undefined) {
|
||||
var t = new Object();
|
||||
t.response = decodeURIComponent(str.split(needle)[1]);
|
||||
self.evaluate('webviewclosed',[t]);
|
||||
showStep("step2");
|
||||
} else {
|
||||
console.error("No valid configuration found in the entered string.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Pebble = new gbPebble();
|
||||
|
||||
var jsConfigFile = GBjs.getAppConfigurationFile();
|
||||
var storedPreset = GBjs.getAppStoredPreset();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
if (jsConfigFile != null) {
|
||||
loadScript(jsConfigFile, function() {
|
||||
Pebble.evaluate('ready', [{'type': "ready"}]); //callback object apparently needed by some watchfaces
|
||||
if (getURLVariable('config') == 'true') {
|
||||
showStep("step2");
|
||||
var json_string = getURLVariable('json');
|
||||
var t = new Object();
|
||||
t.response = json_string;
|
||||
if (json_string != '') {
|
||||
Pebble.evaluate('webviewclosed',[t]);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (storedPreset === undefined) {
|
||||
var presetElements = document.getElementsByClassName("load_presets");
|
||||
for (var i = 0; i < presetElements.length; i ++) {
|
||||
presetElements[i].style.display = 'none';
|
||||
}
|
||||
}
|
||||
Pebble.evaluate('showConfiguration');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, false);
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,175 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XMI timestamp="2016-01-26T23:02:14" verified="false" xmi.version="1.2" xmlns:UML="http://schema.omg.org/spec/UML/1.3">
|
||||
<XMI.header>
|
||||
<XMI.documentation>
|
||||
<XMI.exporter>umbrello uml modeller http://umbrello.kde.org</XMI.exporter>
|
||||
<XMI.exporterVersion>1.6.9</XMI.exporterVersion>
|
||||
<XMI.exporterEncoding>UnicodeUTF8</XMI.exporterEncoding>
|
||||
</XMI.documentation>
|
||||
<XMI.metamodel xmi.name="UML" href="UML.xml" xmi.version="1.3"/>
|
||||
</XMI.header>
|
||||
<XMI.content>
|
||||
<UML:Model isAbstract="false" isRoot="false" isSpecification="false" name="UML Model" isLeaf="false" xmi.id="m1">
|
||||
<UML:Namespace.ownedElement>
|
||||
<UML:Stereotype isRoot="false" isAbstract="false" isSpecification="false" name="folder" isLeaf="false" namespace="m1" visibility="public" xmi.id="folder"/>
|
||||
<UML:Stereotype isRoot="false" isAbstract="false" isSpecification="false" name="datatype" isLeaf="false" namespace="m1" visibility="public" xmi.id="datatype"/>
|
||||
<UML:Stereotype isRoot="false" isAbstract="false" isSpecification="false" name="interface" isLeaf="false" namespace="m1" visibility="public" xmi.id="interface"/>
|
||||
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Logical View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Logical View">
|
||||
<UML:Namespace.ownedElement>
|
||||
<UML:Package isRoot="false" isAbstract="false" isSpecification="false" name="Datatypes" isLeaf="false" stereotype="folder" namespace="Logical View" visibility="public" xmi.id="Datatypes">
|
||||
<UML:Namespace.ownedElement>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="int" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="tM1MfT3dGbDn"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="char" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="MZCr2zI6yZo6"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="bool" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="mtFc0pEuADEp"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="float" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="hIRvrcjDBt2B"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="double" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="tIHrr3vdvBFv"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="short" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="yRn8f2wKINF9"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="long" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="xwL6qLHheXWT"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="unsigned int" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="aS7VdKRbkCiP"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="unsigned short" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="EejU1wcwrx2h"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="unsigned long" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="GJMsqsMRIRuv"/>
|
||||
<UML:DataType isRoot="false" isAbstract="false" isSpecification="false" name="string" isLeaf="false" stereotype="datatype" namespace="Datatypes" visibility="public" xmi.id="jLVTWBskpZFo"/>
|
||||
</UML:Namespace.ownedElement>
|
||||
</UML:Package>
|
||||
<UML:Interface isRoot="false" isAbstract="true" isSpecification="false" name="DeviceService" isLeaf="false" stereotype="interface" namespace="Logical View" visibility="public" xmi.id="VA6qSCiBtNc5"/>
|
||||
<UML:Interface isRoot="false" isAbstract="true" isSpecification="false" name="DeviceSupport" isLeaf="false" stereotype="interface" namespace="Logical View" visibility="public" xmi.id="AKxHpDCgOPhk"/>
|
||||
</UML:Namespace.ownedElement>
|
||||
<XMI.extension xmi.extender="umbrello">
|
||||
<diagrams>
|
||||
<diagram showgrid="0" snapgrid="0" backgroundcolor="#ffffff" zoom="100" type="1" linecolor="#ff0000" usefillcolor="1" griddotcolor="#d3d3d3" showops="1" showscope="1" localid="-1" showpackage="1" showpubliconly="0" canvaswidth="0" isopen="0" showatts="1" xmi.id="31Hk7F5Bq9ac" snapcsgrid="0" snapx="25" name="class diagram" showattsig="1" textcolor="#000000" showstereotype="1" showattribassocs="1" linewidth="0" fillcolor="#ffff00" documentation="" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" canvasheight="0" showopsig="1" snapy="25">
|
||||
<widgets/>
|
||||
<messages/>
|
||||
<associations/>
|
||||
</diagram>
|
||||
</diagrams>
|
||||
</XMI.extension>
|
||||
</UML:Model>
|
||||
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Use Case View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Use Case View">
|
||||
<UML:Namespace.ownedElement/>
|
||||
</UML:Model>
|
||||
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Component View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Component View">
|
||||
<UML:Namespace.ownedElement>
|
||||
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="Activities" executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="CDLTeFMNZrp5"/>
|
||||
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="DeviceCommunicationService" executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="72YBMP5WcQzE">
|
||||
<UML:Namespace.ownedElement>
|
||||
<UML:Port isRoot="false" isAbstract="false" isSpecification="false" name="pin" isLeaf="false" namespace="72YBMP5WcQzE" visibility="public" xmi.id="xhXzgXGeAGP8"/>
|
||||
<UML:Port isRoot="false" isAbstract="false" isSpecification="false" name="pin2" isLeaf="false" namespace="72YBMP5WcQzE" visibility="public" xmi.id="LcpVBJq9yM8d"/>
|
||||
</UML:Namespace.ownedElement>
|
||||
</UML:Component>
|
||||
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="Frontend2" executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="zd0ny2K62qHM"/>
|
||||
<UML:Artifact isRoot="false" isAbstract="false" drawas="0" isSpecification="false" name="Service" isLeaf="false" namespace="Component View" visibility="public" xmi.id="jjfTCGwVsjy4"/>
|
||||
<UML:Abstraction supplier="VA6qSCiBtNc5" isSpecification="false" name="" namespace="Component View" client="72YBMP5WcQzE" visibility="public" xmi.id="3ZwKCCl82Cel"/>
|
||||
<UML:Association isSpecification="false" name="invoke" namespace="Component View" visibility="public" xmi.id="ZmBbwqKcFFuU">
|
||||
<UML:Association.connection>
|
||||
<UML:AssociationEnd isNavigable="false" changeability="changeable" isSpecification="false" type="CDLTeFMNZrp5" name="" aggregation="none" visibility="public" xmi.id="8FlknwQC5gyi"/>
|
||||
<UML:AssociationEnd isNavigable="true" changeability="changeable" isSpecification="false" type="VA6qSCiBtNc5" name="" aggregation="none" visibility="public" xmi.id="0RPtmPzL9Au6"/>
|
||||
</UML:Association.connection>
|
||||
</UML:Association>
|
||||
<UML:Component isRoot="false" isAbstract="false" isSpecification="false" name="Concrete Device Impl." executable="0" isLeaf="false" namespace="Component View" visibility="public" xmi.id="tDF2L96KKnVj"/>
|
||||
<UML:Association isSpecification="false" name="invoke" namespace="Component View" visibility="public" xmi.id="60vlquxGX0xa">
|
||||
<UML:Association.connection>
|
||||
<UML:AssociationEnd isNavigable="false" changeability="changeable" isSpecification="false" type="72YBMP5WcQzE" name="" aggregation="none" visibility="public" xmi.id="EElpRDBsrFht"/>
|
||||
<UML:AssociationEnd isNavigable="true" changeability="changeable" isSpecification="false" type="AKxHpDCgOPhk" name="" aggregation="none" visibility="public" xmi.id="zEQUpqXtpTfd"/>
|
||||
</UML:Association.connection>
|
||||
</UML:Association>
|
||||
<UML:Abstraction supplier="AKxHpDCgOPhk" isSpecification="false" name="" namespace="Component View" client="tDF2L96KKnVj" visibility="public" xmi.id="LcusapQo1BbT"/>
|
||||
</UML:Namespace.ownedElement>
|
||||
<XMI.extension xmi.extender="umbrello">
|
||||
<diagrams>
|
||||
<diagram showgrid="0" snapgrid="0" backgroundcolor="#ffffff" zoom="105" type="7" linecolor="#ff0000" usefillcolor="1" griddotcolor="#d3d3d3" showops="1" showscope="1" localid="-1" showpackage="1" showpubliconly="0" canvaswidth="1931" isopen="1" showatts="1" xmi.id="UpkLkm005Mtw" snapcsgrid="0" snapx="25" name="Gadgetbridge" showattsig="1" textcolor="#000000" showstereotype="1" showattribassocs="1" linewidth="0" fillcolor="#ffff00" documentation="" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" canvasheight="1133" showopsig="1" snapy="25">
|
||||
<widgets>
|
||||
<componentwidget textcolor="#000000" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" height="102" linewidth="0" width="165" showstereotype="1" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="0" x="-1652,09756097561" y="-881,8487804878049" localid="2hnpF0djpL2Z" usesdiagramfillcolor="0" xmi.id="CDLTeFMNZrp5" isinstance="0"/>
|
||||
<componentwidget textcolor="#000000" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" height="60" linewidth="0" width="246" showstereotype="1" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="0" x="-1654" y="-645" localid="dMZABODC4z1H" usesdiagramfillcolor="0" xmi.id="72YBMP5WcQzE" isinstance="0"/>
|
||||
<interfacewidget linecolor="none" usefillcolor="1" usesdiagramfillcolor="0" x="-1589,839897589076" isinstance="0" width="40" localid="bZYTIWVHJeGR" showscope="1" height="40" drawascircle="1" showpubliconly="0" showpackage="0" xmi.id="VA6qSCiBtNc5" showattributes="0" textcolor="#000000" showstereotype="1" linewidth="0" usesdiagramusefillcolor="0" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" showopsigs="601" showattsigs="601" showoperations="1" y="-716,9293791337742">
|
||||
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-91,07317073170748" isinstance="0" width="88" localid="wsTYjTmCdq05" height="19" posttext="" xmi.id="3iFu3ryeOW6f" role="700" textcolor="none" showstereotype="1" linewidth="none" text="DeviceService" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="10,97560975609758"/>
|
||||
</interfacewidget>
|
||||
<componentwidget textcolor="#000000" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" height="60" linewidth="0" width="195" showstereotype="1" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="0" x="-1374,768315059297" y="-548,2509733721189" localid="L9H7YahrGQFd" usesdiagramfillcolor="0" xmi.id="tDF2L96KKnVj" isinstance="0"/>
|
||||
<interfacewidget linecolor="none" usefillcolor="1" usesdiagramfillcolor="0" x="-1320,4" isinstance="0" width="40" localid="Xn4rYXasfszD" showscope="1" height="40" drawascircle="1" showpubliconly="0" showpackage="0" xmi.id="AKxHpDCgOPhk" showattributes="0" textcolor="#000000" showstereotype="1" linewidth="0" usesdiagramusefillcolor="0" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" showopsigs="601" showattsigs="601" showoperations="1" y="-640,1853658536585">
|
||||
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-6,602439024390151" isinstance="0" width="91" localid="Qe3g5GJDaFUO" height="19" posttext="" xmi.id="Qe4p2lZZXbQ3" role="700" textcolor="none" showstereotype="1" linewidth="none" text="DeviceSupport" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="-15,0634146341464"/>
|
||||
</interfacewidget>
|
||||
</widgets>
|
||||
<messages/>
|
||||
<associations>
|
||||
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="72YBMP5WcQzE" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="94" type="511" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="VA6qSCiBtNc5" xmi.id="3ZwKCCl82Cel" indexa="1">
|
||||
<linepath layout="Polyline">
|
||||
<startpoint startx="-1569,718796671645" starty="-645"/>
|
||||
<endpoint endx="-1569,718796671645" endy="-676,9293791337742"/>
|
||||
</linepath>
|
||||
</assocwidget>
|
||||
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="CDLTeFMNZrp5" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="96" type="512" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="VA6qSCiBtNc5" xmi.id="ZmBbwqKcFFuU" indexa="1">
|
||||
<linepath layout="Polyline">
|
||||
<startpoint startx="-1571,551409184105" starty="-779,8487804878049"/>
|
||||
<endpoint endx="-1571,551409184105" endy="-716,9293791337742"/>
|
||||
</linepath>
|
||||
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-1567,112384793861" isinstance="0" width="44" localid="yCgBPagYnPIa" height="19" posttext="" xmi.id="LilOMj6M9VGv" role="703" textcolor="none" showstereotype="1" linewidth="none" text="invoke" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="-758,1110310303017"/>
|
||||
</assocwidget>
|
||||
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="72YBMP5WcQzE" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="2" type="512" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="AKxHpDCgOPhk" xmi.id="60vlquxGX0xa" indexa="1">
|
||||
<linepath layout="Polyline">
|
||||
<startpoint startx="-1408" starty="-617,0146341463413"/>
|
||||
<endpoint endx="-1320,4" endy="-617,0146341463413"/>
|
||||
</linepath>
|
||||
<floatingtext linecolor="none" usefillcolor="1" usesdiagramfillcolor="1" x="-1382,614634146342" isinstance="0" width="44" localid="S2UTD5uasDos" height="19" posttext="" xmi.id="BrpNNGMiEFrg" role="703" textcolor="none" showstereotype="1" linewidth="none" text="invoke" usesdiagramusefillcolor="1" fillcolor="none" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" pretext="" y="-613,3439024390243"/>
|
||||
</assocwidget>
|
||||
<assocwidget totalcountb="2" textcolor="none" fillcolor="#ffff00" font="Liberation Sans,10,-1,5,50,0,0,0,0,0" widgetaid="tDF2L96KKnVj" linewidth="none" totalcounta="2" linecolor="none" usefillcolor="1" usesdiagramusefillcolor="53" type="511" indexb="1" seqnum="" usesdiagramfillcolor="0" widgetbid="AKxHpDCgOPhk" xmi.id="LcusapQo1BbT" indexa="1">
|
||||
<linepath layout="Polyline">
|
||||
<startpoint startx="-1299,912195121952" starty="-548,2509733721189"/>
|
||||
<endpoint endx="-1299,912195121952" endy="-600,1853658536585"/>
|
||||
</linepath>
|
||||
</assocwidget>
|
||||
</associations>
|
||||
</diagram>
|
||||
</diagrams>
|
||||
</XMI.extension>
|
||||
</UML:Model>
|
||||
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Deployment View" isLeaf="false" namespace="m1" visibility="public" xmi.id="Deployment View">
|
||||
<UML:Namespace.ownedElement/>
|
||||
</UML:Model>
|
||||
<UML:Model isRoot="false" isAbstract="false" isSpecification="false" name="Entity Relationship Model" isLeaf="false" namespace="m1" visibility="public" xmi.id="Entity Relationship Model">
|
||||
<UML:Namespace.ownedElement/>
|
||||
</UML:Model>
|
||||
</UML:Namespace.ownedElement>
|
||||
</UML:Model>
|
||||
</XMI.content>
|
||||
<XMI.extensions xmi.extender="umbrello">
|
||||
<docsettings viewid="UpkLkm005Mtw" uniqueid="BrpNNGMiEFrg" documentation=""/>
|
||||
<listview>
|
||||
<listitem type="800" id="Views" open="1">
|
||||
<listitem type="821" id="Component View" open="1">
|
||||
<listitem type="822" id="CDLTeFMNZrp5" open="1"/>
|
||||
<listitem type="822" id="tDF2L96KKnVj" open="1"/>
|
||||
<listitem type="822" id="72YBMP5WcQzE" open="1">
|
||||
<listitem type="845" id="xhXzgXGeAGP8" open="1"/>
|
||||
<listitem type="845" id="LcpVBJq9yM8d" open="1"/>
|
||||
</listitem>
|
||||
<listitem type="822" id="zd0ny2K62qHM" open="1"/>
|
||||
<listitem type="819" label="Gadgetbridge" id="UpkLkm005Mtw" open="0"/>
|
||||
<listitem type="824" id="jjfTCGwVsjy4" open="1"/>
|
||||
</listitem>
|
||||
<listitem type="827" id="Deployment View" open="1"/>
|
||||
<listitem type="836" id="Entity Relationship Model" open="1"/>
|
||||
<listitem type="801" id="Logical View" open="1">
|
||||
<listitem type="807" label="class diagram" id="31Hk7F5Bq9ac" open="0"/>
|
||||
<listitem type="830" id="Datatypes" open="0">
|
||||
<listitem type="829" id="mtFc0pEuADEp" open="1"/>
|
||||
<listitem type="829" id="MZCr2zI6yZo6" open="1"/>
|
||||
<listitem type="829" id="tIHrr3vdvBFv" open="1"/>
|
||||
<listitem type="829" id="hIRvrcjDBt2B" open="1"/>
|
||||
<listitem type="829" id="tM1MfT3dGbDn" open="1"/>
|
||||
<listitem type="829" id="xwL6qLHheXWT" open="1"/>
|
||||
<listitem type="829" id="yRn8f2wKINF9" open="1"/>
|
||||
<listitem type="829" id="jLVTWBskpZFo" open="1"/>
|
||||
<listitem type="829" id="aS7VdKRbkCiP" open="1"/>
|
||||
<listitem type="829" id="GJMsqsMRIRuv" open="1"/>
|
||||
<listitem type="829" id="EejU1wcwrx2h" open="1"/>
|
||||
</listitem>
|
||||
<listitem type="817" id="VA6qSCiBtNc5" open="1"/>
|
||||
<listitem type="817" id="AKxHpDCgOPhk" open="1"/>
|
||||
</listitem>
|
||||
<listitem type="802" id="Use Case View" open="1"/>
|
||||
</listitem>
|
||||
</listview>
|
||||
<codegeneration>
|
||||
<codegenerator language="C++"/>
|
||||
</codegeneration>
|
||||
</XMI.extensions>
|
||||
</XMI>
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,39 @@
|
|||
<configuration debug="true">
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.classic.android.LogcatAppender">
|
||||
<!-- encoders are by default assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||
<encoder>
|
||||
<pattern>%msg</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${GB_LOGFILES_DIR}/gadgetbridge.log</file>
|
||||
|
||||
<lazy>true</lazy>
|
||||
<!-- encoders are by default assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>${GB_LOGFILES_DIR}/gadgetbridge-%d{yyyy-MM-dd}.%i.log.zip</fileNamePattern>
|
||||
<maxHistory>10</maxHistory>
|
||||
<timeBasedFileNamingAndTriggeringPolicy
|
||||
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<!-- or whenever the file size reaches 50MB -->
|
||||
<maxFileSize>2MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<!--<pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>-->
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{1} - %msg%n</pattern>
|
||||
<!-- to debug crashes, set immediateFlush to true, otherwise keep it false to improve throughput -->
|
||||
<immediateFlush>false</immediateFlush>
|
||||
<!--<pattern>%date [%thread] %-5level %logger{25} - %msg%n</pattern>-->
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
</configuration>
|
|
@ -1,107 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceProtocol;
|
||||
|
||||
public abstract class AbstractBTDeviceSupport extends AbstractDeviceSupport {
|
||||
|
||||
private GBDeviceProtocol gbDeviceProtocol;
|
||||
private GBDeviceIoThread gbDeviceIOThread;
|
||||
|
||||
protected abstract GBDeviceProtocol createDeviceProtocol();
|
||||
|
||||
protected abstract GBDeviceIoThread createDeviceIOThread();
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// currently only one thread allowed
|
||||
if (gbDeviceIOThread != null) {
|
||||
gbDeviceIOThread.quit();
|
||||
try {
|
||||
gbDeviceIOThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
gbDeviceIOThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized GBDeviceProtocol getDeviceProtocol() {
|
||||
if (gbDeviceProtocol == null) {
|
||||
gbDeviceProtocol = createDeviceProtocol();
|
||||
}
|
||||
return gbDeviceProtocol;
|
||||
}
|
||||
|
||||
public synchronized GBDeviceIoThread getDeviceIOThread() {
|
||||
if (gbDeviceIOThread == null) {
|
||||
gbDeviceIOThread = createDeviceIOThread();
|
||||
}
|
||||
return gbDeviceIOThread;
|
||||
}
|
||||
|
||||
protected void sendToDevice(byte[] bytes) {
|
||||
if (bytes != null && gbDeviceIOThread != null) {
|
||||
gbDeviceIOThread.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSMS(String from, String body) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeSMS(from, body);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmail(String from, String subject, String body) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeEmail(from, subject, body);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime(long ts) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeSetTime(ts);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(String number, String name, GBCommand command) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeSetCallState(number, name, command);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(String artist, String album, String track) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeSetMusicInfo(artist, album, track);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFirmwareVersionReq() {
|
||||
byte[] bytes = gbDeviceProtocol.encodeFirmwareVersionReq();
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatteryInfoReq() {
|
||||
byte[] bytes = gbDeviceProtocol.encodeBatteryInfoReq();
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
byte[] bytes = gbDeviceProtocol.encodeAppInfoReq();
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(int id, int index) {
|
||||
byte[] bytes = gbDeviceProtocol.encodeAppDelete(id, index);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhoneVersion(byte os) {
|
||||
byte[] bytes = gbDeviceProtocol.encodePhoneVersion(os);
|
||||
sendToDevice(bytes);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
|
||||
// TODO: support option for a single reminder notification when notifications could not be delivered?
|
||||
// conditions: app was running and received notifications, but device was not connected.
|
||||
// maybe need to check for "unread notifications" on device for that.
|
||||
public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
private GBDevice gbDevice;
|
||||
private BluetoothAdapter btAdapter;
|
||||
private Context context;
|
||||
|
||||
public void initialize(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) {
|
||||
this.gbDevice = gbDevice;
|
||||
this.btAdapter = btAdapter;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return gbDevice.isConnected();
|
||||
}
|
||||
|
||||
protected boolean isInitialized() {
|
||||
return gbDevice.isInitialized();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice getDevice() {
|
||||
return gbDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BluetoothAdapter getBluetoothAdapter() {
|
||||
return btAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
|
||||
|
||||
|
||||
public class AppManagerActivity extends Activity {
|
||||
public static final String ACTION_REFRESH_APPLIST
|
||||
= "nodomain.freeyourgadget.gadgetbride.appmanager.action.refresh_applist";
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(ControlCenter.ACTION_QUIT)) {
|
||||
finish();
|
||||
} else if (action.equals(ACTION_REFRESH_APPLIST)) {
|
||||
appList.clear();
|
||||
int appCount = intent.getIntExtra("app_count", 0);
|
||||
for (Integer i = 0; i < appCount; i++) {
|
||||
String appName = intent.getStringExtra("app_name" + i.toString());
|
||||
String appCreator = intent.getStringExtra("app_creator" + i.toString());
|
||||
int id = intent.getIntExtra("app_id" + i.toString(), -1);
|
||||
int index = intent.getIntExtra("app_index" + i.toString(), -1);
|
||||
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
|
||||
|
||||
appList.add(new GBDeviceApp(id, index, appName, appCreator, "", appType));
|
||||
}
|
||||
mGBDeviceAppAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
final List<GBDeviceApp> appList = new ArrayList<>();
|
||||
private final String TAG = this.getClass().getSimpleName();
|
||||
private ListView appListView;
|
||||
private GBDeviceAppAdapter mGBDeviceAppAdapter;
|
||||
private GBDeviceApp selectedApp = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_appmanager);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
appListView = (ListView) findViewById(R.id.appListView);
|
||||
mGBDeviceAppAdapter = new GBDeviceAppAdapter(this, appList);
|
||||
appListView.setAdapter(this.mGBDeviceAppAdapter);
|
||||
registerForContextMenu(appListView);
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ControlCenter.ACTION_QUIT);
|
||||
filter.addAction(ACTION_REFRESH_APPLIST);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
Intent startIntent = new Intent(this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_REQUEST_APPINFO);
|
||||
startService(startIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(
|
||||
R.menu.appmanager_context, menu);
|
||||
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
selectedApp = appList.get(acmi.position);
|
||||
menu.setHeaderTitle(selectedApp.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.appmanager_app_delete:
|
||||
if (selectedApp != null) {
|
||||
Intent deleteIntent = new Intent(this, BluetoothCommunicationService.class);
|
||||
deleteIntent.setAction(BluetoothCommunicationService.ACTION_DELETEAPP);
|
||||
deleteIntent.putExtra("app_id", selectedApp.getId());
|
||||
deleteIntent.putExtra("app_index", selectedApp.getIndex());
|
||||
startService(deleteIntent);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBDevice.State;
|
||||
import nodomain.freeyourgadget.gadgetbridge.miband.MiBandSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.pebble.PebbleIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.pebble.PebbleSupport;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class BluetoothCommunicationService extends Service {
|
||||
public static final String ACTION_START
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.start";
|
||||
public static final String ACTION_CONNECT
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.connect";
|
||||
public static final String ACTION_NOTIFICATION_GENERIC
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.notification_generic";
|
||||
public static final String ACTION_NOTIFICATION_SMS
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.notification_sms";
|
||||
public static final String ACTION_NOTIFICATION_EMAIL
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.notification_email";
|
||||
public static final String ACTION_CALLSTATE
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.callstate";
|
||||
public static final String ACTION_SETTIME
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.settime";
|
||||
public static final String ACTION_SETMUSICINFO
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.setmusicinfo";
|
||||
public static final String ACTION_REQUEST_VERSIONINFO
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.request_versioninfo";
|
||||
public static final String ACTION_REQUEST_APPINFO
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.request_appinfo";
|
||||
public static final String ACTION_DELETEAPP
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.deleteapp";
|
||||
public static final String ACTION_INSTALL_PEBBLEAPP
|
||||
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.install_pebbbleapp";
|
||||
|
||||
private static final String TAG = "CommunicationService";
|
||||
private BluetoothAdapter mBtAdapter = null;
|
||||
private GBDeviceIoThread mGBDeviceIoThread = null;
|
||||
|
||||
private boolean mStarted = false;
|
||||
|
||||
private GBDevice mGBDevice = null;
|
||||
private DeviceSupport mDeviceSupport;
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
|
||||
GBDevice device = intent.getParcelableExtra("device");
|
||||
if (mGBDevice.equals(device)) {
|
||||
mGBDevice = device;
|
||||
GB.setReceiversEnableState(mDeviceSupport.useAutoConnect() || mGBDevice.isConnected(), context);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
|
||||
if (intent == null) {
|
||||
Log.i(TAG, "no intent");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
|
||||
if (action == null) {
|
||||
Log.i(TAG, "no action");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (!mStarted && !action.equals(ACTION_START)) {
|
||||
// using the service before issuing ACTION_START
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (mStarted && action.equals(ACTION_START)) {
|
||||
// using ACTION_START when the service has already been started
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
if (!action.equals(ACTION_START) && !action.equals(ACTION_CONNECT)) {
|
||||
if (mDeviceSupport == null || (!isConnected() && !mDeviceSupport.useAutoConnect())) {
|
||||
// trying to send notification without valid Bluetooth connection
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.equals(ACTION_CONNECT)) {
|
||||
//Check the system status
|
||||
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (mBtAdapter == null) {
|
||||
Toast.makeText(this, "Bluetooth is not supported.", Toast.LENGTH_SHORT).show();
|
||||
} else if (!mBtAdapter.isEnabled()) {
|
||||
Toast.makeText(this, "Bluetooth is disabled.", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
String btDeviceAddress = intent.getStringExtra("device_address");
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
sharedPrefs.edit().putString("last_device_address", btDeviceAddress).commit();
|
||||
|
||||
if (btDeviceAddress != null && !isConnected() && !isConnecting()) {
|
||||
if (mDeviceSupport != null) {
|
||||
mDeviceSupport.dispose();
|
||||
mDeviceSupport = null;
|
||||
}
|
||||
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(btDeviceAddress);
|
||||
if (btDevice != null) {
|
||||
if (btDevice.getName() == null || btDevice.getName().equals("MI")) { //FIXME: workaround for Miband not being paired
|
||||
mGBDevice = new GBDevice(btDeviceAddress, "MI", GBDevice.Type.MIBAND);
|
||||
mDeviceSupport = new MiBandSupport();
|
||||
} else if (btDevice.getName().indexOf("Pebble") == 0) {
|
||||
mGBDevice = new GBDevice(btDeviceAddress, btDevice.getName(), GBDevice.Type.PEBBLE);
|
||||
mDeviceSupport = new PebbleSupport();
|
||||
}
|
||||
if (mDeviceSupport != null) {
|
||||
mDeviceSupport.initialize(mGBDevice, mBtAdapter, this);
|
||||
mDeviceSupport.connect();
|
||||
if (mDeviceSupport instanceof AbstractBTDeviceSupport) {
|
||||
mGBDeviceIoThread = ((AbstractBTDeviceSupport) mDeviceSupport).getDeviceIOThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (action.equals(ACTION_NOTIFICATION_GENERIC)) {
|
||||
String title = intent.getStringExtra("notification_title");
|
||||
String body = intent.getStringExtra("notification_body");
|
||||
mDeviceSupport.onSMS(title, body);
|
||||
} else if (action.equals(ACTION_NOTIFICATION_SMS)) {
|
||||
String sender = intent.getStringExtra("notification_sender");
|
||||
String body = intent.getStringExtra("notification_body");
|
||||
String senderName = getContactDisplayNameByNumber(sender);
|
||||
mDeviceSupport.onSMS(senderName, body);
|
||||
} else if (action.equals(ACTION_NOTIFICATION_EMAIL)) {
|
||||
String sender = intent.getStringExtra("notification_sender");
|
||||
String subject = intent.getStringExtra("notification_subject");
|
||||
String body = intent.getStringExtra("notification_body");
|
||||
mDeviceSupport.onEmail(sender, subject, body);
|
||||
} else if (action.equals(ACTION_CALLSTATE)) {
|
||||
GBCommand command = GBCommand.values()[intent.getIntExtra("call_command", 0)]; // UGLY
|
||||
String phoneNumber = intent.getStringExtra("call_phonenumber");
|
||||
String callerName = null;
|
||||
if (phoneNumber != null) {
|
||||
callerName = getContactDisplayNameByNumber(phoneNumber);
|
||||
}
|
||||
mDeviceSupport.onSetCallState(phoneNumber, callerName, command);
|
||||
} else if (action.equals(ACTION_SETTIME)) {
|
||||
mDeviceSupport.onSetTime(-1);
|
||||
} else if (action.equals(ACTION_SETMUSICINFO)) {
|
||||
String artist = intent.getStringExtra("music_artist");
|
||||
String album = intent.getStringExtra("music_album");
|
||||
String track = intent.getStringExtra("music_track");
|
||||
mDeviceSupport.onSetMusicInfo(artist, album, track);
|
||||
} else if (action.equals(ACTION_REQUEST_VERSIONINFO)) {
|
||||
if (mGBDevice != null && mGBDevice.getFirmwareVersion() == null) {
|
||||
mDeviceSupport.onFirmwareVersionReq();
|
||||
} else {
|
||||
mGBDevice.sendDeviceUpdateIntent(this);
|
||||
}
|
||||
} else if (action.equals(ACTION_REQUEST_APPINFO)) {
|
||||
mDeviceSupport.onAppInfoReq();
|
||||
} else if (action.equals(ACTION_DELETEAPP)) {
|
||||
int id = intent.getIntExtra("app_id", -1);
|
||||
int index = intent.getIntExtra("app_index", -1);
|
||||
mDeviceSupport.onAppDelete(id, index);
|
||||
} else if (action.equals(ACTION_INSTALL_PEBBLEAPP)) {
|
||||
String uriString = intent.getStringExtra("app_uri");
|
||||
if (uriString != null) {
|
||||
Log.i(TAG, "will try to install app");
|
||||
((PebbleIoThread) mGBDeviceIoThread).installApp(Uri.parse(uriString));
|
||||
}
|
||||
} else if (action.equals(ACTION_START)) {
|
||||
startForeground(GB.NOTIFICATION_ID, GB.createNotification("Gadgetbridge running", this));
|
||||
mStarted = true;
|
||||
}
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
return mGBDevice != null && mGBDevice.getState() == State.CONNECTED;
|
||||
}
|
||||
|
||||
private boolean isConnecting() {
|
||||
return mGBDevice != null && mGBDevice.getState() == State.CONNECTING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
GB.setReceiversEnableState(false, this); // disable BroadcastReceivers
|
||||
|
||||
if (mDeviceSupport != null) {
|
||||
mDeviceSupport.dispose();
|
||||
}
|
||||
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
nm.cancel(GB.NOTIFICATION_ID); // need to do this because the updated notification wont be cancelled when service stops
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private String getContactDisplayNameByNumber(String number) {
|
||||
Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
|
||||
String name = number;
|
||||
|
||||
if (number == null || number.equals("")) {
|
||||
return name;
|
||||
}
|
||||
|
||||
ContentResolver contentResolver = getContentResolver();
|
||||
Cursor contactLookup = contentResolver.query(uri, null, null, null, null);
|
||||
|
||||
try {
|
||||
if (contactLookup != null && contactLookup.getCount() > 0) {
|
||||
contactLookup.moveToNext();
|
||||
name = contactLookup.getString(contactLookup.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
|
||||
}
|
||||
} finally {
|
||||
if (contactLookup != null) {
|
||||
contactLookup.close();
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
|
||||
public class BluetoothStateChangeReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
|
||||
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
|
||||
if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_ON) {
|
||||
|
||||
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
|
||||
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (!sharedPrefs.getBoolean("general_autoconnectonbluetooth", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String deviceAddress = sharedPrefs.getString("last_device_address", null);
|
||||
Intent startIntent = new Intent(context, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_START);
|
||||
context.startService(startIntent);
|
||||
if (deviceAddress != null) {
|
||||
Intent connectIntent = new Intent(context, BluetoothCommunicationService.class);
|
||||
connectIntent.setAction(BluetoothCommunicationService.ACTION_CONNECT);
|
||||
connectIntent.putExtra("device_address", deviceAddress);
|
||||
context.startService(connectIntent);
|
||||
}
|
||||
} else if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1) == BluetoothAdapter.STATE_OFF) {
|
||||
Intent stopIntent = new Intent(context, BluetoothCommunicationService.class);
|
||||
context.stopService(stopIntent);
|
||||
|
||||
Intent quitIntent = new Intent(ControlCenter.ACTION_QUIT);
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class ControlCenter extends Activity {
|
||||
|
||||
|
||||
public static final String ACTION_QUIT
|
||||
= "nodomain.freeyourgadget.gadgetbride.controlcenter.action.quit";
|
||||
|
||||
public static final String ACTION_REFRESH_DEVICELIST
|
||||
= "nodomain.freeyourgadget.gadgetbride.controlcenter.action.set_version";
|
||||
|
||||
TextView hintTextView;
|
||||
ListView deviceListView;
|
||||
GBDeviceAdapter mGBDeviceAdapter;
|
||||
final List<GBDevice> deviceList = new ArrayList<>();
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(ACTION_QUIT)) {
|
||||
finish();
|
||||
} else if (action.equals(ACTION_REFRESH_DEVICELIST)) {
|
||||
refreshPairedDevices();
|
||||
} else if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
|
||||
GBDevice dev = intent.getParcelableExtra("device");
|
||||
if (dev.getAddress() != null) {
|
||||
int index = deviceList.indexOf(dev); // search by address
|
||||
if (index >= 0) {
|
||||
deviceList.set(index, dev);
|
||||
}
|
||||
}
|
||||
refreshPairedDevices();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_controlcenter);
|
||||
hintTextView = (TextView) findViewById(R.id.hintTextView);
|
||||
deviceListView = (ListView) findViewById(R.id.deviceListView);
|
||||
mGBDeviceAdapter = new GBDeviceAdapter(this, deviceList);
|
||||
deviceListView.setAdapter(this.mGBDeviceAdapter);
|
||||
deviceListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView parent, View v, int position, long id) {
|
||||
if (deviceList.get(position).isConnected()) {
|
||||
Intent startIntent = new Intent(ControlCenter.this, AppManagerActivity.class);
|
||||
startActivity(startIntent);
|
||||
} else {
|
||||
Intent startIntent = new Intent(ControlCenter.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_CONNECT);
|
||||
startIntent.putExtra("device_address", deviceList.get(position).getAddress());
|
||||
|
||||
startService(startIntent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_QUIT);
|
||||
filter.addAction(ACTION_REFRESH_DEVICELIST);
|
||||
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
refreshPairedDevices();
|
||||
/*
|
||||
* Ask for permission to intercept notifications on first run.
|
||||
*/
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (sharedPrefs.getBoolean("firstrun", true)) {
|
||||
sharedPrefs.edit().putBoolean("firstrun", false).commit();
|
||||
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
|
||||
startActivity(enableIntent);
|
||||
}
|
||||
Intent startIntent = new Intent(this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_START);
|
||||
startService(startIntent);
|
||||
|
||||
Intent versionInfoIntent = new Intent(this, BluetoothCommunicationService.class);
|
||||
versionInfoIntent.setAction(BluetoothCommunicationService.ACTION_REQUEST_VERSIONINFO);
|
||||
startService(versionInfoIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
Intent settingsIntent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(settingsIntent);
|
||||
return true;
|
||||
case R.id.action_debug:
|
||||
Intent debugIntent = new Intent(this, DebugActivity.class);
|
||||
startActivity(debugIntent);
|
||||
return true;
|
||||
case R.id.action_quit:
|
||||
Intent stopIntent = new Intent(this, BluetoothCommunicationService.class);
|
||||
stopService(stopIntent);
|
||||
|
||||
Intent quitIntent = new Intent(ControlCenter.ACTION_QUIT);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(quitIntent);
|
||||
return true;
|
||||
case R.id.action_refresh:
|
||||
refreshPairedDevices();
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void refreshPairedDevices() {
|
||||
boolean connected = false;
|
||||
List<GBDevice> availableDevices = new ArrayList<>();
|
||||
for (GBDevice device : deviceList) {
|
||||
if (device.isConnected() || device.isConnecting()) {
|
||||
connected = true;
|
||||
availableDevices.add(device);
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
|
||||
if (btAdapter == null) {
|
||||
Toast.makeText(this, "Bluetooth is not supported.", Toast.LENGTH_SHORT).show();
|
||||
} else if (!btAdapter.isEnabled()) {
|
||||
Toast.makeText(this, "Bluetooth is disabled.", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Set<BluetoothDevice> pairedDevices = btAdapter.getBondedDevices();
|
||||
for (BluetoothDevice pairedDevice : pairedDevices) {
|
||||
GBDevice.Type deviceType;
|
||||
if (pairedDevice.getName().indexOf("Pebble") == 0) {
|
||||
deviceType = GBDevice.Type.PEBBLE;
|
||||
} else if (pairedDevice.getName().equals("MI")) {
|
||||
deviceType = GBDevice.Type.MIBAND;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
GBDevice device = new GBDevice(pairedDevice.getAddress(), pairedDevice.getName(), deviceType);
|
||||
if (!availableDevices.contains(device)) {
|
||||
availableDevices.add(device);
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
String miAddr = sharedPrefs.getString("development_miaddr", null);
|
||||
if (miAddr != null && miAddr.length() > 0) {
|
||||
GBDevice miDevice = new GBDevice(miAddr, "MI", GBDevice.Type.MIBAND);
|
||||
if (!availableDevices.contains(miDevice)) {
|
||||
availableDevices.add(miDevice);
|
||||
}
|
||||
}
|
||||
deviceList.retainAll(availableDevices);
|
||||
for (GBDevice dev : availableDevices) {
|
||||
if (!deviceList.contains(dev)) {
|
||||
deviceList.add(dev);
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
hintTextView.setText("tap connected device for App Mananger");
|
||||
} else if (!deviceList.isEmpty()) {
|
||||
hintTextView.setText("tap a device to connect");
|
||||
}
|
||||
}
|
||||
mGBDeviceAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
|
||||
public class DebugActivity extends Activity {
|
||||
Button sendSMSButton;
|
||||
Button sendEmailButton;
|
||||
Button incomingCallButton;
|
||||
Button outgoingCallButton;
|
||||
Button startCallButton;
|
||||
Button endCallButton;
|
||||
Button testNotificationButton;
|
||||
Button setMusicInfoButton;
|
||||
Button setTimeButton;
|
||||
EditText editContent;
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(ControlCenter.ACTION_QUIT)) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_debug);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
registerReceiver(mReceiver, new IntentFilter(ControlCenter.ACTION_QUIT));
|
||||
|
||||
editContent = (EditText) findViewById(R.id.editContent);
|
||||
sendSMSButton = (Button) findViewById(R.id.sendSMSButton);
|
||||
sendSMSButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_NOTIFICATION_GENERIC);
|
||||
startIntent.putExtra("notification_title", "Gadgetbridge");
|
||||
startIntent.putExtra("notification_body", editContent.getText().toString());
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
sendEmailButton = (Button) findViewById(R.id.sendEmailButton);
|
||||
sendEmailButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_NOTIFICATION_EMAIL);
|
||||
startIntent.putExtra("notification_sender", "Gadgetbridge");
|
||||
startIntent.putExtra("notification_subject", "Test");
|
||||
startIntent.putExtra("notification_body", editContent.getText().toString());
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
incomingCallButton = (Button) findViewById(R.id.incomingCallButton);
|
||||
incomingCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
|
||||
startIntent.putExtra("call_phonenumber", editContent.getText().toString());
|
||||
startIntent.putExtra("call_command", GBCommand.CALL_INCOMING.ordinal());
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
outgoingCallButton = (Button) findViewById(R.id.outgoingCallButton);
|
||||
outgoingCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
|
||||
startIntent.putExtra("call_phonenumber", editContent.getText().toString());
|
||||
startIntent.putExtra("call_command", GBCommand.CALL_OUTGOING.ordinal());
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
startCallButton = (Button) findViewById(R.id.startCallButton);
|
||||
startCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
|
||||
startIntent.putExtra("call_command", GBCommand.CALL_START.ordinal());
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
endCallButton = (Button) findViewById(R.id.endCallButton);
|
||||
endCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
|
||||
startIntent.putExtra("call_command", GBCommand.CALL_END.ordinal());
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
setMusicInfoButton = (Button) findViewById(R.id.setMusicInfoButton);
|
||||
setMusicInfoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_SETMUSICINFO);
|
||||
startIntent.putExtra("music_artist", editContent.getText().toString() + "(artist)");
|
||||
startIntent.putExtra("music_album", editContent.getText().toString() + "(album)");
|
||||
startIntent.putExtra("music_track", editContent.getText().toString() + "(track)");
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeButton = (Button) findViewById(R.id.setTimeButton);
|
||||
setTimeButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
|
||||
startIntent.setAction(BluetoothCommunicationService.ACTION_SETTIME);
|
||||
startService(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
testNotificationButton = (Button) findViewById(R.id.testNotificationButton);
|
||||
testNotificationButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
testNotification();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void testNotification() {
|
||||
NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this);
|
||||
ncomp.setContentTitle("Test Notification");
|
||||
ncomp.setContentText("This is a Test Notification from Gadgetbridge");
|
||||
ncomp.setTicker("This is a Test Notification from Gadgetbridge");
|
||||
ncomp.setSmallIcon(R.drawable.ic_notification);
|
||||
ncomp.setAutoCancel(true);
|
||||
nManager.notify((int) System.currentTimeMillis(), ncomp.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Provides support for a specific device. Has hooks to manage the life cycle
|
||||
* of a device: instances of this interface will be created, initialized, and disposed
|
||||
* as needed.
|
||||
* <p/>
|
||||
* Implementations need to act accordingly, in order to establish, reestablish or close
|
||||
* the connection to the device.
|
||||
* <p/>
|
||||
* This interface is agnostic to the kind transport, i.e. whether the device is connected
|
||||
* via Bluetooth, Bluetooth LE, Wifi or something else.
|
||||
*/
|
||||
public interface DeviceSupport extends EventHandler {
|
||||
public void initialize(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context);
|
||||
|
||||
public boolean isConnected();
|
||||
|
||||
public boolean connect();
|
||||
|
||||
public void dispose();
|
||||
|
||||
public GBDevice getDevice();
|
||||
|
||||
public BluetoothAdapter getBluetoothAdapter();
|
||||
|
||||
public Context getContext();
|
||||
|
||||
public boolean useAutoConnect();
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
public interface EventHandler {
|
||||
public void onSMS(String from, String body);
|
||||
|
||||
public void onEmail(String from, String subject, String body);
|
||||
|
||||
public void onSetTime(long ts);
|
||||
|
||||
public void onSetCallState(String number, String name, GBCommand command);
|
||||
|
||||
public void onSetMusicInfo(String artist, String album, String track);
|
||||
|
||||
public void onFirmwareVersionReq();
|
||||
|
||||
public void onBatteryInfoReq();
|
||||
|
||||
public void onAppInfoReq();
|
||||
|
||||
public void onAppDelete(int id, int index);
|
||||
|
||||
public void onPhoneVersion(byte os);
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.K9Receiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.MusicPlaybackReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.PhoneCallReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
|
||||
|
||||
public class GB {
|
||||
public static final int NOTIFICATION_ID = 1;
|
||||
private static final String TAG = "GB";
|
||||
|
||||
public static Notification createNotification(String text, Context context) {
|
||||
Intent notificationIntent = new Intent(context, ControlCenter.class);
|
||||
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0,
|
||||
notificationIntent, 0);
|
||||
|
||||
return new NotificationCompat.Builder(context)
|
||||
.setContentTitle("Gadgetbridge")
|
||||
.setTicker(text)
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true).build();
|
||||
}
|
||||
|
||||
public static void updateNotification(String text, Context context) {
|
||||
Notification notification = createNotification(text, context);
|
||||
|
||||
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
nm.notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
public static void setReceiversEnableState(boolean enable, Context context) {
|
||||
Log.i(TAG, "Setting broadcast receivers to: " + enable);
|
||||
final Class<?>[] receiverClasses = {
|
||||
PhoneCallReceiver.class,
|
||||
SMSReceiver.class,
|
||||
K9Receiver.class,
|
||||
MusicPlaybackReceiver.class,
|
||||
//NotificationListener.class, // disabling this leads to loss of permission to read notifications
|
||||
};
|
||||
|
||||
int newState;
|
||||
|
||||
if (enable) {
|
||||
newState = PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
|
||||
} else {
|
||||
newState = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
|
||||
}
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
|
||||
for (Class<?> receiverClass : receiverClasses) {
|
||||
ComponentName compName = new ComponentName(context, receiverClass);
|
||||
|
||||
pm.setComponentEnabledSetting(compName, newState, PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,594 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Normano64
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.NotificationManager.Policy;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.ContactsContract.PhoneLookup;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.NotificationCollectorMonitorService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
/**
|
||||
* Main Application class that initializes and provides access to certain things like
|
||||
* logging and DB access.
|
||||
*/
|
||||
public class GBApplication extends Application {
|
||||
// Since this class must not log to slf4j, we use plain android.util.Log
|
||||
private static final String TAG = "GBApplication";
|
||||
public static final String DATABASE_NAME = "Gadgetbridge";
|
||||
|
||||
private static GBApplication context;
|
||||
private static final Lock dbLock = new ReentrantLock();
|
||||
private static DeviceService deviceService;
|
||||
private static SharedPreferences sharedPrefs;
|
||||
private static final String PREFS_VERSION = "shared_preferences_version";
|
||||
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
|
||||
private static final int CURRENT_PREFS_VERSION = 2;
|
||||
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
|
||||
private static Prefs prefs;
|
||||
private static GBPrefs gbPrefs;
|
||||
private static LockHandler lockHandler;
|
||||
/**
|
||||
* Note: is null on Lollipop and Kitkat
|
||||
*/
|
||||
private static NotificationManager notificationManager;
|
||||
|
||||
public static final String ACTION_QUIT
|
||||
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
|
||||
public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change";
|
||||
|
||||
private static GBApplication app;
|
||||
|
||||
private static Logging logging = new Logging() {
|
||||
@Override
|
||||
protected String createLogDirectory() throws IOException {
|
||||
if (GBEnvironment.env().isLocalTest()) {
|
||||
return System.getProperty(Logging.PROP_LOGFILES_DIR);
|
||||
} else {
|
||||
File dir = FileUtils.getExternalFilesDir();
|
||||
return dir.getAbsolutePath();
|
||||
}
|
||||
}
|
||||
};
|
||||
private static Locale language;
|
||||
|
||||
private DeviceManager deviceManager;
|
||||
|
||||
public static void quit() {
|
||||
GB.log("Quitting Gadgetbridge...", GB.INFO, null);
|
||||
Intent quitIntent = new Intent(GBApplication.ACTION_QUIT);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(quitIntent);
|
||||
GBApplication.deviceService().quit();
|
||||
}
|
||||
|
||||
public GBApplication() {
|
||||
context = this;
|
||||
// don't do anything here, add it to onCreate instead
|
||||
}
|
||||
|
||||
public static Logging getLogging() {
|
||||
return logging;
|
||||
}
|
||||
|
||||
protected DeviceService createDeviceService() {
|
||||
return new GBDeviceService(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
app = this;
|
||||
super.onCreate();
|
||||
|
||||
if (lockHandler != null) {
|
||||
// guard against multiple invocations (robolectric)
|
||||
return;
|
||||
}
|
||||
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs = new Prefs(sharedPrefs);
|
||||
gbPrefs = new GBPrefs(prefs);
|
||||
|
||||
if (!GBEnvironment.isEnvironmentSetup()) {
|
||||
GBEnvironment.setupEnvironment(GBEnvironment.createDeviceEnvironment());
|
||||
// setup db after the environment is set up, but don't do it in test mode
|
||||
// in test mode, it's done individually, see TestBase
|
||||
setupDatabase();
|
||||
}
|
||||
|
||||
// don't do anything here before we set up logging, otherwise
|
||||
// slf4j may be implicitly initialized before we properly configured it.
|
||||
setupLogging(isFileLoggingEnabled());
|
||||
|
||||
if (getPrefsFileVersion() != CURRENT_PREFS_VERSION) {
|
||||
migratePrefs(getPrefsFileVersion());
|
||||
}
|
||||
|
||||
setupExceptionHandler();
|
||||
|
||||
deviceManager = new DeviceManager(this);
|
||||
String language = prefs.getString("language", "default");
|
||||
setLanguage(language);
|
||||
|
||||
deviceService = createDeviceService();
|
||||
loadAppsBlackList();
|
||||
loadCalendarsBlackList();
|
||||
|
||||
if (isRunningMarshmallowOrLater()) {
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
//the following will ensure the notification manager is kept alive
|
||||
startService(new Intent(this, NotificationCollectorMonitorService.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(int level) {
|
||||
super.onTrimMemory(level);
|
||||
if (level >= TRIM_MEMORY_BACKGROUND) {
|
||||
if (!hasBusyDevice()) {
|
||||
DBHelper.clearSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if at least a single device is busy, e.g synchronizing activity data
|
||||
* or something similar.
|
||||
* Note: busy is not the same as connected or initialized!
|
||||
*/
|
||||
private boolean hasBusyDevice() {
|
||||
List<GBDevice> devices = getDeviceManager().getDevices();
|
||||
for (GBDevice device : devices) {
|
||||
if (device.isBusy()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void setupLogging(boolean enabled) {
|
||||
logging.setupLogging(enabled);
|
||||
}
|
||||
|
||||
private void setupExceptionHandler() {
|
||||
LoggingExceptionHandler handler = new LoggingExceptionHandler(Thread.getDefaultUncaughtExceptionHandler());
|
||||
Thread.setDefaultUncaughtExceptionHandler(handler);
|
||||
}
|
||||
|
||||
public static boolean isFileLoggingEnabled() {
|
||||
return prefs.getBoolean("log_to_file", false);
|
||||
}
|
||||
|
||||
public static boolean minimizeNotification() {
|
||||
return prefs.getBoolean("minimize_priority", false);
|
||||
}
|
||||
|
||||
public void setupDatabase() {
|
||||
DaoMaster.OpenHelper helper;
|
||||
GBEnvironment env = GBEnvironment.env();
|
||||
if (env.isTest()) {
|
||||
helper = new DaoMaster.DevOpenHelper(this, null, null);
|
||||
} else {
|
||||
helper = new DBOpenHelper(this, DATABASE_NAME, null);
|
||||
}
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
DaoMaster daoMaster = new DaoMaster(db);
|
||||
if (lockHandler == null) {
|
||||
lockHandler = new LockHandler();
|
||||
}
|
||||
lockHandler.init(daoMaster, helper);
|
||||
}
|
||||
|
||||
public static Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the facade for talking to devices. Devices are managed by
|
||||
* an Android Service and this facade provides access to its functionality.
|
||||
*
|
||||
* @return the facade for talking to the service/devices.
|
||||
*/
|
||||
public static DeviceService deviceService() {
|
||||
return deviceService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DBHandler instance for reading/writing or throws GBException
|
||||
* when that was not successful
|
||||
* If acquiring was successful, callers must call #releaseDB when they
|
||||
* are done (from the same thread that acquired the lock!
|
||||
* <p>
|
||||
* Callers must not hold a reference to the returned instance because it
|
||||
* will be invalidated at some point.
|
||||
*
|
||||
* @return the DBHandler
|
||||
* @throws GBException
|
||||
* @see #releaseDB()
|
||||
*/
|
||||
public static DBHandler acquireDB() throws GBException {
|
||||
try {
|
||||
if (dbLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||
return lockHandler;
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Log.i(TAG, "Interrupted while waiting for DB lock");
|
||||
}
|
||||
throw new GBException("Unable to access the database.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the database lock.
|
||||
*
|
||||
* @throws IllegalMonitorStateException if the current thread is not owning the lock
|
||||
* @see #acquireDB()
|
||||
*/
|
||||
public static void releaseDB() {
|
||||
dbLock.unlock();
|
||||
}
|
||||
|
||||
public static boolean isRunningLollipopOrLater() {
|
||||
return VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
|
||||
}
|
||||
|
||||
public static boolean isRunningMarshmallowOrLater() {
|
||||
return VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
|
||||
private static boolean isPrioritySender(int prioritySenders, String number) {
|
||||
if (prioritySenders == Policy.PRIORITY_SENDERS_ANY) {
|
||||
return true;
|
||||
} else {
|
||||
Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
|
||||
String[] projection = new String[]{PhoneLookup._ID, PhoneLookup.STARRED};
|
||||
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
|
||||
boolean exists = false;
|
||||
int starred = 0;
|
||||
try {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
exists = true;
|
||||
starred = cursor.getInt(cursor.getColumnIndexOrThrow(PhoneLookup.STARRED));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
if (prioritySenders == Policy.PRIORITY_SENDERS_CONTACTS && exists) {
|
||||
return true;
|
||||
} else if (prioritySenders == Policy.PRIORITY_SENDERS_STARRED && starred == 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public static boolean isPriorityNumber(int priorityType, String number) {
|
||||
NotificationManager.Policy notificationPolicy = notificationManager.getNotificationPolicy();
|
||||
if (priorityType == Policy.PRIORITY_CATEGORY_MESSAGES) {
|
||||
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_MESSAGES) == Policy.PRIORITY_CATEGORY_MESSAGES) {
|
||||
return isPrioritySender(notificationPolicy.priorityMessageSenders, number);
|
||||
}
|
||||
} else if (priorityType == Policy.PRIORITY_CATEGORY_CALLS) {
|
||||
if ((notificationPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_CALLS) == Policy.PRIORITY_CATEGORY_CALLS) {
|
||||
return isPrioritySender(notificationPolicy.priorityCallSenders, number);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public static int getGrantedInterruptionFilter() {
|
||||
if (prefs.getBoolean("notification_filter", false) && GBApplication.isRunningMarshmallowOrLater()) {
|
||||
if (notificationManager.isNotificationPolicyAccessGranted()) {
|
||||
return notificationManager.getCurrentInterruptionFilter();
|
||||
}
|
||||
}
|
||||
return NotificationManager.INTERRUPTION_FILTER_ALL;
|
||||
}
|
||||
|
||||
private static HashSet<String> apps_blacklist = null;
|
||||
|
||||
public static boolean appIsBlacklisted(String packageName) {
|
||||
if (apps_blacklist == null) {
|
||||
GB.log("appIsBlacklisted: apps_blacklist is null!", GB.INFO, null);
|
||||
}
|
||||
return apps_blacklist != null && apps_blacklist.contains(packageName);
|
||||
}
|
||||
|
||||
public static void setAppsBlackList(Set<String> packageNames) {
|
||||
if (packageNames == null) {
|
||||
GB.log("Set null apps_blacklist", GB.INFO, null);
|
||||
apps_blacklist = new HashSet<>();
|
||||
} else {
|
||||
apps_blacklist = new HashSet<>(packageNames);
|
||||
}
|
||||
GB.log("New apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
|
||||
saveAppsBlackList();
|
||||
}
|
||||
|
||||
private static void loadAppsBlackList() {
|
||||
GB.log("Loading apps_blacklist", GB.INFO, null);
|
||||
apps_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
|
||||
if (apps_blacklist == null) {
|
||||
apps_blacklist = new HashSet<>();
|
||||
}
|
||||
GB.log("Loaded apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
|
||||
}
|
||||
|
||||
private static void saveAppsBlackList() {
|
||||
GB.log("Saving apps_blacklist with " + apps_blacklist.size() + " entries", GB.INFO, null);
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
if (apps_blacklist.isEmpty()) {
|
||||
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
|
||||
} else {
|
||||
Prefs.putStringSet(editor, GBPrefs.PACKAGE_BLACKLIST, apps_blacklist);
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static void addAppToBlacklist(String packageName) {
|
||||
if (apps_blacklist.add(packageName)) {
|
||||
saveAppsBlackList();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeFromAppsBlacklist(String packageName) {
|
||||
GB.log("Removing from apps_blacklist: " + packageName, GB.INFO, null);
|
||||
apps_blacklist.remove(packageName);
|
||||
saveAppsBlackList();
|
||||
}
|
||||
|
||||
private static HashSet<String> calendars_blacklist = null;
|
||||
|
||||
public static boolean calendarIsBlacklisted(String calendarDisplayName) {
|
||||
if (calendars_blacklist == null) {
|
||||
GB.log("calendarIsBlacklisted: calendars_blacklist is null!", GB.INFO, null);
|
||||
}
|
||||
return calendars_blacklist != null && calendars_blacklist.contains(calendarDisplayName);
|
||||
}
|
||||
|
||||
public static void setCalendarsBlackList(Set<String> calendarNames) {
|
||||
if (calendarNames == null) {
|
||||
GB.log("Set null apps_blacklist", GB.INFO, null);
|
||||
calendars_blacklist = new HashSet<>();
|
||||
} else {
|
||||
calendars_blacklist = new HashSet<>(calendarNames);
|
||||
}
|
||||
GB.log("New calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
|
||||
saveCalendarsBlackList();
|
||||
}
|
||||
|
||||
public static void addCalendarToBlacklist(String calendarDisplayName) {
|
||||
if (calendars_blacklist.add(calendarDisplayName)) {
|
||||
saveCalendarsBlackList();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeFromCalendarBlacklist(String calendarDisplayName) {
|
||||
calendars_blacklist.remove(calendarDisplayName);
|
||||
saveCalendarsBlackList();
|
||||
}
|
||||
|
||||
private static void loadCalendarsBlackList() {
|
||||
GB.log("Loading calendars_blacklist", GB.INFO, null);
|
||||
calendars_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
|
||||
if (calendars_blacklist == null) {
|
||||
calendars_blacklist = new HashSet<>();
|
||||
}
|
||||
GB.log("Loaded calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
|
||||
}
|
||||
|
||||
private static void saveCalendarsBlackList() {
|
||||
GB.log("Saving calendars_blacklist with " + calendars_blacklist.size() + " entries", GB.INFO, null);
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
if (calendars_blacklist.isEmpty()) {
|
||||
editor.putStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
|
||||
} else {
|
||||
Prefs.putStringSet(editor, GBPrefs.CALENDAR_BLACKLIST, calendars_blacklist);
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes both the old Activity database and the new one recreates it with empty tables.
|
||||
*
|
||||
* @return true on successful deletion
|
||||
*/
|
||||
public static synchronized boolean deleteActivityDatabase(Context context) {
|
||||
// TODO: flush, close, reopen db
|
||||
if (lockHandler != null) {
|
||||
lockHandler.closeDb();
|
||||
}
|
||||
boolean result = deleteOldActivityDatabase(context);
|
||||
result &= getContext().deleteDatabase(DATABASE_NAME);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the legacy (pre 0.12) Activity database
|
||||
*
|
||||
* @return true on successful deletion
|
||||
*/
|
||||
public static synchronized boolean deleteOldActivityDatabase(Context context) {
|
||||
DBHelper dbHelper = new DBHelper(context);
|
||||
boolean result = true;
|
||||
if (dbHelper.existsDB("ActivityDatabase")) {
|
||||
result = getContext().deleteDatabase("ActivityDatabase");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int getPrefsFileVersion() {
|
||||
try {
|
||||
return Integer.parseInt(sharedPrefs.getString(PREFS_VERSION, "0")); //0 is legacy
|
||||
} catch (Exception e) {
|
||||
//in version 1 this was an int
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void migratePrefs(int oldVersion) {
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
switch (oldVersion) {
|
||||
case 0:
|
||||
String legacyGender = sharedPrefs.getString("mi_user_gender", null);
|
||||
String legacyHeight = sharedPrefs.getString("mi_user_height_cm", null);
|
||||
String legacyWeigth = sharedPrefs.getString("mi_user_weight_kg", null);
|
||||
String legacyYOB = sharedPrefs.getString("mi_user_year_of_birth", null);
|
||||
if (legacyGender != null) {
|
||||
int gender = "male".equals(legacyGender) ? 1 : "female".equals(legacyGender) ? 0 : 2;
|
||||
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(gender));
|
||||
editor.remove("mi_user_gender");
|
||||
}
|
||||
if (legacyHeight != null) {
|
||||
editor.putString(ActivityUser.PREF_USER_HEIGHT_CM, legacyHeight);
|
||||
editor.remove("mi_user_height_cm");
|
||||
}
|
||||
if (legacyWeigth != null) {
|
||||
editor.putString(ActivityUser.PREF_USER_WEIGHT_KG, legacyWeigth);
|
||||
editor.remove("mi_user_weight_kg");
|
||||
}
|
||||
if (legacyYOB != null) {
|
||||
editor.putString(ActivityUser.PREF_USER_YEAR_OF_BIRTH, legacyYOB);
|
||||
editor.remove("mi_user_year_of_birth");
|
||||
}
|
||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||
break;
|
||||
case 1:
|
||||
//migrate the integer version of gender introduced in version 1 to a string value, needed for the way Android accesses the shared preferences
|
||||
int legacyGender_1 = 2;
|
||||
try {
|
||||
legacyGender_1 = sharedPrefs.getInt(ActivityUser.PREF_USER_GENDER, 2);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not access legacy activity gender", e);
|
||||
}
|
||||
editor.putString(ActivityUser.PREF_USER_GENDER, Integer.toString(legacyGender_1));
|
||||
//also silently migrate the version to a string value
|
||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||
break;
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static void setLanguage(String lang) {
|
||||
if (lang.equals("default")) {
|
||||
language = Resources.getSystem().getConfiguration().locale;
|
||||
} else {
|
||||
language = new Locale(lang);
|
||||
}
|
||||
updateLanguage(language);
|
||||
}
|
||||
|
||||
public static void updateLanguage(Locale locale) {
|
||||
AndroidUtils.setLanguage(context, locale);
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(ACTION_LANGUAGE_CHANGE);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
public static LimitedQueue getIDSenderLookup() {
|
||||
return mIDSenderLookup;
|
||||
}
|
||||
|
||||
public static boolean isDarkThemeEnabled() {
|
||||
return prefs.getString("pref_key_theme", context.getString(R.string.pref_theme_value_light)).equals(context.getString(R.string.pref_theme_value_dark));
|
||||
}
|
||||
|
||||
public static int getTextColor(Context context) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
theme.resolveAttribute(R.attr.textColorPrimary, typedValue, true);
|
||||
return typedValue.data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
updateLanguage(getLanguage());
|
||||
}
|
||||
|
||||
public static int getBackgroundColor(Context context) {
|
||||
TypedValue typedValue = new TypedValue();
|
||||
Resources.Theme theme = context.getTheme();
|
||||
theme.resolveAttribute(android.R.attr.background, typedValue, true);
|
||||
return typedValue.data;
|
||||
}
|
||||
|
||||
public static Prefs getPrefs() {
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public static GBPrefs getGBPrefs() {
|
||||
return gbPrefs;
|
||||
}
|
||||
|
||||
public DeviceManager getDeviceManager() {
|
||||
return deviceManager;
|
||||
}
|
||||
|
||||
public static GBApplication app() {
|
||||
return app;
|
||||
}
|
||||
|
||||
public static Locale getLanguage() {
|
||||
return language;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.telephony.ITelephony;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandCallControl;
|
||||
|
||||
public class GBCallControlReceiver extends BroadcastReceiver {
|
||||
public static final String ACTION_CALLCONTROL = "nodomain.freeyourgadget.gadgetbridge.callcontrol";
|
||||
private final String TAG = this.getClass().getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
GBDeviceCommandCallControl.Command callCmd = GBDeviceCommandCallControl.Command.values()[intent.getIntExtra("command", 0)];
|
||||
switch (callCmd) {
|
||||
case END:
|
||||
case START:
|
||||
try {
|
||||
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
Class clazz = Class.forName(telephonyManager.getClass().getName());
|
||||
Method method = clazz.getDeclaredMethod("getITelephony");
|
||||
method.setAccessible(true);
|
||||
ITelephony telephonyService = (ITelephony) method.invoke(telephonyManager);
|
||||
if (callCmd == GBDeviceCommandCallControl.Command.END) {
|
||||
telephonyService.endCall();
|
||||
} else {
|
||||
telephonyService.answerRingingCall();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "could not start or hangup call");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
public enum GBCommand {
|
||||
|
||||
UNDEFINEND,
|
||||
|
||||
CALL_ACCEPT,
|
||||
CALL_END,
|
||||
CALL_INCOMING,
|
||||
CALL_OUTGOING,
|
||||
CALL_REJECT,
|
||||
CALL_START,
|
||||
|
||||
MUSIC_PLAY,
|
||||
MUSIC_PAUSE,
|
||||
MUSIC_PLAYPAUSE,
|
||||
MUSIC_NEXT,
|
||||
MUSIC_PREVIOUS,
|
||||
|
||||
APP_INFO_NAME,
|
||||
VERSION_FIRMWARE
|
||||
}
|
|
@ -1,217 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.Log;
|
||||
|
||||
public class GBDevice implements Parcelable {
|
||||
public static final String ACTION_DEVICE_CHANGED
|
||||
= "nodomain.freeyourgadget.gadgetbride.gbdevice.action.device_changed";
|
||||
public static final Creator<GBDevice> CREATOR = new Creator<GBDevice>() {
|
||||
@Override
|
||||
public GBDevice createFromParcel(Parcel source) {
|
||||
return new GBDevice(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice[] newArray(int size) {
|
||||
return new GBDevice[size];
|
||||
}
|
||||
};
|
||||
private static final String TAG = GBDevice.class.getSimpleName();
|
||||
private final String mName;
|
||||
private final String mAddress;
|
||||
private final Type mType;
|
||||
private String mFirmwareVersion = null;
|
||||
private String mHardwareVersion = null;
|
||||
private State mState = State.NOT_CONNECTED;
|
||||
private short mBatteryLevel = 50; // unknown
|
||||
private String mBatteryState;
|
||||
|
||||
public GBDevice(String address, String name, Type type) {
|
||||
mAddress = address;
|
||||
mName = name;
|
||||
mType = type;
|
||||
validate();
|
||||
}
|
||||
|
||||
private GBDevice(Parcel in) {
|
||||
mName = in.readString();
|
||||
mAddress = in.readString();
|
||||
mType = Type.values()[in.readInt()];
|
||||
mFirmwareVersion = in.readString();
|
||||
mHardwareVersion = in.readString();
|
||||
mState = State.values()[in.readInt()];
|
||||
mBatteryLevel = (short) in.readInt();
|
||||
mBatteryState = in.readString();
|
||||
validate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mName);
|
||||
dest.writeString(mAddress);
|
||||
dest.writeInt(mType.ordinal());
|
||||
dest.writeString(mFirmwareVersion);
|
||||
dest.writeString(mHardwareVersion);
|
||||
dest.writeInt(mState.ordinal());
|
||||
dest.writeInt(mBatteryLevel);
|
||||
dest.writeString(mBatteryState);
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (getAddress() == null) {
|
||||
throw new IllegalArgumentException("address must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return mAddress;
|
||||
}
|
||||
|
||||
public String getFirmwareVersion() {
|
||||
return mFirmwareVersion;
|
||||
}
|
||||
|
||||
public void setFirmwareVersion(String firmwareVersion) {
|
||||
mFirmwareVersion = firmwareVersion;
|
||||
}
|
||||
|
||||
public String getHardwareVersion() {
|
||||
return mHardwareVersion;
|
||||
}
|
||||
|
||||
public void setHardwareVersion(String hardwareVersion) {
|
||||
mHardwareVersion = hardwareVersion;
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return mState.ordinal() >= State.CONNECTED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return mState.ordinal() >= State.INITIALIZED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isConnecting() {
|
||||
return mState == State.CONNECTING;
|
||||
}
|
||||
|
||||
public State getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
String getStateString() {
|
||||
switch (mState) {
|
||||
case NOT_CONNECTED:
|
||||
return "not connected"; // TODO: do not hardcode
|
||||
case CONNECTING:
|
||||
return "connecting";
|
||||
case CONNECTED:
|
||||
return "connected";
|
||||
case INITIALIZED:
|
||||
return "initialized";
|
||||
}
|
||||
return "unknown state";
|
||||
}
|
||||
|
||||
public String getInfoString() {
|
||||
//FIXME: ugly
|
||||
if (mFirmwareVersion != null) {
|
||||
if (mHardwareVersion != null) {
|
||||
return getStateString() + " (HW: " + mHardwareVersion + " FW: " + mFirmwareVersion + ")";
|
||||
}
|
||||
return getStateString() + " (FW: " + mFirmwareVersion + ")";
|
||||
} else {
|
||||
return getStateString();
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
// TODO: this doesn't really belong here
|
||||
public void sendDeviceUpdateIntent(Context context) {
|
||||
Intent deviceUpdateIntent = new Intent(ACTION_DEVICE_CHANGED);
|
||||
deviceUpdateIntent.putExtra("device", this);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(deviceUpdateIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof GBDevice)) {
|
||||
return false;
|
||||
}
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (((GBDevice) obj).getAddress().equals(this.mAddress)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mAddress.hashCode() ^ 37;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranges from 0-100 (percent)
|
||||
*
|
||||
* @return the battery level in range 0-100
|
||||
*/
|
||||
public short getBatteryLevel() {
|
||||
return mBatteryLevel;
|
||||
}
|
||||
|
||||
public void setBatteryLevel(short batteryLevel) {
|
||||
if (mBatteryLevel >= 0 && mBatteryLevel <= 100) {
|
||||
mBatteryLevel = batteryLevel;
|
||||
} else {
|
||||
Log.e(TAG, "Battery level musts be within range 0-100: " + batteryLevel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the battery state.
|
||||
*/
|
||||
public String getBatteryState() {
|
||||
return mBatteryState != null ? mBatteryState : "(unknown)";
|
||||
}
|
||||
|
||||
public void setBatteryState(String batteryState) {
|
||||
mBatteryState = batteryState;
|
||||
}
|
||||
|
||||
public enum State {
|
||||
// Note: the order is important!
|
||||
NOT_CONNECTED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
INITIALIZED
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
PEBBLE,
|
||||
MIBAND
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
public class GBDeviceApp {
|
||||
private final String name;
|
||||
private final String creator;
|
||||
private final String version;
|
||||
private final int id;
|
||||
private final int index;
|
||||
private final Type type;
|
||||
|
||||
public GBDeviceApp(int id, int index, String name, String creator, String version, Type type) {
|
||||
this.id = id;
|
||||
this.index = index;
|
||||
this.name = name;
|
||||
this.creator = creator;
|
||||
this.version = version;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
WATCHFACE,
|
||||
APP_GENERIC,
|
||||
APP_ACTIVITYTRACKER,
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public abstract class GBDeviceIoThread extends Thread {
|
||||
protected final GBDevice gbDevice;
|
||||
private final Context context;
|
||||
|
||||
public GBDeviceIoThread(GBDevice gbDevice, Context context) {
|
||||
this.gbDevice = gbDevice;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public GBDevice getDevice() {
|
||||
return gbDevice;
|
||||
}
|
||||
|
||||
protected boolean connect(String btDeviceAddress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
}
|
||||
|
||||
synchronized public void write(byte[] bytes) {
|
||||
}
|
||||
|
||||
public void quit() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
/**
|
||||
* Some more or less useful utility methods to aid local (non-device) testing.
|
||||
*/
|
||||
public class GBEnvironment {
|
||||
// DO NOT USE A LOGGER HERE. Will break LoggingTest!
|
||||
// private static final Logger LOG = LoggerFactory.getLogger(GBEnvironment.class);
|
||||
|
||||
private static GBEnvironment environment;
|
||||
private boolean localTest;
|
||||
private boolean deviceTest;
|
||||
|
||||
public static GBEnvironment createLocalTestEnvironment() {
|
||||
GBEnvironment env = new GBEnvironment();
|
||||
env.localTest = true;
|
||||
return env;
|
||||
}
|
||||
|
||||
static GBEnvironment createDeviceEnvironment() {
|
||||
return new GBEnvironment();
|
||||
}
|
||||
|
||||
public final boolean isTest() {
|
||||
return localTest || deviceTest;
|
||||
}
|
||||
|
||||
public boolean isLocalTest() {
|
||||
return localTest;
|
||||
}
|
||||
|
||||
public static synchronized GBEnvironment env() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
static synchronized boolean isEnvironmentSetup() {
|
||||
return environment != null;
|
||||
}
|
||||
|
||||
public synchronized static void setupEnvironment(GBEnvironment env) {
|
||||
environment = env;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
public class GBException extends Exception {
|
||||
public GBException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public GBException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public GBException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public GBException() {
|
||||
super();
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.os.SystemClock;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.protocol.GBDeviceCommandMusicControl;
|
||||
|
||||
public class GBMusicControlReceiver extends BroadcastReceiver {
|
||||
private final String TAG = this.getClass().getSimpleName();
|
||||
|
||||
public static final String ACTION_MUSICCONTROL = "nodomain.freeyourgadget.gadgetbridge.musiccontrol";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
GBDeviceCommandMusicControl.Command musicCmd = GBDeviceCommandMusicControl.Command.values()[intent.getIntExtra("command", 0)];
|
||||
int keyCode = -1;
|
||||
int volumeAdjust = AudioManager.ADJUST_LOWER;
|
||||
|
||||
switch (musicCmd) {
|
||||
case NEXT:
|
||||
keyCode = KeyEvent.KEYCODE_MEDIA_NEXT;
|
||||
break;
|
||||
case PREVIOUS:
|
||||
keyCode = KeyEvent.KEYCODE_MEDIA_PREVIOUS;
|
||||
break;
|
||||
case PLAY:
|
||||
keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
|
||||
break;
|
||||
case PAUSE:
|
||||
keyCode = KeyEvent.KEYCODE_MEDIA_PAUSE;
|
||||
break;
|
||||
case PLAYPAUSE:
|
||||
keyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE;
|
||||
break;
|
||||
case VOLUMEUP:
|
||||
// change default and fall through, :P
|
||||
volumeAdjust = AudioManager.ADJUST_RAISE;
|
||||
case VOLUMEDOWN:
|
||||
AudioManager audioManager =
|
||||
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, volumeAdjust, 0);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyCode != -1) {
|
||||
long eventtime = SystemClock.uptimeMillis();
|
||||
|
||||
Intent downIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
|
||||
KeyEvent downEvent = new KeyEvent(eventtime, eventtime, KeyEvent.ACTION_DOWN, keyCode, 0);
|
||||
downIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent);
|
||||
context.sendOrderedBroadcast(downIntent, null);
|
||||
|
||||
Intent upIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null);
|
||||
KeyEvent upEvent = new KeyEvent(eventtime, eventtime, KeyEvent.ACTION_UP, keyCode, 0);
|
||||
upIntent.putExtra(Intent.EXTRA_KEY_EVENT, upEvent);
|
||||
context.sendOrderedBroadcast(upIntent, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
|
||||
/**
|
||||
* Provides lowlevel access to the database.
|
||||
*/
|
||||
public class LockHandler implements DBHandler {
|
||||
|
||||
private DaoMaster daoMaster = null;
|
||||
private DaoSession session = null;
|
||||
private SQLiteOpenHelper helper = null;
|
||||
|
||||
public LockHandler() {
|
||||
}
|
||||
|
||||
public void init(DaoMaster daoMaster, DaoMaster.OpenHelper helper) {
|
||||
if (isValid()) {
|
||||
throw new IllegalStateException("DB must be closed before initializing it again");
|
||||
}
|
||||
if (daoMaster == null) {
|
||||
throw new IllegalArgumentException("daoMaster must not be null");
|
||||
}
|
||||
if (helper == null) {
|
||||
throw new IllegalArgumentException("helper must not be null");
|
||||
}
|
||||
this.daoMaster = daoMaster;
|
||||
this.helper = helper;
|
||||
|
||||
session = daoMaster.newSession();
|
||||
if (session == null) {
|
||||
throw new RuntimeException("Unable to create database session");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoMaster getDaoMaster() {
|
||||
return daoMaster;
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
return daoMaster != null;
|
||||
}
|
||||
|
||||
private void ensureValid() {
|
||||
if (!isValid()) {
|
||||
throw new IllegalStateException("LockHandler is not in a valid state");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
ensureValid();
|
||||
GBApplication.releaseDB();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void openDb() {
|
||||
if (session != null) {
|
||||
throw new IllegalStateException("session must be null");
|
||||
}
|
||||
// this will create completely new db instances and in turn update this handler through #init()
|
||||
GBApplication.app().setupDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void closeDb() {
|
||||
if (session == null) {
|
||||
throw new IllegalStateException("session must not be null");
|
||||
}
|
||||
session.clear();
|
||||
session.getDatabase().close();
|
||||
session = null;
|
||||
helper = null;
|
||||
daoMaster = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLiteOpenHelper getHelper() {
|
||||
ensureValid();
|
||||
return helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DaoSession getDaoSession() {
|
||||
ensureValid();
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLiteDatabase getDatabase() {
|
||||
ensureValid();
|
||||
return daoMaster.getDatabase();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
/* Copyright (C) 2016-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.Appender;
|
||||
import ch.qos.logback.core.FileAppender;
|
||||
import ch.qos.logback.core.encoder.Encoder;
|
||||
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
|
||||
import ch.qos.logback.core.util.StatusPrinter;
|
||||
|
||||
public abstract class Logging {
|
||||
public static final String PROP_LOGFILES_DIR = "GB_LOGFILES_DIR";
|
||||
|
||||
private FileAppender<ILoggingEvent> fileLogger;
|
||||
|
||||
public void setupLogging(boolean enable) {
|
||||
try {
|
||||
if (fileLogger == null) {
|
||||
init();
|
||||
}
|
||||
if (enable) {
|
||||
startFileLogger();
|
||||
} else {
|
||||
stopFileLogger();
|
||||
}
|
||||
getLogger().info("Gadgetbridge version: " + BuildConfig.VERSION_NAME);
|
||||
} catch (IOException ex) {
|
||||
Log.e("GBApplication", "External files dir not available, cannot log to file", ex);
|
||||
stopFileLogger();
|
||||
}
|
||||
}
|
||||
|
||||
public void debugLoggingConfiguration() {
|
||||
// For debugging problems with the logback configuration
|
||||
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
// print logback's internal status
|
||||
StatusPrinter.print(lc);
|
||||
// Logger logger = LoggerFactory.getLogger(Logging.class);
|
||||
}
|
||||
|
||||
protected abstract String createLogDirectory() throws IOException;
|
||||
|
||||
protected void init() throws IOException {
|
||||
String dir = createLogDirectory();
|
||||
if (dir == null) {
|
||||
throw new IllegalArgumentException("log directory must not be null");
|
||||
}
|
||||
// used by assets/logback.xml since the location cannot be statically determined
|
||||
System.setProperty(PROP_LOGFILES_DIR, dir);
|
||||
rememberFileLogger();
|
||||
}
|
||||
|
||||
private Logger getLogger() {
|
||||
return LoggerFactory.getLogger(Logging.class);
|
||||
}
|
||||
|
||||
private void startFileLogger() {
|
||||
if (fileLogger != null && !fileLogger.isStarted()) {
|
||||
addFileLogger(fileLogger);
|
||||
fileLogger.setLazy(false); // hack to make sure that start() actually opens the file
|
||||
fileLogger.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopFileLogger() {
|
||||
if (fileLogger != null && fileLogger.isStarted()) {
|
||||
fileLogger.stop();
|
||||
removeFileLogger(fileLogger);
|
||||
}
|
||||
}
|
||||
|
||||
private void rememberFileLogger() {
|
||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
fileLogger = (FileAppender<ILoggingEvent>) root.getAppender("FILE");
|
||||
}
|
||||
|
||||
private void addFileLogger(Appender<ILoggingEvent> fileLogger) {
|
||||
try {
|
||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
if (!root.isAttached(fileLogger)) {
|
||||
root.addAppender(fileLogger);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("GBApplication", "Error adding logger FILE appender", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFileLogger(Appender<ILoggingEvent> fileLogger) {
|
||||
try {
|
||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
if (root.isAttached(fileLogger)) {
|
||||
root.detachAppender(fileLogger);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.e("GBApplication", "Error removing logger FILE appender", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public FileAppender<ILoggingEvent> getFileLogger() {
|
||||
return fileLogger;
|
||||
}
|
||||
|
||||
public boolean setImmediateFlush(boolean enable) {
|
||||
FileAppender<ILoggingEvent> fileLogger = getFileLogger();
|
||||
Encoder<ILoggingEvent> encoder = fileLogger.getEncoder();
|
||||
if (encoder instanceof LayoutWrappingEncoder) {
|
||||
((LayoutWrappingEncoder) encoder).setImmediateFlush(enable);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isImmediateFlush() {
|
||||
FileAppender<ILoggingEvent> fileLogger = getFileLogger();
|
||||
Encoder<ILoggingEvent> encoder = fileLogger.getEncoder();
|
||||
if (encoder instanceof LayoutWrappingEncoder) {
|
||||
return ((LayoutWrappingEncoder) encoder).isImmediateFlush();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String formatBytes(byte[] bytes) {
|
||||
if (bytes == null) {
|
||||
return "(null)";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(bytes.length * 5);
|
||||
for (byte b : bytes) {
|
||||
builder.append(String.format("0x%2x", b));
|
||||
builder.append(" ");
|
||||
}
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
public static void logBytes(Logger logger, byte[] value) {
|
||||
if (value != null) {
|
||||
for (byte b : value) {
|
||||
logger.warn("DATA: " + String.format("0x%2x", b));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* Copyright (C) 2015-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Catches otherwise uncaught exceptions, logs them and terminates the app.
|
||||
*/
|
||||
public class LoggingExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LoggingExceptionHandler.class);
|
||||
private final Thread.UncaughtExceptionHandler mDelegate;
|
||||
|
||||
public LoggingExceptionHandler(Thread.UncaughtExceptionHandler delegate) {
|
||||
mDelegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread thread, Throwable ex) {
|
||||
LOG.error("Uncaught exception: " + ex.getMessage(), ex);
|
||||
if (mDelegate != null) {
|
||||
mDelegate.uncaughtException(thread, ex);
|
||||
} else {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setupSimplePreferencesScreen();
|
||||
}
|
||||
|
||||
private void setupSimplePreferencesScreen() {
|
||||
// Add 'general' preferences.
|
||||
addPreferencesFromResource(R.xml.pref_general);
|
||||
|
||||
// Add 'date' preferences, and a corresponding header.
|
||||
PreferenceCategory fakeHeaderDateTime = new PreferenceCategory(this);
|
||||
fakeHeaderDateTime.setTitle(R.string.pref_header_datetime);
|
||||
getPreferenceScreen().addPreference(fakeHeaderDateTime);
|
||||
addPreferencesFromResource(R.xml.pref_datetime);
|
||||
|
||||
// Add 'notifications' preferences, and a corresponding header.
|
||||
PreferenceCategory fakeHeader = new PreferenceCategory(this);
|
||||
fakeHeader.setTitle(R.string.pref_header_notifications);
|
||||
getPreferenceScreen().addPreference(fakeHeader);
|
||||
addPreferencesFromResource(R.xml.pref_notification);
|
||||
|
||||
Preference pref = (Preference) findPreference("notifications_generic");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Add 'development' preferences, and a corresponding header.
|
||||
PreferenceCategory fakeHeaderDev = new PreferenceCategory(this);
|
||||
fakeHeaderDev.setTitle(R.string.pref_header_development);
|
||||
getPreferenceScreen().addPreference(fakeHeaderDev);
|
||||
addPreferencesFromResource(R.xml.pref_development);
|
||||
|
||||
|
||||
final Preference developmentMiaddr = findPreference("development_miaddr");
|
||||
bindPreferenceSummaryToValue(developmentMiaddr);
|
||||
|
||||
developmentMiaddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
|
||||
preference.setSummary(newVal.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Bind the summaries of EditText/List/Dialog/Ringtone preferences to
|
||||
// their values. When their values change, their summaries are updated
|
||||
// to reflect the new value, per the Android Design guidelines.
|
||||
|
||||
//bindPreferenceSummaryToValue(findPreference("notifications_sms_whenscreenon"));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A preference value change listener that updates the preference's summary
|
||||
* to reflect its new value.
|
||||
*/
|
||||
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
||||
String stringValue = value.toString();
|
||||
|
||||
if (preference instanceof ListPreference) {
|
||||
// For list preferences, look up the correct display value in
|
||||
// the preference's 'entries' list.
|
||||
ListPreference listPreference = (ListPreference) preference;
|
||||
int index = listPreference.findIndexOfValue(stringValue);
|
||||
|
||||
// Set the summary to reflect the new value.
|
||||
preference.setSummary(
|
||||
index >= 0
|
||||
? listPreference.getEntries()[index]
|
||||
: null);
|
||||
|
||||
} else {
|
||||
// For all other preferences, set the summary to the value's
|
||||
// simple string representation.
|
||||
preference.setSummary(stringValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Binds a preference's summary to its value. More specifically, when the
|
||||
* preference's value is changed, its summary (line of text below the
|
||||
* preference title) is updated to reflect the value. The summary is also
|
||||
* immediately updated upon calling this method. The exact display format is
|
||||
* dependent on the type of preference.
|
||||
*
|
||||
* @see #sBindPreferenceSummaryToValueListener
|
||||
*/
|
||||
private static void bindPreferenceSummaryToValue(Preference preference) {
|
||||
// Set the listener to watch for value changes.
|
||||
preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
|
||||
|
||||
// Trigger the listener immediately with the preference's
|
||||
// current value.
|
||||
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(preference.getContext())
|
||||
.getString(preference.getKey(), ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/* Copyright (C) 2016-2017 0nse, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* Implementation of SleepAlarmWidget functionality. When pressing the widget, an alarm will be set
|
||||
* to trigger after a predefined number of hours. A toast will confirm the user about this. The
|
||||
* value is retrieved using ActivityUser.().getSleepDuration().
|
||||
*/
|
||||
public class SleepAlarmWidget extends AppWidgetProvider {
|
||||
|
||||
/**
|
||||
* This is our dedicated action to detect when the widget has been clicked.
|
||||
*/
|
||||
public static final String ACTION =
|
||||
"nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK";
|
||||
|
||||
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
|
||||
int appWidgetId) {
|
||||
|
||||
// Construct the RemoteViews object
|
||||
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.sleep_alarm_widget);
|
||||
|
||||
// Add our own click intent
|
||||
Intent intent = new Intent(ACTION);
|
||||
PendingIntent clickPI = PendingIntent.getBroadcast(
|
||||
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
views.setOnClickPendingIntent(R.id.sleepalarmwidget_text, clickPI);
|
||||
|
||||
// Instruct the widget manager to update the widget
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
||||
// There may be multiple widgets active, so update all of them
|
||||
for (int appWidgetId : appWidgetIds) {
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnabled(Context context) {
|
||||
// Enter relevant functionality for when the first widget is created
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisabled(Context context) {
|
||||
// Enter relevant functionality for when the last widget is disabled
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
super.onReceive(context, intent);
|
||||
if (ACTION.equals(intent.getAction())) {
|
||||
int userSleepDuration = new ActivityUser().getSleepDuration();
|
||||
// current timestamp
|
||||
GregorianCalendar calendar = new GregorianCalendar();
|
||||
// add preferred sleep duration
|
||||
calendar.add(Calendar.HOUR_OF_DAY, userSleepDuration);
|
||||
|
||||
|
||||
// overwrite the first alarm and activate it
|
||||
GBAlarm alarm = GBAlarm.createSingleShot(0, true, calendar);
|
||||
alarm.store();
|
||||
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
setAlarmViaAlarmManager(context, calendar.getTimeInMillis());
|
||||
}
|
||||
|
||||
int hours = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
int minutes = calendar.get(Calendar.MINUTE);
|
||||
|
||||
GB.toast(context,
|
||||
String.format(context.getString(R.string.appwidget_alarms_set), hours, minutes),
|
||||
Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the Android alarm manager to create the alarm icon in the status bar.
|
||||
*
|
||||
* @param packageContext {@code Context}: A Context of the application package implementing this
|
||||
* class.
|
||||
* @param triggerTime {@code long}: time at which the underlying alarm is triggered in wall time
|
||||
* milliseconds since the epoch
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void setAlarmViaAlarmManager(Context packageContext, long triggerTime) {
|
||||
AlarmManager am = (AlarmManager) packageContext.getSystemService(Context.ALARM_SERVICE);
|
||||
// TODO: launch the alarm configuration activity when clicking the alarm in the status bar
|
||||
Intent intent = new Intent(packageContext, ConfigureAlarms.class);
|
||||
PendingIntent pi = PendingIntent.getBroadcast(packageContext, 0, intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
am.setAlarmClock(new AlarmManager.AlarmClockInfo(triggerTime, pi), pi);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/* Copyright (C) 2015-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractFragmentPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private final Set<AbstractGBFragment> fragments = new HashSet<>();
|
||||
private Object primaryFragment;
|
||||
|
||||
public AbstractFragmentPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
Object fragment = super.instantiateItem(container, position);
|
||||
if (fragment instanceof AbstractGBFragment) {
|
||||
fragments.add((AbstractGBFragment) fragment);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
super.destroyItem(container, position, object);
|
||||
fragments.remove(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||
super.setPrimaryItem(container, position, object);
|
||||
if (object != primaryFragment) {
|
||||
primaryFragment = object;
|
||||
setCurrentFragment(primaryFragment);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCurrentFragment(Object newCurrentFragment) {
|
||||
for (AbstractGBFragment frag : fragments) {
|
||||
if (frag != newCurrentFragment) {
|
||||
frag.onMadeInvisibleInActivity();
|
||||
} else {
|
||||
frag.onMadeVisibleInActivityInternal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
|
||||
|
||||
public abstract class AbstractGBActivity extends AppCompatActivity implements GBActivity {
|
||||
private boolean isLanguageInvalid = false;
|
||||
|
||||
public static final int NONE = 0;
|
||||
public static final int NO_ACTIONBAR = 1;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case GBApplication.ACTION_LANGUAGE_CHANGE:
|
||||
setLanguage(GBApplication.getLanguage(), true);
|
||||
break;
|
||||
case GBApplication.ACTION_QUIT:
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void setLanguage(Locale language, boolean invalidateLanguage) {
|
||||
if (invalidateLanguage) {
|
||||
isLanguageInvalid = true;
|
||||
}
|
||||
AndroidUtils.setLanguage(this, language);
|
||||
}
|
||||
|
||||
public static void init(GBActivity activity) {
|
||||
init(activity, NONE);
|
||||
}
|
||||
|
||||
public static void init(GBActivity activity, int flags) {
|
||||
if (GBApplication.isDarkThemeEnabled()) {
|
||||
if ((flags & NO_ACTIONBAR) != 0) {
|
||||
activity.setTheme(R.style.GadgetbridgeThemeDark_NoActionBar);
|
||||
} else {
|
||||
activity.setTheme(R.style.GadgetbridgeThemeDark);
|
||||
}
|
||||
} else {
|
||||
if ((flags & NO_ACTIONBAR) != 0) {
|
||||
activity.setTheme(R.style.GadgetbridgeTheme_NoActionBar);
|
||||
} else {
|
||||
activity.setTheme(R.style.GadgetbridgeTheme);
|
||||
}
|
||||
}
|
||||
activity.setLanguage(GBApplication.getLanguage(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBApplication.ACTION_QUIT);
|
||||
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
init(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (isLanguageInvalid) {
|
||||
isLanguageInvalid = false;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, walkjivefly
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
/**
|
||||
* Abstract base class for fragments. Provides hooks that are called when
|
||||
* the fragment is made visible and invisible in the activity. also allows
|
||||
* the fragment to define the title to be shown in the activity.
|
||||
*
|
||||
* @see AbstractGBFragmentActivity
|
||||
*/
|
||||
public abstract class AbstractGBFragment extends Fragment {
|
||||
private boolean mVisibleInActivity;
|
||||
|
||||
/**
|
||||
* Called when this fragment has been fully scrolled into the activity.
|
||||
*
|
||||
* @see #isVisibleInActivity()
|
||||
* @see #onMadeInvisibleInActivity()
|
||||
*/
|
||||
protected void onMadeVisibleInActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this fragment has been scrolled out of the activity.
|
||||
*
|
||||
* @see #isVisibleInActivity()
|
||||
* @see #onMadeVisibleInActivity()
|
||||
*/
|
||||
protected void onMadeInvisibleInActivity() {
|
||||
mVisibleInActivity = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this fragment is currently visible in the hosting
|
||||
* activity, not taking into account whether the screen is enabled at all.
|
||||
*/
|
||||
public boolean isVisibleInActivity() {
|
||||
return mVisibleInActivity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected abstract CharSequence getTitle();
|
||||
|
||||
/**
|
||||
* Internal
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public void onMadeVisibleInActivityInternal() {
|
||||
mVisibleInActivity = true;
|
||||
if (isVisible()) {
|
||||
onMadeVisibleInActivity();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
|
||||
/**
|
||||
* A base activity that supports paging through fragments by swiping.
|
||||
* Subclasses will have to add a ViewPager to their layout and add something
|
||||
* like this to hook it to the fragments:
|
||||
* <p/>
|
||||
* <pre>
|
||||
* // Set up the ViewPager with the sections adapter.
|
||||
* ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
|
||||
* viewPager.setAdapter(getPagerAdapter());
|
||||
* </pre>
|
||||
*
|
||||
* @see AbstractGBFragment
|
||||
*/
|
||||
public abstract class AbstractGBFragmentActivity extends AbstractGBActivity {
|
||||
/**
|
||||
* The {@link android.support.v4.view.PagerAdapter} that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentPagerAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory. If this becomes too memory intensive, it
|
||||
* may be best to switch to a
|
||||
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
|
||||
*/
|
||||
private AbstractFragmentPagerAdapter mSectionsPagerAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = createFragmentPagerAdapter(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
public AbstractFragmentPagerAdapter getPagerAdapter() {
|
||||
return mSectionsPagerAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PagerAdapter that will create the fragments to be used with this
|
||||
* activity. The fragments should typically extend AbstractGBFragment
|
||||
*
|
||||
* @param fragmentManager
|
||||
* @return
|
||||
*/
|
||||
protected abstract AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager);
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian
|
||||
Fischer, Daniele Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.InputType;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
|
||||
/**
|
||||
* A settings activity with support for preferences directly displaying their value.
|
||||
* If you combine such preferences with a custom OnPreferenceChangeListener, you have
|
||||
* to set that listener in #onCreate, *not* in #onPostCreate, otherwise the value will
|
||||
* not be displayed.
|
||||
*/
|
||||
public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivity implements GBActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class);
|
||||
|
||||
private boolean isLanguageInvalid = false;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case GBApplication.ACTION_LANGUAGE_CHANGE:
|
||||
setLanguage(GBApplication.getLanguage(), true);
|
||||
break;
|
||||
case GBApplication.ACTION_QUIT:
|
||||
finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A preference value change listener that updates the preference's summary
|
||||
* to reflect its new value.
|
||||
*/
|
||||
private static class SimpleSetSummaryOnChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
||||
if (preference instanceof EditTextPreference) {
|
||||
if ((((EditTextPreference) preference).getEditText().getKeyListener().getInputType() & InputType.TYPE_CLASS_NUMBER) != 0) {
|
||||
if ("".equals(String.valueOf(value))) {
|
||||
// reject empty numeric input
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateSummary(preference, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void updateSummary(Preference preference, Object value) {
|
||||
String stringValue = String.valueOf(value);
|
||||
|
||||
if (preference instanceof ListPreference) {
|
||||
// For list preferences, look up the correct display value in
|
||||
// the preference's 'entries' list.
|
||||
ListPreference listPreference = (ListPreference) preference;
|
||||
int index = listPreference.findIndexOfValue(stringValue);
|
||||
|
||||
// Set the summary to reflect the new value.
|
||||
preference.setSummary(
|
||||
index >= 0
|
||||
? listPreference.getEntries()[index]
|
||||
: null);
|
||||
|
||||
} else {
|
||||
// For all other preferences, set the summary to the value's
|
||||
// simple string representation.
|
||||
preference.setSummary(stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExtraSetSummaryOnChangeListener extends SimpleSetSummaryOnChangeListener {
|
||||
private final Preference.OnPreferenceChangeListener prefChangeListener;
|
||||
|
||||
public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener prefChangeListener) {
|
||||
this.prefChangeListener = prefChangeListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
||||
boolean result = prefChangeListener.onPreferenceChange(preference, value);
|
||||
if (result) {
|
||||
return super.onPreferenceChange(preference, value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final SimpleSetSummaryOnChangeListener sBindPreferenceSummaryToValueListener = new SimpleSetSummaryOnChangeListener();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AbstractGBActivity.init(this);
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBApplication.ACTION_QUIT);
|
||||
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
for (String prefKey : getPreferenceKeysWithSummary()) {
|
||||
final Preference pref = findPreference(prefKey);
|
||||
if (pref != null) {
|
||||
bindPreferenceSummaryToValue(pref);
|
||||
} else {
|
||||
LOG.error("Unknown preference key: " + prefKey + ", unable to display value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (isLanguageInvalid) {
|
||||
isLanguageInvalid = false;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses should reimplement this to return the keys of those
|
||||
* preferences which should print its values as a summary below the
|
||||
* preference name.
|
||||
*/
|
||||
protected String[] getPreferenceKeysWithSummary() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a preference's summary to its value. More specifically, when the
|
||||
* preference's value is changed, its summary (line of text below the
|
||||
* preference title) is updated to reflect the value. The summary is also
|
||||
* immediately updated upon calling this method. The exact display format is
|
||||
* dependent on the type of preference.
|
||||
*
|
||||
* @see #sBindPreferenceSummaryToValueListener
|
||||
*/
|
||||
private static void bindPreferenceSummaryToValue(Preference preference) {
|
||||
// Set the listener to watch for value changes.
|
||||
SimpleSetSummaryOnChangeListener listener = null;
|
||||
Preference.OnPreferenceChangeListener existingListener = preference.getOnPreferenceChangeListener();
|
||||
if (existingListener != null) {
|
||||
listener = new ExtraSetSummaryOnChangeListener(existingListener);
|
||||
} else {
|
||||
listener = sBindPreferenceSummaryToValueListener;
|
||||
}
|
||||
preference.setOnPreferenceChangeListener(listener);
|
||||
|
||||
// Trigger the listener immediately with the preference's current value.
|
||||
try {
|
||||
listener.updateSummary(preference,
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(preference.getContext())
|
||||
.getString(preference.getKey(), ""));
|
||||
} catch (ClassCastException cce) {
|
||||
//the preference is not a string, use the provided summary
|
||||
//TODO: it shows true/false instead of the xml summary
|
||||
listener.updateSummary(preference, preference.getSummary());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void setLanguage(Locale language, boolean invalidateLanguage) {
|
||||
if (invalidateLanguage) {
|
||||
isLanguageInvalid = true;
|
||||
}
|
||||
AndroidUtils.setLanguage(this, language);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
|
||||
public class AlarmDetails extends AbstractGBActivity {
|
||||
|
||||
private GBAlarm alarm;
|
||||
private TimePicker timePicker;
|
||||
private CheckedTextView cbSmartWakeup;
|
||||
private CheckedTextView cbMonday;
|
||||
private CheckedTextView cbTuesday;
|
||||
private CheckedTextView cbWednesday;
|
||||
private CheckedTextView cbThursday;
|
||||
private CheckedTextView cbFriday;
|
||||
private CheckedTextView cbSaturday;
|
||||
private CheckedTextView cbSunday;
|
||||
private GBDevice device;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_alarm_details);
|
||||
|
||||
alarm = getIntent().getParcelableExtra("alarm");
|
||||
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
|
||||
timePicker = (TimePicker) findViewById(R.id.alarm_time_picker);
|
||||
cbSmartWakeup = (CheckedTextView) findViewById(R.id.alarm_cb_smart_wakeup);
|
||||
cbMonday = (CheckedTextView) findViewById(R.id.alarm_cb_monday);
|
||||
cbTuesday = (CheckedTextView) findViewById(R.id.alarm_cb_tuesday);
|
||||
cbWednesday = (CheckedTextView) findViewById(R.id.alarm_cb_wednesday);
|
||||
cbThursday = (CheckedTextView) findViewById(R.id.alarm_cb_thursday);
|
||||
cbFriday = (CheckedTextView) findViewById(R.id.alarm_cb_friday);
|
||||
cbSaturday = (CheckedTextView) findViewById(R.id.alarm_cb_saturday);
|
||||
cbSunday = (CheckedTextView) findViewById(R.id.alarm_cb_sunday);
|
||||
|
||||
|
||||
cbSmartWakeup.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbMonday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbTuesday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbWednesday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbThursday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbFriday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbSaturday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
cbSunday.setOnClickListener(new View.OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
((CheckedTextView) v).toggle();
|
||||
}
|
||||
});
|
||||
|
||||
timePicker.setIs24HourView(DateFormat.is24HourFormat(GBApplication.getContext()));
|
||||
timePicker.setCurrentHour(alarm.getHour());
|
||||
timePicker.setCurrentMinute(alarm.getMinute());
|
||||
|
||||
cbSmartWakeup.setChecked(alarm.isSmartWakeup());
|
||||
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
|
||||
cbSmartWakeup.setVisibility(smartAlarmVisibility);
|
||||
|
||||
cbMonday.setChecked(alarm.getRepetition(GBAlarm.ALARM_MON));
|
||||
cbTuesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_TUE));
|
||||
cbWednesday.setChecked(alarm.getRepetition(GBAlarm.ALARM_WED));
|
||||
cbThursday.setChecked(alarm.getRepetition(GBAlarm.ALARM_THU));
|
||||
cbFriday.setChecked(alarm.getRepetition(GBAlarm.ALARM_FRI));
|
||||
cbSaturday.setChecked(alarm.getRepetition(GBAlarm.ALARM_SAT));
|
||||
cbSunday.setChecked(alarm.getRepetition(GBAlarm.ALARM_SUN));
|
||||
|
||||
}
|
||||
|
||||
private boolean supportsSmartWakeup() {
|
||||
if (device != null) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
return coordinator.supportsSmartWakeup(device);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// back button
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void updateAlarm() {
|
||||
alarm.setSmartWakeup(supportsSmartWakeup() && cbSmartWakeup.isChecked());
|
||||
alarm.setRepetition(cbMonday.isChecked(), cbTuesday.isChecked(), cbWednesday.isChecked(), cbThursday.isChecked(), cbFriday.isChecked(), cbSaturday.isChecked(), cbSunday.isChecked());
|
||||
alarm.setHour(timePicker.getCurrentHour());
|
||||
alarm.setMinute(timePicker.getCurrentMinute());
|
||||
alarm.store();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
updateAlarm();
|
||||
super.onPause();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class AndroidPairingActivity extends AbstractGBActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_android_pairing);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
|
||||
|
||||
|
||||
public class AppBlacklistActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppBlacklistActivity.class);
|
||||
|
||||
private AppBlacklistAdapter appBlacklistAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_appblacklist);
|
||||
RecyclerView appListView = (RecyclerView) findViewById(R.id.appListView);
|
||||
appListView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
appBlacklistAdapter = new AppBlacklistAdapter(R.layout.item_app_blacklist, this);
|
||||
|
||||
appListView.setAdapter(appBlacklistAdapter);
|
||||
|
||||
SearchView searchView = (SearchView) findViewById(R.id.appListViewSearch);
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
appBlacklistAdapter.getFilter().filter(newText);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceActivity;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
|
||||
* to be used with AppCompat.
|
||||
*
|
||||
* This technique can be used with an {@link android.app.Activity} class, not just
|
||||
* {@link android.preference.PreferenceActivity}.
|
||||
*/
|
||||
public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
|
||||
|
||||
private AppCompatDelegate mDelegate;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
getDelegate().installViewFactory();
|
||||
getDelegate().onCreate(savedInstanceState);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
getDelegate().onPostCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public ActionBar getSupportActionBar() {
|
||||
return getDelegate().getSupportActionBar();
|
||||
}
|
||||
|
||||
public void setSupportActionBar(@Nullable Toolbar toolbar) {
|
||||
getDelegate().setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MenuInflater getMenuInflater() {
|
||||
return getDelegate().getMenuInflater();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(@LayoutRes int layoutResID) {
|
||||
getDelegate().setContentView(layoutResID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view) {
|
||||
getDelegate().setContentView(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().setContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addContentView(View view, ViewGroup.LayoutParams params) {
|
||||
getDelegate().addContentView(view, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
getDelegate().onPostResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTitleChanged(CharSequence title, int color) {
|
||||
super.onTitleChanged(title, color);
|
||||
getDelegate().setTitle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
getDelegate().onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
getDelegate().onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
getDelegate().onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidateOptionsMenu() {
|
||||
getDelegate().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private AppCompatDelegate getDelegate() {
|
||||
if (mDelegate == null) {
|
||||
mDelegate = AppCompatDelegate.create(this, null);
|
||||
}
|
||||
return mDelegate;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.here.HereConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AudioEffect;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AudioEffectType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
|
||||
public class AudioSettingsActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AudioSettingsActivity.class);
|
||||
private SeekBar seekBar;
|
||||
private TextView volume_text;
|
||||
|
||||
// private boolean[] enabledEffects;
|
||||
|
||||
private int volume;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_audio_settings);
|
||||
LOG.debug("Create Audio Settings interface");
|
||||
|
||||
// FIXME: read enabled effects too
|
||||
// FIXME: and EQ values XD
|
||||
seekBar = (SeekBar) findViewById(R.id.volume_seekbar);
|
||||
volume_text = (TextView) findViewById(R.id.volume_seekbar_volume);
|
||||
|
||||
int startingVolume = 30; // FIXME: how do I read it?
|
||||
seekBar.setProgress(startingVolume);
|
||||
setdB(startingVolume);
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int position, boolean fromUser) {
|
||||
// HERE's volume range is from 0xdb (-30dB on their app) to 0xff (+6dB)
|
||||
// 0xff - 0xdb = 36 -> seekbar max value
|
||||
// volume = seekbar + Min_volume (= 0xdb = 219)
|
||||
volume = position + 219;
|
||||
// LOG.debug("Volume = " + (byte)volume + " = " + volume + "= " + position);
|
||||
AudioEffect eff = new AudioEffect(AudioEffectType.VOLUME, volume);
|
||||
GBApplication.deviceService().onSetAudioProperty(eff);
|
||||
// Show the volume on the UI in dB (range -30;6)
|
||||
setdB(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
}
|
||||
});
|
||||
|
||||
// FIXME: we need to read the current value and display this.
|
||||
// right now, I'm just showing 0 dB, Called after changeListener sets the value on
|
||||
// the device too.
|
||||
|
||||
Map<Integer, AudioEffectType> switchIds = new HashMap<Integer, AudioEffectType>();
|
||||
switchIds.put(R.id.audio_effect_echo, AudioEffectType.ECHO);
|
||||
switchIds.put(R.id.audio_effect_bassboost, AudioEffectType.BASSBOOST);
|
||||
switchIds.put(R.id.audio_effect_fuzz, AudioEffectType.FUZZ);
|
||||
switchIds.put(R.id.audio_effect_flange, AudioEffectType.FLANGE);
|
||||
switchIds.put(R.id.audio_effect_reverb, AudioEffectType.REVERB);
|
||||
switchIds.put(R.id.audio_effect_noisemask, AudioEffectType.NOISEMASK);
|
||||
switchIds.put(R.id.audio_effect_bitcrusher, AudioEffectType.BITCRUSHER);
|
||||
switchIds.put(R.id.audio_effect_chorus, AudioEffectType.CHORUS);
|
||||
|
||||
for (int id : switchIds.keySet()) {
|
||||
final AudioEffectType effect = switchIds.get(id);
|
||||
Switch s = (Switch) findViewById(id);
|
||||
s.setOnCheckedChangeListener(
|
||||
new CompoundButton.OnCheckedChangeListener() {
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
LOG.info("Toggled " + effect.name());
|
||||
applyEffect(new AudioEffect(effect, isChecked));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<String> defaultPresets = new ArrayList<String>();
|
||||
defaultPresets.add("8bit");
|
||||
defaultPresets.add("Example");
|
||||
|
||||
RadioGroup presets = (RadioGroup)findViewById(R.id.audio_presets);
|
||||
for (String preset : defaultPresets) {
|
||||
RadioButton radioButton = new RadioButton(getBaseContext());
|
||||
radioButton.setText(preset);
|
||||
presets.addView(radioButton);
|
||||
radioButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
RadioButton r = (RadioButton) findViewById(getCheckedPreset());
|
||||
applyPreset(r.getText().toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private int getCheckedPreset() {
|
||||
return ((RadioGroup)findViewById(R.id.audio_presets)).getCheckedRadioButtonId();
|
||||
}
|
||||
|
||||
private void setdB(int volume) {
|
||||
volume_text.setText(" " + (volume - 30) + " dB");
|
||||
}
|
||||
|
||||
private void applyPreset(String preset) {
|
||||
LOG.info("Applying preset " + preset);
|
||||
switch(preset) {
|
||||
case "8bit":
|
||||
LOG.debug("8bit");
|
||||
ArrayList<Object> values = new ArrayList<Object>();
|
||||
values.add("3byte");
|
||||
values.add(256.0f); // bits
|
||||
values.add(20000.0f); // freq
|
||||
AudioEffect effect = new AudioEffect(AudioEffectType.BITCRUSHER,
|
||||
true, values);
|
||||
GBApplication.deviceService().onSetAudioProperty(effect);
|
||||
|
||||
break;
|
||||
default:
|
||||
LOG.error("Missing preset! (Programming error!?");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyEffect(AudioEffect effect) {
|
||||
GBApplication.deviceService().onSetAudioProperty(effect);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/* Copyright (C) 2017 Carsten Pfeiffer, Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class CalBlacklistActivity extends AbstractGBActivity {
|
||||
|
||||
private final String[] EVENT_PROJECTION = new String[]{
|
||||
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
|
||||
CalendarContract.Calendars.CALENDAR_COLOR
|
||||
};
|
||||
private ArrayList<Calendar> calendarsArrayList;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_calblacklist);
|
||||
ListView calListView = (ListView) findViewById(R.id.calListView);
|
||||
|
||||
final Uri uri = CalendarContract.Calendars.CONTENT_URI;
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
|
||||
GB.toast(this, "Calendar permission not granted. Nothing to do.", Toast.LENGTH_SHORT, GB.WARN);
|
||||
return;
|
||||
}
|
||||
try (Cursor cur = getContentResolver().query(uri, EVENT_PROJECTION, null, null, null)) {
|
||||
calendarsArrayList = new ArrayList<>();
|
||||
while (cur != null && cur.moveToNext()) {
|
||||
calendarsArrayList.add(new Calendar(cur.getString(0), cur.getInt(1)));
|
||||
}
|
||||
}
|
||||
|
||||
ArrayAdapter<Calendar> calAdapter = new CalendarListAdapter(this, calendarsArrayList);
|
||||
calListView.setAdapter(calAdapter);
|
||||
calListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int i, long id) {
|
||||
Calendar item = calendarsArrayList.get(i);
|
||||
CheckBox selected = (CheckBox) view.findViewById(R.id.item_checkbox);
|
||||
toggleEntry(view);
|
||||
if (selected.isChecked()) {
|
||||
GBApplication.addCalendarToBlacklist(item.displayName);
|
||||
} else {
|
||||
GBApplication.removeFromCalendarBlacklist(item.displayName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void toggleEntry(View view) {
|
||||
TextView name = (TextView) view.findViewById(R.id.calendar_name);
|
||||
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
|
||||
|
||||
name.setPaintFlags(name.getPaintFlags() ^ Paint.STRIKE_THRU_TEXT_FLAG);
|
||||
checked.toggle();
|
||||
}
|
||||
|
||||
class Calendar {
|
||||
private final String displayName;
|
||||
private final int color;
|
||||
|
||||
public Calendar(String displayName, int color) {
|
||||
this.displayName = displayName;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
private class CalendarListAdapter extends ArrayAdapter<Calendar> {
|
||||
|
||||
CalendarListAdapter(@NonNull Context context, @NonNull List<Calendar> calendars) {
|
||||
super(context, 0, calendars);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
|
||||
Calendar item = getItem(position);
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) super.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
view = inflater.inflate(R.layout.item_cal_blacklist, parent, false);
|
||||
}
|
||||
|
||||
View color = view.findViewById(R.id.calendar_color);
|
||||
TextView name = (TextView) view.findViewById(R.id.calendar_name);
|
||||
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
|
||||
|
||||
if (GBApplication.calendarIsBlacklisted(item.displayName) && !checked.isChecked()) {
|
||||
toggleEntry(view);
|
||||
}
|
||||
color.setBackgroundColor(item.color);
|
||||
name.setText(item.displayName);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBAlarmListAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ALARMS;
|
||||
|
||||
|
||||
public class ConfigureAlarms extends AbstractGBActivity {
|
||||
|
||||
private static final int REQ_CONFIGURE_ALARM = 1;
|
||||
|
||||
private GBAlarmListAdapter mGBAlarmListAdapter;
|
||||
private Set<String> preferencesAlarmListSet;
|
||||
private boolean avoidSendAlarmsToDevice;
|
||||
private GBDevice device;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_configure_alarms);
|
||||
|
||||
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
|
||||
if (preferencesAlarmListSet.isEmpty()) {
|
||||
//initialize the preferences
|
||||
preferencesAlarmListSet = new HashSet<>(Arrays.asList(GBAlarm.DEFAULT_ALARMS));
|
||||
prefs.getPreferences().edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).apply();
|
||||
}
|
||||
|
||||
mGBAlarmListAdapter = new GBAlarmListAdapter(this, preferencesAlarmListSet);
|
||||
|
||||
RecyclerView alarmsRecyclerView = (RecyclerView) findViewById(R.id.alarm_list);
|
||||
alarmsRecyclerView.setHasFixedSize(true);
|
||||
alarmsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
alarmsRecyclerView.setAdapter(mGBAlarmListAdapter);
|
||||
updateAlarmsFromPrefs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (!avoidSendAlarmsToDevice) {
|
||||
sendAlarmsToDevice();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQ_CONFIGURE_ALARM) {
|
||||
avoidSendAlarmsToDevice = false;
|
||||
updateAlarmsFromPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAlarmsFromPrefs() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
preferencesAlarmListSet = prefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
|
||||
int reservedSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
|
||||
|
||||
mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet, reservedSlots);
|
||||
mGBAlarmListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// back button
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void configureAlarm(GBAlarm alarm) {
|
||||
avoidSendAlarmsToDevice = true;
|
||||
Intent startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
|
||||
startIntent.putExtra("alarm", alarm);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
|
||||
startActivityForResult(startIntent, REQ_CONFIGURE_ALARM);
|
||||
}
|
||||
|
||||
private GBDevice getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
private void sendAlarmsToDevice() {
|
||||
GBApplication.deviceService().onSetAlarms(mGBAlarmListAdapter.getAlarmList());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Canvas;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.view.GravityCompat;
|
||||
import android.support.v4.widget.DrawerLayout;
|
||||
import android.support.v7.app.ActionBarDrawerToggle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.cketti.library.changelog.ChangeLog;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
//TODO: extend AbstractGBActivity, but it requires actionbar that is not available
|
||||
public class ControlCenterv2 extends AppCompatActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener, GBActivity {
|
||||
|
||||
//needed for KK compatibility
|
||||
static {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
private DeviceManager deviceManager;
|
||||
private ImageView background;
|
||||
|
||||
private List<GBDevice> deviceList;
|
||||
private GBDeviceAdapterv2 mGBDeviceAdapter;
|
||||
private RecyclerView deviceListView;
|
||||
|
||||
private boolean isLanguageInvalid = false;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case GBApplication.ACTION_LANGUAGE_CHANGE:
|
||||
setLanguage(GBApplication.getLanguage(), true);
|
||||
break;
|
||||
case GBApplication.ACTION_QUIT:
|
||||
finish();
|
||||
break;
|
||||
case DeviceManager.ACTION_DEVICES_CHANGED:
|
||||
refreshPairedDevices();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_controlcenterv2);
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
||||
fab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
launchDiscoveryActivity();
|
||||
}
|
||||
});
|
||||
|
||||
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
|
||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
|
||||
this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close);
|
||||
drawer.setDrawerListener(toggle);
|
||||
toggle.syncState();
|
||||
|
||||
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
|
||||
//end of material design boilerplate
|
||||
deviceManager = ((GBApplication) getApplication()).getDeviceManager();
|
||||
|
||||
deviceListView = (RecyclerView) findViewById(R.id.deviceListView);
|
||||
deviceListView.setHasFixedSize(true);
|
||||
deviceListView.setLayoutManager(new LinearLayoutManager(this));
|
||||
background = (ImageView) findViewById(R.id.no_items_bg);
|
||||
|
||||
deviceList = deviceManager.getDevices();
|
||||
mGBDeviceAdapter = new GBDeviceAdapterv2(this, deviceList);
|
||||
|
||||
deviceListView.setAdapter(this.mGBDeviceAdapter);
|
||||
|
||||
ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.LEFT , ItemTouchHelper.RIGHT) {
|
||||
@Override
|
||||
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
if(dX>50)
|
||||
dX = 50;
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
||||
GB.toast(getBaseContext(), "onMove", Toast.LENGTH_LONG, GB.ERROR);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
GB.toast(getBaseContext(), "onSwiped", Toast.LENGTH_LONG, GB.ERROR);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
|
||||
RecyclerView.ViewHolder viewHolder, float dX, float dY,
|
||||
int actionState, boolean isCurrentlyActive) {
|
||||
}
|
||||
});
|
||||
|
||||
//uncomment to enable fixed-swipe to reveal more actions
|
||||
//swipeToDismissTouchHelper.attachToRecyclerView(deviceListView);
|
||||
|
||||
|
||||
registerForContextMenu(deviceListView);
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
|
||||
filterLocal.addAction(GBApplication.ACTION_QUIT);
|
||||
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
refreshPairedDevices();
|
||||
|
||||
/*
|
||||
* Ask for permission to intercept notifications on first run.
|
||||
*/
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
if (prefs.getBoolean("firstrun", true)) {
|
||||
prefs.getPreferences().edit().putBoolean("firstrun", false).apply();
|
||||
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
|
||||
startActivity(enableIntent);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
checkAndRequestPermissions();
|
||||
}
|
||||
|
||||
ChangeLog cl = createChangeLog();
|
||||
if (cl.isFirstRun()) {
|
||||
cl.getLogDialog().show();
|
||||
}
|
||||
|
||||
GBApplication.deviceService().start();
|
||||
|
||||
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class));
|
||||
} else {
|
||||
GBApplication.deviceService().requestDeviceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (isLanguageInvalid) {
|
||||
isLanguageInvalid = false;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForContextMenu(deviceListView);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
|
||||
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
||||
drawer.closeDrawer(GravityCompat.START);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
|
||||
|
||||
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
|
||||
drawer.closeDrawer(GravityCompat.START);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
Intent settingsIntent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(settingsIntent);
|
||||
return true;
|
||||
case R.id.action_debug:
|
||||
Intent debugIntent = new Intent(this, DebugActivity.class);
|
||||
startActivity(debugIntent);
|
||||
return true;
|
||||
case R.id.action_db_management:
|
||||
Intent dbIntent = new Intent(this, DbManagementActivity.class);
|
||||
startActivity(dbIntent);
|
||||
return true;
|
||||
case R.id.action_quit:
|
||||
GBApplication.quit();
|
||||
return true;
|
||||
case R.id.donation_link:
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
return true;
|
||||
case R.id.external_changelog:
|
||||
ChangeLog cl = createChangeLog();
|
||||
cl.getFullLogDialog().show();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ChangeLog createChangeLog() {
|
||||
String css = ChangeLog.DEFAULT_CSS;
|
||||
css += "body { "
|
||||
+ "color: " + AndroidUtils.getTextColorHex(getBaseContext()) + "; "
|
||||
+ "background-color: " + AndroidUtils.getBackgroundColorHex(getBaseContext()) + ";" +
|
||||
"}";
|
||||
return new ChangeLog(this, css);
|
||||
}
|
||||
private void launchDiscoveryActivity() {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class));
|
||||
}
|
||||
|
||||
private void refreshPairedDevices() {
|
||||
List<GBDevice> deviceList = deviceManager.getDevices();
|
||||
if (deviceList.isEmpty()) {
|
||||
background.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
background.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
mGBDeviceAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private void checkAndRequestPermissions() {
|
||||
List<String> wantedPermissions = new ArrayList<>();
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_CONTACTS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.CALL_PHONE);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_PHONE_STATE);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_SMS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.SEND_SMS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_CALENDAR);
|
||||
|
||||
if (!wantedPermissions.isEmpty())
|
||||
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);
|
||||
}
|
||||
|
||||
public void setLanguage(Locale language, boolean invalidateLanguage) {
|
||||
if (invalidateLanguage) {
|
||||
isLanguageInvalid = true;
|
||||
}
|
||||
AndroidUtils.setLanguage(this, language);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ImportExportSharedPreferences;
|
||||
|
||||
|
||||
public class DbManagementActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DbManagementActivity.class);
|
||||
private static SharedPreferences sharedPrefs;
|
||||
private ImportExportSharedPreferences shared_file = new ImportExportSharedPreferences();
|
||||
|
||||
private Button exportDBButton;
|
||||
private Button importDBButton;
|
||||
private Button deleteOldActivityDBButton;
|
||||
private Button deleteDBButton;
|
||||
private TextView dbPath;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_db_management);
|
||||
|
||||
dbPath = (TextView) findViewById(R.id.activity_db_management_path);
|
||||
dbPath.setText(getExternalPath());
|
||||
|
||||
exportDBButton = (Button) findViewById(R.id.exportDBButton);
|
||||
exportDBButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
exportDB();
|
||||
}
|
||||
});
|
||||
importDBButton = (Button) findViewById(R.id.importDBButton);
|
||||
importDBButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
importDB();
|
||||
}
|
||||
});
|
||||
|
||||
int oldDBVisibility = hasOldActivityDatabase() ? View.VISIBLE : View.GONE;
|
||||
|
||||
deleteOldActivityDBButton = (Button) findViewById(R.id.deleteOldActivityDB);
|
||||
deleteOldActivityDBButton.setVisibility(oldDBVisibility);
|
||||
deleteOldActivityDBButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
deleteOldActivityDbFile();
|
||||
}
|
||||
});
|
||||
|
||||
deleteDBButton = (Button) findViewById(R.id.emptyDBButton);
|
||||
deleteDBButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
deleteActivityDatabase();
|
||||
}
|
||||
});
|
||||
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
}
|
||||
|
||||
private boolean hasOldActivityDatabase() {
|
||||
return new DBHelper(this).existsDB("ActivityDatabase");
|
||||
}
|
||||
|
||||
private String getExternalPath() {
|
||||
try {
|
||||
return FileUtils.getExternalFilesDir().getAbsolutePath();
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("Unable to get external files dir", ex);
|
||||
}
|
||||
return getString(R.string.dbmanagementactivvity_cannot_access_export_path);
|
||||
}
|
||||
|
||||
private void exportShared() {
|
||||
// BEGIN EXAMPLE
|
||||
File myPath = null;
|
||||
try {
|
||||
myPath = FileUtils.getExternalFilesDir();
|
||||
File myFile = new File(myPath, "Export_preference");
|
||||
shared_file.exportToFile(sharedPrefs,myFile,null);
|
||||
} catch (IOException ex) {
|
||||
GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_shared, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void importShared() {
|
||||
// BEGIN EXAMPLE
|
||||
File myPath = null;
|
||||
try {
|
||||
myPath = FileUtils.getExternalFilesDir();
|
||||
File myFile = new File(myPath, "Export_preference");
|
||||
shared_file.importFromFile(sharedPrefs,myFile );
|
||||
} catch (Exception ex) {
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportDB() {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
exportShared();
|
||||
DBHelper helper = new DBHelper(this);
|
||||
File dir = FileUtils.getExternalFilesDir();
|
||||
File destFile = helper.exportDB(dbHandler, dir);
|
||||
GB.toast(this, getString(R.string.dbmanagementactivity_exported_to, destFile.getAbsolutePath()), Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(this, getString(R.string.dbmanagementactivity_error_exporting_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void importDB() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.dbmanagementactivity_import_data_title)
|
||||
.setMessage(R.string.dbmanagementactivity_overwrite_database_confirmation)
|
||||
.setPositiveButton(R.string.dbmanagementactivity_overwrite, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
importShared();
|
||||
DBHelper helper = new DBHelper(DbManagementActivity.this);
|
||||
File dir = FileUtils.getExternalFilesDir();
|
||||
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
|
||||
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
|
||||
helper.importDB(dbHandler, sourceFile);
|
||||
helper.validateDB(sqLiteOpenHelper);
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_import_successful), Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_error_importing_db, ex.getMessage()), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void deleteActivityDatabase() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.dbmanagementactivity_delete_activity_data_title)
|
||||
.setMessage(R.string.dbmanagementactivity_really_delete_entire_db)
|
||||
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (GBApplication.deleteActivityDatabase(DbManagementActivity.this)) {
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_database_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
|
||||
} else {
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void deleteOldActivityDbFile() {
|
||||
new AlertDialog.Builder(this).setCancelable(true);
|
||||
new AlertDialog.Builder(this).setTitle(R.string.dbmanagementactivity_delete_old_activity_db);
|
||||
new AlertDialog.Builder(this).setMessage(R.string.dbmanagementactivity_delete_old_activitydb_confirmation);
|
||||
new AlertDialog.Builder(this).setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (GBApplication.deleteOldActivityDatabase(DbManagementActivity.this)) {
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_successfully_deleted), Toast.LENGTH_SHORT, GB.INFO);
|
||||
} else {
|
||||
GB.toast(DbManagementActivity.this, getString(R.string.dbmanagementactivity_old_activity_db_deletion_failed), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
}
|
||||
});
|
||||
new AlertDialog.Builder(this).setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
}
|
||||
});
|
||||
new AlertDialog.Builder(this).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, ivanovlev, Kasha, Lem Dulfo, Steffen Liebergeld
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.RemoteInput;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class DebugActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DebugActivity.class);
|
||||
|
||||
private static final String EXTRA_REPLY = "reply";
|
||||
private static final String ACTION_REPLY
|
||||
= "nodomain.freeyourgadget.gadgetbridge.DebugActivity.action.reply";
|
||||
|
||||
private Spinner sendTypeSpinner;
|
||||
private Button sendButton;
|
||||
private Button incomingCallButton;
|
||||
private Button outgoingCallButton;
|
||||
private Button startCallButton;
|
||||
private Button endCallButton;
|
||||
private Button testNotificationButton;
|
||||
private Button setMusicInfoButton;
|
||||
private Button setTimeButton;
|
||||
private Button rebootButton;
|
||||
private Button HeartRateButton;
|
||||
private Button testNewFunctionalityButton;
|
||||
|
||||
private EditText editContent;
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_REPLY: {
|
||||
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
CharSequence reply = remoteInput.getCharSequence(EXTRA_REPLY);
|
||||
LOG.info("got wearable reply: " + reply);
|
||||
GB.toast(context, "got wearable reply: " + reply, Toast.LENGTH_SHORT, GB.INFO);
|
||||
break;
|
||||
}
|
||||
case DeviceService.ACTION_HEARTRATE_MEASUREMENT: {
|
||||
int hrValue = intent.getIntExtra(DeviceService.EXTRA_HEART_RATE_VALUE, -1);
|
||||
GB.toast(DebugActivity.this, "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_debug);
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_REPLY);
|
||||
filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
registerReceiver(mReceiver, filter); // for ACTION_REPLY
|
||||
|
||||
editContent = (EditText) findViewById(R.id.editContent);
|
||||
|
||||
ArrayList<String> spinnerArray = new ArrayList<>();
|
||||
for (NotificationType notificationType : NotificationType.values()) {
|
||||
spinnerArray.add(notificationType.name());
|
||||
}
|
||||
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, spinnerArray);
|
||||
sendTypeSpinner = (Spinner) findViewById(R.id.sendTypeSpinner);
|
||||
sendTypeSpinner.setAdapter(spinnerArrayAdapter);
|
||||
|
||||
sendButton = (Button) findViewById(R.id.sendButton);
|
||||
sendButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
NotificationSpec notificationSpec = new NotificationSpec();
|
||||
String testString = editContent.getText().toString();
|
||||
notificationSpec.phoneNumber = testString;
|
||||
notificationSpec.body = testString;
|
||||
notificationSpec.sender = testString;
|
||||
notificationSpec.subject = testString;
|
||||
notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
|
||||
notificationSpec.pebbleColor = notificationSpec.type.color;
|
||||
notificationSpec.id = -1;
|
||||
GBApplication.deviceService().onNotification(notificationSpec);
|
||||
}
|
||||
});
|
||||
|
||||
incomingCallButton = (Button) findViewById(R.id.incomingCallButton);
|
||||
incomingCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_INCOMING;
|
||||
callSpec.number = editContent.getText().toString();
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
});
|
||||
outgoingCallButton = (Button) findViewById(R.id.outgoingCallButton);
|
||||
outgoingCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_OUTGOING;
|
||||
callSpec.number = editContent.getText().toString();
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
});
|
||||
|
||||
startCallButton = (Button) findViewById(R.id.startCallButton);
|
||||
startCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_START;
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
});
|
||||
endCallButton = (Button) findViewById(R.id.endCallButton);
|
||||
endCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_END;
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
});
|
||||
|
||||
rebootButton = (Button) findViewById(R.id.rebootButton);
|
||||
rebootButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GBApplication.deviceService().onReboot();
|
||||
}
|
||||
});
|
||||
HeartRateButton = (Button) findViewById(R.id.HearRateButton);
|
||||
HeartRateButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GB.toast("Measuring heart rate, please wait...", Toast.LENGTH_LONG, GB.INFO);
|
||||
GBApplication.deviceService().onHeartRateTest();
|
||||
}
|
||||
});
|
||||
|
||||
setMusicInfoButton = (Button) findViewById(R.id.setMusicInfoButton);
|
||||
setMusicInfoButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
MusicSpec musicSpec = new MusicSpec();
|
||||
String testString = editContent.getText().toString();
|
||||
musicSpec.artist = testString + "(artist)";
|
||||
musicSpec.album = testString + "(album)";
|
||||
musicSpec.track = testString + "(track)";
|
||||
musicSpec.duration = 10;
|
||||
musicSpec.trackCount = 5;
|
||||
musicSpec.trackNr = 2;
|
||||
|
||||
GBApplication.deviceService().onSetMusicInfo(musicSpec);
|
||||
|
||||
MusicStateSpec stateSpec = new MusicStateSpec();
|
||||
stateSpec.position = 0;
|
||||
stateSpec.state = 0x01; // playing
|
||||
stateSpec.playRate = 100;
|
||||
stateSpec.repeat = 1;
|
||||
stateSpec.shuffle = 1;
|
||||
|
||||
GBApplication.deviceService().onSetMusicState(stateSpec);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeButton = (Button) findViewById(R.id.setTimeButton);
|
||||
setTimeButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GBApplication.deviceService().onSetTime();
|
||||
}
|
||||
});
|
||||
|
||||
testNotificationButton = (Button) findViewById(R.id.testNotificationButton);
|
||||
testNotificationButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
testNotification();
|
||||
}
|
||||
});
|
||||
|
||||
testNewFunctionalityButton = (Button) findViewById(R.id.testNewFunctionality);
|
||||
testNewFunctionalityButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
testNewFunctionality();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void testNewFunctionality() {
|
||||
GBApplication.deviceService().onTestNewFunction();
|
||||
}
|
||||
|
||||
private void testNotification() {
|
||||
Intent notificationIntent = new Intent(getApplicationContext(), DebugActivity.class);
|
||||
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0,
|
||||
notificationIntent, 0);
|
||||
|
||||
NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(EXTRA_REPLY)
|
||||
.build();
|
||||
|
||||
Intent replyIntent = new Intent(ACTION_REPLY);
|
||||
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(this, 0, replyIntent, 0);
|
||||
|
||||
NotificationCompat.Action action =
|
||||
new NotificationCompat.Action.Builder(android.R.drawable.ic_input_add, "Reply", replyPendingIntent)
|
||||
.addRemoteInput(remoteInput)
|
||||
.build();
|
||||
|
||||
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender().addAction(action);
|
||||
|
||||
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this)
|
||||
.setContentTitle(getString(R.string.test_notification))
|
||||
.setContentText(getString(R.string.this_is_a_test_notification_from_gadgetbridge))
|
||||
.setTicker(getString(R.string.this_is_a_test_notification_from_gadgetbridge))
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.extend(wearableExtender);
|
||||
|
||||
nManager.notify((int) System.currentTimeMillis(), ncomp.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
||||
public interface DeviceSelectionCallback {
|
||||
void invoke(GBDevice device);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,614 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, JohnnySun, Lem Dulfo, Uwe Hermann
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.bluetooth.le.ScanRecord;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
import android.bluetooth.le.ScanSettings;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY;
|
||||
import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY;
|
||||
|
||||
public class DiscoveryActivity extends AbstractGBActivity implements AdapterView.OnItemClickListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivity.class);
|
||||
private static final long SCAN_DURATION = 60000; // 60s
|
||||
|
||||
private ScanCallback newLeScanCallback = null;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
|
||||
if (isScanning != Scanning.SCANNING_BTLE && isScanning != Scanning.SCANNING_NEW_BTLE) {
|
||||
discoveryStarted(Scanning.SCANNING_BT);
|
||||
}
|
||||
break;
|
||||
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// continue with LE scan, if available
|
||||
if (isScanning == Scanning.SCANNING_BT) {
|
||||
checkAndRequestLocationPermission();
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
startDiscovery(Scanning.SCANNING_NEW_BTLE);
|
||||
} else {
|
||||
startDiscovery(Scanning.SCANNING_BTLE);
|
||||
}
|
||||
} else {
|
||||
discoveryFinished();
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
case BluetoothAdapter.ACTION_STATE_CHANGED:
|
||||
int oldState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF);
|
||||
int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
|
||||
bluetoothStateChanged(oldState, newState);
|
||||
break;
|
||||
case BluetoothDevice.ACTION_FOUND: {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
|
||||
handleDeviceFound(device, rssi);
|
||||
break;
|
||||
}
|
||||
case BluetoothDevice.ACTION_UUID: {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
|
||||
Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
|
||||
ParcelUuid[] uuids2 = AndroidUtils.toParcelUUids(uuids);
|
||||
handleDeviceFound(device, rssi, uuids2);
|
||||
break;
|
||||
}
|
||||
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (device != null && bondingDevice != null && device.getAddress().equals(bondingDevice.getMacAddress())) {
|
||||
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
handleDeviceBonded();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void connectAndFinish(GBDevice device) {
|
||||
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_trying_to_connect_to, device.getName()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
GBApplication.deviceService().connect(device, true);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void createBond(final GBDeviceCandidate deviceCandidate, int bondingStyle) {
|
||||
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
|
||||
return;
|
||||
}
|
||||
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_ASK) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setCancelable(true)
|
||||
.setTitle(DiscoveryActivity.this.getString(R.string.discovery_pair_title, deviceCandidate.getName()))
|
||||
.setMessage(DiscoveryActivity.this.getString(R.string.discovery_pair_question))
|
||||
.setPositiveButton(DiscoveryActivity.this.getString(R.string.discovery_yes_pair), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
doCreatePair(deviceCandidate);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.discovery_dont_pair, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
|
||||
connectAndFinish(device);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
doCreatePair(deviceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
private void doCreatePair(GBDeviceCandidate deviceCandidate) {
|
||||
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_attempting_to_pair, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
if (deviceCandidate.getDevice().createBond()) {
|
||||
// async, wait for bonding event to finish this activity
|
||||
LOG.info("Bonding in progress...");
|
||||
bondingDevice = deviceCandidate;
|
||||
} else {
|
||||
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_bonding_failed_immediately, deviceCandidate.getName()), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDeviceBonded() {
|
||||
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_successfully_bonded, bondingDevice.getName()), Toast.LENGTH_SHORT, GB.INFO);
|
||||
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(bondingDevice);
|
||||
connectAndFinish(device);
|
||||
}
|
||||
|
||||
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
|
||||
@Override
|
||||
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
|
||||
LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
|
||||
logMessageContent(scanRecord);
|
||||
handleDeviceFound(device, (short) rssi);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// why use a method to get callback?
|
||||
// because this callback need API >= 21
|
||||
// we cant add @TARGETAPI("Lollipop") at class header
|
||||
// so use a method with SDK check to return this callback
|
||||
private ScanCallback getScanCallback() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
newLeScanCallback = new ScanCallback() {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
super.onScanResult(callbackType, result);
|
||||
try {
|
||||
ScanRecord scanRecord = result.getScanRecord();
|
||||
ParcelUuid[] uuids = null;
|
||||
if (scanRecord != null) {
|
||||
//logMessageContent(scanRecord.getBytes());
|
||||
List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
|
||||
if (serviceUuids != null) {
|
||||
uuids = serviceUuids.toArray(new ParcelUuid[0]);
|
||||
}
|
||||
}
|
||||
LOG.warn(result.getDevice().getName() + ": " +
|
||||
((scanRecord != null) ? scanRecord.getBytes().length : -1));
|
||||
handleDeviceFound(result.getDevice(), (short) result.getRssi(), uuids);
|
||||
} catch (NullPointerException e) {
|
||||
LOG.warn("Error handling scan result", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return newLeScanCallback;
|
||||
}
|
||||
|
||||
public void logMessageContent(byte[] value) {
|
||||
if (value != null) {
|
||||
for (byte b : value) {
|
||||
LOG.warn("DATA: " + String.format("0x%2x", b) + " - " + (char) (b & 0xff));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable stopRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
stopDiscovery();
|
||||
}
|
||||
};
|
||||
|
||||
private ProgressBar progressView;
|
||||
private BluetoothAdapter adapter;
|
||||
private final ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
|
||||
private DeviceCandidateAdapter cadidateListAdapter;
|
||||
private Button startButton;
|
||||
private Scanning isScanning = Scanning.SCANNING_OFF;
|
||||
private GBDeviceCandidate bondingDevice;
|
||||
|
||||
private enum Scanning {
|
||||
SCANNING_BT,
|
||||
SCANNING_BTLE,
|
||||
SCANNING_NEW_BTLE,
|
||||
SCANNING_OFF
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_discovery);
|
||||
startButton = (Button) findViewById(R.id.discovery_start);
|
||||
startButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onStartButtonClick(startButton);
|
||||
}
|
||||
});
|
||||
|
||||
progressView = (ProgressBar) findViewById(R.id.discovery_progressbar);
|
||||
progressView.setProgress(0);
|
||||
progressView.setIndeterminate(true);
|
||||
progressView.setVisibility(View.GONE);
|
||||
ListView deviceCandidatesView = (ListView) findViewById(R.id.discovery_deviceCandidatesView);
|
||||
|
||||
cadidateListAdapter = new DeviceCandidateAdapter(this, deviceCandidates);
|
||||
deviceCandidatesView.setAdapter(cadidateListAdapter);
|
||||
deviceCandidatesView.setOnItemClickListener(this);
|
||||
|
||||
IntentFilter bluetoothIntents = new IntentFilter();
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_UUID);
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
|
||||
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
|
||||
bluetoothIntents.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
|
||||
|
||||
registerReceiver(bluetoothReceiver, bluetoothIntents);
|
||||
|
||||
startDiscovery();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
ArrayList<Parcelable> restoredCandidates = savedInstanceState.getParcelableArrayList("deviceCandidates");
|
||||
if (restoredCandidates != null) {
|
||||
deviceCandidates.clear();
|
||||
for (Parcelable p : restoredCandidates) {
|
||||
deviceCandidates.add((GBDeviceCandidate) p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onStartButtonClick(View button) {
|
||||
LOG.debug("Start Button clicked");
|
||||
if (isScanning()) {
|
||||
stopDiscovery();
|
||||
} else {
|
||||
startDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
try {
|
||||
unregisterReceiver(bluetoothReceiver);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Tried to unregister Bluetooth Receiver that wasn't registered.");
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void handleDeviceFound(BluetoothDevice device, short rssi) {
|
||||
ParcelUuid[] uuids = device.getUuids();
|
||||
if (uuids == null) {
|
||||
if (device.fetchUuidsWithSdp()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleDeviceFound(device, rssi, uuids);
|
||||
}
|
||||
|
||||
|
||||
private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
|
||||
LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
|
||||
if (LOG.isDebugEnabled()) {
|
||||
if (uuids != null && uuids.length > 0) {
|
||||
for (ParcelUuid uuid : uuids) {
|
||||
LOG.debug(" supports uuid: " + uuid.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
|
||||
return; // ignore already bonded devices
|
||||
}
|
||||
|
||||
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
|
||||
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
|
||||
if (deviceType.isSupported()) {
|
||||
candidate.setDeviceType(deviceType);
|
||||
LOG.info("Recognized supported device: " + candidate);
|
||||
int index = deviceCandidates.indexOf(candidate);
|
||||
if (index >= 0) {
|
||||
deviceCandidates.set(index, candidate); // replace
|
||||
} else {
|
||||
deviceCandidates.add(candidate);
|
||||
}
|
||||
cadidateListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre: bluetooth is available, enabled and scanning is off.
|
||||
* Post: BT is discovering
|
||||
*/
|
||||
private void startDiscovery() {
|
||||
if (isScanning()) {
|
||||
LOG.warn("Not starting discovery, because already scanning.");
|
||||
return;
|
||||
}
|
||||
startDiscovery(Scanning.SCANNING_BT);
|
||||
}
|
||||
|
||||
private void startDiscovery(Scanning what) {
|
||||
LOG.info("Starting discovery: " + what);
|
||||
discoveryStarted(what); // just to make sure
|
||||
if (ensureBluetoothReady()) {
|
||||
if (what == Scanning.SCANNING_BT) {
|
||||
startBTDiscovery();
|
||||
} else if (what == Scanning.SCANNING_BTLE) {
|
||||
if (GB.supportsBluetoothLE()) {
|
||||
startBTLEDiscovery();
|
||||
} else {
|
||||
discoveryFinished();
|
||||
}
|
||||
} else if (what == Scanning.SCANNING_NEW_BTLE) {
|
||||
if (GB.supportsBluetoothLE()) {
|
||||
startNEWBTLEDiscovery();
|
||||
} else {
|
||||
discoveryFinished();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
discoveryFinished();
|
||||
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isScanning() {
|
||||
return isScanning != Scanning.SCANNING_OFF;
|
||||
}
|
||||
|
||||
private void stopDiscovery() {
|
||||
LOG.info("Stopping discovery");
|
||||
if (isScanning()) {
|
||||
Scanning wasScanning = isScanning;
|
||||
// unfortunately, we don't always get a call back when stopping the scan, so
|
||||
// we do it manually; BEFORE stopping the scan!
|
||||
discoveryFinished();
|
||||
|
||||
if (wasScanning == Scanning.SCANNING_BT) {
|
||||
stopBTDiscovery();
|
||||
} else if (wasScanning == Scanning.SCANNING_BTLE) {
|
||||
stopBTLEDiscovery();
|
||||
} else if (wasScanning == Scanning.SCANNING_NEW_BTLE) {
|
||||
stopNewBTLEDiscovery();
|
||||
}
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopBTLEDiscovery() {
|
||||
adapter.stopLeScan(leScanCallback);
|
||||
}
|
||||
|
||||
private void stopBTDiscovery() {
|
||||
adapter.cancelDiscovery();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void stopNewBTLEDiscovery() {
|
||||
adapter.getBluetoothLeScanner().stopScan(newLeScanCallback);
|
||||
}
|
||||
|
||||
private void bluetoothStateChanged(int oldState, int newState) {
|
||||
discoveryFinished();
|
||||
if (newState == BluetoothAdapter.STATE_ON) {
|
||||
this.adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
startButton.setEnabled(true);
|
||||
} else {
|
||||
this.adapter = null;
|
||||
startButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void discoveryFinished() {
|
||||
isScanning = Scanning.SCANNING_OFF;
|
||||
progressView.setVisibility(View.GONE);
|
||||
startButton.setText(getString(R.string.discovery_start_scanning));
|
||||
}
|
||||
|
||||
private void discoveryStarted(Scanning what) {
|
||||
isScanning = what;
|
||||
progressView.setVisibility(View.VISIBLE);
|
||||
startButton.setText(getString(R.string.discovery_stop_scanning));
|
||||
}
|
||||
|
||||
private boolean ensureBluetoothReady() {
|
||||
boolean available = checkBluetoothAvailable();
|
||||
startButton.setEnabled(available);
|
||||
if (available) {
|
||||
adapter.cancelDiscovery();
|
||||
// must not return the result of cancelDiscovery()
|
||||
// appears to return false when currently not scanning
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkBluetoothAvailable() {
|
||||
BluetoothManager bluetoothService = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
|
||||
if (bluetoothService == null) {
|
||||
LOG.warn("No bluetooth available");
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
BluetoothAdapter adapter = bluetoothService.getAdapter();
|
||||
if (adapter == null) {
|
||||
LOG.warn("No bluetooth available");
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
if (!adapter.isEnabled()) {
|
||||
LOG.warn("Bluetooth not enabled");
|
||||
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
startActivity(enableBtIntent);
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
this.adapter = adapter;
|
||||
return true;
|
||||
}
|
||||
|
||||
// New BTLE Discovery use startScan (List<ScanFilter> filters,
|
||||
// ScanSettings settings,
|
||||
// ScanCallback callback)
|
||||
// It's added on API21
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void startNEWBTLEDiscovery() {
|
||||
// Only use new API when user uses Lollipop+ device
|
||||
LOG.info("Start New BTLE Discovery");
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
|
||||
adapter.getBluetoothLeScanner().startScan(getScanFilters(), getScanSettings(), getScanCallback());
|
||||
}
|
||||
|
||||
private List<ScanFilter> getScanFilters() {
|
||||
List<ScanFilter> allFilters = new ArrayList<>();
|
||||
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
|
||||
allFilters.addAll(coordinator.createBLEScanFilters());
|
||||
}
|
||||
return allFilters;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private ScanSettings getScanSettings() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return new ScanSettings.Builder()
|
||||
.setScanMode(SCAN_MODE_LOW_LATENCY)
|
||||
.setMatchMode(MATCH_MODE_STICKY)
|
||||
.build();
|
||||
} else {
|
||||
return new ScanSettings.Builder()
|
||||
.setScanMode(SCAN_MODE_LOW_LATENCY)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private void startBTLEDiscovery() {
|
||||
LOG.info("Starting BTLE Discovery");
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
|
||||
adapter.startLeScan(leScanCallback);
|
||||
}
|
||||
|
||||
private void startBTDiscovery() {
|
||||
LOG.info("Starting BT Discovery");
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
|
||||
adapter.startDiscovery();
|
||||
}
|
||||
|
||||
private void checkAndRequestLocationPermission() {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private Message getPostMessage(Runnable runnable) {
|
||||
Message m = Message.obtain(handler, runnable);
|
||||
m.obj = runnable;
|
||||
return m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
|
||||
if (deviceCandidate == null) {
|
||||
LOG.error("Device candidate clicked, but item not found");
|
||||
return;
|
||||
}
|
||||
|
||||
stopDiscovery();
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
|
||||
LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass());
|
||||
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
|
||||
if (pairingActivity != null) {
|
||||
Intent intent = new Intent(this, pairingActivity);
|
||||
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
|
||||
int bondingStyle = coordinator.getBondingStyle(device);
|
||||
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
|
||||
LOG.info("No bonding needed, according to coordinator, so connecting right away");
|
||||
connectAndFinish(device);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BluetoothDevice btDevice = adapter.getRemoteDevice(deviceCandidate.getMacAddress());
|
||||
switch (btDevice.getBondState()) {
|
||||
case BluetoothDevice.BOND_NONE: {
|
||||
createBond(deviceCandidate, bondingStyle);
|
||||
break;
|
||||
}
|
||||
case BluetoothDevice.BOND_BONDING:
|
||||
// async, wait for bonding event to finish this activity
|
||||
bondingDevice = deviceCandidate;
|
||||
break;
|
||||
case BluetoothDevice.BOND_BONDED:
|
||||
handleDeviceBonded();
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error pairing device: " + deviceCandidate.getMacAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,392 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo, Uwe Hermann
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.Criteria;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Scanner;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class ExternalPebbleJSActivity extends AbstractGBActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalPebbleJSActivity.class);
|
||||
|
||||
private UUID appUuid;
|
||||
private Uri confUri;
|
||||
private GBDevice mGBDevice = null;
|
||||
private WebView myWebView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
appUuid = (UUID) extras.getSerializable(DeviceService.EXTRA_APP_UUID);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
|
||||
setContentView(R.layout.activity_external_pebble_js);
|
||||
|
||||
myWebView = (WebView) findViewById(R.id.configureWebview);
|
||||
myWebView.clearCache(true);
|
||||
myWebView.setWebViewClient(new GBWebClient());
|
||||
myWebView.setWebChromeClient(new GBChromeClient());
|
||||
WebSettings webSettings = myWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
//needed to access the DOM
|
||||
webSettings.setDomStorageEnabled(true);
|
||||
//needed for localstorage
|
||||
webSettings.setDatabaseEnabled(true);
|
||||
|
||||
JSInterface gbJSInterface = new JSInterface(this);
|
||||
myWebView.addJavascriptInterface(gbJSInterface, "GBjs");
|
||||
|
||||
myWebView.loadUrl("file:///android_asset/app_config/configure.html");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent incoming) {
|
||||
incoming.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
super.onNewIntent(incoming);
|
||||
confUri = incoming.getData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
String queryString = "";
|
||||
|
||||
if (confUri != null) {
|
||||
//getting back with configuration data
|
||||
try {
|
||||
appUuid = UUID.fromString(confUri.getHost());
|
||||
queryString = confUri.getEncodedQuery();
|
||||
} catch (IllegalArgumentException e) {
|
||||
GB.toast("returned uri: " + confUri.toString(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
myWebView.loadUrl("file:///android_asset/app_config/configure.html?" + queryString);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private JSONObject getAppConfigurationKeys() {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, appUuid.toString() + ".json");
|
||||
if (configurationFile.exists()) {
|
||||
String jsonstring = FileUtils.getStringFromFile(configurationFile);
|
||||
JSONObject json = new JSONObject(jsonstring);
|
||||
return json.getJSONObject("appKeys");
|
||||
}
|
||||
} catch (IOException | JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isLocationEnabledForWatchApp() {
|
||||
return true; //as long as we don't give watchapp internet access it's not a problem
|
||||
}
|
||||
|
||||
private class GBChromeClient extends WebChromeClient {
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
||||
if (ConsoleMessage.MessageLevel.ERROR.equals(consoleMessage.messageLevel())) {
|
||||
GB.toast(consoleMessage.message(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
//TODO: show error page
|
||||
}
|
||||
return super.onConsoleMessage(consoleMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class GBWebClient extends WebViewClient {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(i);
|
||||
} else {
|
||||
url = url.replaceFirst("^pebblejs://close#", "file:///android_asset/app_config/configure.html?config=true&json=");
|
||||
view.loadUrl(url);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class JSInterface {
|
||||
|
||||
Context mContext;
|
||||
|
||||
public JSInterface(Context c) {
|
||||
mContext = c;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void gbLog(String msg) {
|
||||
Log.d("WEBVIEW", msg);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void sendAppMessage(String msg) {
|
||||
LOG.debug("from WEBVIEW: " + msg);
|
||||
JSONObject knownKeys = getAppConfigurationKeys();
|
||||
|
||||
try {
|
||||
JSONObject in = new JSONObject(msg);
|
||||
JSONObject out = new JSONObject();
|
||||
String inKey, outKey;
|
||||
boolean passKey;
|
||||
for (Iterator<String> key = in.keys(); key.hasNext(); ) {
|
||||
passKey = false;
|
||||
inKey = key.next();
|
||||
outKey = null;
|
||||
int pebbleAppIndex = knownKeys.optInt(inKey, -1);
|
||||
if (pebbleAppIndex != -1) {
|
||||
passKey = true;
|
||||
outKey = String.valueOf(pebbleAppIndex);
|
||||
} else {
|
||||
//do not discard integer keys (see https://developer.pebble.com/guides/communication/using-pebblekit-js/ )
|
||||
Scanner scanner = new Scanner(inKey);
|
||||
if (scanner.hasNextInt() && inKey.equals("" + scanner.nextInt())) {
|
||||
passKey = true;
|
||||
outKey = inKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (passKey) {
|
||||
Object obj = in.get(inKey);
|
||||
out.put(outKey, obj);
|
||||
} else {
|
||||
GB.toast("Discarded key " + inKey + ", not found in the local configuration and is not an integer key.", Toast.LENGTH_SHORT, GB.WARN);
|
||||
}
|
||||
|
||||
}
|
||||
LOG.info(out.toString());
|
||||
GBApplication.deviceService().onAppConfiguration(appUuid, out.toString());
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getActiveWatchInfo() {
|
||||
JSONObject wi = new JSONObject();
|
||||
try {
|
||||
wi.put("firmware", mGBDevice.getFirmwareVersion());
|
||||
wi.put("platform", PebbleUtils.getPlatformName(mGBDevice.getModel()));
|
||||
wi.put("model", PebbleUtils.getModel(mGBDevice.getModel()));
|
||||
//TODO: use real info
|
||||
wi.put("language", "en");
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//Json not supported apparently, we need to cast back and forth
|
||||
return wi.toString();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppConfigurationFile() {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, appUuid.toString() + "_config.js");
|
||||
if (configurationFile.exists()) {
|
||||
return "file:///" + configurationFile.getAbsolutePath();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppStoredPreset() {
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File configurationFile = new File(destDir, appUuid.toString() + "_preset.json");
|
||||
if (configurationFile.exists()) {
|
||||
return FileUtils.getStringFromFile(configurationFile);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
GB.toast("Error reading presets", Toast.LENGTH_LONG, GB.ERROR);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void saveAppStoredPreset(String msg) {
|
||||
Writer writer;
|
||||
|
||||
try {
|
||||
File destDir = new File(FileUtils.getExternalFilesDir() + "/pbw-cache");
|
||||
File presetsFile = new File(destDir, appUuid.toString() + "_preset.json");
|
||||
writer = new BufferedWriter(new FileWriter(presetsFile));
|
||||
writer.write(msg);
|
||||
writer.close();
|
||||
GB.toast("Presets stored", Toast.LENGTH_SHORT, GB.INFO);
|
||||
} catch (IOException e) {
|
||||
GB.toast("Error storing presets", Toast.LENGTH_LONG, GB.ERROR);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppUUID() {
|
||||
return appUuid.toString();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getAppLocalstoragePrefix() {
|
||||
String prefix = mGBDevice.getAddress() + appUuid.toString();
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
byte[] bytes = prefix.getBytes("UTF-8");
|
||||
digest.update(bytes, 0, bytes.length);
|
||||
bytes = digest.digest();
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
sb.append(String.format("%02X", bytes[i]));
|
||||
}
|
||||
return sb.toString().toLowerCase();
|
||||
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public String getWatchToken() {
|
||||
//specification says: A string that is guaranteed to be identical for each Pebble device for the same app across different mobile devices. The token is unique to your app and cannot be used to track Pebble devices across applications. see https://developer.pebble.com/docs/js/Pebble/
|
||||
return "gb" + appUuid.toString();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void closeActivity() {
|
||||
NavUtils.navigateUpFromSameTask((ExternalPebbleJSActivity) mContext);
|
||||
}
|
||||
|
||||
|
||||
@JavascriptInterface
|
||||
public String getCurrentPosition() {
|
||||
if (!isLocationEnabledForWatchApp()) {
|
||||
return "";
|
||||
}
|
||||
//we need to override this because the coarse location is not enough for the android webview, we should add the permission for fine location.
|
||||
JSONObject geoPosition = new JSONObject();
|
||||
JSONObject coords = new JSONObject();
|
||||
try {
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
geoPosition.put("timestamp", (System.currentTimeMillis() / 1000) - 86400); //let JS know this value is really old
|
||||
|
||||
coords.put("latitude", prefs.getFloat("location_latitude", 0));
|
||||
coords.put("longitude", prefs.getFloat("location_longitude", 0));
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
|
||||
prefs.getBoolean("use_updated_location_if_available", false)) {
|
||||
LocationManager locationManager = (LocationManager) getApplicationContext().getSystemService(Context.LOCATION_SERVICE);
|
||||
Criteria criteria = new Criteria();
|
||||
String provider = locationManager.getBestProvider(criteria, false);
|
||||
if (provider != null) {
|
||||
Location lastKnownLocation = locationManager.getLastKnownLocation(provider);
|
||||
if (lastKnownLocation != null) {
|
||||
geoPosition.put("timestamp", lastKnownLocation.getTime());
|
||||
|
||||
coords.put("latitude", (float) lastKnownLocation.getLatitude());
|
||||
coords.put("longitude", (float) lastKnownLocation.getLongitude());
|
||||
coords.put("accuracy", lastKnownLocation.getAccuracy());
|
||||
coords.put("altitude", lastKnownLocation.getAltitude());
|
||||
coords.put("speed", lastKnownLocation.getSpeed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
geoPosition.put("coords", coords);
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return geoPosition.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class FwAppInstallerActivity extends AbstractGBActivity implements InstallActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FwAppInstallerActivity.class);
|
||||
private static final String ITEM_DETAILS = "details";
|
||||
|
||||
private TextView fwAppInstallTextView;
|
||||
private Button installButton;
|
||||
private Uri uri;
|
||||
private GBDevice device;
|
||||
private InstallHandler installHandler;
|
||||
private boolean mayConnect;
|
||||
|
||||
private ProgressBar mProgressBar;
|
||||
private ListView itemListView;
|
||||
private final List<ItemWithDetails> mItems = new ArrayList<>();
|
||||
private ItemWithDetailsAdapter mItemAdapter;
|
||||
|
||||
private ListView detailsListView;
|
||||
private ItemWithDetailsAdapter mDetailsItemAdapter;
|
||||
private ArrayList<ItemWithDetails> mDetails = new ArrayList<>();
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
|
||||
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (device != null) {
|
||||
refreshBusyState(device);
|
||||
if (!device.isInitialized()) {
|
||||
setInstallEnabled(false);
|
||||
if (mayConnect) {
|
||||
GB.toast(FwAppInstallerActivity.this, getString(R.string.connecting), Toast.LENGTH_SHORT, GB.INFO);
|
||||
connect();
|
||||
} else {
|
||||
setInfoText(getString(R.string.fwappinstaller_connection_state, device.getStateString()));
|
||||
}
|
||||
} else {
|
||||
validateInstallation();
|
||||
}
|
||||
}
|
||||
} else if (GB.ACTION_DISPLAY_MESSAGE.equals(action)) {
|
||||
String message = intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE);
|
||||
int severity = intent.getIntExtra(GB.DISPLAY_MESSAGE_SEVERITY, GB.INFO);
|
||||
addMessage(message, severity);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void refreshBusyState(GBDevice dev) {
|
||||
if (dev.isConnecting() || dev.isBusy()) {
|
||||
mProgressBar.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
boolean wasBusy = mProgressBar.getVisibility() != View.GONE;
|
||||
if (wasBusy) {
|
||||
mProgressBar.setVisibility(View.GONE);
|
||||
// done!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
mayConnect = false; // only do that once per #onCreate
|
||||
GBApplication.deviceService().connect(device);
|
||||
}
|
||||
|
||||
private void validateInstallation() {
|
||||
if (installHandler != null) {
|
||||
installHandler.validateInstallation(this, device);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_appinstaller);
|
||||
|
||||
GBDevice dev = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (dev != null) {
|
||||
device = dev;
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
mDetails = savedInstanceState.getParcelableArrayList(ITEM_DETAILS);
|
||||
if (mDetails == null) {
|
||||
mDetails = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
mayConnect = true;
|
||||
itemListView = (ListView) findViewById(R.id.itemListView);
|
||||
mItemAdapter = new ItemWithDetailsAdapter(this, mItems);
|
||||
itemListView.setAdapter(mItemAdapter);
|
||||
fwAppInstallTextView = (TextView) findViewById(R.id.infoTextView);
|
||||
installButton = (Button) findViewById(R.id.installButton);
|
||||
mProgressBar = (ProgressBar) findViewById(R.id.installProgressBar);
|
||||
detailsListView = (ListView) findViewById(R.id.detailsListView);
|
||||
mDetailsItemAdapter = new ItemWithDetailsAdapter(this, mDetails);
|
||||
mDetailsItemAdapter.setSize(ItemWithDetailsAdapter.SIZE_SMALL);
|
||||
detailsListView.setAdapter(mDetailsItemAdapter);
|
||||
setInstallEnabled(false);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
filter.addAction(GB.ACTION_DISPLAY_MESSAGE);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
installButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
setInstallEnabled(false);
|
||||
installHandler.onStartInstall(device);
|
||||
GBApplication.deviceService().onInstallApp(uri);
|
||||
}
|
||||
});
|
||||
|
||||
uri = getIntent().getData();
|
||||
if (uri == null) { //for "share" intent
|
||||
uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
installHandler = findInstallHandlerFor(uri);
|
||||
if (installHandler == null) {
|
||||
setInfoText(getString(R.string.installer_activity_unable_to_find_handler));
|
||||
} else {
|
||||
setInfoText(getString(R.string.installer_activity_wait_while_determining_status));
|
||||
|
||||
// needed to get the device
|
||||
if (device == null || !device.isConnected()) {
|
||||
connect();
|
||||
} else {
|
||||
GBApplication.deviceService().requestDeviceInfo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelableArrayList(ITEM_DETAILS, mDetails);
|
||||
}
|
||||
|
||||
private InstallHandler findInstallHandlerFor(Uri uri) {
|
||||
for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
|
||||
InstallHandler handler = coordinator.findInstallHandler(uri, this);
|
||||
if (handler != null) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInfoText(String text) {
|
||||
fwAppInstallTextView.setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getInfoText() {
|
||||
return fwAppInstallTextView.getText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstallEnabled(boolean enable) {
|
||||
boolean enabled = device != null && device.isConnected() && enable;
|
||||
installButton.setEnabled(enabled);
|
||||
installButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearInstallItems() {
|
||||
mItems.clear();
|
||||
mItemAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstallItem(ItemWithDetails item) {
|
||||
mItems.clear();
|
||||
mItems.add(item);
|
||||
mItemAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void addMessage(String message, int severity) {
|
||||
mDetails.add(new GenericItem(message));
|
||||
mDetailsItemAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public interface GBActivity {
|
||||
void setLanguage(Locale language, boolean invalidateLanguage);
|
||||
void setTheme(int themeId);
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* Copyright (C) 2016-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
public class HeartRateUtils {
|
||||
public static final int MAX_HEART_RATE_VALUE = 250;
|
||||
public static final int MIN_HEART_RATE_VALUE = 0;
|
||||
/**
|
||||
* The maxiumum gap between two hr measurements in which
|
||||
* we interpolate between the measurements. Otherwise, two
|
||||
* distinct measurements will be shown.
|
||||
*
|
||||
* Value is in minutes
|
||||
*/
|
||||
public static final int MAX_HR_MEASUREMENTS_GAP_MINUTES = 10;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
|
||||
public interface InstallActivity {
|
||||
CharSequence getInfoText();
|
||||
|
||||
void setInfoText(String text);
|
||||
|
||||
void setInstallEnabled(boolean enable);
|
||||
|
||||
void clearInstallItems();
|
||||
|
||||
void setInstallItem(ItemWithDetails item);
|
||||
|
||||
}
|
|
@ -0,0 +1,356 @@
|
|||
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti, Normano64
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.location.Criteria;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_STEPS_GOAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_WEIGHT_KG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_YEAR_OF_BIRTH;
|
||||
|
||||
public class SettingsActivity extends AbstractSettingsActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsActivity.class);
|
||||
|
||||
public static final String PREF_MEASUREMENT_SYSTEM = "measurement_system";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
|
||||
Preference pref = findPreference("notifications_generic");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
pref = findPreference("pref_key_miband");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent enableIntent = new Intent(SettingsActivity.this, MiBandPreferencesActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
pref = findPreference("pref_key_blacklist");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent enableIntent = new Intent(SettingsActivity.this, AppBlacklistActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
pref = findPreference("pref_key_blacklist_calendars");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent enableIntent = new Intent(SettingsActivity.this, CalBlacklistActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
pref = findPreference("pebble_emu_addr");
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
|
||||
preference.setSummary(newVal.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
pref = findPreference("pebble_emu_port");
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
|
||||
preference.setSummary(newVal.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
pref = findPreference("log_to_file");
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
boolean doEnable = Boolean.TRUE.equals(newVal);
|
||||
try {
|
||||
if (doEnable) {
|
||||
FileUtils.getExternalFilesDir(); // ensures that it is created
|
||||
}
|
||||
GBApplication.setupLogging(doEnable);
|
||||
} catch (IOException ex) {
|
||||
GB.toast(getApplicationContext(),
|
||||
getString(R.string.error_creating_directory_for_logfiles, ex.getLocalizedMessage()),
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
pref = findPreference("language");
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
String newLang = newVal.toString();
|
||||
try {
|
||||
GBApplication.setLanguage(newLang);
|
||||
recreate();
|
||||
} catch (Exception ex) {
|
||||
GB.toast(getApplicationContext(),
|
||||
"Error setting language: " + ex.getLocalizedMessage(),
|
||||
Toast.LENGTH_LONG,
|
||||
GB.ERROR,
|
||||
ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
final Preference unit = findPreference(PREF_MEASUREMENT_SYSTEM);
|
||||
unit.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GBApplication.deviceService().onSendConfiguration(PREF_MEASUREMENT_SYSTEM);
|
||||
}
|
||||
});
|
||||
preference.setSummary(newVal.toString());
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!GBApplication.isRunningMarshmallowOrLater()) {
|
||||
pref = findPreference("notification_filter");
|
||||
PreferenceCategory category = (PreferenceCategory) findPreference("pref_key_notifications");
|
||||
category.removePreference(pref);
|
||||
}
|
||||
|
||||
pref = findPreference("location_aquire");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(SettingsActivity.this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, 0);
|
||||
}
|
||||
|
||||
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
|
||||
Criteria criteria = new Criteria();
|
||||
String provider = locationManager.getBestProvider(criteria, false);
|
||||
if (provider != null) {
|
||||
Location location = locationManager.getLastKnownLocation(provider);
|
||||
if (location != null) {
|
||||
setLocationPreferences(location);
|
||||
} else {
|
||||
locationManager.requestSingleUpdate(provider, new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
setLocationPreferences(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
LOG.info("provider status changed to " + status + " (" + provider + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
LOG.info("provider enabled (" + provider + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
LOG.info("provider disabled (" + provider + ")");
|
||||
GB.toast(SettingsActivity.this, getString(R.string.toast_enable_networklocationprovider), 3000, 0);
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("No location provider found, did you deny location permission?");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
pref = findPreference("canned_messages_dismisscall_send");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
ArrayList<String> messages = new ArrayList<>();
|
||||
for (int i = 1; i <= 16; i++) {
|
||||
String message = prefs.getString("canned_message_dismisscall_" + i, null);
|
||||
if (message != null && !message.equals("")) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
CannedMessagesSpec cannedMessagesSpec = new CannedMessagesSpec();
|
||||
cannedMessagesSpec.type = CannedMessagesSpec.TYPE_MISSEDCALLS;
|
||||
cannedMessagesSpec.cannedMessages = messages.toArray(new String[messages.size()]);
|
||||
GBApplication.deviceService().onSetCannedMessages(cannedMessagesSpec);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Get all receivers of Media Buttons
|
||||
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
|
||||
|
||||
PackageManager pm = getPackageManager();
|
||||
List<ResolveInfo> mediaReceivers = pm.queryBroadcastReceivers(mediaButtonIntent,
|
||||
PackageManager.GET_INTENT_FILTERS | PackageManager.GET_RESOLVED_FILTER);
|
||||
|
||||
|
||||
CharSequence[] newEntries = new CharSequence[mediaReceivers.size() + 1];
|
||||
CharSequence[] newValues = new CharSequence[mediaReceivers.size() + 1];
|
||||
newEntries[0] = getString(R.string.pref_default);
|
||||
newValues[0] = "default";
|
||||
|
||||
int i = 1;
|
||||
for (ResolveInfo resolveInfo : mediaReceivers) {
|
||||
newEntries[i] = resolveInfo.activityInfo.loadLabel(pm);
|
||||
newValues[i] = resolveInfo.activityInfo.packageName;
|
||||
i++;
|
||||
}
|
||||
|
||||
final ListPreference audioPlayer = (ListPreference) findPreference("audio_player");
|
||||
audioPlayer.setEntries(newEntries);
|
||||
audioPlayer.setEntryValues(newValues);
|
||||
audioPlayer.setDefaultValue(newValues[0]);
|
||||
}
|
||||
|
||||
/*
|
||||
* delayed execution so that the preferences are applied first
|
||||
*/
|
||||
private void invokeLater(Runnable runnable) {
|
||||
getListView().post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getPreferenceKeysWithSummary() {
|
||||
return new String[]{
|
||||
"pebble_emu_addr",
|
||||
"pebble_emu_port",
|
||||
"pebble_reconnect_attempts",
|
||||
"location_latitude",
|
||||
"location_longitude",
|
||||
"canned_reply_suffix",
|
||||
"canned_reply_1",
|
||||
"canned_reply_2",
|
||||
"canned_reply_3",
|
||||
"canned_reply_4",
|
||||
"canned_reply_5",
|
||||
"canned_reply_6",
|
||||
"canned_reply_7",
|
||||
"canned_reply_8",
|
||||
"canned_reply_9",
|
||||
"canned_reply_10",
|
||||
"canned_reply_11",
|
||||
"canned_reply_12",
|
||||
"canned_reply_13",
|
||||
"canned_reply_14",
|
||||
"canned_reply_15",
|
||||
"canned_reply_16",
|
||||
"canned_message_dismisscall_1",
|
||||
"canned_message_dismisscall_2",
|
||||
"canned_message_dismisscall_3",
|
||||
"canned_message_dismisscall_4",
|
||||
"canned_message_dismisscall_5",
|
||||
"canned_message_dismisscall_6",
|
||||
"canned_message_dismisscall_7",
|
||||
"canned_message_dismisscall_8",
|
||||
"canned_message_dismisscall_9",
|
||||
"canned_message_dismisscall_10",
|
||||
"canned_message_dismisscall_11",
|
||||
"canned_message_dismisscall_12",
|
||||
"canned_message_dismisscall_13",
|
||||
"canned_message_dismisscall_14",
|
||||
"canned_message_dismisscall_15",
|
||||
"canned_message_dismisscall_16",
|
||||
PREF_USER_YEAR_OF_BIRTH,
|
||||
PREF_USER_HEIGHT_CM,
|
||||
PREF_USER_WEIGHT_KG,
|
||||
PREF_USER_SLEEP_DURATION,
|
||||
PREF_USER_STEPS_GOAL,
|
||||
};
|
||||
}
|
||||
|
||||
private void setLocationPreferences(Location location) {
|
||||
String latitude = String.format(Locale.US, "%.6g", location.getLatitude());
|
||||
String longitude = String.format(Locale.US, "%.6g", location.getLongitude());
|
||||
LOG.info("got location. Lat: " + latitude + " Lng: " + longitude);
|
||||
GB.toast(SettingsActivity.this, getString(R.string.toast_aqurired_networklocation), 2000, 0);
|
||||
EditTextPreference pref_latitude = (EditTextPreference) findPreference("location_latitude");
|
||||
EditTextPreference pref_longitude = (EditTextPreference) findPreference("location_longitude");
|
||||
pref_latitude.setText(latitude);
|
||||
pref_longitude.setText(longitude);
|
||||
pref_latitude.setSummary(latitude);
|
||||
pref_longitude.setSummary(longitude);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.SeekBar;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
|
||||
public class VibrationActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VibrationActivity.class);
|
||||
private SeekBar seekBar;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_vibration);
|
||||
|
||||
seekBar = (SeekBar) findViewById(R.id.vibration_seekbar);
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (progress > 0) { // 1-16
|
||||
progress = progress * 16 - 1; // max 255
|
||||
}
|
||||
GBApplication.deviceService().onSetConstantVibration(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,487 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ExternalPebbleJSActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAppAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||
|
||||
|
||||
public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
public static final String ACTION_REFRESH_APPLIST
|
||||
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
|
||||
|
||||
private ItemTouchHelper appManagementTouchHelper;
|
||||
|
||||
protected abstract List<GBDeviceApp> getSystemAppsInCategory();
|
||||
|
||||
protected abstract String getSortFilename();
|
||||
|
||||
protected abstract boolean isCacheManager();
|
||||
|
||||
protected abstract boolean filterApp(GBDeviceApp gbDeviceApp);
|
||||
|
||||
public void startDragging(RecyclerView.ViewHolder viewHolder) {
|
||||
appManagementTouchHelper.startDrag(viewHolder);
|
||||
}
|
||||
|
||||
protected void onChangedAppOrder() {
|
||||
List<UUID> uuidList = new ArrayList<>();
|
||||
for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getAppList()) {
|
||||
uuidList.add(gbDeviceApp.getUUID());
|
||||
}
|
||||
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuidList);
|
||||
}
|
||||
|
||||
protected void refreshList() {
|
||||
appList.clear();
|
||||
ArrayList uuids = AppManagerActivity.getUuidsFromFile(getSortFilename());
|
||||
List<GBDeviceApp> systemApps = getSystemAppsInCategory();
|
||||
boolean needsRewrite = false;
|
||||
for (GBDeviceApp systemApp : systemApps) {
|
||||
if (!uuids.contains(systemApp.getUUID())) {
|
||||
uuids.add(systemApp.getUUID());
|
||||
needsRewrite = true;
|
||||
}
|
||||
}
|
||||
if (needsRewrite) {
|
||||
AppManagerActivity.rewriteAppOrderFile(getSortFilename(), uuids);
|
||||
}
|
||||
appList.addAll(getCachedApps(uuids));
|
||||
}
|
||||
|
||||
private void refreshListFromPebble(Intent intent) {
|
||||
appList.clear();
|
||||
int appCount = intent.getIntExtra("app_count", 0);
|
||||
for (Integer i = 0; i < appCount; i++) {
|
||||
String appName = intent.getStringExtra("app_name" + i.toString());
|
||||
String appCreator = intent.getStringExtra("app_creator" + i.toString());
|
||||
UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
|
||||
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
|
||||
|
||||
GBDeviceApp app = new GBDeviceApp(uuid, appName, appCreator, "", appType);
|
||||
app.setOnDevice(true);
|
||||
if (filterApp(app)) {
|
||||
appList.add(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(ACTION_REFRESH_APPLIST)) {
|
||||
if (intent.hasExtra("app_count")) {
|
||||
LOG.info("got app info from pebble");
|
||||
if (!isCacheManager()) {
|
||||
LOG.info("will refresh list based on data from pebble");
|
||||
refreshListFromPebble(intent);
|
||||
}
|
||||
} else if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3 || isCacheManager()) {
|
||||
refreshList();
|
||||
}
|
||||
mGBDeviceAppAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected final List<GBDeviceApp> appList = new ArrayList<>();
|
||||
private GBDeviceAppAdapter mGBDeviceAppAdapter;
|
||||
protected GBDevice mGBDevice = null;
|
||||
|
||||
protected List<GBDeviceApp> getCachedApps(List<UUID> uuids) {
|
||||
List<GBDeviceApp> cachedAppList = new ArrayList<>();
|
||||
File cachePath;
|
||||
try {
|
||||
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not get external dir while reading pbw cache.");
|
||||
return cachedAppList;
|
||||
}
|
||||
|
||||
File[] files;
|
||||
if (uuids == null) {
|
||||
files = cachePath.listFiles();
|
||||
} else {
|
||||
files = new File[uuids.size()];
|
||||
int index = 0;
|
||||
for (UUID uuid : uuids) {
|
||||
files[index++] = new File(uuid.toString() + ".pbw");
|
||||
}
|
||||
}
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.getName().endsWith(".pbw")) {
|
||||
String baseName = file.getName().substring(0, file.getName().length() - 4);
|
||||
//metadata
|
||||
File jsonFile = new File(cachePath, baseName + ".json");
|
||||
//configuration
|
||||
File configFile = new File(cachePath, baseName + "_config.js");
|
||||
try {
|
||||
String jsonstring = FileUtils.getStringFromFile(jsonFile);
|
||||
JSONObject json = new JSONObject(jsonstring);
|
||||
cachedAppList.add(new GBDeviceApp(json, configFile.exists()));
|
||||
} catch (Exception e) {
|
||||
LOG.info("could not read json file for " + baseName);
|
||||
//FIXME: this is really ugly, if we do not find system uuids in pbw cache add them manually. Also duplicated code
|
||||
switch (baseName) {
|
||||
case "8f3c8686-31a1-4f5f-91f5-01600c9bdc59":
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
|
||||
break;
|
||||
case "1f03293d-47af-4f28-b960-f2b02a6dd757":
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
break;
|
||||
case "b2cae818-10f8-46df-ad2b-98ad2254a3c1":
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
break;
|
||||
case "67a32d95-ef69-46d4-a0b9-854cc62f97f9":
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
break;
|
||||
case "18e443ce-38fd-47c8-84d5-6d0c775fbe55":
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
break;
|
||||
case "0863fc6a-66c5-4f62-ab8a-82ed00a98b5d":
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
break;
|
||||
}
|
||||
/*
|
||||
else if (baseName.equals("4dab81a6-d2fc-458a-992c-7a1f3b96a970")) {
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
} else if (baseName.equals("cf1e816a-9db0-4511-bbb8-f60c48ca8fac")) {
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
}
|
||||
*/
|
||||
if (mGBDevice != null) {
|
||||
if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
|
||||
if (baseName.equals(PebbleProtocol.UUID_PEBBLE_HEALTH.toString())) {
|
||||
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
|
||||
if (baseName.equals(PebbleProtocol.UUID_WORKOUT.toString())) {
|
||||
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
|
||||
if (baseName.equals("3af858c3-16cb-4561-91e7-f1ad2df8725f")) {
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
|
||||
}
|
||||
if (baseName.equals(PebbleProtocol.UUID_WEATHER.toString())) {
|
||||
cachedAppList.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uuids == null) {
|
||||
cachedAppList.add(new GBDeviceApp(UUID.fromString(baseName), baseName, "N/A", "", GBDeviceApp.Type.UNKNOWN));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedAppList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
mGBDevice = ((AppManagerActivity) getActivity()).getGBDevice();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_REFRESH_APPLIST);
|
||||
|
||||
LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver, filter);
|
||||
|
||||
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3) {
|
||||
GBApplication.deviceService().onAppInfoReq();
|
||||
if (isCacheManager()) {
|
||||
refreshList();
|
||||
}
|
||||
} else {
|
||||
refreshList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
final FloatingActionButton appListFab = ((FloatingActionButton) getActivity().findViewById(R.id.fab));
|
||||
View rootView = inflater.inflate(R.layout.activity_appmanager, container, false);
|
||||
|
||||
RecyclerView appListView = (RecyclerView) (rootView.findViewById(R.id.appListView));
|
||||
|
||||
appListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy > 0) {
|
||||
appListFab.hide();
|
||||
} else if (dy < 0) {
|
||||
appListFab.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
appListView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
mGBDeviceAppAdapter = new GBDeviceAppAdapter(appList, R.layout.item_pebble_watchapp, this);
|
||||
appListView.setAdapter(mGBDeviceAppAdapter);
|
||||
|
||||
ItemTouchHelper.Callback appItemTouchHelperCallback = new AppItemTouchHelperCallback(mGBDeviceAppAdapter);
|
||||
appManagementTouchHelper = new ItemTouchHelper(appItemTouchHelperCallback);
|
||||
|
||||
appManagementTouchHelper.attachToRecyclerView(appListView);
|
||||
return rootView;
|
||||
}
|
||||
|
||||
protected void sendOrderToDevice(String concatFilename) {
|
||||
ArrayList<UUID> uuids = new ArrayList<>();
|
||||
for (GBDeviceApp gbDeviceApp : mGBDeviceAppAdapter.getAppList()) {
|
||||
uuids.add(gbDeviceApp.getUUID());
|
||||
}
|
||||
if (concatFilename != null) {
|
||||
ArrayList<UUID> concatUuids = AppManagerActivity.getUuidsFromFile(concatFilename);
|
||||
uuids.addAll(concatUuids);
|
||||
}
|
||||
GBApplication.deviceService().onAppReorder(uuids.toArray(new UUID[uuids.size()]));
|
||||
}
|
||||
|
||||
public boolean openPopupMenu(View view, GBDeviceApp deviceApp) {
|
||||
PopupMenu popupMenu = new PopupMenu(getContext(), view);
|
||||
popupMenu.getMenuInflater().inflate(R.menu.appmanager_context, popupMenu.getMenu());
|
||||
Menu menu = popupMenu.getMenu();
|
||||
final GBDeviceApp selectedApp = deviceApp;
|
||||
|
||||
if (!selectedApp.isInCache()) {
|
||||
menu.removeItem(R.id.appmanager_app_reinstall);
|
||||
menu.removeItem(R.id.appmanager_app_delete_cache);
|
||||
}
|
||||
if (!PebbleProtocol.UUID_PEBBLE_HEALTH.equals(selectedApp.getUUID())) {
|
||||
menu.removeItem(R.id.appmanager_health_activate);
|
||||
menu.removeItem(R.id.appmanager_health_deactivate);
|
||||
}
|
||||
if (!PebbleProtocol.UUID_WORKOUT.equals(selectedApp.getUUID())) {
|
||||
menu.removeItem(R.id.appmanager_hrm_activate);
|
||||
menu.removeItem(R.id.appmanager_hrm_deactivate);
|
||||
}
|
||||
if (!PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
|
||||
menu.removeItem(R.id.appmanager_weather_activate);
|
||||
menu.removeItem(R.id.appmanager_weather_deactivate);
|
||||
menu.removeItem(R.id.appmanager_weather_install_provider);
|
||||
}
|
||||
if (selectedApp.getType() == GBDeviceApp.Type.APP_SYSTEM || selectedApp.getType() == GBDeviceApp.Type.WATCHFACE_SYSTEM) {
|
||||
menu.removeItem(R.id.appmanager_app_delete);
|
||||
}
|
||||
if (!selectedApp.isConfigurable()) {
|
||||
menu.removeItem(R.id.appmanager_app_configure);
|
||||
}
|
||||
|
||||
if (PebbleProtocol.UUID_WEATHER.equals(selectedApp.getUUID())) {
|
||||
PackageManager pm = getActivity().getPackageManager();
|
||||
try {
|
||||
pm.getPackageInfo("ru.gelin.android.weather.notification", PackageManager.GET_ACTIVITIES);
|
||||
menu.removeItem(R.id.appmanager_weather_install_provider);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
menu.removeItem(R.id.appmanager_weather_activate);
|
||||
menu.removeItem(R.id.appmanager_weather_deactivate);
|
||||
}
|
||||
}
|
||||
|
||||
switch (selectedApp.getType()) {
|
||||
case WATCHFACE:
|
||||
case APP_GENERIC:
|
||||
case APP_ACTIVITYTRACKER:
|
||||
break;
|
||||
default:
|
||||
menu.removeItem(R.id.appmanager_app_openinstore);
|
||||
}
|
||||
//menu.setHeaderTitle(selectedApp.getName());
|
||||
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
return onContextItemSelected(item, selectedApp);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
popupMenu.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onContextItemSelected(MenuItem item, GBDeviceApp selectedApp) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.appmanager_app_delete_cache:
|
||||
String baseName;
|
||||
try {
|
||||
baseName = FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID();
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not get external dir while trying to access pbw cache.");
|
||||
return true;
|
||||
}
|
||||
|
||||
String[] suffixToDelete = new String[]{".pbw", ".json", "_config.js", "_preset.json"};
|
||||
|
||||
for (String suffix : suffixToDelete) {
|
||||
File fileToDelete = new File(baseName + suffix);
|
||||
if (!fileToDelete.delete()) {
|
||||
LOG.warn("could not delete file from pbw cache: " + fileToDelete.toString());
|
||||
} else {
|
||||
LOG.info("deleted file: " + fileToDelete.toString());
|
||||
}
|
||||
}
|
||||
AppManagerActivity.deleteFromAppOrderFile("pbwcacheorder.txt", selectedApp.getUUID()); // FIXME: only if successful
|
||||
// fall through
|
||||
case R.id.appmanager_app_delete:
|
||||
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 3) {
|
||||
AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchapps", selectedApp.getUUID()); // FIXME: only if successful
|
||||
AppManagerActivity.deleteFromAppOrderFile(mGBDevice.getAddress() + ".watchfaces", selectedApp.getUUID()); // FIXME: only if successful
|
||||
Intent refreshIntent = new Intent(AbstractAppManagerFragment.ACTION_REFRESH_APPLIST);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(refreshIntent);
|
||||
}
|
||||
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
|
||||
return true;
|
||||
case R.id.appmanager_app_reinstall:
|
||||
File cachePath;
|
||||
try {
|
||||
cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache/" + selectedApp.getUUID() + ".pbw");
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not get external dir while trying to access pbw cache.");
|
||||
return true;
|
||||
}
|
||||
GBApplication.deviceService().onInstallApp(Uri.fromFile(cachePath));
|
||||
return true;
|
||||
case R.id.appmanager_health_activate:
|
||||
GBApplication.deviceService().onInstallApp(Uri.parse("fake://health"));
|
||||
return true;
|
||||
case R.id.appmanager_hrm_activate:
|
||||
GBApplication.deviceService().onInstallApp(Uri.parse("fake://hrm"));
|
||||
return true;
|
||||
case R.id.appmanager_weather_activate:
|
||||
GBApplication.deviceService().onInstallApp(Uri.parse("fake://weather"));
|
||||
return true;
|
||||
case R.id.appmanager_health_deactivate:
|
||||
case R.id.appmanager_hrm_deactivate:
|
||||
case R.id.appmanager_weather_deactivate:
|
||||
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
|
||||
return true;
|
||||
case R.id.appmanager_weather_install_provider:
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/app/ru.gelin.android.weather.notification")));
|
||||
return true;
|
||||
case R.id.appmanager_app_configure:
|
||||
GBApplication.deviceService().onAppStart(selectedApp.getUUID(), true);
|
||||
|
||||
Intent startIntent = new Intent(getContext().getApplicationContext(), ExternalPebbleJSActivity.class);
|
||||
startIntent.putExtra(DeviceService.EXTRA_APP_UUID, selectedApp.getUUID());
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, mGBDevice);
|
||||
startActivity(startIntent);
|
||||
return true;
|
||||
case R.id.appmanager_app_openinstore:
|
||||
String url = "https://apps.getpebble.com/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1?query=" + selectedApp.getName() + "&dev_settings=true";
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(url));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public class AppItemTouchHelperCallback extends ItemTouchHelper.Callback {
|
||||
|
||||
private final GBDeviceAppAdapter gbDeviceAppAdapter;
|
||||
|
||||
public AppItemTouchHelperCallback(GBDeviceAppAdapter gbDeviceAppAdapter) {
|
||||
this.gbDeviceAppAdapter = gbDeviceAppAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
//app reordering is not possible on old firmwares
|
||||
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) < 3 && !isCacheManager()) {
|
||||
return 0;
|
||||
}
|
||||
//we only support up and down movement and only for moving, not for swiping apps away
|
||||
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
||||
gbDeviceAppAdapter.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
//nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
onChangedAppOrder();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
|
||||
public class AppManagerActivity extends AbstractGBFragmentActivity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractAppManagerFragment.class);
|
||||
private int READ_REQUEST_CODE = 42;
|
||||
|
||||
private GBDevice mGBDevice = null;
|
||||
|
||||
public GBDevice getGBDevice() {
|
||||
return mGBDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_fragmentappmanager);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
||||
assert fab != null;
|
||||
fab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, READ_REQUEST_CODE);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
|
||||
if (viewPager != null) {
|
||||
viewPager.setAdapter(getPagerAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager) {
|
||||
return new SectionsPagerAdapter(fragmentManager);
|
||||
}
|
||||
|
||||
public static synchronized void deleteFromAppOrderFile(String filename, UUID uuid) {
|
||||
ArrayList<UUID> uuids = getUuidsFromFile(filename);
|
||||
uuids.remove(uuid);
|
||||
rewriteAppOrderFile(filename, uuids);
|
||||
}
|
||||
|
||||
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
|
||||
|
||||
SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
// getItem is called to instantiate the fragment for the given page.
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new AppManagerFragmentCache();
|
||||
case 1:
|
||||
return new AppManagerFragmentInstalledApps();
|
||||
case 2:
|
||||
return new AppManagerFragmentInstalledWatchfaces();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getString(R.string.appmanager_cached_watchapps_watchfaces);
|
||||
case 1:
|
||||
return getString(R.string.appmanager_installed_watchapps);
|
||||
case 2:
|
||||
return getString(R.string.appmanager_installed_watchfaces);
|
||||
case 3:
|
||||
}
|
||||
return super.getPageTitle(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
|
||||
static synchronized void rewriteAppOrderFile(String filename, List<UUID> uuids) {
|
||||
try (BufferedWriter out = new BufferedWriter(new FileWriter(FileUtils.getExternalFilesDir() + "/" + filename))) {
|
||||
for (UUID uuid : uuids) {
|
||||
out.write(uuid.toString());
|
||||
out.newLine();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("can't write app order to file!");
|
||||
}
|
||||
}
|
||||
|
||||
synchronized public static void addToAppOrderFile(String filename, UUID uuid) {
|
||||
ArrayList<UUID> uuids = getUuidsFromFile(filename);
|
||||
if (!uuids.contains(uuid)) {
|
||||
uuids.add(uuid);
|
||||
rewriteAppOrderFile(filename, uuids);
|
||||
}
|
||||
}
|
||||
|
||||
static synchronized ArrayList<UUID> getUuidsFromFile(String filename) {
|
||||
ArrayList<UUID> uuids = new ArrayList<>();
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(FileUtils.getExternalFilesDir() + "/" + filename))) {
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
uuids.add(UUID.fromString(line));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not read sort file");
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
|
||||
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
|
||||
Intent startIntent = new Intent(AppManagerActivity.this, FwAppInstallerActivity.class);
|
||||
startIntent.setAction(Intent.ACTION_VIEW);
|
||||
startIntent.setDataAndType(resultData.getData(), null);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
|
||||
public class AppManagerFragmentCache extends AbstractAppManagerFragment {
|
||||
@Override
|
||||
public void refreshList() {
|
||||
appList.clear();
|
||||
appList.addAll(getCachedApps(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isCacheManager() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<GBDeviceApp> getSystemAppsInCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSortFilename() {
|
||||
return "pbwcacheorder.txt";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa, Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PebbleUtils;
|
||||
|
||||
public class AppManagerFragmentInstalledApps extends AbstractAppManagerFragment {
|
||||
|
||||
@Override
|
||||
protected List<GBDeviceApp> getSystemAppsInCategory() {
|
||||
List<GBDeviceApp> systemApps = new ArrayList<>();
|
||||
//systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
//systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
systemApps.add(new GBDeviceApp(UUID.fromString("1f03293d-47af-4f28-b960-f2b02a6dd757"), "Music (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_NOTIFICATIONS, "Notifications (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
systemApps.add(new GBDeviceApp(UUID.fromString("67a32d95-ef69-46d4-a0b9-854cc62f97f9"), "Alarms (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
systemApps.add(new GBDeviceApp(UUID.fromString("18e443ce-38fd-47c8-84d5-6d0c775fbe55"), "Watchfaces (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
|
||||
if (mGBDevice != null) {
|
||||
if (PebbleUtils.hasHealth(mGBDevice.getModel())) {
|
||||
systemApps.add(new GBDeviceApp(UUID.fromString("0863fc6a-66c5-4f62-ab8a-82ed00a98b5d"), "Send Text (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_PEBBLE_HEALTH, "Health (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
}
|
||||
if (PebbleUtils.hasHRM(mGBDevice.getModel())) {
|
||||
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WORKOUT, "Workout (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
}
|
||||
if (PebbleUtils.getFwMajor(mGBDevice.getFirmwareVersion()) >= 4) {
|
||||
systemApps.add(new GBDeviceApp(PebbleProtocol.UUID_WEATHER, "Weather (System)", "Pebble Inc.", "", GBDeviceApp.Type.APP_SYSTEM));
|
||||
}
|
||||
}
|
||||
|
||||
return systemApps;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isCacheManager() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSortFilename() {
|
||||
return mGBDevice.getAddress() + ".watchapps";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onChangedAppOrder() {
|
||||
super.onChangedAppOrder();
|
||||
sendOrderToDevice(mGBDevice.getAddress() + ".watchfaces");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
|
||||
return gbDeviceApp.getType() == GBDeviceApp.Type.APP_ACTIVITYTRACKER || gbDeviceApp.getType() == GBDeviceApp.Type.APP_GENERIC;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/* Copyright (C) 2016-2017 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
|
||||
public class AppManagerFragmentInstalledWatchfaces extends AbstractAppManagerFragment {
|
||||
|
||||
@Override
|
||||
protected List<GBDeviceApp> getSystemAppsInCategory() {
|
||||
List<GBDeviceApp> systemWatchfaces = new ArrayList<>();
|
||||
systemWatchfaces.add(new GBDeviceApp(UUID.fromString("8f3c8686-31a1-4f5f-91f5-01600c9bdc59"), "Tic Toc (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
|
||||
systemWatchfaces.add(new GBDeviceApp(UUID.fromString("3af858c3-16cb-4561-91e7-f1ad2df8725f"), "Kickstart (System)", "Pebble Inc.", "", GBDeviceApp.Type.WATCHFACE_SYSTEM));
|
||||
return systemWatchfaces;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isCacheManager() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSortFilename() {
|
||||
return mGBDevice.getAddress() + ".watchfaces";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onChangedAppOrder() {
|
||||
super.onChangedAppOrder();
|
||||
sendOrderToDevice(mGBDevice.getAddress() + ".watchapps");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean filterApp(GBDeviceApp gbDeviceApp) {
|
||||
if (gbDeviceApp.getType() == GBDeviceApp.Type.WATCHFACE) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,852 @@
|
|||
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti, walkjivefly
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import com.github.mikephil.charting.charts.BarChart;
|
||||
import com.github.mikephil.charting.charts.BarLineChartBase;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.components.AxisBase;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.BarData;
|
||||
import com.github.mikephil.charting.data.BarDataSet;
|
||||
import com.github.mikephil.charting.data.BarEntry;
|
||||
import com.github.mikephil.charting.data.ChartData;
|
||||
import com.github.mikephil.charting.data.CombinedData;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.data.LineData;
|
||||
import com.github.mikephil.charting.data.LineDataSet;
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
|
||||
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
|
||||
/**
|
||||
* A base class fragment to be used with ChartsActivity. The fragment can supply
|
||||
* a title to be displayed in the activity by returning non-null in #getTitle()
|
||||
* Broadcast events can be received by overriding #onReceive(Context,Intent).
|
||||
* The chart can be refreshed by calling #refresh()
|
||||
* Implement refreshInBackground(DBHandler, GBDevice) to fetch the samples from the DB,
|
||||
* and add the samples to the chart. The actual rendering, which must be performed in the UI
|
||||
* thread, must be done in #renderCharts().
|
||||
* Access functionality of the hosting activity with #getHost()
|
||||
* <p/>
|
||||
* The hosting ChartsHost activity provides a section for displaying a date or date range
|
||||
* being the basis for the chart, as well as two buttons for moving backwards and forward
|
||||
* in time. The date is held by the activity, so that it can be shared by multiple chart
|
||||
* fragments. It is still the responsibility of the (currently visible) chart fragment
|
||||
* to set the desired date in the ChartsActivity via #setDateRange(Date,Date).
|
||||
* The default implementations #handleDatePrev(Date,Date) and #handleDateNext(Date,Date)
|
||||
* shift the date by one day.
|
||||
*/
|
||||
public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
protected final int ANIM_TIME = 250;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractChartFragment.class);
|
||||
|
||||
private final Set<String> mIntentFilterActions;
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
AbstractChartFragment.this.onReceive(context, intent);
|
||||
}
|
||||
};
|
||||
private boolean mChartDirty = true;
|
||||
private AsyncTask refreshTask;
|
||||
|
||||
public boolean isChartDirty() {
|
||||
return mChartDirty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract String getTitle();
|
||||
|
||||
public boolean supportsHeartrate(GBDevice device) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
return coordinator != null && coordinator.supportsHeartRateMeasurement(device);
|
||||
}
|
||||
|
||||
protected static final class ActivityConfig {
|
||||
public final int type;
|
||||
public final String label;
|
||||
public final Integer color;
|
||||
|
||||
public ActivityConfig(int kind, String label, Integer color) {
|
||||
this.type = kind;
|
||||
this.label = label;
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
protected ActivityConfig akActivity;
|
||||
protected ActivityConfig akLightSleep;
|
||||
protected ActivityConfig akDeepSleep;
|
||||
protected ActivityConfig akNotWorn;
|
||||
|
||||
|
||||
protected int BACKGROUND_COLOR;
|
||||
protected int DESCRIPTION_COLOR;
|
||||
protected int CHART_TEXT_COLOR;
|
||||
protected int LEGEND_TEXT_COLOR;
|
||||
protected int HEARTRATE_COLOR;
|
||||
protected int HEARTRATE_FILL_COLOR;
|
||||
protected int AK_ACTIVITY_COLOR;
|
||||
protected int AK_DEEP_SLEEP_COLOR;
|
||||
protected int AK_LIGHT_SLEEP_COLOR;
|
||||
protected int AK_NOT_WORN_COLOR;
|
||||
|
||||
protected String HEARTRATE_LABEL;
|
||||
|
||||
protected AbstractChartFragment(String... intentFilterActions) {
|
||||
mIntentFilterActions = new HashSet<>();
|
||||
if (intentFilterActions != null) {
|
||||
mIntentFilterActions.addAll(Arrays.asList(intentFilterActions));
|
||||
}
|
||||
mIntentFilterActions.add(ChartsHost.DATE_NEXT);
|
||||
mIntentFilterActions.add(ChartsHost.DATE_PREV);
|
||||
mIntentFilterActions.add(ChartsHost.REFRESH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
init();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
for (String action : mIntentFilterActions) {
|
||||
filter.addAction(action);
|
||||
}
|
||||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filter);
|
||||
}
|
||||
|
||||
protected void init() {
|
||||
TypedValue runningColor = new TypedValue();
|
||||
BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
|
||||
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
|
||||
CHART_TEXT_COLOR = ContextCompat.getColor(getContext(), R.color.secondarytext);
|
||||
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
|
||||
HEARTRATE_FILL_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_fill);
|
||||
getContext().getTheme().resolveAttribute(R.attr.chart_activity, runningColor, true);
|
||||
AK_ACTIVITY_COLOR = runningColor.data;
|
||||
getContext().getTheme().resolveAttribute(R.attr.chart_deep_sleep, runningColor, true);
|
||||
AK_DEEP_SLEEP_COLOR = runningColor.data;
|
||||
getContext().getTheme().resolveAttribute(R.attr.chart_light_sleep, runningColor, true);
|
||||
AK_LIGHT_SLEEP_COLOR = runningColor.data;
|
||||
getContext().getTheme().resolveAttribute(R.attr.chart_not_worn, runningColor, true);
|
||||
AK_NOT_WORN_COLOR = runningColor.data;
|
||||
|
||||
HEARTRATE_LABEL = getContext().getString(R.string.charts_legend_heartrate);
|
||||
|
||||
akActivity = new ActivityConfig(ActivityKind.TYPE_ACTIVITY, getString(R.string.abstract_chart_fragment_kind_activity), AK_ACTIVITY_COLOR);
|
||||
akLightSleep = new ActivityConfig(ActivityKind.TYPE_LIGHT_SLEEP, getString(R.string.abstract_chart_fragment_kind_light_sleep), AK_LIGHT_SLEEP_COLOR);
|
||||
akDeepSleep = new ActivityConfig(ActivityKind.TYPE_DEEP_SLEEP, getString(R.string.abstract_chart_fragment_kind_deep_sleep), AK_DEEP_SLEEP_COLOR);
|
||||
akNotWorn = new ActivityConfig(ActivityKind.TYPE_NOT_WORN, getString(R.string.abstract_chart_fragment_kind_not_worn), AK_NOT_WORN_COLOR);
|
||||
}
|
||||
|
||||
private void setStartDate(Date date) {
|
||||
getChartsHost().setStartDate(date);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ChartsHost getChartsHost() {
|
||||
return (ChartsHost) getActivity();
|
||||
}
|
||||
|
||||
private void setEndDate(Date date) {
|
||||
getChartsHost().setEndDate(date);
|
||||
}
|
||||
|
||||
public Date getStartDate() {
|
||||
return getChartsHost().getStartDate();
|
||||
}
|
||||
|
||||
public Date getEndDate() {
|
||||
return getChartsHost().getEndDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this fragment has been fully scrolled into the activity.
|
||||
*
|
||||
* @see #isVisibleInActivity()
|
||||
* @see #onMadeInvisibleInActivity()
|
||||
*/
|
||||
@Override
|
||||
protected void onMadeVisibleInActivity() {
|
||||
super.onMadeVisibleInActivity();
|
||||
showDateBar(true);
|
||||
if (isChartDirty()) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
protected void showDateBar(boolean show) {
|
||||
getChartsHost().getDateBar().setVisibility(show ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
||||
protected void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (ChartsHost.REFRESH.equals(action)) {
|
||||
refresh();
|
||||
} else if (ChartsHost.DATE_NEXT.equals(action)) {
|
||||
handleDateNext(getStartDate(), getEndDate());
|
||||
} else if (ChartsHost.DATE_PREV.equals(action)) {
|
||||
handleDatePrev(getStartDate(), getEndDate());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation shifts the dates by one day, if visible
|
||||
* and calls #refreshIfVisible().
|
||||
*
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
*/
|
||||
protected void handleDatePrev(Date startDate, Date endDate) {
|
||||
if (isVisibleInActivity()) {
|
||||
if (!shiftDates(startDate, endDate, -1)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
refreshIfVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation shifts the dates by one day, if visible
|
||||
* and calls #refreshIfVisible().
|
||||
*
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
*/
|
||||
protected void handleDateNext(Date startDate, Date endDate) {
|
||||
if (isVisibleInActivity()) {
|
||||
if (!shiftDates(startDate, endDate, +1)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
refreshIfVisible();
|
||||
}
|
||||
|
||||
protected void refreshIfVisible() {
|
||||
if (isVisibleInActivity()) {
|
||||
refresh();
|
||||
} else {
|
||||
mChartDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts the given dates by offset days. offset may be positive or negative.
|
||||
*
|
||||
* @param startDate
|
||||
* @param endDate
|
||||
* @param offset a positive or negative number of days to shift the dates
|
||||
* @return true if the shift was successful and false otherwise
|
||||
*/
|
||||
protected boolean shiftDates(Date startDate, Date endDate, int offset) {
|
||||
Date newStart = DateTimeUtils.shiftByDays(startDate, offset);
|
||||
Date newEnd = DateTimeUtils.shiftByDays(endDate, offset);
|
||||
|
||||
return setDateRange(newStart, newEnd);
|
||||
}
|
||||
|
||||
protected Integer getColorFor(int activityKind) {
|
||||
switch (activityKind) {
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
return akDeepSleep.color;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
return akLightSleep.color;
|
||||
case ActivityKind.TYPE_ACTIVITY:
|
||||
return akActivity.color;
|
||||
}
|
||||
return akActivity.color;
|
||||
}
|
||||
|
||||
protected SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
return coordinator.getSampleProvider(device, db.getDaoSession());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all kinds of samples for the given device.
|
||||
* To be called from a background thread.
|
||||
*
|
||||
* @param device
|
||||
* @param tsFrom
|
||||
* @param tsTo
|
||||
*/
|
||||
protected List<? extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends AbstractActivitySample> provider = getProvider(db, device);
|
||||
return provider.getActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
|
||||
protected List<? extends ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
|
||||
return provider.getSleepSamples(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
protected void configureChartDefaults(Chart<?> chart) {
|
||||
chart.getXAxis().setValueFormatter(new TimestampValueFormatter());
|
||||
chart.getDescription().setText("");
|
||||
|
||||
// if enabled, the chart will always start at zero on the y-axis
|
||||
chart.setNoDataText(getString(R.string.chart_no_data_synchronize));
|
||||
|
||||
// disable value highlighting
|
||||
chart.setHighlightPerTapEnabled(false);
|
||||
|
||||
// enable touch gestures
|
||||
chart.setTouchEnabled(true);
|
||||
|
||||
// commented out: this has weird bugs/sideeffects at least on WeekStepsCharts
|
||||
// where only the first Day-label is drawn, because AxisRenderer.computeAxisValues(float,float)
|
||||
// appears to have an overflow when calculating 'n' (number of entries)
|
||||
// chart.getXAxis().setGranularity(60*5);
|
||||
|
||||
setupLegend(chart);
|
||||
}
|
||||
|
||||
protected void configureBarLineChartDefaults(BarLineChartBase<?> chart) {
|
||||
configureChartDefaults(chart);
|
||||
if (chart instanceof BarChart) {
|
||||
((BarChart) chart).setFitBars(true);
|
||||
}
|
||||
|
||||
// enable scaling and dragging
|
||||
chart.setDragEnabled(true);
|
||||
chart.setScaleEnabled(true);
|
||||
|
||||
// if disabled, scaling can be done on x- and y-axis separately
|
||||
// chart.setPinchZoom(true);
|
||||
|
||||
chart.setDrawGridBackground(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will invoke a background task to read the data from the
|
||||
* database, analyze it, prepare it for the charts and eventually call
|
||||
* #renderCharts
|
||||
*/
|
||||
protected void refresh() {
|
||||
ChartsHost chartsHost = getChartsHost();
|
||||
if (chartsHost != null) {
|
||||
if (chartsHost.getDevice() != null) {
|
||||
mChartDirty = false;
|
||||
updateDateInfo(getStartDate(), getEndDate());
|
||||
if (refreshTask != null && refreshTask.getStatus() != AsyncTask.Status.FINISHED) {
|
||||
refreshTask.cancel(true);
|
||||
}
|
||||
refreshTask = createRefreshTask("Visualizing data", getActivity()).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method reads the data from the database, analyzes and prepares it for
|
||||
* the charts. This will be called from a background task, so there must not be
|
||||
* any UI access. #updateChartsInUIThread and #renderCharts will be automatically called after this method.
|
||||
*/
|
||||
protected abstract ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device);
|
||||
|
||||
/**
|
||||
* Triggers the actual (re-) rendering of the chart.
|
||||
* Always called from the UI thread.
|
||||
*/
|
||||
protected abstract void renderCharts();
|
||||
|
||||
protected DefaultChartsData<CombinedData> refresh(GBDevice gbDevice, List<? extends ActivitySample> samples) {
|
||||
// Calendar cal = GregorianCalendar.getInstance();
|
||||
// cal.clear();
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
// Date date;
|
||||
// String dateStringFrom = "";
|
||||
// String dateStringTo = "";
|
||||
// ArrayList<String> xLabels = null;
|
||||
|
||||
LOG.info("" + getTitle() + ": number of samples:" + samples.size());
|
||||
CombinedData combinedData;
|
||||
if (samples.size() > 1) {
|
||||
boolean annotate = true;
|
||||
boolean use_steps_as_movement;
|
||||
|
||||
int last_type = ActivityKind.TYPE_UNKNOWN;
|
||||
|
||||
int numEntries = samples.size();
|
||||
List<BarEntry> activityEntries = new ArrayList<>(numEntries);
|
||||
boolean hr = supportsHeartrate(gbDevice);
|
||||
List<Entry> heartrateEntries = hr ? new ArrayList<Entry>(numEntries) : null;
|
||||
List<Integer> colors = new ArrayList<>(numEntries); // this is kinda inefficient...
|
||||
int lastHrSampleIndex = -1;
|
||||
|
||||
for (int i = 0; i < numEntries; i++) {
|
||||
ActivitySample sample = samples.get(i);
|
||||
int type = sample.getKind();
|
||||
int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
|
||||
// System.out.println(ts);
|
||||
// ts = i;
|
||||
// determine start and end dates
|
||||
// if (i == 0) {
|
||||
// cal.setTimeInMillis(ts * 1000L); // make sure it's converted to long
|
||||
// date = cal.getTime();
|
||||
// dateStringFrom = dateFormat.format(date);
|
||||
// } else if (i == samples.size() - 1) {
|
||||
// cal.setTimeInMillis(ts * 1000L); // same here
|
||||
// date = cal.getTime();
|
||||
// dateStringTo = dateFormat.format(date);
|
||||
// }
|
||||
|
||||
float movement = sample.getIntensity();
|
||||
|
||||
float value = movement;
|
||||
switch (type) {
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
value += SleepUtils.Y_VALUE_DEEP_SLEEP;
|
||||
colors.add(akDeepSleep.color);
|
||||
break;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
colors.add(akLightSleep.color);
|
||||
break;
|
||||
case ActivityKind.TYPE_NOT_WORN:
|
||||
value = SleepUtils.Y_VALUE_DEEP_SLEEP; //a small value, just to show something on the graphs
|
||||
colors.add(akNotWorn.color);
|
||||
break;
|
||||
default:
|
||||
// short steps = sample.getSteps();
|
||||
// if (use_steps_as_movement && steps != 0) {
|
||||
// // I'm not sure using steps for this is actually a good idea
|
||||
// movement = steps;
|
||||
// }
|
||||
// value = ((float) movement) / movement_divisor;
|
||||
colors.add(akActivity.color);
|
||||
}
|
||||
activityEntries.add(createBarEntry(value, ts));
|
||||
if (hr && isValidHeartRateValue(sample.getHeartRate())) {
|
||||
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
|
||||
heartrateEntries.add(createLineEntry(0, ts - 1));
|
||||
}
|
||||
|
||||
heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts));
|
||||
lastHrSampleIndex = ts;
|
||||
}
|
||||
|
||||
String xLabel = "";
|
||||
if (annotate) {
|
||||
// cal.setTimeInMillis((ts + tsOffset) * 1000L);
|
||||
// date = cal.getTime();
|
||||
// String dateString = annotationDateFormat.format(date);
|
||||
// xLabel = dateString;
|
||||
// if (last_type != type) {
|
||||
// if (isSleep(last_type) && !isSleep(type)) {
|
||||
// // woken up
|
||||
// LimitLine line = new LimitLine(i, dateString);
|
||||
// line.enableDashedLine(8, 8, 0);
|
||||
// line.setTextColor(Color.WHITE);
|
||||
// line.setTextSize(15);
|
||||
// chart.getXAxis().addLimitLine(line);
|
||||
// } else if (!isSleep(last_type) && isSleep(type)) {
|
||||
// // fallen asleep
|
||||
// LimitLine line = new LimitLine(i, dateString);
|
||||
// line.enableDashedLine(8, 8, 0);
|
||||
// line.setTextSize(15);
|
||||
// line.setTextColor(Color.WHITE);
|
||||
// chart.getXAxis().addLimitLine(line);
|
||||
// }
|
||||
// }
|
||||
// last_type = type;
|
||||
}
|
||||
}
|
||||
|
||||
BarDataSet activitySet = createActivitySet(activityEntries, colors, "Activity");
|
||||
// create a data object with the datasets
|
||||
// combinedData = new CombinedData(xLabels);
|
||||
combinedData = new CombinedData();
|
||||
List<IBarDataSet> list = new ArrayList<>();
|
||||
list.add(activitySet);
|
||||
BarData barData = new BarData(list);
|
||||
barData.setBarWidth(200f);
|
||||
// barData.setGroupSpace(0);
|
||||
combinedData.setData(barData);
|
||||
|
||||
if (hr && heartrateEntries.size() > 0) {
|
||||
LineDataSet heartrateSet = createHeartrateSet(heartrateEntries, "Heart Rate");
|
||||
LineData lineData = new LineData(heartrateSet);
|
||||
combinedData.setData(lineData);
|
||||
}
|
||||
|
||||
// chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo));
|
||||
// chart.setDescriptionPosition(?, ?);
|
||||
} else {
|
||||
combinedData = new CombinedData();
|
||||
}
|
||||
|
||||
IAxisValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation);
|
||||
return new DefaultChartsData(combinedData, xValueFormatter);
|
||||
}
|
||||
|
||||
protected boolean isValidHeartRateValue(int value) {
|
||||
return value > HeartRateUtils.MIN_HEART_RATE_VALUE && value < HeartRateUtils.MAX_HEART_RATE_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this to supply the samples to be displayed.
|
||||
*
|
||||
* @param db
|
||||
* @param device
|
||||
* @param tsFrom
|
||||
* @param tsTo
|
||||
* @return
|
||||
*/
|
||||
protected abstract List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
|
||||
|
||||
protected abstract void setupLegend(Chart chart);
|
||||
|
||||
protected BarEntry createBarEntry(float value, int xValue) {
|
||||
return new BarEntry(xValue, value);
|
||||
}
|
||||
|
||||
protected Entry createLineEntry(float value, int xValue) {
|
||||
return new Entry(xValue, value);
|
||||
}
|
||||
|
||||
protected BarDataSet createActivitySet(List<BarEntry> values, List<Integer> colors, String label) {
|
||||
BarDataSet set1 = new BarDataSet(values, label);
|
||||
set1.setColors(colors);
|
||||
// set1.setDrawCubic(true);
|
||||
// set1.setCubicIntensity(0.2f);
|
||||
// //set1.setDrawFilled(true);
|
||||
// set1.setDrawCircles(false);
|
||||
// set1.setLineWidth(2f);
|
||||
// set1.setCircleSize(5f);
|
||||
// set1.setFillColor(ColorTemplate.getHoloBlue());
|
||||
set1.setDrawValues(false);
|
||||
// set1.setHighLightColor(Color.rgb(128, 0, 255));
|
||||
// set1.setColor(Color.rgb(89, 178, 44));
|
||||
set1.setValueTextColor(CHART_TEXT_COLOR);
|
||||
set1.setAxisDependency(YAxis.AxisDependency.LEFT);
|
||||
return set1;
|
||||
}
|
||||
|
||||
protected LineDataSet createHeartrateSet(List<Entry> values, String label) {
|
||||
LineDataSet set1 = new LineDataSet(values, label);
|
||||
set1.setLineWidth(2.2f);
|
||||
set1.setColor(HEARTRATE_COLOR);
|
||||
// set1.setDrawCubic(true);
|
||||
set1.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
|
||||
set1.setCubicIntensity(0.1f);
|
||||
set1.setDrawCircles(false);
|
||||
// set1.setCircleRadius(2f);
|
||||
// set1.setDrawFilled(true);
|
||||
// set1.setColor(getResources().getColor(android.R.color.background_light));
|
||||
// set1.setCircleColor(HEARTRATE_COLOR);
|
||||
// set1.setFillColor(ColorTemplate.getHoloBlue());
|
||||
// set1.setHighLightColor(Color.rgb(128, 0, 255));
|
||||
// set1.setColor(Color.rgb(89, 178, 44));
|
||||
set1.setDrawValues(true);
|
||||
set1.setValueTextColor(CHART_TEXT_COLOR);
|
||||
set1.setAxisDependency(YAxis.AxisDependency.RIGHT);
|
||||
return set1;
|
||||
}
|
||||
|
||||
protected BarDataSet createDeepSleepSet(List<BarEntry> values, String label) {
|
||||
BarDataSet set1 = new BarDataSet(values, label);
|
||||
// set1.setDrawCubic(true);
|
||||
// set1.setCubicIntensity(0.2f);
|
||||
// //set1.setDrawFilled(true);
|
||||
// set1.setDrawCircles(false);
|
||||
// set1.setLineWidth(2f);
|
||||
// set1.setCircleSize(5f);
|
||||
// set1.setFillColor(ColorTemplate.getHoloBlue());
|
||||
set1.setDrawValues(false);
|
||||
// set1.setHighLightColor(Color.rgb(244, 117, 117));
|
||||
// set1.setColor(Color.rgb(76, 90, 255));
|
||||
set1.setValueTextColor(CHART_TEXT_COLOR);
|
||||
return set1;
|
||||
}
|
||||
|
||||
protected BarDataSet createLightSleepSet(List<BarEntry> values, String label) {
|
||||
BarDataSet set1 = new BarDataSet(values, label);
|
||||
|
||||
// set1.setDrawCubic(true);
|
||||
// set1.setCubicIntensity(0.2f);
|
||||
// //set1.setDrawFilled(true);
|
||||
// set1.setDrawCircles(false);
|
||||
// set1.setLineWidth(2f);
|
||||
// set1.setCircleSize(5f);
|
||||
// set1.setFillColor(ColorTemplate.getHoloBlue());
|
||||
set1.setDrawValues(false);
|
||||
// set1.setHighLightColor(Color.rgb(244, 117, 117));
|
||||
// set1.setColor(Color.rgb(182, 191, 255));
|
||||
set1.setValueTextColor(CHART_TEXT_COLOR);
|
||||
// set1.setColor(Color.CYAN);
|
||||
return set1;
|
||||
}
|
||||
|
||||
protected RefreshTask createRefreshTask(String task, Context context) {
|
||||
return new RefreshTask(task, context);
|
||||
}
|
||||
|
||||
public class RefreshTask extends DBAccess {
|
||||
private ChartsData chartsData;
|
||||
|
||||
public RefreshTask(String task, Context context) {
|
||||
super(task, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInBackground(DBHandler db) {
|
||||
ChartsHost chartsHost = getChartsHost();
|
||||
if (chartsHost != null) {
|
||||
chartsData = refreshInBackground(chartsHost, db, chartsHost.getDevice());
|
||||
} else {
|
||||
cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Object o) {
|
||||
super.onPostExecute(o);
|
||||
FragmentActivity activity = getActivity();
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||||
updateChartsnUIThread(chartsData);
|
||||
renderCharts();
|
||||
} else {
|
||||
LOG.info("Not rendering charts because activity is not available anymore");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void updateChartsnUIThread(ChartsData chartsData);
|
||||
|
||||
/**
|
||||
* Returns true if the date was successfully shifted, and false if the shift
|
||||
* was ignored, e.g. when the to-value is in the future.
|
||||
*
|
||||
* @param from
|
||||
* @param to
|
||||
*/
|
||||
public boolean setDateRange(Date from, Date to) {
|
||||
if (from.compareTo(to) > 0) {
|
||||
throw new IllegalArgumentException("Bad date range: " + from + ".." + to);
|
||||
}
|
||||
Date now = new Date();
|
||||
if (to.after(now)) {
|
||||
return false;
|
||||
}
|
||||
setStartDate(from);
|
||||
setEndDate(to);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void updateDateInfo(Date from, Date to) {
|
||||
if (from.equals(to)) {
|
||||
getChartsHost().setDateInfo(DateTimeUtils.formatDate(from));
|
||||
} else {
|
||||
getChartsHost().setDateInfo(DateTimeUtils.formatDateRange(from, to));
|
||||
}
|
||||
}
|
||||
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device) {
|
||||
int tsStart = getTSStart();
|
||||
int tsEnd = getTSEnd();
|
||||
List<ActivitySample> samples = (List<ActivitySample>) getSamples(db, device, tsStart, tsEnd);
|
||||
ensureStartAndEndSamples(samples, tsStart, tsEnd);
|
||||
// List<ActivitySample> samples2 = new ArrayList<>();
|
||||
// int min = Math.min(samples.size(), 10);
|
||||
// int min = Math.min(samples.size(), 10);
|
||||
// for (int i = 0; i < min; i++) {
|
||||
// samples2.add(samples.get(i));
|
||||
// }
|
||||
// return samples2;
|
||||
return samples;
|
||||
}
|
||||
|
||||
protected void ensureStartAndEndSamples(List<ActivitySample> samples, int tsStart, int tsEnd) {
|
||||
if (samples == null || samples.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ActivitySample lastSample = samples.get(samples.size() - 1);
|
||||
if (lastSample.getTimestamp() < tsEnd) {
|
||||
samples.add(createTrailingActivitySample(lastSample, tsEnd));
|
||||
}
|
||||
|
||||
ActivitySample firstSample = samples.get(0);
|
||||
if (firstSample.getTimestamp() > tsStart) {
|
||||
samples.add(createTrailingActivitySample(firstSample, tsStart));
|
||||
}
|
||||
}
|
||||
|
||||
private ActivitySample createTrailingActivitySample(ActivitySample referenceSample, int timestamp) {
|
||||
TrailingActivitySample sample = new TrailingActivitySample();
|
||||
if (referenceSample instanceof AbstractActivitySample) {
|
||||
AbstractActivitySample reference = (AbstractActivitySample) referenceSample;
|
||||
sample.setUserId(reference.getUserId());
|
||||
sample.setDeviceId(reference.getDeviceId());
|
||||
sample.setProvider(reference.getProvider());
|
||||
}
|
||||
sample.setTimestamp(timestamp);
|
||||
return sample;
|
||||
}
|
||||
|
||||
private int getTSEnd() {
|
||||
return toTimestamp(getEndDate());
|
||||
}
|
||||
|
||||
private int getTSStart() {
|
||||
return toTimestamp(getStartDate());
|
||||
}
|
||||
|
||||
private int toTimestamp(Date date) {
|
||||
return (int) ((date.getTime() / 1000));
|
||||
}
|
||||
|
||||
public static class DefaultChartsData<T extends ChartData<?>> extends ChartsData {
|
||||
private final T data;
|
||||
private IAxisValueFormatter xValueFormatter;
|
||||
|
||||
public DefaultChartsData(T data, IAxisValueFormatter xValueFormatter) {
|
||||
this.xValueFormatter = xValueFormatter;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public IAxisValueFormatter getXValueFormatter() {
|
||||
return xValueFormatter;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class SampleXLabelFormatter implements IAxisValueFormatter {
|
||||
private final TimestampTranslation tsTranslation;
|
||||
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
|
||||
// SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||
Calendar cal = GregorianCalendar.getInstance();
|
||||
|
||||
public SampleXLabelFormatter(TimestampTranslation tsTranslation) {
|
||||
this.tsTranslation = tsTranslation;
|
||||
|
||||
}
|
||||
// TODO: this does not work. Cannot use precomputed labels
|
||||
@Override
|
||||
public String getFormattedValue(float value, AxisBase axis) {
|
||||
cal.clear();
|
||||
int ts = (int) value;
|
||||
cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L);
|
||||
Date date = cal.getTime();
|
||||
String dateString = annotationDateFormat.format(date);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class PreformattedXIndexLabelFormatter implements IAxisValueFormatter {
|
||||
private ArrayList<String> xLabels;
|
||||
|
||||
public PreformattedXIndexLabelFormatter(ArrayList<String> xLabels) {
|
||||
this.xLabels = xLabels;
|
||||
|
||||
}
|
||||
@Override
|
||||
public String getFormattedValue(float value, AxisBase axis) {
|
||||
int index = (int) value;
|
||||
if (xLabels == null || index >= xLabels.size()) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
return xLabels.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Awkward class that helps in translating long timestamp
|
||||
* values to float (sic!) values. It basically rebases all
|
||||
* timestamps to a base (the very first) timestamp value.
|
||||
*
|
||||
* It does this so that the large timestamp values can be used
|
||||
* floating point values, where the mantissa is just 24 bits.
|
||||
*/
|
||||
protected static class TimestampTranslation {
|
||||
private int tsOffset = -1;
|
||||
|
||||
public int shorten(int timestamp) {
|
||||
if (tsOffset == -1) {
|
||||
tsOffset = timestamp;
|
||||
return 0;
|
||||
}
|
||||
return timestamp - tsOffset;
|
||||
}
|
||||
|
||||
public int toOriginalValue(int timestamp) {
|
||||
if (tsOffset == -1) {
|
||||
return timestamp;
|
||||
}
|
||||
return timestamp + tsOffset;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
/* Copyright (C) 2015-2017 0nse, Alberto, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.github.mikephil.charting.charts.BarChart;
|
||||
import com.github.mikephil.charting.charts.PieChart;
|
||||
import com.github.mikephil.charting.components.LimitLine;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.BarData;
|
||||
import com.github.mikephil.charting.data.BarDataSet;
|
||||
import com.github.mikephil.charting.data.BarEntry;
|
||||
import com.github.mikephil.charting.data.PieData;
|
||||
import com.github.mikephil.charting.data.PieDataSet;
|
||||
import com.github.mikephil.charting.data.PieEntry;
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
|
||||
import com.github.mikephil.charting.formatter.IValueFormatter;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
|
||||
|
||||
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
|
||||
|
||||
private Locale mLocale;
|
||||
private int mTargetValue = 0;
|
||||
|
||||
private PieChart mTodayPieChart;
|
||||
private BarChart mWeekChart;
|
||||
|
||||
private int mOffsetHours = getOffsetHours();
|
||||
|
||||
@Override
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(chartsHost.getEndDate());
|
||||
//NB: we could have omitted the day, but this way we can move things to the past easily
|
||||
DayData dayData = refreshDayPie(db, day, device);
|
||||
DefaultChartsData weekBeforeData = refreshWeekBeforeData(db, mWeekChart, day, device);
|
||||
|
||||
return new MyChartsData(dayData, weekBeforeData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(ChartsData chartsData) {
|
||||
MyChartsData mcd = (MyChartsData) chartsData;
|
||||
|
||||
setupLegend(mWeekChart);
|
||||
mTodayPieChart.setCenterText(mcd.getDayData().centerText);
|
||||
mTodayPieChart.setData(mcd.getDayData().data);
|
||||
|
||||
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
mWeekChart.setData(mcd.getWeekBeforeData().getData());
|
||||
mWeekChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
mWeekChart.invalidate();
|
||||
mTodayPieChart.invalidate();
|
||||
}
|
||||
|
||||
private DefaultChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
|
||||
day = (Calendar) day.clone(); // do not modify the caller's argument
|
||||
day.add(Calendar.DATE, -7);
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
ArrayList<String> labels = new ArrayList<String>();
|
||||
|
||||
for (int counter = 0; counter < 7; counter++) {
|
||||
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
|
||||
|
||||
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
|
||||
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
|
||||
day.add(Calendar.DATE, 1);
|
||||
}
|
||||
|
||||
BarDataSet set = new BarDataSet(entries, "");
|
||||
set.setColors(getColors());
|
||||
set.setValueFormatter(getBarValueFormatter());
|
||||
|
||||
BarData barData = new BarData(set);
|
||||
barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
|
||||
barData.setValueTextSize(10f);
|
||||
|
||||
LimitLine target = new LimitLine(mTargetValue);
|
||||
barChart.getAxisLeft().removeAllLimitLines();
|
||||
barChart.getAxisLeft().addLimitLine(target);
|
||||
|
||||
return new DefaultChartsData(barData, new PreformattedXIndexLabelFormatter(labels));
|
||||
}
|
||||
|
||||
private DayData refreshDayPie(DBHandler db, Calendar day, GBDevice device) {
|
||||
|
||||
PieData data = new PieData();
|
||||
List<PieEntry> entries = new ArrayList<>();
|
||||
PieDataSet set = new PieDataSet(entries, "");
|
||||
|
||||
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
|
||||
float totalValues[] = getTotalsForActivityAmounts(amounts);
|
||||
String[] pieLabels = getPieLabels();
|
||||
float totalValue = 0;
|
||||
for (int i = 0; i < totalValues.length; i++) {
|
||||
float value = totalValues[i];
|
||||
totalValue += value;
|
||||
entries.add(new PieEntry(value, pieLabels[i]));
|
||||
}
|
||||
|
||||
set.setColors(getColors());
|
||||
|
||||
if (totalValues.length < 2) {
|
||||
if (totalValue < mTargetValue) {
|
||||
entries.add(new PieEntry((mTargetValue - totalValue)));
|
||||
set.addColor(Color.GRAY);
|
||||
}
|
||||
}
|
||||
|
||||
data.setDataSet(set);
|
||||
|
||||
if (totalValues.length < 2) {
|
||||
data.setDrawValues(false);
|
||||
}
|
||||
else {
|
||||
set.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
|
||||
set.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
|
||||
set.setValueTextColor(DESCRIPTION_COLOR);
|
||||
set.setValueTextSize(13f);
|
||||
set.setValueFormatter(getPieValueFormatter());
|
||||
}
|
||||
|
||||
return new DayData(data, formatPieValue((int) totalValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
mLocale = getResources().getConfiguration().locale;
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
|
||||
|
||||
int goal = getGoal();
|
||||
if (goal >= 0) {
|
||||
mTargetValue = goal;
|
||||
}
|
||||
|
||||
mTodayPieChart = (PieChart) rootView.findViewById(R.id.todaystepschart);
|
||||
mWeekChart = (BarChart) rootView.findViewById(R.id.weekstepschart);
|
||||
|
||||
setupWeekChart();
|
||||
setupTodayPieChart();
|
||||
|
||||
// refresh immediately instead of use refreshIfVisible(), for perceived performance
|
||||
refresh();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void setupTodayPieChart() {
|
||||
mTodayPieChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mTodayPieChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
mTodayPieChart.setEntryLabelColor(DESCRIPTION_COLOR);
|
||||
mTodayPieChart.getDescription().setText(getPieDescription(mTargetValue));
|
||||
// mTodayPieChart.setNoDataTextDescription("");
|
||||
mTodayPieChart.setNoDataText("");
|
||||
mTodayPieChart.getLegend().setEnabled(false);
|
||||
}
|
||||
|
||||
private void setupWeekChart() {
|
||||
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
mWeekChart.getDescription().setText("");
|
||||
mWeekChart.setFitBars(true);
|
||||
|
||||
configureBarLineChartDefaults(mWeekChart);
|
||||
|
||||
XAxis x = mWeekChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
x.setPosition(XAxis.XAxisPosition.BOTTOM);
|
||||
|
||||
YAxis y = mWeekChart.getAxisLeft();
|
||||
y.setDrawGridLines(false);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
y.setDrawZeroLine(true);
|
||||
y.setSpaceBottom(0);
|
||||
y.setAxisMinimum(0);
|
||||
y.setValueFormatter(getYAxisFormatter());
|
||||
y.setEnabled(true);
|
||||
|
||||
YAxis yAxisRight = mWeekChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(false);
|
||||
yAxisRight.setDrawLabels(false);
|
||||
yAxisRight.setDrawTopYLabelEntry(false);
|
||||
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
||||
}
|
||||
|
||||
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) {
|
||||
int startTs;
|
||||
int endTs;
|
||||
|
||||
day = (Calendar) day.clone(); // do not modify the caller's argument
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, offsetHours);
|
||||
|
||||
startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
endTs = startTs + 24 * 60 * 60 - 1;
|
||||
|
||||
return getSamples(db, device, startTs, endTs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return super.getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
private static class DayData {
|
||||
private final PieData data;
|
||||
private final CharSequence centerText;
|
||||
|
||||
DayData(PieData data, String centerText) {
|
||||
this.data = data;
|
||||
this.centerText = centerText;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyChartsData extends ChartsData {
|
||||
private final DefaultChartsData<BarData> weekBeforeData;
|
||||
private final DayData dayData;
|
||||
|
||||
MyChartsData(DayData dayData, DefaultChartsData<BarData> weekBeforeData) {
|
||||
this.dayData = dayData;
|
||||
this.weekBeforeData = weekBeforeData;
|
||||
}
|
||||
|
||||
DayData getDayData() {
|
||||
return dayData;
|
||||
}
|
||||
|
||||
DefaultChartsData<BarData> getWeekBeforeData() {
|
||||
return weekBeforeData;
|
||||
}
|
||||
}
|
||||
|
||||
private ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) {
|
||||
|
||||
LimitedQueue activityAmountCache = null;
|
||||
ActivityAmounts amounts = null;
|
||||
|
||||
Activity activity = getActivity();
|
||||
int key = (int) (day.getTimeInMillis() / 1000) + (mOffsetHours * 3600);
|
||||
if (activity != null) {
|
||||
activityAmountCache = ((ChartsActivity) activity).mActivityAmountCache;
|
||||
amounts = (ActivityAmounts) (activityAmountCache.lookup(key));
|
||||
}
|
||||
|
||||
if (amounts == null) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
amounts = analysis.calculateActivityAmounts(getSamplesOfDay(db, day, mOffsetHours, device));
|
||||
if (activityAmountCache != null) {
|
||||
activityAmountCache.add(key, amounts);
|
||||
}
|
||||
}
|
||||
|
||||
return amounts;
|
||||
}
|
||||
|
||||
abstract int getGoal();
|
||||
|
||||
abstract int getOffsetHours();
|
||||
|
||||
abstract float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts);
|
||||
|
||||
abstract String formatPieValue(int value);
|
||||
|
||||
abstract String[] getPieLabels();
|
||||
|
||||
abstract IValueFormatter getPieValueFormatter();
|
||||
|
||||
abstract IValueFormatter getBarValueFormatter();
|
||||
|
||||
abstract IAxisValueFormatter getYAxisFormatter();
|
||||
|
||||
abstract int[] getColors();
|
||||
|
||||
abstract String getPieDescription(int targetValue);
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Vebryn
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
|
||||
class ActivityAnalysis {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class);
|
||||
|
||||
// store raw steps and duration
|
||||
protected HashMap<Integer, Long> stats = new HashMap<Integer, Long>();
|
||||
// max speed determined from samples
|
||||
private int maxSpeed = 0;
|
||||
|
||||
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
|
||||
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
|
||||
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
|
||||
ActivityAmount notWorn = new ActivityAmount(ActivityKind.TYPE_NOT_WORN);
|
||||
ActivityAmount activity = new ActivityAmount(ActivityKind.TYPE_ACTIVITY);
|
||||
|
||||
ActivityAmount previousAmount = null;
|
||||
ActivitySample previousSample = null;
|
||||
for (ActivitySample sample : samples) {
|
||||
ActivityAmount amount;
|
||||
switch (sample.getKind()) {
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
amount = deepSleep;
|
||||
break;
|
||||
case ActivityKind.TYPE_LIGHT_SLEEP:
|
||||
amount = lightSleep;
|
||||
break;
|
||||
case ActivityKind.TYPE_NOT_WORN:
|
||||
amount = notWorn;
|
||||
break;
|
||||
case ActivityKind.TYPE_ACTIVITY:
|
||||
default:
|
||||
amount = activity;
|
||||
break;
|
||||
}
|
||||
|
||||
int steps = sample.getSteps();
|
||||
if (steps > 0) {
|
||||
amount.addSteps(steps);
|
||||
}
|
||||
|
||||
if (previousSample != null) {
|
||||
long timeDifference = sample.getTimestamp() - previousSample.getTimestamp();
|
||||
if (previousSample.getRawKind() == sample.getRawKind()) {
|
||||
amount.addSeconds(timeDifference);
|
||||
} else {
|
||||
long sharedTimeDifference = (long) (timeDifference / 2.0f);
|
||||
previousAmount.addSeconds(sharedTimeDifference);
|
||||
amount.addSeconds(sharedTimeDifference);
|
||||
}
|
||||
|
||||
// add time
|
||||
if (steps > 0 && sample.getKind() == ActivityKind.TYPE_ACTIVITY) {
|
||||
if (steps > maxSpeed) {
|
||||
maxSpeed = steps;
|
||||
}
|
||||
|
||||
if (!stats.containsKey(steps)) {
|
||||
LOG.info("Adding: " + steps);
|
||||
stats.put(steps, timeDifference);
|
||||
} else {
|
||||
long time = stats.get(steps);
|
||||
LOG.info("Updating: " + steps + " " + timeDifference + time);
|
||||
stats.put(steps, timeDifference + time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
previousAmount = amount;
|
||||
previousSample = sample;
|
||||
}
|
||||
|
||||
ActivityAmounts result = new ActivityAmounts();
|
||||
if (deepSleep.getTotalSeconds() > 0) {
|
||||
result.addAmount(deepSleep);
|
||||
}
|
||||
if (lightSleep.getTotalSeconds() > 0) {
|
||||
result.addAmount(lightSleep);
|
||||
}
|
||||
if (activity.getTotalSeconds() > 0) {
|
||||
result.addAmount(activity);
|
||||
}
|
||||
result.calculatePercentages();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int calculateTotalSteps(List<? extends ActivitySample> samples) {
|
||||
int totalSteps = 0;
|
||||
for (ActivitySample sample : samples) {
|
||||
int steps = sample.getSteps();
|
||||
if (steps > 0) {
|
||||
totalSteps += sample.getSteps();
|
||||
}
|
||||
}
|
||||
return totalSteps;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.github.mikephil.charting.animation.Easing;
|
||||
import com.github.mikephil.charting.charts.BarLineChartBase;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.components.LegendEntry;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
|
||||
|
||||
public class ActivitySleepChartFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class);
|
||||
|
||||
private BarLineChartBase mChart;
|
||||
|
||||
private int mSmartAlarmFrom = -1;
|
||||
private int mSmartAlarmTo = -1;
|
||||
private int mTimestampFrom = -1;
|
||||
private int mSmartAlarmGoneOff = -1;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_charts, container, false);
|
||||
|
||||
mChart = (BarLineChartBase) rootView.findViewById(R.id.activitysleepchart);
|
||||
|
||||
setupChart();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.activity_sleepchart_activity_and_sleep);
|
||||
}
|
||||
|
||||
private void setupChart() {
|
||||
mChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
configureBarLineChartDefaults(mChart);
|
||||
|
||||
|
||||
XAxis x = mChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
|
||||
YAxis y = mChart.getAxisLeft();
|
||||
y.setDrawGridLines(false);
|
||||
// y.setDrawLabels(false);
|
||||
// TODO: make fixed max value optional
|
||||
y.setAxisMaximum(1f);
|
||||
y.setAxisMinimum(0);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
// y.setLabelCount(5);
|
||||
y.setEnabled(true);
|
||||
|
||||
YAxis yAxisRight = mChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
|
||||
yAxisRight.setDrawLabels(true);
|
||||
yAxisRight.setDrawTopYLabelEntry(true);
|
||||
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
||||
yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
|
||||
yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
|
||||
|
||||
// refresh immediately instead of use refreshIfVisible(), for perceived performance
|
||||
refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(ChartsHost.REFRESH)) {
|
||||
// TODO: use LimitLines to visualize smart alarms?
|
||||
mSmartAlarmFrom = intent.getIntExtra("smartalarm_from", -1);
|
||||
mSmartAlarmTo = intent.getIntExtra("smartalarm_to", -1);
|
||||
mTimestampFrom = intent.getIntExtra("recording_base_timestamp", -1);
|
||||
mSmartAlarmGoneOff = intent.getIntExtra("alarm_gone_off", -1);
|
||||
refresh();
|
||||
} else {
|
||||
super.onReceive(context, intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device);
|
||||
return refresh(device, samples);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(ChartsData chartsData) {
|
||||
DefaultChartsData dcd = (DefaultChartsData) chartsData;
|
||||
mChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
mChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
mChart.getXAxis().setValueFormatter(dcd.getXValueFormatter());
|
||||
mChart.setData(dcd.getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
|
||||
// mChart.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(5);
|
||||
|
||||
LegendEntry activityEntry = new LegendEntry();
|
||||
activityEntry.label = akActivity.label;
|
||||
activityEntry.formColor = akActivity.color;
|
||||
legendEntries.add(activityEntry);
|
||||
|
||||
LegendEntry lightSleepEntry = new LegendEntry();
|
||||
lightSleepEntry.label = akLightSleep.label;
|
||||
lightSleepEntry.formColor = akLightSleep.color;
|
||||
legendEntries.add(lightSleepEntry);
|
||||
|
||||
LegendEntry deepSleepEntry = new LegendEntry();
|
||||
deepSleepEntry.label = akDeepSleep.label;
|
||||
deepSleepEntry.formColor = akDeepSleep.color;
|
||||
legendEntries.add(deepSleepEntry);
|
||||
|
||||
LegendEntry notWornEntry = new LegendEntry();
|
||||
notWornEntry.label = akNotWorn.label;
|
||||
notWornEntry.formColor = akNotWorn.color;
|
||||
legendEntries.add(notWornEntry);
|
||||
|
||||
if (supportsHeartrate(getChartsHost().getDevice())) {
|
||||
LegendEntry hrEntry = new LegendEntry();
|
||||
hrEntry.label = HEARTRATE_LABEL;
|
||||
hrEntry.formColor = HEARTRATE_COLOR;
|
||||
legendEntries.add(hrEntry);
|
||||
}
|
||||
chart.getLegend().setCustom(legendEntries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,367 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Vebryn
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
|
||||
public class ChartsActivity extends AbstractGBFragmentActivity implements ChartsHost {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChartsActivity.class);
|
||||
|
||||
private Button mPrevButton;
|
||||
private Button mNextButton;
|
||||
private TextView mDateControl;
|
||||
|
||||
private Date mStartDate;
|
||||
private Date mEndDate;
|
||||
private SwipeRefreshLayout swipeLayout;
|
||||
private ViewPager viewPager;
|
||||
|
||||
LimitedQueue mActivityAmountCache = new LimitedQueue(60);
|
||||
|
||||
private static class ShowDurationDialog extends Dialog {
|
||||
private final String mDuration;
|
||||
private TextView durationLabel;
|
||||
|
||||
ShowDurationDialog(String duration, Context context) {
|
||||
super(context);
|
||||
mDuration = duration;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_charts_durationdialog);
|
||||
|
||||
durationLabel = (TextView) findViewById(R.id.charts_duration_label);
|
||||
setDuration(mDuration);
|
||||
}
|
||||
|
||||
public void setDuration(String duration) {
|
||||
if (mDuration != null) {
|
||||
durationLabel.setText(duration);
|
||||
} else {
|
||||
durationLabel.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
refreshBusyState(dev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
private GBDevice mGBDevice;
|
||||
private ViewGroup dateBar;
|
||||
|
||||
private void refreshBusyState(GBDevice dev) {
|
||||
if (dev.isBusy()) {
|
||||
swipeLayout.setRefreshing(true);
|
||||
} else {
|
||||
boolean wasBusy = swipeLayout.isRefreshing();
|
||||
swipeLayout.setRefreshing(false);
|
||||
if (wasBusy) {
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
|
||||
}
|
||||
}
|
||||
enableSwipeRefresh(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_charts);
|
||||
|
||||
initDates();
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
swipeLayout = (SwipeRefreshLayout) findViewById(R.id.activity_swipe_layout);
|
||||
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
fetchActivityData();
|
||||
}
|
||||
});
|
||||
enableSwipeRefresh(true);
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
viewPager = (ViewPager) findViewById(R.id.charts_pager);
|
||||
viewPager.setAdapter(getPagerAdapter());
|
||||
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
enableSwipeRefresh(state == ViewPager.SCROLL_STATE_IDLE);
|
||||
}
|
||||
});
|
||||
|
||||
dateBar = (ViewGroup) findViewById(R.id.charts_date_bar);
|
||||
mDateControl = (TextView) findViewById(R.id.charts_text_date);
|
||||
mDateControl.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String detailedDuration = formatDetailedDuration();
|
||||
new ShowDurationDialog(detailedDuration, ChartsActivity.this).show();
|
||||
}
|
||||
});
|
||||
|
||||
mPrevButton = (Button) findViewById(R.id.charts_previous);
|
||||
mPrevButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handlePrevButtonClicked();
|
||||
}
|
||||
});
|
||||
mNextButton = (Button) findViewById(R.id.charts_next);
|
||||
mNextButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handleNextButtonClicked();
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayout mainLayout = (LinearLayout) findViewById(R.id.charts_main_layout);
|
||||
}
|
||||
|
||||
private String formatDetailedDuration() {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||
String dateStringFrom = dateFormat.format(getStartDate());
|
||||
String dateStringTo = dateFormat.format(getEndDate());
|
||||
|
||||
return getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo);
|
||||
}
|
||||
|
||||
protected void initDates() {
|
||||
setEndDate(new Date());
|
||||
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDevice getDevice() {
|
||||
return mGBDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStartDate(Date startDate) {
|
||||
mStartDate = startDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEndDate(Date endDate) {
|
||||
mEndDate = endDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getStartDate() {
|
||||
return mStartDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getEndDate() {
|
||||
return mEndDate;
|
||||
}
|
||||
|
||||
private void handleNextButtonClicked() {
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(DATE_NEXT));
|
||||
}
|
||||
|
||||
private void handlePrevButtonClicked() {
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(DATE_PREV));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.menu_charts, menu);
|
||||
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
|
||||
if (!mGBDevice.isConnected() || !coordinator.supportsActivityDataFetching()) {
|
||||
menu.removeItem(R.id.charts_fetch_activity_data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.charts_fetch_activity_data:
|
||||
fetchActivityData();
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void enableSwipeRefresh(boolean enable) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
|
||||
swipeLayout.setEnabled(enable && coordinator.allowFetchActivityData(mGBDevice));
|
||||
}
|
||||
|
||||
private void fetchActivityData() {
|
||||
if (getDevice().isInitialized()) {
|
||||
GBApplication.deviceService().onFetchActivityData();
|
||||
} else {
|
||||
swipeLayout.setRefreshing(false);
|
||||
GB.toast(this, getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDateInfo(String dateInfo) {
|
||||
mDateControl.setText(dateInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager) {
|
||||
return new SectionsPagerAdapter(fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewGroup getDateBar() {
|
||||
return dateBar;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link FragmentStatePagerAdapter} that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
|
||||
|
||||
SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
// getItem is called to instantiate the fragment for the given page.
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new ActivitySleepChartFragment();
|
||||
case 1:
|
||||
return new SleepChartFragment();
|
||||
case 2:
|
||||
return new WeekSleepChartFragment();
|
||||
case 3:
|
||||
return new WeekStepsChartFragment();
|
||||
case 4:
|
||||
return new SpeedZonesFragment();
|
||||
case 5:
|
||||
return new LiveActivityFragment();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
// Show 5 or 6 total pages.
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
|
||||
if (coordinator.supportsRealtimeData()) {
|
||||
return 6;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getString(R.string.activity_sleepchart_activity_and_sleep);
|
||||
case 1:
|
||||
return getString(R.string.sleepchart_your_sleep);
|
||||
case 2:
|
||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
||||
case 3:
|
||||
return getString(R.string.weekstepschart_steps_a_week);
|
||||
case 4:
|
||||
return getString(R.string.stats_title);
|
||||
case 5:
|
||||
return getString(R.string.liveactivity_live_activity);
|
||||
}
|
||||
return super.getPageTitle(position);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/* Copyright (C) 2016-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
public abstract class ChartsData {
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public interface ChartsHost {
|
||||
String DATE_PREV = ChartsActivity.class.getName().concat(".date_prev");
|
||||
String DATE_NEXT = ChartsActivity.class.getName().concat(".date_next");
|
||||
String REFRESH = ChartsActivity.class.getName().concat(".refresh");
|
||||
|
||||
GBDevice getDevice();
|
||||
|
||||
void setStartDate(Date startDate);
|
||||
|
||||
void setEndDate(Date endDate);
|
||||
|
||||
Date getStartDate();
|
||||
|
||||
Date getEndDate();
|
||||
|
||||
void setDateInfo(String dateInfo);
|
||||
|
||||
ViewGroup getDateBar();
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import com.github.mikephil.charting.charts.BarChart;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.renderer.BarChartRenderer;
|
||||
|
||||
/**
|
||||
* A BarChart with some specific customization, like
|
||||
* <li>allowing to animate a single entry's values without going over 0</li>
|
||||
*/
|
||||
public class CustomBarChart extends BarChart {
|
||||
|
||||
private Entry entry = null;
|
||||
private SingleEntryValueAnimator singleEntryAnimator;
|
||||
|
||||
public CustomBarChart(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public CustomBarChart(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CustomBarChart(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public void setSinglAnimationEntry(Entry entry) {
|
||||
this.entry = entry;
|
||||
|
||||
if (entry != null) {
|
||||
// single entry animation mode
|
||||
singleEntryAnimator = new SingleEntryValueAnimator(entry, new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
// ViewCompat.postInvalidateOnAnimation(Chart.this);
|
||||
postInvalidate();
|
||||
}
|
||||
});
|
||||
mAnimator = singleEntryAnimator;
|
||||
mRenderer = new BarChartRenderer(this, singleEntryAnimator, getViewPortHandler());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to set the next value for the Entry to be animated.
|
||||
* Call animateY() when ready to do that.
|
||||
*
|
||||
* @param nextValue
|
||||
*/
|
||||
public void setSingleEntryYValue(float nextValue) {
|
||||
if (singleEntryAnimator != null) {
|
||||
singleEntryAnimator.setEntryYValue(nextValue);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,530 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.mikephil.charting.charts.BarLineChartBase;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.components.LimitLine;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.BarData;
|
||||
import com.github.mikephil.charting.data.BarDataSet;
|
||||
import com.github.mikephil.charting.data.BarEntry;
|
||||
import com.github.mikephil.charting.data.ChartData;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.data.LineData;
|
||||
import com.github.mikephil.charting.data.LineDataSet;
|
||||
import com.github.mikephil.charting.utils.Utils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Measurement;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class LiveActivityFragment extends AbstractChartFragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LiveActivityFragment.class);
|
||||
private static final int MAX_STEPS_PER_MINUTE = 300;
|
||||
private static final int MIN_STEPS_PER_MINUTE = 60;
|
||||
private static final int RESET_COUNT = 10; // reset the max steps per minute value every 10s
|
||||
|
||||
private BarEntry totalStepsEntry;
|
||||
private BarEntry stepsPerMinuteEntry;
|
||||
private BarDataSet mStepsPerMinuteData;
|
||||
private BarDataSet mTotalStepsData;
|
||||
private LineDataSet mHistorySet;
|
||||
private BarLineChartBase mStepsPerMinuteHistoryChart;
|
||||
private CustomBarChart mStepsPerMinuteCurrentChart;
|
||||
private CustomBarChart mTotalStepsChart;
|
||||
|
||||
private final Steps mSteps = new Steps();
|
||||
private ScheduledExecutorService pulseScheduler;
|
||||
private int maxStepsResetCounter;
|
||||
private List<Measurement> heartRateValues;
|
||||
private LineDataSet mHeartRateSet;
|
||||
private int mHeartRate;
|
||||
private TimestampTranslation tsTranslation;
|
||||
|
||||
private class Steps {
|
||||
private int steps;
|
||||
private int lastTimestamp;
|
||||
private int currentStepsPerMinute;
|
||||
private int maxStepsPerMinute;
|
||||
private int lastStepsPerMinute;
|
||||
|
||||
public int getStepsPerMinute(boolean reset) {
|
||||
lastStepsPerMinute = currentStepsPerMinute;
|
||||
int result = currentStepsPerMinute;
|
||||
if (reset) {
|
||||
currentStepsPerMinute = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getTotalSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public int getMaxStepsPerMinute() {
|
||||
return maxStepsPerMinute;
|
||||
}
|
||||
|
||||
public void updateCurrentSteps(int stepsDelta, int timestamp) {
|
||||
try {
|
||||
if (steps == 0) {
|
||||
steps += stepsDelta;
|
||||
lastTimestamp = timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
int timeDelta = timestamp - lastTimestamp;
|
||||
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
|
||||
if (currentStepsPerMinute > maxStepsPerMinute) {
|
||||
maxStepsPerMinute = currentStepsPerMinute;
|
||||
maxStepsResetCounter = 0;
|
||||
}
|
||||
steps += stepsDelta;
|
||||
lastTimestamp = timestamp;
|
||||
} catch (Exception ex) {
|
||||
GB.toast(LiveActivityFragment.this.getContext(), ex.getMessage(), Toast.LENGTH_SHORT, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private int calculateStepsPerMinute(int stepsDelta, int seconds) {
|
||||
if (stepsDelta == 0) {
|
||||
return 0; // not walking or not enough data per mills?
|
||||
}
|
||||
if (seconds <= 0) {
|
||||
throw new IllegalArgumentException("delta in seconds is <= 0 -- time change?");
|
||||
}
|
||||
|
||||
int oneMinute = 60;
|
||||
float factor = oneMinute / seconds;
|
||||
int result = (int) (stepsDelta * factor);
|
||||
if (result > MAX_STEPS_PER_MINUTE) {
|
||||
// ignore, return previous value instead
|
||||
result = lastStepsPerMinute;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case DeviceService.ACTION_REALTIME_SAMPLES: {
|
||||
ActivitySample sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
|
||||
addSample(sample);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void addSample(ActivitySample sample) {
|
||||
int heartRate = sample.getHeartRate();
|
||||
int timestamp = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (isValidHeartRateValue(heartRate)) {
|
||||
setCurrentHeartRate(heartRate, timestamp);
|
||||
}
|
||||
int steps = sample.getSteps();
|
||||
if (steps != ActivitySample.NOT_MEASURED) {
|
||||
addEntries(steps, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
private int translateTimestampFrom(Intent intent) {
|
||||
return translateTimestamp(intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
private int translateTimestamp(long tsMillis) {
|
||||
int timestamp = (int) (tsMillis / 1000); // translate to seconds
|
||||
return tsTranslation.shorten(timestamp); // and shorten
|
||||
}
|
||||
|
||||
private void setCurrentHeartRate(int heartRate, int timestamp) {
|
||||
addHistoryDataSet(true);
|
||||
mHeartRate = heartRate;
|
||||
}
|
||||
|
||||
private int getCurrentHeartRate() {
|
||||
int result = mHeartRate;
|
||||
mHeartRate = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addEntries(int steps, int timestamp) {
|
||||
mSteps.updateCurrentSteps(steps, timestamp);
|
||||
if (++maxStepsResetCounter > RESET_COUNT) {
|
||||
maxStepsResetCounter = 0;
|
||||
mSteps.maxStepsPerMinute = 0;
|
||||
}
|
||||
// Or: count down the steps until goal reached? And then flash GOAL REACHED -> Set stretch goal
|
||||
LOG.info("Steps: " + steps + ", total: " + mSteps.getTotalSteps() + ", current: " + mSteps.getStepsPerMinute(false));
|
||||
|
||||
// addEntries();
|
||||
}
|
||||
|
||||
private void addEntries(int timestamp) {
|
||||
mTotalStepsChart.setSingleEntryYValue(mSteps.getTotalSteps());
|
||||
YAxis stepsPerMinuteCurrentYAxis = mStepsPerMinuteCurrentChart.getAxisLeft();
|
||||
int maxStepsPerMinute = mSteps.getMaxStepsPerMinute();
|
||||
// int extraRoom = maxStepsPerMinute/5;
|
||||
// buggy in MPAndroidChart? Disable.
|
||||
// stepsPerMinuteCurrentYAxis.setAxisMaxValue(Math.max(MIN_STEPS_PER_MINUTE, maxStepsPerMinute + extraRoom));
|
||||
LimitLine target = new LimitLine(maxStepsPerMinute);
|
||||
stepsPerMinuteCurrentYAxis.removeAllLimitLines();
|
||||
stepsPerMinuteCurrentYAxis.addLimitLine(target);
|
||||
|
||||
int stepsPerMinute = mSteps.getStepsPerMinute(true);
|
||||
mStepsPerMinuteCurrentChart.setSingleEntryYValue(stepsPerMinute);
|
||||
|
||||
if (!addHistoryDataSet(false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChartData data = mStepsPerMinuteHistoryChart.getData();
|
||||
if (stepsPerMinute < 0) {
|
||||
stepsPerMinute = 0;
|
||||
}
|
||||
mHistorySet.addEntry(new Entry(timestamp, stepsPerMinute));
|
||||
int hr = getCurrentHeartRate();
|
||||
if (hr < 0) {
|
||||
hr = 0;
|
||||
}
|
||||
mHeartRateSet.addEntry(new Entry(timestamp, hr));
|
||||
}
|
||||
|
||||
private boolean addHistoryDataSet(boolean force) {
|
||||
if (mStepsPerMinuteHistoryChart.getData() == null) {
|
||||
// ignore the first default value to keep the "no-data-description" visible
|
||||
if (force || mSteps.getTotalSteps() > 0) {
|
||||
LineData data = new LineData();
|
||||
data.addDataSet(mHistorySet);
|
||||
data.addDataSet(mHeartRateSet);
|
||||
mStepsPerMinuteHistoryChart.setData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||
heartRateValues = new ArrayList<>();
|
||||
tsTranslation = new TimestampTranslation();
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false);
|
||||
|
||||
mStepsPerMinuteCurrentChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_per_minute_current);
|
||||
mTotalStepsChart = (CustomBarChart) rootView.findViewById(R.id.livechart_steps_total);
|
||||
mStepsPerMinuteHistoryChart = (BarLineChartBase) rootView.findViewById(R.id.livechart_steps_per_minute_history);
|
||||
|
||||
totalStepsEntry = new BarEntry(1, 0);
|
||||
stepsPerMinuteEntry = new BarEntry(1, 0);
|
||||
|
||||
mStepsPerMinuteData = setupCurrentChart(mStepsPerMinuteCurrentChart, stepsPerMinuteEntry, getString(R.string.live_activity_current_steps_per_minute));
|
||||
mTotalStepsData = setupTotalStepsChart(mTotalStepsChart, totalStepsEntry, getString(R.string.live_activity_total_steps));
|
||||
setupHistoryChart(mStepsPerMinuteHistoryChart);
|
||||
|
||||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
enableRealtimeTracking(false);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
enableRealtimeTracking(true);
|
||||
}
|
||||
|
||||
private ScheduledExecutorService startActivityPulse() {
|
||||
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
|
||||
service.scheduleAtFixedRate(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
FragmentActivity activity = LiveActivityFragment.this.getActivity();
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
pulse();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 0, getPulseIntervalMillis(), TimeUnit.MILLISECONDS);
|
||||
return service;
|
||||
}
|
||||
|
||||
private void stopActivityPulse() {
|
||||
if (pulseScheduler != null) {
|
||||
pulseScheduler.shutdownNow();
|
||||
pulseScheduler = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in the UI thread.
|
||||
*/
|
||||
private void pulse() {
|
||||
addEntries(translateTimestamp(System.currentTimeMillis()));
|
||||
|
||||
LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData();
|
||||
if (historyData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
historyData.notifyDataChanged();
|
||||
mTotalStepsData.notifyDataSetChanged();
|
||||
mStepsPerMinuteData.notifyDataSetChanged();
|
||||
mStepsPerMinuteHistoryChart.notifyDataSetChanged();
|
||||
|
||||
renderCharts();
|
||||
|
||||
// have to enable it again and again to keep it measureing
|
||||
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
|
||||
}
|
||||
|
||||
private int getPulseIntervalMillis() {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMadeVisibleInActivity() {
|
||||
super.onMadeVisibleInActivity();
|
||||
enableRealtimeTracking(true);
|
||||
}
|
||||
|
||||
private void enableRealtimeTracking(boolean enable) {
|
||||
if (enable && pulseScheduler != null) {
|
||||
// already running
|
||||
return;
|
||||
}
|
||||
|
||||
GBApplication.deviceService().onEnableRealtimeSteps(enable);
|
||||
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(enable);
|
||||
if (enable) {
|
||||
if (getActivity() != null) {
|
||||
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
pulseScheduler = startActivityPulse();
|
||||
} else {
|
||||
stopActivityPulse();
|
||||
if (getActivity() != null) {
|
||||
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMadeInvisibleInActivity() {
|
||||
enableRealtimeTracking(false);
|
||||
super.onMadeInvisibleInActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
onMadeInvisibleInActivity();
|
||||
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private BarDataSet setupCurrentChart(CustomBarChart chart, BarEntry entry, String title) {
|
||||
mStepsPerMinuteCurrentChart.getAxisLeft().setAxisMaxValue(MAX_STEPS_PER_MINUTE);
|
||||
return setupCommonChart(chart, entry, title);
|
||||
}
|
||||
|
||||
private BarDataSet setupCommonChart(CustomBarChart chart, BarEntry entry, String title) {
|
||||
chart.setSinglAnimationEntry(entry);
|
||||
|
||||
// chart.getXAxis().setPosition(XAxis.XAxisPosition.TOP);
|
||||
chart.getXAxis().setDrawLabels(false);
|
||||
chart.getXAxis().setEnabled(false);
|
||||
chart.getXAxis().setTextColor(CHART_TEXT_COLOR);
|
||||
chart.getAxisLeft().setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
chart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
chart.getDescription().setText(title);
|
||||
// chart.setNoDataTextDescription("");
|
||||
chart.setNoDataText("");
|
||||
chart.getAxisRight().setEnabled(false);
|
||||
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
List<Integer> colors = new ArrayList<>();
|
||||
|
||||
entries.add(new BarEntry(0, 0));
|
||||
entries.add(entry);
|
||||
entries.add(new BarEntry(2, 0));
|
||||
colors.add(akActivity.color);
|
||||
colors.add(akActivity.color);
|
||||
colors.add(akActivity.color);
|
||||
// //we don't want labels
|
||||
// xLabels.add("");
|
||||
// xLabels.add("");
|
||||
// xLabels.add("");
|
||||
|
||||
BarDataSet set = new BarDataSet(entries, "");
|
||||
set.setDrawValues(false);
|
||||
set.setColors(colors);
|
||||
BarData data = new BarData(set);
|
||||
// data.setGroupSpace(0);
|
||||
chart.setData(data);
|
||||
|
||||
chart.getLegend().setEnabled(false);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private BarDataSet setupTotalStepsChart(CustomBarChart chart, BarEntry entry, String label) {
|
||||
mTotalStepsChart.getAxisLeft().setAxisMaximum(5000); // TODO: use daily goal - already reached steps
|
||||
return setupCommonChart(chart, entry, label); // at the moment, these look the same
|
||||
}
|
||||
|
||||
private void setupHistoryChart(BarLineChartBase chart) {
|
||||
configureBarLineChartDefaults(chart);
|
||||
|
||||
chart.setTouchEnabled(false); // no zooming or anything, because it's updated all the time
|
||||
chart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
chart.getDescription().setText(getString(R.string.live_activity_steps_per_minute_history));
|
||||
chart.setNoDataText(getString(R.string.live_activity_start_your_activity));
|
||||
chart.getLegend().setEnabled(false);
|
||||
Paint infoPaint = chart.getPaint(Chart.PAINT_INFO);
|
||||
infoPaint.setTextSize(Utils.convertDpToPixel(20f));
|
||||
infoPaint.setFakeBoldText(true);
|
||||
chart.setPaint(infoPaint, Chart.PAINT_INFO);
|
||||
|
||||
XAxis x = chart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
|
||||
YAxis y = chart.getAxisLeft();
|
||||
y.setDrawGridLines(false);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
y.setEnabled(true);
|
||||
y.setAxisMinimum(0);
|
||||
|
||||
YAxis yAxisRight = chart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(true);
|
||||
yAxisRight.setDrawLabels(true);
|
||||
yAxisRight.setDrawTopYLabelEntry(false);
|
||||
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
||||
yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE);
|
||||
yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE);
|
||||
|
||||
mHistorySet = new LineDataSet(new ArrayList<Entry>(), getString(R.string.live_activity_steps_history));
|
||||
mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT);
|
||||
mHistorySet.setColor(akActivity.color);
|
||||
mHistorySet.setDrawCircles(false);
|
||||
mHistorySet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
|
||||
mHistorySet.setDrawFilled(true);
|
||||
mHistorySet.setDrawValues(false);
|
||||
|
||||
mHeartRateSet = createHeartrateSet(new ArrayList<Entry>(), getString(R.string.live_activity_heart_rate));
|
||||
mHeartRateSet.setDrawValues(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getContext().getString(R.string.liveactivity_live_activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDateBar(boolean show) {
|
||||
// never show the data bar
|
||||
super.showDateBar(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void refresh() {
|
||||
// do nothing, we don't have any db interaction
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(ChartsData chartsData) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
mStepsPerMinuteCurrentChart.animateY(150);
|
||||
mTotalStepsChart.animateY(150);
|
||||
mStepsPerMinuteHistoryChart.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
throw new UnsupportedOperationException("no db access supported for live activity");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart chart) {
|
||||
// no legend
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/* Copyright (C) 2015-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
|
||||
import com.github.mikephil.charting.animation.ChartAnimator;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SingleEntryValueAnimator extends ChartAnimator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SingleEntryValueAnimator.class);
|
||||
|
||||
private final Entry entry;
|
||||
private final ValueAnimator.AnimatorUpdateListener listener;
|
||||
private float previousValue;
|
||||
|
||||
public SingleEntryValueAnimator(Entry singleEntry, ValueAnimator.AnimatorUpdateListener listener) {
|
||||
super(listener);
|
||||
this.listener = listener;
|
||||
entry = singleEntry;
|
||||
}
|
||||
|
||||
public void setEntryYValue(float value) {
|
||||
this.previousValue = entry.getY();
|
||||
entry.setY(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animateY(int durationMillis) {
|
||||
// we start with the previous value and animate the change to the
|
||||
// next value.
|
||||
// as our animation values are not used as absolute values, but as factors,
|
||||
// we have to calculate the proper factors in advance. The entry already has
|
||||
// the new value, so we create a factor to calculate the old value from the
|
||||
// new value.
|
||||
|
||||
float startAnim;
|
||||
float endAnim = 1f;
|
||||
if (entry.getY() == 0f) {
|
||||
startAnim = 0f;
|
||||
} else {
|
||||
startAnim = previousValue / entry.getY();
|
||||
}
|
||||
|
||||
// LOG.debug("anim factors: " + startAnim + ", " + endAnim);
|
||||
|
||||
ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "phaseY", startAnim, endAnim);
|
||||
animatorY.setDuration(durationMillis);
|
||||
animatorY.addUpdateListener(listener);
|
||||
animatorY.start();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.github.mikephil.charting.animation.Easing;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.charts.CombinedChart;
|
||||
import com.github.mikephil.charting.charts.PieChart;
|
||||
import com.github.mikephil.charting.components.LegendEntry;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.CombinedData;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.data.PieData;
|
||||
import com.github.mikephil.charting.data.PieDataSet;
|
||||
import com.github.mikephil.charting.data.PieEntry;
|
||||
import com.github.mikephil.charting.formatter.IValueFormatter;
|
||||
import com.github.mikephil.charting.utils.ViewPortHandler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
|
||||
|
||||
public class SleepChartFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class);
|
||||
|
||||
private CombinedChart mActivityChart;
|
||||
private PieChart mSleepAmountChart;
|
||||
|
||||
private int mSmartAlarmFrom = -1;
|
||||
private int mSmartAlarmTo = -1;
|
||||
private int mTimestampFrom = -1;
|
||||
private int mSmartAlarmGoneOff = -1;
|
||||
|
||||
@Override
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device);
|
||||
|
||||
MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples);
|
||||
DefaultChartsData chartsData = refresh(device, samples);
|
||||
|
||||
return new MyChartsData(mySleepChartsData, chartsData);
|
||||
}
|
||||
|
||||
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<? extends ActivitySample> samples) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
|
||||
PieData data = new PieData();
|
||||
List<PieEntry> entries = new ArrayList<>();
|
||||
List<Integer> colors = new ArrayList<>();
|
||||
// int index = 0;
|
||||
long totalSeconds = 0;
|
||||
for (ActivityAmount amount : amounts.getAmounts()) {
|
||||
if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) {
|
||||
long value = amount.getTotalSeconds();
|
||||
totalSeconds += value;
|
||||
// entries.add(new PieEntry(value, index++));
|
||||
entries.add(new PieEntry(value, amount.getName(getActivity())));
|
||||
colors.add(getColorFor(amount.getActivityKind()));
|
||||
// data.addXValue(amount.getName(getActivity()));
|
||||
}
|
||||
}
|
||||
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(totalSeconds, TimeUnit.SECONDS);
|
||||
PieDataSet set = new PieDataSet(entries, "");
|
||||
set.setValueFormatter(new IValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
|
||||
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
|
||||
}
|
||||
});
|
||||
set.setColors(colors);
|
||||
set.setValueTextColor(DESCRIPTION_COLOR);
|
||||
set.setValueTextSize(13f);
|
||||
set.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
|
||||
set.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
|
||||
data.setDataSet(set);
|
||||
|
||||
//setupLegend(pieChart);
|
||||
return new MySleepChartsData(totalSleep, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(ChartsData chartsData) {
|
||||
MyChartsData mcd = (MyChartsData) chartsData;
|
||||
mSleepAmountChart.setCenterText(mcd.getPieData().getTotalSleep());
|
||||
mSleepAmountChart.setData(mcd.getPieData().getPieData());
|
||||
|
||||
mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter());
|
||||
mActivityChart.setData(mcd.getChartsData().getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.sleepchart_your_sleep);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
|
||||
|
||||
mActivityChart = (CombinedChart) rootView.findViewById(R.id.sleepchart);
|
||||
mSleepAmountChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
|
||||
|
||||
setupActivityChart();
|
||||
setupSleepAmountChart();
|
||||
|
||||
// refresh immediately instead of use refreshIfVisible(), for perceived performance
|
||||
refresh();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(ChartsHost.REFRESH)) {
|
||||
// TODO: use LimitLines to visualize smart alarms?
|
||||
mSmartAlarmFrom = intent.getIntExtra("smartalarm_from", -1);
|
||||
mSmartAlarmTo = intent.getIntExtra("smartalarm_to", -1);
|
||||
mTimestampFrom = intent.getIntExtra("recording_base_timestamp", -1);
|
||||
mSmartAlarmGoneOff = intent.getIntExtra("alarm_gone_off", -1);
|
||||
refresh();
|
||||
} else {
|
||||
super.onReceive(context, intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSleepAmountChart() {
|
||||
mSleepAmountChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mSleepAmountChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
mSleepAmountChart.setEntryLabelColor(DESCRIPTION_COLOR);
|
||||
mSleepAmountChart.getDescription().setText("");
|
||||
// mSleepAmountChart.getDescription().setNoDataTextDescription("");
|
||||
mSleepAmountChart.setNoDataText("");
|
||||
mSleepAmountChart.getLegend().setEnabled(false);
|
||||
}
|
||||
|
||||
private void setupActivityChart() {
|
||||
mActivityChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mActivityChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
configureBarLineChartDefaults(mActivityChart);
|
||||
|
||||
XAxis x = mActivityChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
|
||||
YAxis y = mActivityChart.getAxisLeft();
|
||||
y.setDrawGridLines(false);
|
||||
// y.setDrawLabels(false);
|
||||
// TODO: make fixed max value optional
|
||||
y.setAxisMaximum(1f);
|
||||
y.setAxisMinimum(0);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
// y.setLabelCount(5);
|
||||
y.setEnabled(true);
|
||||
|
||||
YAxis yAxisRight = mActivityChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(supportsHeartrate(getChartsHost().getDevice()));
|
||||
yAxisRight.setDrawLabels(true);
|
||||
yAxisRight.setDrawTopYLabelEntry(true);
|
||||
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
||||
yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE);
|
||||
yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(3);
|
||||
LegendEntry lightSleepEntry = new LegendEntry();
|
||||
lightSleepEntry.label = akLightSleep.label;
|
||||
lightSleepEntry.formColor = akLightSleep.color;
|
||||
legendEntries.add(lightSleepEntry);
|
||||
|
||||
LegendEntry deepSleepEntry = new LegendEntry();
|
||||
deepSleepEntry.label = akDeepSleep.label;
|
||||
deepSleepEntry.formColor = akDeepSleep.color;
|
||||
legendEntries.add(deepSleepEntry);
|
||||
|
||||
if (supportsHeartrate(getChartsHost().getDevice())) {
|
||||
LegendEntry hrEntry = new LegendEntry();
|
||||
hrEntry.label = HEARTRATE_LABEL;
|
||||
hrEntry.formColor = HEARTRATE_COLOR;
|
||||
legendEntries.add(hrEntry);
|
||||
}
|
||||
chart.getLegend().setCustom(legendEntries);
|
||||
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
// temporary fix for totally wrong sleep amounts
|
||||
// return super.getSleepSamples(db, device, tsFrom, tsTo);
|
||||
return super.getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
mActivityChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
|
||||
mSleepAmountChart.invalidate();
|
||||
}
|
||||
|
||||
private static class MySleepChartsData extends ChartsData {
|
||||
private String totalSleep;
|
||||
private final PieData pieData;
|
||||
|
||||
public MySleepChartsData(String totalSleep, PieData pieData) {
|
||||
this.totalSleep = totalSleep;
|
||||
this.pieData = pieData;
|
||||
}
|
||||
|
||||
public PieData getPieData() {
|
||||
return pieData;
|
||||
}
|
||||
|
||||
public CharSequence getTotalSleep() {
|
||||
return totalSleep;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyChartsData extends ChartsData {
|
||||
private final DefaultChartsData<CombinedData> chartsData;
|
||||
private final MySleepChartsData pieData;
|
||||
|
||||
public MyChartsData(MySleepChartsData pieData, DefaultChartsData<CombinedData> chartsData) {
|
||||
this.pieData = pieData;
|
||||
this.chartsData = chartsData;
|
||||
}
|
||||
|
||||
public MySleepChartsData getPieData() {
|
||||
return pieData;
|
||||
}
|
||||
|
||||
public DefaultChartsData<CombinedData> getChartsData() {
|
||||
return chartsData;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class SleepUtils {
|
||||
public static final float Y_VALUE_DEEP_SLEEP = 0.01f;
|
||||
public static final float Y_VALUE_LIGHT_SLEEP = 0.016f;
|
||||
|
||||
public static boolean isSleep(byte type) {
|
||||
return type == ActivityKind.TYPE_DEEP_SLEEP || type == ActivityKind.TYPE_LIGHT_SLEEP;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti, Vebryn
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.charts.HorizontalBarChart;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.BarData;
|
||||
import com.github.mikephil.charting.data.BarDataSet;
|
||||
import com.github.mikephil.charting.data.BarEntry;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
|
||||
|
||||
public class SpeedZonesFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(SpeedZonesFragment.class);
|
||||
|
||||
private HorizontalBarChart mStatsChart;
|
||||
|
||||
@Override
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device);
|
||||
|
||||
MySpeedZonesData mySpeedZonesData = refreshStats(samples);
|
||||
|
||||
return new MyChartsData(mySpeedZonesData);
|
||||
}
|
||||
|
||||
private MySpeedZonesData refreshStats(List<? extends ActivitySample> samples) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
analysis.calculateActivityAmounts(samples);
|
||||
BarData data = new BarData();
|
||||
data.setValueTextColor(CHART_TEXT_COLOR);
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
|
||||
ActivityUser user = new ActivityUser();
|
||||
/*double distanceFactorCm;
|
||||
if (user.getGender() == user.GENDER_MALE){
|
||||
distanceFactorCm = user.getHeightCm() * user.GENDER_MALE_DISTANCE_FACTOR / 1000;
|
||||
} else {
|
||||
distanceFactorCm = user.getHeightCm() * user.GENDER_FEMALE_DISTANCE_FACTOR / 1000;
|
||||
}*/
|
||||
|
||||
for (Map.Entry<Integer, Long> entry : analysis.stats.entrySet()) {
|
||||
entries.add(new BarEntry(entry.getKey(), entry.getValue() / 60));
|
||||
}
|
||||
|
||||
BarDataSet set = new BarDataSet(entries, "");
|
||||
set.setValueTextColor(CHART_TEXT_COLOR);
|
||||
set.setColors(getColorFor(ActivityKind.TYPE_ACTIVITY));
|
||||
//set.setDrawValues(false);
|
||||
//data.setBarWidth(0.1f);
|
||||
data.addDataSet(set);
|
||||
|
||||
return new MySpeedZonesData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(ChartsData chartsData) {
|
||||
MyChartsData mcd = (MyChartsData) chartsData;
|
||||
mStatsChart.setData(mcd.getChartsData().getBarData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.stats_title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_statschart, container, false);
|
||||
|
||||
mStatsChart = (HorizontalBarChart) rootView.findViewById(R.id.statschart);
|
||||
setupStatsChart();
|
||||
|
||||
// refresh immediately instead of use refreshIfVisible(), for perceived performance
|
||||
refresh();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void setupStatsChart() {
|
||||
mStatsChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mStatsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
mStatsChart.setNoDataText("");
|
||||
mStatsChart.getLegend().setEnabled(false);
|
||||
mStatsChart.setTouchEnabled(false);
|
||||
mStatsChart.getDescription().setText("");
|
||||
|
||||
XAxis right = mStatsChart.getXAxis(); //believe it or not, the X axis is vertical for HorizontalBarChart
|
||||
right.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
YAxis bottom = mStatsChart.getAxisRight();
|
||||
bottom.setTextColor(CHART_TEXT_COLOR);
|
||||
bottom.setGranularity(1f);
|
||||
|
||||
YAxis top = mStatsChart.getAxisLeft();
|
||||
top.setTextColor(CHART_TEXT_COLOR);
|
||||
top.setGranularity(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return super.getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart chart) {
|
||||
// no legend here, it is all about the steps here
|
||||
chart.getLegend().setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
mStatsChart.invalidate();
|
||||
}
|
||||
|
||||
private static class MySpeedZonesData extends ChartsData {
|
||||
private final BarData barData;
|
||||
|
||||
MySpeedZonesData(BarData barData) {
|
||||
this.barData = barData;
|
||||
}
|
||||
|
||||
BarData getBarData() {
|
||||
return barData;
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyChartsData extends ChartsData {
|
||||
private final MySpeedZonesData chartsData;
|
||||
|
||||
MyChartsData(MySpeedZonesData chartsData) {
|
||||
this.chartsData = chartsData;
|
||||
}
|
||||
|
||||
MySpeedZonesData getChartsData() {
|
||||
return chartsData;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/* Copyright (C) 2016-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import com.github.mikephil.charting.components.AxisBase;
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
public class TimestampValueFormatter implements IAxisValueFormatter {
|
||||
private final Calendar cal;
|
||||
// private DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||
private DateFormat dateFormat;
|
||||
|
||||
public TimestampValueFormatter() {
|
||||
this(new SimpleDateFormat("HH:mm"));
|
||||
|
||||
}
|
||||
|
||||
public TimestampValueFormatter(DateFormat dateFormat) {
|
||||
this.dateFormat = dateFormat;
|
||||
cal = GregorianCalendar.getInstance();
|
||||
cal.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormattedValue(float value, AxisBase axis) {
|
||||
cal.setTimeInMillis((int) value * 1000L);
|
||||
Date date = cal.getTime();
|
||||
String dateString = dateFormat.format(date);
|
||||
return dateString;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/* Copyright (C) 2016-2017 Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
|
||||
public class TrailingActivitySample extends AbstractActivitySample {
|
||||
private int timestamp;
|
||||
private long userId;
|
||||
private long deviceId;
|
||||
|
||||
@Override
|
||||
public void setTimestamp(int timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserId(long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDeviceId(long deviceId) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.components.AxisBase;
|
||||
import com.github.mikephil.charting.components.LegendEntry;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
|
||||
import com.github.mikephil.charting.formatter.IValueFormatter;
|
||||
import com.github.mikephil.charting.utils.ViewPortHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
|
||||
public class WeekSleepChartFragment extends AbstractWeekChartFragment {
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getPieDescription(int targetValue) {
|
||||
return getString(R.string.weeksleepchart_today_sleep_description, DateTimeUtils.formatDurationHoursMinutes(targetValue, TimeUnit.MINUTES));
|
||||
}
|
||||
|
||||
@Override
|
||||
int getGoal() {
|
||||
return GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_SLEEP_DURATION, 8) * 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getOffsetHours() {
|
||||
return -12;
|
||||
}
|
||||
|
||||
@Override
|
||||
float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts) {
|
||||
long totalSecondsDeepSleep = 0;
|
||||
long totalSecondsLightSleep = 0;
|
||||
for (ActivityAmount amount : activityAmounts.getAmounts()) {
|
||||
if (amount.getActivityKind() == ActivityKind.TYPE_DEEP_SLEEP) {
|
||||
totalSecondsDeepSleep += amount.getTotalSeconds();
|
||||
} else if (amount.getActivityKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
|
||||
totalSecondsLightSleep += amount.getTotalSeconds();
|
||||
}
|
||||
}
|
||||
return new float[]{(int) (totalSecondsDeepSleep / 60), (int) (totalSecondsLightSleep / 60)};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String formatPieValue(int value) {
|
||||
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
String[] getPieLabels() {
|
||||
return new String[]{getString(R.string.abstract_chart_fragment_kind_deep_sleep), getString(R.string.abstract_chart_fragment_kind_light_sleep)};
|
||||
}
|
||||
|
||||
@Override
|
||||
IValueFormatter getPieValueFormatter() {
|
||||
return new IValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
|
||||
return formatPieValue((int) value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
IValueFormatter getBarValueFormatter() {
|
||||
return new IValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
|
||||
return DateTimeUtils.minutesToHHMM((int) value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
IAxisValueFormatter getYAxisFormatter() {
|
||||
return new IAxisValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value, AxisBase axis) {
|
||||
return DateTimeUtils.minutesToHHMM((int) value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
int[] getColors() {
|
||||
return new int[]{akDeepSleep.color, akLightSleep.color};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(2);
|
||||
|
||||
LegendEntry lightSleepEntry = new LegendEntry();
|
||||
lightSleepEntry.label = akLightSleep.label;
|
||||
lightSleepEntry.formColor = akLightSleep.color;
|
||||
legendEntries.add(lightSleepEntry);
|
||||
|
||||
LegendEntry deepSleepEntry = new LegendEntry();
|
||||
deepSleepEntry.label = akDeepSleep.label;
|
||||
deepSleepEntry.formColor = akDeepSleep.color;
|
||||
legendEntries.add(deepSleepEntry);
|
||||
|
||||
chart.getLegend().setCustom(legendEntries);
|
||||
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
|
||||
Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
|
||||
import com.github.mikephil.charting.formatter.IValueFormatter;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
|
||||
public class WeekStepsChartFragment extends AbstractWeekChartFragment {
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.weekstepschart_steps_a_week);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getPieDescription(int targetValue) {
|
||||
return getString(R.string.weeksteps_today_steps_description, String.valueOf(targetValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
int getGoal() {
|
||||
return GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, 10000);
|
||||
}
|
||||
|
||||
@Override
|
||||
int getOffsetHours() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts) {
|
||||
int totalSteps = 0;
|
||||
for (ActivityAmount amount : activityAmounts.getAmounts()) {
|
||||
totalSteps += amount.getTotalSteps();
|
||||
amount.getTotalSteps();
|
||||
}
|
||||
return new float[]{totalSteps};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String formatPieValue(int value) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
String[] getPieLabels() {
|
||||
return new String[]{""};
|
||||
}
|
||||
|
||||
@Override
|
||||
IValueFormatter getPieValueFormatter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
IValueFormatter getBarValueFormatter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
IAxisValueFormatter getYAxisFormatter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
int[] getColors() {
|
||||
return new int[]{akActivity.color};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart chart) {
|
||||
// no legend here, it is all about the steps here
|
||||
chart.getLegend().setEnabled(false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/* Copyright (C) 2017 Carsten Pfeiffer, Daniele Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapter.AppBLViewHolder> implements Filterable {
|
||||
|
||||
private List<ApplicationInfo> applicationInfoList;
|
||||
private final int mLayoutId;
|
||||
private final Context mContext;
|
||||
private final PackageManager mPm;
|
||||
private final IdentityHashMap<ApplicationInfo, String> mNameMap;
|
||||
|
||||
private ApplicationFilter applicationFilter;
|
||||
|
||||
public AppBlacklistAdapter(int layoutId, Context context) {
|
||||
mLayoutId = layoutId;
|
||||
mContext = context;
|
||||
mPm = context.getPackageManager();
|
||||
|
||||
applicationInfoList = mPm.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
|
||||
// sort the package list by label and blacklist status
|
||||
mNameMap = new IdentityHashMap<>(applicationInfoList.size());
|
||||
for (ApplicationInfo ai : applicationInfoList) {
|
||||
CharSequence name = mPm.getApplicationLabel(ai);
|
||||
if (name == null) {
|
||||
name = ai.packageName;
|
||||
}
|
||||
if (GBApplication.appIsBlacklisted(ai.packageName)) {
|
||||
// sort blacklisted first by prefixing with a '!'
|
||||
name = "!" + name;
|
||||
}
|
||||
mNameMap.put(ai, name.toString());
|
||||
}
|
||||
|
||||
Collections.sort(applicationInfoList, new Comparator<ApplicationInfo>() {
|
||||
@Override
|
||||
public int compare(ApplicationInfo ai1, ApplicationInfo ai2) {
|
||||
final String s1 = mNameMap.get(ai1);
|
||||
final String s2 = mNameMap.get(ai2);
|
||||
return s1.compareTo(s2);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppBlacklistAdapter.AppBLViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(mContext).inflate(mLayoutId, parent, false);
|
||||
return new AppBLViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AppBlacklistAdapter.AppBLViewHolder holder, int position) {
|
||||
final ApplicationInfo appInfo = applicationInfoList.get(position);
|
||||
|
||||
holder.deviceAppVersionAuthorLabel.setText(appInfo.packageName);
|
||||
holder.deviceAppNameLabel.setText(mNameMap.get(appInfo));
|
||||
holder.deviceImageView.setImageDrawable(appInfo.loadIcon(mPm));
|
||||
|
||||
holder.checkbox.setChecked(GBApplication.appIsBlacklisted(appInfo.packageName));
|
||||
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox));
|
||||
checkBox.toggle();
|
||||
if (checkBox.isChecked()) {
|
||||
GBApplication.addAppToBlacklist(appInfo.packageName);
|
||||
} else {
|
||||
GBApplication.removeFromAppsBlacklist(appInfo.packageName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return applicationInfoList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
if (applicationFilter == null)
|
||||
applicationFilter = new ApplicationFilter(this, applicationInfoList);
|
||||
return applicationFilter;
|
||||
}
|
||||
|
||||
public class AppBLViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
final CheckBox checkbox;
|
||||
final ImageView deviceImageView;
|
||||
final TextView deviceAppVersionAuthorLabel;
|
||||
final TextView deviceAppNameLabel;
|
||||
|
||||
AppBLViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
checkbox = (CheckBox) itemView.findViewById(R.id.item_checkbox);
|
||||
deviceImageView = (ImageView) itemView.findViewById(R.id.item_image);
|
||||
deviceAppVersionAuthorLabel = (TextView) itemView.findViewById(R.id.item_details);
|
||||
deviceAppNameLabel = (TextView) itemView.findViewById(R.id.item_name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ApplicationFilter extends Filter {
|
||||
|
||||
private final AppBlacklistAdapter adapter;
|
||||
private final List<ApplicationInfo> originalList;
|
||||
private final List<ApplicationInfo> filteredList;
|
||||
|
||||
private ApplicationFilter(AppBlacklistAdapter adapter, List<ApplicationInfo> originalList) {
|
||||
super();
|
||||
this.originalList = new ArrayList<>(originalList);
|
||||
this.filteredList = new ArrayList<>();
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter.FilterResults performFiltering(CharSequence filter) {
|
||||
filteredList.clear();
|
||||
final Filter.FilterResults results = new Filter.FilterResults();
|
||||
|
||||
if (filter == null || filter.length() == 0)
|
||||
filteredList.addAll(originalList);
|
||||
else {
|
||||
final String filterPattern = filter.toString().toLowerCase().trim();
|
||||
|
||||
for (ApplicationInfo ai : originalList) {
|
||||
CharSequence name = mPm.getApplicationLabel(ai);
|
||||
if (name.toString().contains(filterPattern) ||
|
||||
(ai.packageName.contains(filterPattern))) {
|
||||
filteredList.add(ai);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.values = filteredList;
|
||||
results.count = filteredList.size();
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, Filter.FilterResults filterResults) {
|
||||
adapter.applicationInfoList.clear();
|
||||
adapter.applicationInfoList.addAll((List<ApplicationInfo>) filterResults.values);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* Adapter for displaying GBDeviceCandate instances.
|
||||
*/
|
||||
public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public DeviceCandidateAdapter(Context context, List<GBDeviceCandidate> deviceCandidates) {
|
||||
super(context, 0, deviceCandidates);
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
GBDeviceCandidate device = getItem(position);
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
view = inflater.inflate(R.layout.item_with_details, parent, false);
|
||||
}
|
||||
ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_image);
|
||||
TextView deviceNameLabel = (TextView) view.findViewById(R.id.item_name);
|
||||
TextView deviceAddressLabel = (TextView) view.findViewById(R.id.item_details);
|
||||
|
||||
String name = formatDeviceCandidate(device);
|
||||
deviceNameLabel.setText(name);
|
||||
deviceAddressLabel.setText(device.getMacAddress());
|
||||
deviceImageView.setImageResource(device.getDeviceType().getIcon());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private String formatDeviceCandidate(GBDeviceCandidate device) {
|
||||
if (device.getRssi() > GBDevice.RSSI_UNKNOWN) {
|
||||
return context.getString(R.string.device_with_rssi, device.getName(), GB.formatRssi(device.getRssi()));
|
||||
}
|
||||
return device.getName();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.CardView;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
|
||||
/**
|
||||
* Adapter for displaying GBAlarm instances.
|
||||
*/
|
||||
public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.ViewHolder> {
|
||||
|
||||
|
||||
private final Context mContext;
|
||||
private List<GBAlarm> alarmList;
|
||||
|
||||
public GBAlarmListAdapter(Context context, List<GBAlarm> alarmList) {
|
||||
this.mContext = context;
|
||||
this.alarmList = alarmList;
|
||||
}
|
||||
|
||||
public GBAlarmListAdapter(Context context, Set<String> preferencesAlarmListSet) {
|
||||
this.mContext = context;
|
||||
alarmList = new ArrayList<>();
|
||||
|
||||
for (String alarmString : preferencesAlarmListSet) {
|
||||
alarmList.add(new GBAlarm(alarmString));
|
||||
}
|
||||
|
||||
Collections.sort(alarmList);
|
||||
}
|
||||
|
||||
public void setAlarmList(Set<String> preferencesAlarmListSet, int reservedSlots) {
|
||||
alarmList = new ArrayList<>();
|
||||
|
||||
for (String alarmString : preferencesAlarmListSet) {
|
||||
alarmList.add(new GBAlarm(alarmString));
|
||||
}
|
||||
|
||||
Collections.sort(alarmList);
|
||||
|
||||
//cannot do this earlier because the Set is not guaranteed to be in order by ID
|
||||
alarmList.subList(alarmList.size() - reservedSlots, alarmList.size()).clear();
|
||||
}
|
||||
|
||||
public ArrayList<? extends Alarm> getAlarmList() {
|
||||
return (ArrayList) alarmList;
|
||||
}
|
||||
|
||||
|
||||
public void update(GBAlarm alarm) {
|
||||
for (GBAlarm a : alarmList) {
|
||||
if (alarm.equals(a)) {
|
||||
a = alarm;
|
||||
}
|
||||
}
|
||||
alarm.store();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBAlarmListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_alarm, parent, false);
|
||||
ViewHolder vh = new ViewHolder(view);
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, final int position) {
|
||||
|
||||
final GBAlarm alarm = alarmList.get(position);
|
||||
|
||||
holder.alarmDayMonday.setChecked(alarm.getRepetition(Alarm.ALARM_MON));
|
||||
holder.alarmDayTuesday.setChecked(alarm.getRepetition(Alarm.ALARM_TUE));
|
||||
holder.alarmDayWednesday.setChecked(alarm.getRepetition(Alarm.ALARM_WED));
|
||||
holder.alarmDayThursday.setChecked(alarm.getRepetition(Alarm.ALARM_THU));
|
||||
holder.alarmDayFriday.setChecked(alarm.getRepetition(Alarm.ALARM_FRI));
|
||||
holder.alarmDaySaturday.setChecked(alarm.getRepetition(Alarm.ALARM_SAT));
|
||||
holder.alarmDaySunday.setChecked(alarm.getRepetition(Alarm.ALARM_SUN));
|
||||
|
||||
holder.isEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
alarm.setEnabled(isChecked);
|
||||
update(alarm);
|
||||
}
|
||||
});
|
||||
|
||||
holder.container.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
((ConfigureAlarms) mContext).configureAlarm(alarm);
|
||||
}
|
||||
});
|
||||
holder.alarmTime.setText(alarm.getTime());
|
||||
holder.isEnabled.setChecked(alarm.isEnabled());
|
||||
if (alarm.isSmartWakeup()) {
|
||||
holder.isSmartWakeup.setVisibility(TextView.VISIBLE);
|
||||
} else {
|
||||
holder.isSmartWakeup.setVisibility(TextView.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return alarmList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
CardView container;
|
||||
|
||||
TextView alarmTime;
|
||||
Switch isEnabled;
|
||||
TextView isSmartWakeup;
|
||||
|
||||
CheckedTextView alarmDayMonday;
|
||||
CheckedTextView alarmDayTuesday;
|
||||
CheckedTextView alarmDayWednesday;
|
||||
CheckedTextView alarmDayThursday;
|
||||
CheckedTextView alarmDayFriday;
|
||||
CheckedTextView alarmDaySaturday;
|
||||
CheckedTextView alarmDaySunday;
|
||||
|
||||
ViewHolder(View view) {
|
||||
super(view);
|
||||
|
||||
container = (CardView) view.findViewById(R.id.card_view);
|
||||
|
||||
alarmTime = (TextView) view.findViewById(R.id.alarm_item_time);
|
||||
isEnabled = (Switch) view.findViewById(R.id.alarm_item_toggle);
|
||||
isSmartWakeup = (TextView) view.findViewById(R.id.alarm_smart_wakeup);
|
||||
|
||||
alarmDayMonday = (CheckedTextView) view.findViewById(R.id.alarm_item_monday);
|
||||
alarmDayTuesday = (CheckedTextView) view.findViewById(R.id.alarm_item_tuesday);
|
||||
alarmDayWednesday = (CheckedTextView) view.findViewById(R.id.alarm_item_wednesday);
|
||||
alarmDayThursday = (CheckedTextView) view.findViewById(R.id.alarm_item_thursday);
|
||||
alarmDayFriday = (CheckedTextView) view.findViewById(R.id.alarm_item_friday);
|
||||
alarmDaySaturday = (CheckedTextView) view.findViewById(R.id.alarm_item_saturday);
|
||||
alarmDaySunday = (CheckedTextView) view.findViewById(R.id.alarm_item_sunday);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
|
||||
|
||||
private final Context context;
|
||||
private final List<GBDevice> deviceList;
|
||||
|
||||
public GBDeviceAdapter(Context context, List<GBDevice> deviceList) {
|
||||
super(context, 0, deviceList);
|
||||
|
||||
this.context = context;
|
||||
this.deviceList = deviceList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
GBDevice device = getItem(position);
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
view = inflater.inflate(R.layout.device_item, parent, false);
|
||||
}
|
||||
TextView deviceStatusLabel = (TextView) view.findViewById(R.id.device_status);
|
||||
TextView deviceNameLabel = (TextView) view.findViewById(R.id.device_name);
|
||||
ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_image);
|
||||
|
||||
deviceStatusLabel.setText(device.getInfoString());
|
||||
deviceNameLabel.setText(device.getName());
|
||||
|
||||
switch (device.getType()) {
|
||||
case PEBBLE:
|
||||
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
|
||||
break;
|
||||
case MIBAND:
|
||||
deviceImageView.setImageResource(R.drawable.ic_device_miband);
|
||||
break;
|
||||
default:
|
||||
deviceImageView.setImageResource(R.drawable.ic_launcher);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,459 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Lem Dulfo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.widget.CardView;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.transition.TransitionManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AudioSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* Adapter for displaying GBDevice instances.
|
||||
*/
|
||||
public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.ViewHolder> {
|
||||
|
||||
private final Context context;
|
||||
private List<GBDevice> deviceList;
|
||||
private int expandedDevicePosition = RecyclerView.NO_POSITION;
|
||||
private ViewGroup parent;
|
||||
|
||||
public GBDeviceAdapterv2(Context context, List<GBDevice> deviceList) {
|
||||
this.context = context;
|
||||
this.deviceList = deviceList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceAdapterv2.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
this.parent = parent;
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.device_itemv2, parent, false);
|
||||
ViewHolder vh = new ViewHolder(view);
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, final int position) {
|
||||
final GBDevice device = deviceList.get(position);
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
|
||||
holder.container.setOnClickListener(new View.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (device.isInitialized() || device.isConnected()) {
|
||||
showTransientSnackbar(R.string.controlcenter_snackbar_need_longpress);
|
||||
} else {
|
||||
showTransientSnackbar(R.string.controlcenter_snackbar_connecting);
|
||||
GBApplication.deviceService().connect(device);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
holder.container.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (device.getState() != GBDevice.State.NOT_CONNECTED) {
|
||||
showTransientSnackbar(R.string.controlcenter_snackbar_disconnecting);
|
||||
GBApplication.deviceService().disconnect();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
holder.deviceImageView.setImageResource(R.drawable.level_list_device);
|
||||
//level-list does not allow negative values, hence we always add 100 to the key.
|
||||
holder.deviceImageView.setImageLevel(device.getType().getKey() + 100 + (device.isInitialized() ? 100 : 0));
|
||||
|
||||
holder.deviceNameLabel.setText(getUniqueDeviceName(device));
|
||||
|
||||
if (device.isBusy()) {
|
||||
holder.deviceStatusLabel.setText(device.getBusyTask());
|
||||
holder.busyIndicator.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.deviceStatusLabel.setText(device.getStateString());
|
||||
holder.busyIndicator.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
//begin of action row
|
||||
//battery
|
||||
holder.batteryStatusBox.setVisibility(View.GONE);
|
||||
short batteryLevel = device.getBatteryLevel();
|
||||
if (batteryLevel != GBDevice.BATTERY_UNKNOWN) {
|
||||
holder.batteryStatusBox.setVisibility(View.VISIBLE);
|
||||
holder.batteryStatusLabel.setText(device.getBatteryLevel() + "%");
|
||||
BatteryState batteryState = device.getBatteryState();
|
||||
if (BatteryState.BATTERY_CHARGING.equals(batteryState) ||
|
||||
BatteryState.BATTERY_CHARGING_FULL.equals(batteryState)) {
|
||||
holder.batteryIcon.setImageLevel(device.getBatteryLevel() + 100);
|
||||
} else {
|
||||
holder.batteryIcon.setImageLevel(device.getBatteryLevel());
|
||||
}
|
||||
}
|
||||
|
||||
//fetch activity data
|
||||
holder.fetchActivityDataBox.setVisibility((device.isInitialized() && coordinator.supportsActivityDataFetching()) ? View.VISIBLE : View.GONE);
|
||||
holder.fetchActivityData.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showTransientSnackbar(R.string.busy_task_fetch_activity_data);
|
||||
GBApplication.deviceService().onFetchActivityData();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
//take screenshot
|
||||
holder.takeScreenshotView.setVisibility((device.isInitialized() && coordinator.supportsScreenshots()) ? View.VISIBLE : View.GONE);
|
||||
holder.takeScreenshotView.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showTransientSnackbar(R.string.controlcenter_snackbar_requested_screenshot);
|
||||
GBApplication.deviceService().onScreenshotReq();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//manage apps
|
||||
holder.manageAppsView.setVisibility((device.isInitialized() && coordinator.supportsAppsManagement()) ? View.VISIBLE : View.GONE);
|
||||
holder.manageAppsView.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
Class<? extends Activity> appsManagementActivity = coordinator.getAppsManagementActivity();
|
||||
if (appsManagementActivity != null) {
|
||||
Intent startIntent = new Intent(context, appsManagementActivity);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//set alarms
|
||||
holder.setAlarmsView.setVisibility(coordinator.supportsAlarmConfiguration() ? View.VISIBLE : View.GONE);
|
||||
holder.setAlarmsView.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(context, ConfigureAlarms.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
//show graphs
|
||||
holder.showActivityGraphs.setVisibility(coordinator.supportsActivityTracking() ? View.VISIBLE : View.GONE);
|
||||
holder.showActivityGraphs.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(context, ChartsActivity.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// audio settings
|
||||
holder.showAudioSettings.setVisibility(device.isInitialized() && coordinator.supportsAudioSettings() ? View.VISIBLE : View.GONE);
|
||||
holder.showAudioSettings.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showTransientSnackbar(R.string.controlcenter_snackbar_requested_screenshot);
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(context, AudioSettingsActivity.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ItemWithDetailsAdapter infoAdapter = new ItemWithDetailsAdapter(context, device.getDeviceInfos());
|
||||
infoAdapter.setHorizontalAlignment(true);
|
||||
holder.deviceInfoList.setAdapter(infoAdapter);
|
||||
justifyListViewHeightBasedOnChildren(holder.deviceInfoList);
|
||||
holder.deviceInfoList.setFocusable(false);
|
||||
|
||||
final boolean detailsShown = position == expandedDevicePosition;
|
||||
boolean showInfoIcon = device.hasDeviceInfos() && !device.isBusy();
|
||||
holder.deviceInfoView.setVisibility(showInfoIcon ? View.VISIBLE : View.GONE);
|
||||
holder.deviceInfoBox.setActivated(detailsShown);
|
||||
holder.deviceInfoBox.setVisibility(detailsShown ? View.VISIBLE : View.GONE);
|
||||
holder.deviceInfoView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
expandedDevicePosition = detailsShown ? -1 : position;
|
||||
TransitionManager.beginDelayedTransition(parent);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
holder.findDevice.setVisibility((device.isInitialized() &&
|
||||
device.getType() != DeviceType.HERE) ? View.VISIBLE : View.GONE);
|
||||
holder.findDevice.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (device.getType() == DeviceType.VIBRATISSIMO) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(context, VibrationActivity.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
return;
|
||||
}
|
||||
GBApplication.deviceService().onFindDevice(true);
|
||||
//TODO: extract string resource if we like this solution.
|
||||
Snackbar.make(parent, R.string.control_center_find_lost_device, Snackbar.LENGTH_INDEFINITE).setAction("Found it!", new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GBApplication.deviceService().onFindDevice(false);
|
||||
}
|
||||
}).setCallback(new Snackbar.Callback() {
|
||||
@Override
|
||||
public void onDismissed(Snackbar snackbar, int event) {
|
||||
GBApplication.deviceService().onFindDevice(false);
|
||||
super.onDismissed(snackbar, event);
|
||||
}
|
||||
}).show();
|
||||
// ProgressDialog.show(
|
||||
// context,
|
||||
// context.getString(R.string.control_center_find_lost_device),
|
||||
// context.getString(R.string.control_center_cancel_to_stop_vibration),
|
||||
// true, true,
|
||||
// new DialogInterface.OnCancelListener() {
|
||||
// @Override
|
||||
// public void onCancel(DialogInterface dialog) {
|
||||
// GBApplication.deviceService().onFindDevice(false);
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
//remove device, hidden under details
|
||||
holder.removeDevice.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
{
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setCancelable(true)
|
||||
.setTitle(context.getString(R.string.controlcenter_delete_device_name, device.getName()))
|
||||
.setMessage(R.string.controlcenter_delete_device_dialogmessage)
|
||||
.setPositiveButton(R.string.Delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
try {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
if (coordinator != null) {
|
||||
coordinator.deleteDevice(device);
|
||||
}
|
||||
DeviceHelper.getInstance().removeBond(device);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(context, "Error deleting device: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
} finally {
|
||||
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// do nothing
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return deviceList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
CardView container;
|
||||
|
||||
ImageView deviceImageView;
|
||||
TextView deviceNameLabel;
|
||||
TextView deviceStatusLabel;
|
||||
|
||||
//actions
|
||||
LinearLayout batteryStatusBox;
|
||||
TextView batteryStatusLabel;
|
||||
ImageView batteryIcon;
|
||||
LinearLayout fetchActivityDataBox;
|
||||
ImageView fetchActivityData;
|
||||
ProgressBar busyIndicator;
|
||||
ImageView takeScreenshotView;
|
||||
ImageView manageAppsView;
|
||||
ImageView setAlarmsView;
|
||||
ImageView showActivityGraphs;
|
||||
ImageView showAudioSettings;
|
||||
|
||||
ImageView deviceInfoView;
|
||||
//overflow
|
||||
final RelativeLayout deviceInfoBox;
|
||||
ListView deviceInfoList;
|
||||
ImageView findDevice;
|
||||
ImageView removeDevice;
|
||||
|
||||
ViewHolder(View view) {
|
||||
super(view);
|
||||
container = (CardView) view.findViewById(R.id.card_view);
|
||||
|
||||
deviceImageView = (ImageView) view.findViewById(R.id.device_image);
|
||||
deviceNameLabel = (TextView) view.findViewById(R.id.device_name);
|
||||
deviceStatusLabel = (TextView) view.findViewById(R.id.device_status);
|
||||
|
||||
//actions
|
||||
batteryStatusBox = (LinearLayout) view.findViewById(R.id.device_battery_status_box);
|
||||
batteryStatusLabel = (TextView) view.findViewById(R.id.battery_status);
|
||||
batteryIcon = (ImageView) view.findViewById(R.id.device_battery_status);
|
||||
fetchActivityDataBox = (LinearLayout) view.findViewById(R.id.device_action_fetch_activity_box);
|
||||
fetchActivityData = (ImageView) view.findViewById(R.id.device_action_fetch_activity);
|
||||
busyIndicator = (ProgressBar) view.findViewById(R.id.device_busy_indicator);
|
||||
takeScreenshotView = (ImageView) view.findViewById(R.id.device_action_take_screenshot);
|
||||
manageAppsView = (ImageView) view.findViewById(R.id.device_action_manage_apps);
|
||||
setAlarmsView = (ImageView) view.findViewById(R.id.device_action_set_alarms);
|
||||
showActivityGraphs = (ImageView) view.findViewById(R.id.device_action_show_activity_graphs);
|
||||
showAudioSettings = (ImageView) view.findViewById(R.id.device_action_show_audio_settings);
|
||||
deviceInfoView = (ImageView) view.findViewById(R.id.device_info_image);
|
||||
|
||||
deviceInfoBox = (RelativeLayout) view.findViewById(R.id.device_item_infos_box);
|
||||
//overflow
|
||||
deviceInfoList = (ListView) view.findViewById(R.id.device_item_infos);
|
||||
findDevice = (ImageView) view.findViewById(R.id.device_action_find);
|
||||
removeDevice = (ImageView) view.findViewById(R.id.device_action_remove);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void justifyListViewHeightBasedOnChildren(ListView listView) {
|
||||
ArrayAdapter adapter = (ArrayAdapter) listView.getAdapter();
|
||||
|
||||
if (adapter == null) {
|
||||
return;
|
||||
}
|
||||
ViewGroup vg = listView;
|
||||
int totalHeight = 0;
|
||||
for (int i = 0; i < adapter.getCount(); i++) {
|
||||
View listItem = adapter.getView(i, null, vg);
|
||||
listItem.measure(0, 0);
|
||||
totalHeight += listItem.getMeasuredHeight();
|
||||
}
|
||||
|
||||
ViewGroup.LayoutParams par = listView.getLayoutParams();
|
||||
par.height = totalHeight + (listView.getDividerHeight() * (adapter.getCount() - 1));
|
||||
listView.setLayoutParams(par);
|
||||
listView.requestLayout();
|
||||
}
|
||||
|
||||
private String getUniqueDeviceName(GBDevice device) {
|
||||
String deviceName = device.getName();
|
||||
if (!isUniqueDeviceName(device, deviceName)) {
|
||||
if (device.getModel() != null) {
|
||||
deviceName = deviceName + " " + device.getModel();
|
||||
if (!isUniqueDeviceName(device, deviceName)) {
|
||||
deviceName = deviceName + " " + device.getShortAddress();
|
||||
}
|
||||
} else {
|
||||
deviceName = deviceName + " " + device.getShortAddress();
|
||||
}
|
||||
}
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
private boolean isUniqueDeviceName(GBDevice device, String deviceName) {
|
||||
for (int i = 0; i < deviceList.size(); i++) {
|
||||
GBDevice item = deviceList.get(i);
|
||||
if (item == device) {
|
||||
continue;
|
||||
}
|
||||
if (deviceName.equals(item.getName())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showTransientSnackbar(int resource) {
|
||||
Snackbar snackbar = Snackbar.make(parent, resource, Snackbar.LENGTH_SHORT);
|
||||
|
||||
View snackbarView = snackbar.getView();
|
||||
|
||||
// change snackbar text color
|
||||
int snackbarTextId = android.support.design.R.id.snackbar_text;
|
||||
TextView textView = (TextView) snackbarView.findViewById(snackbarTextId);
|
||||
//textView.setTextColor();
|
||||
//snackbarView.setBackgroundColor(Color.MAGENTA);
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +1,145 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBDeviceApp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
|
||||
public class GBDeviceAppAdapter extends ArrayAdapter<GBDeviceApp> {
|
||||
/**
|
||||
* Adapter for displaying GBDeviceApp instances.
|
||||
*/
|
||||
|
||||
private final Context context;
|
||||
public class GBDeviceAppAdapter extends RecyclerView.Adapter<GBDeviceAppAdapter.AppViewHolder> {
|
||||
|
||||
private final int mLayoutId;
|
||||
private final List<GBDeviceApp> appList;
|
||||
private final AbstractAppManagerFragment mParentFragment;
|
||||
|
||||
public GBDeviceAppAdapter(Context context, List<GBDeviceApp> appList) {
|
||||
super(context, 0, appList);
|
||||
public List<GBDeviceApp> getAppList() {
|
||||
return appList;
|
||||
}
|
||||
|
||||
this.context = context;
|
||||
this.appList = appList;
|
||||
public GBDeviceAppAdapter(List<GBDeviceApp> list, int layoutId, AbstractAppManagerFragment parentFragment) {
|
||||
mLayoutId = layoutId;
|
||||
appList = list;
|
||||
mParentFragment = parentFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
GBDeviceApp deviceApp = getItem(position);
|
||||
public long getItemId(int position) {
|
||||
return appList.get(position).getUUID().getLeastSignificantBits();
|
||||
}
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return appList.size();
|
||||
}
|
||||
|
||||
view = inflater.inflate(R.layout.device_item, parent, false);
|
||||
}
|
||||
TextView deviceStatusLabel = (TextView) view.findViewById(R.id.device_status);
|
||||
TextView deviceNameLabel = (TextView) view.findViewById(R.id.device_name);
|
||||
ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_image);
|
||||
@Override
|
||||
public GBDeviceAppAdapter.AppViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(mLayoutId, parent, false);
|
||||
return new AppViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final AppViewHolder holder, int position) {
|
||||
final GBDeviceApp deviceApp = appList.get(position);
|
||||
|
||||
holder.mDeviceAppVersionAuthorLabel.setText(GBApplication.getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
|
||||
// FIXME: replace with small icons
|
||||
String appNameLabelText = deviceApp.getName();
|
||||
holder.mDeviceAppNameLabel.setText(appNameLabelText);
|
||||
|
||||
deviceStatusLabel.setText(deviceApp.getVersion() + " by " + deviceApp.getCreator());
|
||||
deviceNameLabel.setText(deviceApp.getName());
|
||||
switch (deviceApp.getType()) {
|
||||
case APP_GENERIC:
|
||||
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
|
||||
break;
|
||||
case APP_ACTIVITYTRACKER:
|
||||
deviceImageView.setImageResource(R.drawable.ic_activitytracker);
|
||||
holder.mDeviceImageView.setImageResource(R.drawable.ic_activitytracker);
|
||||
break;
|
||||
case APP_SYSTEM:
|
||||
holder.mDeviceImageView.setImageResource(R.drawable.ic_systemapp);
|
||||
break;
|
||||
case WATCHFACE:
|
||||
deviceImageView.setImageResource(R.drawable.ic_watchface);
|
||||
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchface);
|
||||
break;
|
||||
default:
|
||||
deviceImageView.setImageResource(R.drawable.ic_device_pebble);
|
||||
holder.mDeviceImageView.setImageResource(R.drawable.ic_watchapp);
|
||||
}
|
||||
|
||||
return view;
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
UUID uuid = deviceApp.getUUID();
|
||||
GBApplication.deviceService().onAppStart(uuid, true);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return mParentFragment.openPopupMenu(view, deviceApp);
|
||||
}
|
||||
});
|
||||
|
||||
holder.mDragHandle.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
mParentFragment.startDragging(holder);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void onItemMove(int from, int to) {
|
||||
Collections.swap(appList, from, to);
|
||||
notifyItemMoved(from, to);
|
||||
}
|
||||
|
||||
public class AppViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView mDeviceAppVersionAuthorLabel;
|
||||
final TextView mDeviceAppNameLabel;
|
||||
final ImageView mDeviceImageView;
|
||||
final ImageView mDragHandle;
|
||||
|
||||
AppViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
mDeviceAppVersionAuthorLabel = (TextView) itemView.findViewById(R.id.item_details);
|
||||
mDeviceAppNameLabel = (TextView) itemView.findViewById(R.id.item_name);
|
||||
mDeviceImageView = (ImageView) itemView.findViewById(R.id.item_image);
|
||||
mDragHandle = (ImageView) itemView.findViewById(R.id.drag_handle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
|
||||
/**
|
||||
* Adapter for displaying generic ItemWithDetails instances.
|
||||
*/
|
||||
public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
|
||||
|
||||
public static final int SIZE_SMALL = 1;
|
||||
public static final int SIZE_MEDIUM = 2;
|
||||
public static final int SIZE_LARGE = 3;
|
||||
private final Context context;
|
||||
private boolean horizontalAlignment;
|
||||
private int size = SIZE_MEDIUM;
|
||||
|
||||
public ItemWithDetailsAdapter(Context context, List<ItemWithDetails> items) {
|
||||
super(context, 0, items);
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setHorizontalAlignment(boolean horizontalAlignment) {
|
||||
this.horizontalAlignment = horizontalAlignment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
ItemWithDetails item = getItem(position);
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
if (horizontalAlignment) {
|
||||
view = inflater.inflate(R.layout.item_with_details_horizontal, parent, false);
|
||||
} else {
|
||||
switch (size) {
|
||||
case SIZE_SMALL:
|
||||
view = inflater.inflate(R.layout.item_with_details_small, parent, false);
|
||||
break;
|
||||
default:
|
||||
view = inflater.inflate(R.layout.item_with_details, parent, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageView iconView = (ImageView) view.findViewById(R.id.item_image);
|
||||
TextView nameView = (TextView) view.findViewById(R.id.item_name);
|
||||
TextView detailsView = (TextView) view.findViewById(R.id.item_details);
|
||||
|
||||
nameView.setText(item.getName());
|
||||
detailsView.setText(item.getDetails());
|
||||
iconView.setImageResource(item.getIcon());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.btle;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.AbstractDeviceSupport;
|
||||
|
||||
/**
|
||||
* @see TransactionBuilder
|
||||
* @see BtLEQueue
|
||||
*/
|
||||
public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback {
|
||||
private static final String TAG = "AbstractBTLEDeviceSupport";
|
||||
|
||||
private BtLEQueue mQueue;
|
||||
private HashMap<UUID, BluetoothGattCharacteristic> mAvailableCharacteristics;
|
||||
private Set<UUID> mSupportedServices = new HashSet<>(4);
|
||||
|
||||
@Override
|
||||
public boolean connect() {
|
||||
if (mQueue == null) {
|
||||
mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, getContext());
|
||||
}
|
||||
return mQueue.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses should populate the given builder to initialize the device (if necessary).
|
||||
*
|
||||
* @param builder
|
||||
* @return the same builder as passed as the argument
|
||||
*/
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (mQueue != null) {
|
||||
mQueue.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send commands like this to the device:
|
||||
* <p>
|
||||
* <code>perform("sms notification").write(someCharacteristic, someByteArray).queue(getQueue());</code>
|
||||
* </p>
|
||||
* TODO: support orchestration of multiple reads and writes depending on returned values
|
||||
*
|
||||
* @see #performConnected(Transaction)
|
||||
* @see #initializeDevice(TransactionBuilder)
|
||||
*/
|
||||
protected TransactionBuilder performInitialized(String taskName) throws IOException {
|
||||
if (!isConnected()) {
|
||||
if (!connect()) {
|
||||
throw new IOException("1: Unable to connect to device: " + getDevice());
|
||||
}
|
||||
}
|
||||
if (!isInitialized()) {
|
||||
// first, add a transaction that performs device initialization
|
||||
TransactionBuilder builder = new TransactionBuilder("Initialize device");
|
||||
initializeDevice(builder).queue(getQueue());
|
||||
}
|
||||
return new TransactionBuilder(taskName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction
|
||||
* @throws IOException
|
||||
* @see {@link #performInitialized(String)}
|
||||
*/
|
||||
protected void performConnected(Transaction transaction) throws IOException {
|
||||
if (!isConnected()) {
|
||||
if (!connect()) {
|
||||
throw new IOException("2: Unable to connect to device: " + getDevice());
|
||||
}
|
||||
}
|
||||
getQueue().add(transaction);
|
||||
}
|
||||
|
||||
public BtLEQueue getQueue() {
|
||||
return mQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses should call this method to add services they support.
|
||||
* Only supported services will be queried for characteristics.
|
||||
*
|
||||
* @param aSupportedService
|
||||
* @see #getCharacteristic(UUID)
|
||||
*/
|
||||
protected void addSupportedService(UUID aSupportedService) {
|
||||
mSupportedServices.add(aSupportedService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the characteristic matching the given UUID. Only characteristics
|
||||
* are returned whose service is marked as supported.
|
||||
*
|
||||
* @param uuid
|
||||
* @return the characteristic for the given UUID or <code>null</code>
|
||||
* @see #addSupportedService(UUID)
|
||||
*/
|
||||
protected BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||
if (mAvailableCharacteristics == null) {
|
||||
return null;
|
||||
}
|
||||
return mAvailableCharacteristics.get(uuid);
|
||||
}
|
||||
|
||||
private void gattServicesDiscovered(List<BluetoothGattService> discoveredGattServices) {
|
||||
mAvailableCharacteristics = null;
|
||||
|
||||
if (discoveredGattServices == null) {
|
||||
return;
|
||||
}
|
||||
Set<UUID> supportedServices = getSupportedServices();
|
||||
|
||||
for (BluetoothGattService service : discoveredGattServices) {
|
||||
if (supportedServices.contains(service.getUuid())) {
|
||||
List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
|
||||
if (characteristics == null || characteristics.isEmpty()) {
|
||||
Log.w(TAG, "Supported LE service " + service.getUuid() + "did not return any characteristics");
|
||||
continue;
|
||||
}
|
||||
mAvailableCharacteristics = new HashMap<>(characteristics.size());
|
||||
for (BluetoothGattCharacteristic characteristic : characteristics) {
|
||||
mAvailableCharacteristics.put(characteristic.getUuid(), characteristic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Set<UUID> getSupportedServices() {
|
||||
return mSupportedServices;
|
||||
}
|
||||
|
||||
// default implementations of event handler methods (gatt callbacks)
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt) {
|
||||
gattServicesDiscovered(getQueue().getSupportedGattServices());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic, int status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic, int status) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
}
|
||||
|
||||
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.btle;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
/**
|
||||
* The Bluedroid implementation only allows performing one GATT request at a time.
|
||||
* As they are asynchronous anyway, we encapsulate every GATT request (read and write)
|
||||
* inside a runnable action.
|
||||
* <p/>
|
||||
* These actions are then executed one after another, ensuring that every action's result
|
||||
* has been posted before invoking the next action.
|
||||
*/
|
||||
public abstract class BtLEAction {
|
||||
private final BluetoothGattCharacteristic characteristic;
|
||||
|
||||
public BtLEAction() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public BtLEAction(BluetoothGattCharacteristic characteristic) {
|
||||
this.characteristic = characteristic;
|
||||
}
|
||||
|
||||
public abstract boolean run(BluetoothGatt gatt);
|
||||
|
||||
/**
|
||||
* Returns the GATT characteristic being read/written/...
|
||||
*
|
||||
* @return the GATT characteristic, or <code>null</code>
|
||||
*/
|
||||
public BluetoothGattCharacteristic getCharacteristic() {
|
||||
return characteristic;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic();
|
||||
String uuid = characteristic == null ? "(null)" : characteristic.getUuid().toString();
|
||||
return getClass().getSimpleName() + " on characteristic: " + uuid;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue