Compare commits
2094 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 | |
cpfeiffer | 518b1ee6f4 | |
cpfeiffer | 1711a7a731 | |
cpfeiffer | bc3c0760d0 | |
cpfeiffer | 5f993c0049 | |
cpfeiffer | 8f4e933e30 | |
cpfeiffer | 9a1f4875fc | |
cpfeiffer | 42420e676b | |
Daniele Gobbetti | b6cbb5d6be | |
cpfeiffer | ab8982e7f2 |
|
@ -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
|
||||
|
|
17
.travis.yml
17
.travis.yml
|
@ -1,16 +1,25 @@
|
|||
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
|
||||
- tools
|
||||
|
||||
# The BuildTools version used by your project
|
||||
- build-tools-23.0.0
|
||||
- build-tools-25.0.2
|
||||
|
||||
# The SDK version used to compile your project
|
||||
- android-23
|
||||
- android-25
|
||||
|
||||
# Additional components
|
||||
- extra-android-m2repository
|
||||
|
@ -20,3 +29,5 @@ android:
|
|||
# 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
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[gadgetbridge.strings]
|
||||
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = app/src/main/res/values/strings.xml
|
||||
source_lang = en_US
|
||||
deviceType = ANDROID
|
||||
minimum_perc = 50
|
637
CHANGELOG.md
637
CHANGELOG.md
|
@ -1,28 +1,589 @@
|
|||
###Changelog
|
||||
### Changelog
|
||||
|
||||
####Version 0.6.1
|
||||
#### 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 implementantion of PebbleKit Intents to make some 3rd party Android apps work with the Pebble (eg. Ventoo)
|
||||
#### 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 contolling all audio players that react to media buttons (can be chosen in settings)
|
||||
* 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 messagess as chat messages, even if arrived via Pebble Intents (nice icon for Pebble FW 3.x)
|
||||
* 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
|
||||
#### 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
|
||||
#### 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
|
||||
|
@ -30,28 +591,28 @@
|
|||
* Device state again visible on lockscreen
|
||||
* Date display and navigation now working properly for all charts
|
||||
|
||||
####Version 0.5.2
|
||||
#### 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 (Marshmellow)
|
||||
* Build target is now Android SDK 23 (Marshmallow)
|
||||
|
||||
####Version 0.5.1
|
||||
#### 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
|
||||
#### 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
|
||||
* Various small bugfixes to the App/FW Installation Activity
|
||||
|
||||
####Version 0.4.6
|
||||
#### 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)
|
||||
|
@ -61,16 +622,16 @@
|
|||
* Small firmware installation improvements
|
||||
* Various refactorings and code cleanups
|
||||
|
||||
####Version 0.4.5
|
||||
#### 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: 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
|
||||
#### 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
|
||||
|
@ -79,24 +640,24 @@
|
|||
* 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
|
||||
#### Version 0.4.3
|
||||
* Mi Band: Support for setting alarms
|
||||
* Mi Band: Bugfix for activity data synchronization
|
||||
|
||||
####Version 0.4.2
|
||||
#### 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
|
||||
#### 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
|
||||
#### 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)
|
||||
|
@ -110,33 +671,33 @@
|
|||
* Fix Debug activity (SMS and E-Mail buttons were broken)
|
||||
* Add Turkish translation contributed by Tarik Sekmen
|
||||
|
||||
####Version 0.3.5
|
||||
#### 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
|
||||
#### 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
|
||||
#### 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
|
||||
#### 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
|
||||
#### Version 0.3.1
|
||||
* Mi Band: Fix for notifications only working in Debug
|
||||
|
||||
####Version 0.3.0
|
||||
#### 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
|
||||
|
@ -144,39 +705,39 @@
|
|||
* 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
|
||||
* 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 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 Activity (tap to connect etc)
|
||||
|
||||
####Version 0.1.3
|
||||
#### 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 "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,13 +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 (by xphnx)
|
||||
ic_device_miband.png (by xphnx)
|
||||
ic_activitytracker.png (by xphnx)
|
||||
ic_watchface.png (by xphnx)
|
||||
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/
|
||||
|
|
127
README.md
127
README.md
|
@ -1,9 +1,17 @@
|
|||
Gadgetbridge
|
||||
============
|
||||
|
||||
Gadgetbridge is an Android (4.4+) Application which will allow you to use your
|
||||
Pebble or Mi Band 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.
|
||||
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.
|
||||
|
||||
|
||||
[Homepage](https://gadgetbridge.org)
|
||||
|
||||
[Blog](https://blog.gadgetbridge.org)
|
||||
|
||||
[![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)
|
||||
|
||||
|
@ -11,102 +19,99 @@ need to create an account and transmit any of your data to the vendor's servers.
|
|||
|
||||
[![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](CHANGELOG.md)
|
||||
[List of changes](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/CHANGELOG.md)
|
||||
|
||||
## Features (Pebble)
|
||||
## 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)
|
||||
|
||||
* 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 notifications (above filtered out)
|
||||
* Dismiss individial notifications or open corresponding app on phone from the action menu (generic notifications)
|
||||
* Dismiss all notifications from the action menu (non-generic notifications)
|
||||
* Music playback info (artist, album, track)
|
||||
* Music control: play/pause, next track, previous track, volume up, volume down
|
||||
* List and remove installed apps/watchfaces
|
||||
* Install watchfaces and firmware files (.pbw and .pbz)
|
||||
* Take and share screenshots from the Pebble's screen
|
||||
* PebbleKit support for 3rd Party Android Apps support (experimental).
|
||||
* Morpheuz sleep data syncronization (experimental)
|
||||
## Features
|
||||
|
||||
## Notes about the Pebble Time
|
||||
Please see [FEATURES.md](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/FEATURES.md)
|
||||
|
||||
All features are also supported on the Pebble Time, except for the following:
|
||||
## Getting Started (Pebble)
|
||||
|
||||
* Listing installed watchfaces (it will simply display the UUIDs of previously installed watchapps, no matter if they are still installed or not)
|
||||
* Firmware installation is untested and will probably not work.
|
||||
|
||||
## How to use (Pebble)
|
||||
|
||||
1. Pair your Pebble through Gadgetbridge's Discovery Activity or the Android Bluetooth Settings
|
||||
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
|
||||
|
||||
## Features (Mi Band)
|
||||
For more information read [this wiki article](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
|
||||
|
||||
* Mi Band notifications (LEDs + vibration) for
|
||||
* Discovery and pairing
|
||||
* Incoming calls
|
||||
* SMS received
|
||||
* K-9 mails received
|
||||
* Generic Android notifications
|
||||
* Synchronize the time to the Mi Band
|
||||
* Display firmware version and battery state
|
||||
* Synchronize activity data
|
||||
* Display sleep data (alpha)
|
||||
* Display sports data (step count) (alpha)
|
||||
* Set alarms on the Mi Band
|
||||
## How to use (Mi Band 1+2)
|
||||
|
||||
## How to use (Mi Band)
|
||||
|
||||
* When starting Gadgetbridge and no device is visible, it will automatically
|
||||
attempt to discover and pair your Mi Band. Alternatively you can invoke this
|
||||
manually via the menu button. It will ask you for some personal info that appears
|
||||
* 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 with all three LEDs during the pairing process,
|
||||
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" item to connect if you're not connected yet.
|
||||
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:
|
||||
|
||||
* 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.
|
||||
**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.
|
||||
|
||||
## Authors (in order of first code contribution)
|
||||
## 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://www.transifex.com/projects/p/gadgetbridge/resource/strings/ or
|
||||
manually.
|
||||
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. Quit Gadgetbridge and restart it
|
||||
3. Reproduce the problem you encountered
|
||||
4. Check the logfile at /sdcard/Android/data/nodomain.freeyourgadget.gadgetbridge/files/gadgetbridge.log
|
||||
5. File an issue at https://github.com/Freeyourgadget/Gadgetbridge/issues/new and possibly provide the logfile
|
||||
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.
|
||||
|
||||
|
|
|
@ -1,19 +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 23
|
||||
buildToolsVersion "23.0.0"
|
||||
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 23
|
||||
versionCode 27
|
||||
versionName "0.6.1"
|
||||
targetSdkVersion 25
|
||||
|
||||
// note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.21.5"
|
||||
versionCode 106
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -37,17 +52,42 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
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:23.0.0'
|
||||
compile 'com.android.support:support-v4:23.0.0'
|
||||
compile 'com.github.tony19:logback-android-classic:1.1.1-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:v2.1.4'
|
||||
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'
|
||||
|
@ -93,7 +133,6 @@ task pmd(type: Pmd) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
task findbugs(type: FindBugs) {
|
||||
ignoreFailures = !ABORT_ON_CHECK_FAILURE
|
||||
effort = "default"
|
||||
|
|
|
@ -2,21 +2,22 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="nodomain.freeyourgadget.gadgetbridge">
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="19"
|
||||
android:targetSdkVersion="23" />
|
||||
<!--
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
-->
|
||||
<!--
|
||||
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-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"
|
||||
|
@ -24,64 +25,55 @@
|
|||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="false" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".GBApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/GadgetbridgeTheme">
|
||||
<activity
|
||||
android:name=".activities.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=".activities.SettingsActivity"
|
||||
android:label="@string/title_activity_settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.ControlCenter" />
|
||||
</activity>
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".devices.miband.MiBandPreferencesActivity"
|
||||
android:label="@string/preferences_miband_settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.SettingsActivity" />
|
||||
</activity>
|
||||
android:label="@string/preferences_miband_settings"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AppManagerActivity"
|
||||
android:label="@string/title_activity_appmanager">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.ControlCenter" />
|
||||
</activity>
|
||||
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">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.SettingsActivity" />
|
||||
</activity>
|
||||
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">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.ControlCenter" />
|
||||
|
||||
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="*/*" />
|
||||
|
@ -102,7 +94,46 @@
|
|||
<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" />
|
||||
|
@ -113,7 +144,6 @@
|
|||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
|
||||
<data android:pathPattern="/.*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
|
||||
|
@ -124,10 +154,20 @@
|
|||
<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? -->
|
||||
|
@ -147,7 +187,46 @@
|
|||
<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" />
|
||||
|
@ -158,7 +237,6 @@
|
|||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.pbw" />
|
||||
|
||||
<data android:pathPattern="/.*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\.pbz" />
|
||||
<data android:pathPattern="/.*\\..*\\..*\\.pbz" />
|
||||
|
@ -169,16 +247,44 @@
|
|||
<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 donwload content provider -->
|
||||
<!-- 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>
|
||||
|
||||
<service
|
||||
|
@ -189,56 +295,31 @@
|
|||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</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">
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".externalevents.TimeChangeReceiver"
|
||||
android:enabled="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
|
||||
<action android:name="android.intent.action.TIME_SET" />
|
||||
</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.PebbleReceiver"
|
||||
android:enabled="false">
|
||||
<activity android:name=".externalevents.WeatherNotificationConfig">
|
||||
<intent-filter>
|
||||
<action android:name="com.getpebble.action.SEND_NOTIFICATION" />
|
||||
<action android:name="ru.gelin.android.weather.notification.ACTION_WEATHER_SKIN_PREFERENCES"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".externalevents.MusicPlaybackReceiver"
|
||||
android:enabled="false">
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".externalevents.AutoStartReceiver"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.music.metachanged"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".externalevents.BluetoothStateChangeReceiver"
|
||||
android:exported="false">
|
||||
|
@ -261,50 +342,100 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!--
|
||||
forcing the DebugActivity to portrait mode avoids crashes with the progress
|
||||
dialog when changing orientation
|
||||
-->
|
||||
<activity
|
||||
android:name=".activities.DebugActivity"
|
||||
android:label="@string/title_activity_debug">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.ControlCenter" />
|
||||
</activity>
|
||||
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">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.ControlCenter" />
|
||||
</activity>
|
||||
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.ControlCenter">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.ControlCenter" />
|
||||
</activity>
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.ConfigureAlarms"
|
||||
android:label="@string/title_activity_set_alarm"
|
||||
android:parentActivityName=".activities.SettingsActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.SettingsActivity" />
|
||||
</activity>
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AlarmDetails"
|
||||
android:label="@string/title_activity_alarm_details"
|
||||
android:parentActivityName=".activities.ConfigureAlarms">
|
||||
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="nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms" />
|
||||
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 |
|
@ -15,15 +15,19 @@
|
|||
<!-- 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}.log.zip</fileNamePattern>
|
||||
<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>
|
||||
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||
<maxFileSize>5MB</maxFileSize>
|
||||
</triggeringPolicy>
|
||||
<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>
|
||||
|
|
|
@ -1,101 +1,249 @@
|
|||
/* 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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.ActivityDatabaseHandler;
|
||||
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 ActivityDatabaseHandler mActivityDatabaseHandler;
|
||||
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;
|
||||
deviceService = createDeviceService();
|
||||
// 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();
|
||||
// For debugging problems with the logback configuration
|
||||
// LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
// print logback's internal status
|
||||
// StatusPrinter.print(lc);
|
||||
// Logger logger = LoggerFactory.getLogger(GBApplication.class);
|
||||
setupLogging(isFileLoggingEnabled());
|
||||
|
||||
GB.environment = GBEnvironment.createDeviceEnvironment();
|
||||
mActivityDatabaseHandler = new ActivityDatabaseHandler(context);
|
||||
loadBlackList();
|
||||
// for testing DB stuff
|
||||
// SQLiteDatabase db = mActivityDatabaseHandler.getWritableDatabase();
|
||||
// db.close();
|
||||
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 sharedPrefs.getBoolean("log_to_file", false);
|
||||
return prefs.getBoolean("log_to_file", false);
|
||||
}
|
||||
|
||||
private void setupLogging() {
|
||||
if (isFileLoggingEnabled()) {
|
||||
try {
|
||||
File dir = FileUtils.getExternalFilesDir();
|
||||
// used by assets/logback.xml since the location cannot be statically determined
|
||||
System.setProperty("GB_LOGFILES_DIR", dir.getAbsolutePath());
|
||||
} catch (IOException ex) {
|
||||
Log.e("GBApplication", "External files dir not available, cannot log to file, ex");
|
||||
System.setProperty("GB_LOGFILES_DIR", "/dev/null");
|
||||
}
|
||||
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 {
|
||||
try {
|
||||
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
root.detachAppender("FILE");
|
||||
} catch (Throwable ex) {
|
||||
System.out.println("Error removing logger FILE appender");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -105,6 +253,9 @@ public class GBApplication extends Application {
|
|||
* 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
|
||||
|
@ -113,7 +264,7 @@ public class GBApplication extends Application {
|
|||
public static DBHandler acquireDB() throws GBException {
|
||||
try {
|
||||
if (dbLock.tryLock(30, TimeUnit.SECONDS)) {
|
||||
return mActivityDatabaseHandler;
|
||||
return lockHandler;
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Log.i(TAG, "Interrupted while waiting for DB lock");
|
||||
|
@ -135,35 +286,309 @@ public class GBApplication extends Application {
|
|||
return VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
|
||||
}
|
||||
|
||||
public static HashSet<String> blacklist = null;
|
||||
public static boolean isRunningMarshmallowOrLater() {
|
||||
return VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
||||
}
|
||||
|
||||
public static void loadBlackList() {
|
||||
blacklist = (HashSet<String>) sharedPrefs.getStringSet("package_blacklist", null);
|
||||
if (blacklist == null) {
|
||||
blacklist = new HashSet<>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveBlackList() {
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
if (blacklist.isEmpty()) {
|
||||
editor.putStringSet("package_blacklist", null);
|
||||
@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 {
|
||||
editor.putStringSet("package_blacklist", blacklist);
|
||||
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 addToBlacklist(String packageName) {
|
||||
if (!blacklist.contains(packageName)) {
|
||||
blacklist.add(packageName);
|
||||
saveBlackList();
|
||||
public static void addAppToBlacklist(String packageName) {
|
||||
if (apps_blacklist.add(packageName)) {
|
||||
saveAppsBlackList();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void removeFromBlacklist(String packageName) {
|
||||
blacklist.remove(packageName);
|
||||
saveBlackList();
|
||||
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,6 +1,29 @@
|
|||
/* 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;
|
||||
|
||||
|
@ -10,9 +33,8 @@ public class GBEnvironment {
|
|||
return env;
|
||||
}
|
||||
|
||||
public static GBEnvironment createDeviceEnvironment() {
|
||||
GBEnvironment env = new GBEnvironment();
|
||||
return env;
|
||||
static GBEnvironment createDeviceEnvironment() {
|
||||
return new GBEnvironment();
|
||||
}
|
||||
|
||||
public final boolean isTest() {
|
||||
|
@ -23,4 +45,15 @@ public class GBEnvironment {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/* 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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,19 @@
|
|||
/* 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;
|
||||
|
@ -8,7 +24,7 @@ import java.util.HashSet;
|
|||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractFragmentPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private Set<AbstractGBFragment> fragments = new HashSet<>();
|
||||
private final Set<AbstractGBFragment> fragments = new HashSet<>();
|
||||
private Object primaryFragment;
|
||||
|
||||
public AbstractFragmentPagerAdapter(FragmentManager fm) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,8 +1,24 @@
|
|||
/* 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;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* Abstract base class for fragments. Provides hooks that are called when
|
||||
|
@ -12,7 +28,7 @@ import android.support.v4.app.FragmentActivity;
|
|||
* @see AbstractGBFragmentActivity
|
||||
*/
|
||||
public abstract class AbstractGBFragment extends Fragment {
|
||||
private boolean mVisibleInactivity;
|
||||
private boolean mVisibleInActivity;
|
||||
|
||||
/**
|
||||
* Called when this fragment has been fully scrolled into the activity.
|
||||
|
@ -21,7 +37,6 @@ public abstract class AbstractGBFragment extends Fragment {
|
|||
* @see #onMadeInvisibleInActivity()
|
||||
*/
|
||||
protected void onMadeVisibleInActivity() {
|
||||
updateActivityTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,7 +46,7 @@ public abstract class AbstractGBFragment extends Fragment {
|
|||
* @see #onMadeVisibleInActivity()
|
||||
*/
|
||||
protected void onMadeInvisibleInActivity() {
|
||||
mVisibleInactivity = false;
|
||||
mVisibleInActivity = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,16 +54,7 @@ public abstract class AbstractGBFragment extends Fragment {
|
|||
* activity, not taking into account whether the screen is enabled at all.
|
||||
*/
|
||||
public boolean isVisibleInActivity() {
|
||||
return mVisibleInactivity;
|
||||
}
|
||||
|
||||
protected void updateActivityTitle() {
|
||||
FragmentActivity activity = (FragmentActivity) getActivity();
|
||||
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||||
if (getTitle() != null) {
|
||||
activity.setTitle(getTitle());
|
||||
}
|
||||
}
|
||||
return mVisibleInActivity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -60,7 +66,7 @@ public abstract class AbstractGBFragment extends Fragment {
|
|||
* @hide
|
||||
*/
|
||||
public void onMadeVisibleInActivityInternal() {
|
||||
mVisibleInactivity = true;
|
||||
mVisibleInActivity = true;
|
||||
if (isVisible()) {
|
||||
onMadeVisibleInActivity();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
/* 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.FragmentActivity;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
|
||||
|
@ -18,7 +33,7 @@ import android.support.v4.app.FragmentPagerAdapter;
|
|||
*
|
||||
* @see AbstractGBFragment
|
||||
*/
|
||||
public abstract class AbstractGBFragmentActivity extends FragmentActivity {
|
||||
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
|
||||
|
|
|
@ -1,26 +1,72 @@
|
|||
/* 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.PreferenceActivity;
|
||||
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 class AbstractSettingsActivity extends PreferenceActivity {
|
||||
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.
|
||||
|
@ -28,12 +74,20 @@ public class AbstractSettingsActivity extends PreferenceActivity {
|
|||
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 = value.toString();
|
||||
String stringValue = String.valueOf(value);
|
||||
|
||||
if (preference instanceof ListPreference) {
|
||||
// For list preferences, look up the correct display value in
|
||||
|
@ -56,15 +110,15 @@ public class AbstractSettingsActivity extends PreferenceActivity {
|
|||
}
|
||||
|
||||
private static class ExtraSetSummaryOnChangeListener extends SimpleSetSummaryOnChangeListener {
|
||||
private Preference.OnPreferenceChangeListener delegate;
|
||||
private final Preference.OnPreferenceChangeListener prefChangeListener;
|
||||
|
||||
public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener delegate) {
|
||||
this.delegate = delegate;
|
||||
public ExtraSetSummaryOnChangeListener(Preference.OnPreferenceChangeListener prefChangeListener) {
|
||||
this.prefChangeListener = prefChangeListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object value) {
|
||||
boolean result = delegate.onPreferenceChange(preference, value);
|
||||
boolean result = prefChangeListener.onPreferenceChange(preference, value);
|
||||
if (result) {
|
||||
return super.onPreferenceChange(preference, value);
|
||||
}
|
||||
|
@ -72,14 +126,24 @@ public class AbstractSettingsActivity extends PreferenceActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private static SimpleSetSummaryOnChangeListener sBindPreferenceSummaryToValueListener = new SimpleSetSummaryOnChangeListener();
|
||||
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);
|
||||
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
for (String prefKey : getPreferenceKeysWithSummary()) {
|
||||
final Preference pref = findPreference(prefKey);
|
||||
if (pref != null) {
|
||||
|
@ -90,6 +154,21 @@ public class AbstractSettingsActivity extends PreferenceActivity {
|
|||
}
|
||||
}
|
||||
|
||||
@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
|
||||
|
@ -141,4 +220,11 @@ public class AbstractSettingsActivity extends PreferenceActivity {
|
|||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void setLanguage(Locale language, boolean invalidateLanguage) {
|
||||
if (invalidateLanguage) {
|
||||
isLanguageInvalid = true;
|
||||
}
|
||||
AndroidUtils.setLanguage(this, language);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +1,117 @@
|
|||
/* 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.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.CheckBox;
|
||||
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 Activity {
|
||||
public class AlarmDetails extends AbstractGBActivity {
|
||||
|
||||
private GBAlarm alarm;
|
||||
private TimePicker timePicker;
|
||||
private CheckBox cbSmartWakeup;
|
||||
private CheckBox cbMonday;
|
||||
private CheckBox cbTuesday;
|
||||
private CheckBox cbWednesday;
|
||||
private CheckBox cbThursday;
|
||||
private CheckBox cbFriday;
|
||||
private CheckBox cbSaturday;
|
||||
private CheckBox cbSunday;
|
||||
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);
|
||||
|
||||
Parcelable p = getIntent().getExtras().getParcelable("alarm");
|
||||
alarm = (GBAlarm) p;
|
||||
alarm = getIntent().getParcelableExtra("alarm");
|
||||
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
|
||||
timePicker = (TimePicker) findViewById(R.id.alarm_time_picker);
|
||||
cbSmartWakeup = (CheckBox) findViewById(R.id.alarm_cb_smart_wakeup);
|
||||
cbMonday = (CheckBox) findViewById(R.id.alarm_cb_mon);
|
||||
cbTuesday = (CheckBox) findViewById(R.id.alarm_cb_tue);
|
||||
cbWednesday = (CheckBox) findViewById(R.id.alarm_cb_wed);
|
||||
cbThursday = (CheckBox) findViewById(R.id.alarm_cb_thu);
|
||||
cbFriday = (CheckBox) findViewById(R.id.alarm_cb_fri);
|
||||
cbSaturday = (CheckBox) findViewById(R.id.alarm_cb_sat);
|
||||
cbSunday = (CheckBox) findViewById(R.id.alarm_cb_sun);
|
||||
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));
|
||||
|
@ -59,12 +123,19 @@ public class AlarmDetails extends Activity {
|
|||
|
||||
}
|
||||
|
||||
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
|
||||
updateAlarm();
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
@ -72,10 +143,16 @@ public class AlarmDetails extends Activity {
|
|||
}
|
||||
|
||||
private void updateAlarm() {
|
||||
alarm.setSmartWakeup(cbSmartWakeup.isChecked());
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
/* 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.app.Activity;
|
||||
import android.os.Bundle;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class AndroidPairingActivity extends Activity {
|
||||
public class AndroidPairingActivity extends AbstractGBActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
|
|
@ -1,107 +1,67 @@
|
|||
/* 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.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
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.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
|
||||
|
||||
|
||||
public class AppBlacklistActivity extends Activity {
|
||||
public class AppBlacklistActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppBlacklistActivity.class);
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(ControlCenter.ACTION_QUIT)) {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private SharedPreferences sharedPrefs;
|
||||
private AppBlacklistAdapter appBlacklistAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_appblacklist);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
RecyclerView appListView = (RecyclerView) findViewById(R.id.appListView);
|
||||
appListView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
final PackageManager pm = getPackageManager();
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
appBlacklistAdapter = new AppBlacklistAdapter(R.layout.item_app_blacklist, this);
|
||||
|
||||
final List<ApplicationInfo> packageList = pm.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
ListView appListView = (ListView) findViewById(R.id.appListView);
|
||||
appListView.setAdapter(appBlacklistAdapter);
|
||||
|
||||
final ArrayAdapter<ApplicationInfo> adapter = new ArrayAdapter<ApplicationInfo>(this, R.layout.item_with_checkbox, packageList) {
|
||||
SearchView searchView = (SearchView) findViewById(R.id.appListViewSearch);
|
||||
searchView.setIconifiedByDefault(false);
|
||||
searchView.setIconified(false);
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
view = inflater.inflate(R.layout.item_with_checkbox, parent, false);
|
||||
}
|
||||
|
||||
ApplicationInfo appInfo = packageList.get(position);
|
||||
TextView deviceAppVersionAuthorLabel = (TextView) view.findViewById(R.id.item_details);
|
||||
TextView deviceAppNameLabel = (TextView) view.findViewById(R.id.item_name);
|
||||
ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_image);
|
||||
CheckBox checkbox = (CheckBox) view.findViewById(R.id.item_checkbox);
|
||||
|
||||
deviceAppVersionAuthorLabel.setText(appInfo.packageName);
|
||||
deviceAppNameLabel.setText(appInfo.loadLabel(pm));
|
||||
deviceImageView.setImageDrawable(appInfo.loadIcon(pm));
|
||||
|
||||
checkbox.setChecked(GBApplication.blacklist.contains(appInfo.packageName));
|
||||
|
||||
return view;
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
appListView.setAdapter(adapter);
|
||||
|
||||
appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView parent, View v, int position, long id) {
|
||||
String packageName = packageList.get(position).packageName;
|
||||
CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox));
|
||||
checkBox.toggle();
|
||||
if (checkBox.isChecked()) {
|
||||
GBApplication.addToBlacklist(packageName);
|
||||
} else {
|
||||
GBApplication.removeFromBlacklist(packageName);
|
||||
}
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
appBlacklistAdapter.getFilter().filter(newText);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ControlCenter.ACTION_QUIT);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -113,10 +73,4 @@ public class AppBlacklistActivity extends Activity {
|
|||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
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.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 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.adapter.GBDeviceAppAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
|
||||
public class AppManagerActivity extends Activity {
|
||||
public static final String ACTION_REFRESH_APPLIST
|
||||
= "nodomain.freeyourgadget.gadgetbridge.appmanager.action.refresh_applist";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
|
||||
|
||||
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());
|
||||
UUID uuid = UUID.fromString(intent.getStringExtra("app_uuid" + i.toString()));
|
||||
GBDeviceApp.Type appType = GBDeviceApp.Type.values()[intent.getIntExtra("app_type" + i.toString(), 0)];
|
||||
|
||||
appList.add(new GBDeviceApp(uuid, appName, appCreator, "", appType));
|
||||
}
|
||||
|
||||
if (sharedPrefs.getBoolean("pebble_force_untested", false)) {
|
||||
appList.addAll(getSystemApps());
|
||||
}
|
||||
|
||||
mGBDeviceAppAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private SharedPreferences sharedPrefs;
|
||||
|
||||
private final List<GBDeviceApp> appList = new ArrayList<>();
|
||||
private GBDeviceAppAdapter mGBDeviceAppAdapter;
|
||||
private GBDeviceApp selectedApp = null;
|
||||
|
||||
private List<GBDeviceApp> getSystemApps() {
|
||||
List<GBDeviceApp> systemApps = new ArrayList<>();
|
||||
systemApps.add(new GBDeviceApp(UUID.fromString("4dab81a6-d2fc-458a-992c-7a1f3b96a970"), "Sports (System)", "Pebble Inc.", "", GBDeviceApp.Type.UNKNOWN));
|
||||
systemApps.add(new GBDeviceApp(UUID.fromString("cf1e816a-9db0-4511-bbb8-f60c48ca8fac"), "Golf (System)", "Pebble Inc.", "", GBDeviceApp.Type.UNKNOWN));
|
||||
|
||||
return systemApps;
|
||||
}
|
||||
|
||||
private List<GBDeviceApp> getCachedApps() {
|
||||
List<GBDeviceApp> cachedAppList = new ArrayList<>();
|
||||
try {
|
||||
File cachePath = new File(FileUtils.getExternalFilesDir().getPath() + "/pbw-cache");
|
||||
File files[] = cachePath.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.getName().endsWith(".pbw")) {
|
||||
UUID uuid = UUID.fromString(file.getName().substring(0, file.getName().length() - 4));
|
||||
cachedAppList.add(new GBDeviceApp(uuid, uuid.toString(), "N/A", "", GBDeviceApp.Type.UNKNOWN));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return cachedAppList;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
|
||||
setContentView(R.layout.activity_appmanager);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
ListView appListView = (ListView) findViewById(R.id.appListView);
|
||||
mGBDeviceAppAdapter = new GBDeviceAppAdapter(this, appList);
|
||||
appListView.setAdapter(this.mGBDeviceAppAdapter);
|
||||
|
||||
appListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView parent, View v, int position, long id) {
|
||||
UUID uuid = appList.get(position).getUUID();
|
||||
GBApplication.deviceService().onAppStart(uuid, true);
|
||||
}
|
||||
});
|
||||
|
||||
registerForContextMenu(appListView);
|
||||
|
||||
appList.addAll(getCachedApps());
|
||||
|
||||
if (sharedPrefs.getBoolean("pebble_force_untested", false)) {
|
||||
appList.addAll(getSystemApps());
|
||||
}
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ControlCenter.ACTION_QUIT);
|
||||
filter.addAction(ACTION_REFRESH_APPLIST);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
GBApplication.deviceService().onAppInfoReq();
|
||||
}
|
||||
|
||||
@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) {
|
||||
GBApplication.deviceService().onAppDelete(selectedApp.getUUID());
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,26 @@
|
|||
/* 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.app.ListActivity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -14,48 +30,71 @@ 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 ListActivity {
|
||||
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);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
|
||||
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));
|
||||
sharedPrefs.edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).commit();
|
||||
prefs.getPreferences().edit().putStringSet(PREF_MIBAND_ALARMS, preferencesAlarmListSet).apply();
|
||||
}
|
||||
|
||||
mGBAlarmListAdapter = new GBAlarmListAdapter(this, preferencesAlarmListSet);
|
||||
|
||||
setListAdapter(mGBAlarmListAdapter);
|
||||
|
||||
RecyclerView alarmsRecyclerView = (RecyclerView) findViewById(R.id.alarm_list);
|
||||
alarmsRecyclerView.setHasFixedSize(true);
|
||||
alarmsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
alarmsRecyclerView.setAdapter(mGBAlarmListAdapter);
|
||||
updateAlarmsFromPrefs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
protected void onPause() {
|
||||
if (!avoidSendAlarmsToDevice) {
|
||||
sendAlarmsToDevice();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
preferencesAlarmListSet = sharedPrefs.getStringSet(PREF_MIBAND_ALARMS, new HashSet<String>());
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQ_CONFIGURE_ALARM) {
|
||||
avoidSendAlarmsToDevice = false;
|
||||
updateAlarmsFromPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
mGBAlarmListAdapter.setAlarmList(preferencesAlarmListSet);
|
||||
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();
|
||||
|
||||
sendAlarmsToDevice();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -63,7 +102,6 @@ public class ConfigureAlarms extends ListActivity {
|
|||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
// back button
|
||||
sendAlarmsToDevice();
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
@ -71,10 +109,15 @@ public class ConfigureAlarms extends ListActivity {
|
|||
}
|
||||
|
||||
public void configureAlarm(GBAlarm alarm) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
|
||||
avoidSendAlarmsToDevice = true;
|
||||
Intent startIntent = new Intent(getApplicationContext(), AlarmDetails.class);
|
||||
startIntent.putExtra("alarm", alarm);
|
||||
startActivity(startIntent);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
|
||||
startActivityForResult(startIntent, REQ_CONFIGURE_ALARM);
|
||||
}
|
||||
|
||||
private GBDevice getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
private void sendAlarmsToDevice() {
|
||||
|
|
|
@ -1,348 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
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.ContextMenu;
|
||||
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;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class ControlCenter extends Activity {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ControlCenter.class);
|
||||
|
||||
public static final String ACTION_QUIT
|
||||
= "nodomain.freeyourgadget.gadgetbridge.controlcenter.action.quit";
|
||||
|
||||
public static final String ACTION_REFRESH_DEVICELIST
|
||||
= "nodomain.freeyourgadget.gadgetbridge.controlcenter.action.set_version";
|
||||
|
||||
private TextView hintTextView;
|
||||
private ListView deviceListView;
|
||||
private GBDeviceAdapter mGBDeviceAdapter;
|
||||
private GBDevice selectedDevice = null;
|
||||
|
||||
private final List<GBDevice> deviceList = new ArrayList<>();
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case ACTION_QUIT:
|
||||
finish();
|
||||
break;
|
||||
case ACTION_REFRESH_DEVICELIST:
|
||||
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
|
||||
refreshPairedDevices();
|
||||
break;
|
||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (dev.getAddress() != null) {
|
||||
int index = deviceList.indexOf(dev); // search by address
|
||||
if (index >= 0) {
|
||||
deviceList.set(index, dev);
|
||||
} else {
|
||||
deviceList.add(dev);
|
||||
}
|
||||
}
|
||||
refreshPairedDevices();
|
||||
|
||||
refreshBusyState(dev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void refreshBusyState(GBDevice dev) {
|
||||
mGBDeviceAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@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) {
|
||||
GBDevice gbDevice = deviceList.get(position);
|
||||
if (gbDevice.isConnected()) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
Class<? extends Activity> primaryActivity = coordinator.getPrimaryActivity();
|
||||
if (primaryActivity != null) {
|
||||
Intent startIntent = new Intent(ControlCenter.this, primaryActivity);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, gbDevice);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
} else {
|
||||
GBApplication.deviceService().connect(deviceList.get(position).getAddress());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerForContextMenu(deviceListView);
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(ACTION_QUIT);
|
||||
filterLocal.addAction(ACTION_REFRESH_DEVICELIST);
|
||||
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
filterLocal.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED));
|
||||
|
||||
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).apply();
|
||||
Intent enableIntent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
|
||||
startActivity(enableIntent);
|
||||
}
|
||||
GBApplication.deviceService().start();
|
||||
|
||||
|
||||
if (GB.isBluetoothEnabled() && deviceList.isEmpty()) {
|
||||
// start discovery when no devices are present
|
||||
startActivity(new Intent(this, DiscoveryActivity.class));
|
||||
} else {
|
||||
GBApplication.deviceService().requestDeviceInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
selectedDevice = deviceList.get(acmi.position);
|
||||
if (selectedDevice != null && selectedDevice.isBusy()) {
|
||||
// no context menu when device is busy
|
||||
return;
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.controlcenter_context, menu);
|
||||
|
||||
if (!selectedDevice.isConnected() || selectedDevice.getType() == DeviceType.PEBBLE) {
|
||||
menu.removeItem(R.id.controlcenter_fetch_activity_data);
|
||||
menu.removeItem(R.id.controlcenter_configure_alarms);
|
||||
}
|
||||
|
||||
if (!selectedDevice.isConnected() || selectedDevice.getType() == DeviceType.MIBAND) {
|
||||
menu.removeItem(R.id.controlcenter_take_screenshot);
|
||||
}
|
||||
|
||||
if (selectedDevice.getState() == GBDevice.State.NOT_CONNECTED) {
|
||||
menu.removeItem(R.id.controlcenter_disconnect);
|
||||
}
|
||||
if (!selectedDevice.isInitialized()) {
|
||||
menu.removeItem(R.id.controlcenter_find_device);
|
||||
}
|
||||
|
||||
menu.setHeaderTitle(selectedDevice.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.controlcenter_start_sleepmonitor:
|
||||
if (selectedDevice != null) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(ControlCenter.this, ChartsActivity.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, selectedDevice);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
return true;
|
||||
case R.id.controlcenter_fetch_activity_data:
|
||||
if (selectedDevice != null) {
|
||||
GBApplication.deviceService().onFetchActivityData();
|
||||
}
|
||||
return true;
|
||||
case R.id.controlcenter_disconnect:
|
||||
if (selectedDevice != null) {
|
||||
selectedDevice = null;
|
||||
GBApplication.deviceService().disconnect();
|
||||
}
|
||||
return true;
|
||||
case R.id.controlcenter_find_device:
|
||||
if (selectedDevice != null) {
|
||||
findDevice(true);
|
||||
ProgressDialog.show(
|
||||
this,
|
||||
getString(R.string.control_center_find_lost_device),
|
||||
getString(R.string.control_center_cancel_to_stop_vibration),
|
||||
true, true,
|
||||
new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
findDevice(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
case R.id.controlcenter_configure_alarms:
|
||||
if (selectedDevice != null) {
|
||||
Intent startIntent;
|
||||
startIntent = new Intent(ControlCenter.this, ConfigureAlarms.class);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
return true;
|
||||
case R.id.controlcenter_take_screenshot:
|
||||
if (selectedDevice != null) {
|
||||
GBApplication.deviceService().onScreenshotReq();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void findDevice(boolean start) {
|
||||
GBApplication.deviceService().onFindDevice(start);
|
||||
}
|
||||
|
||||
@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:
|
||||
GBApplication.deviceService().quit();
|
||||
|
||||
Intent quitIntent = new Intent(ControlCenter.ACTION_QUIT);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(quitIntent);
|
||||
return true;
|
||||
case R.id.action_discover:
|
||||
Intent discoverIntent = new Intent(this, DiscoveryActivity.class);
|
||||
startActivity(discoverIntent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
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, R.string.bluetooth_is_not_supported_, Toast.LENGTH_SHORT).show();
|
||||
} else if (!btAdapter.isEnabled()) {
|
||||
Toast.makeText(this, R.string.bluetooth_is_disabled_, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Set<BluetoothDevice> pairedDevices = btAdapter.getBondedDevices();
|
||||
for (BluetoothDevice pairedDevice : pairedDevices) {
|
||||
DeviceType deviceDeviceType;
|
||||
if (pairedDevice.getName().indexOf("Pebble") == 0) {
|
||||
deviceDeviceType = DeviceType.PEBBLE;
|
||||
} else if (pairedDevice.getName().equals("MI")) {
|
||||
deviceDeviceType = DeviceType.MIBAND;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
GBDevice device = new GBDevice(pairedDevice.getAddress(), pairedDevice.getName(), deviceDeviceType);
|
||||
if (!availableDevices.contains(device)) {
|
||||
availableDevices.add(device);
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
String miAddr = sharedPrefs.getString(MiBandConst.PREF_MIBAND_ADDRESS, "");
|
||||
if (miAddr.length() > 0) {
|
||||
GBDevice miDevice = new GBDevice(miAddr, "MI", DeviceType.MIBAND);
|
||||
if (!availableDevices.contains(miDevice)) {
|
||||
availableDevices.add(miDevice);
|
||||
}
|
||||
}
|
||||
|
||||
String pebbleEmuAddr = sharedPrefs.getString("pebble_emu_addr", "");
|
||||
String pebbleEmuPort = sharedPrefs.getString("pebble_emu_port", "");
|
||||
if (pebbleEmuAddr.length() >= 7 && pebbleEmuPort.length() > 0) {
|
||||
GBDevice pebbleEmuDevice = new GBDevice(pebbleEmuAddr + ":" + pebbleEmuPort, "Pebble qemu", DeviceType.PEBBLE);
|
||||
if (!availableDevices.contains(pebbleEmuDevice)) {
|
||||
availableDevices.add(pebbleEmuDevice);
|
||||
}
|
||||
}
|
||||
|
||||
deviceList.retainAll(availableDevices);
|
||||
for (GBDevice dev : availableDevices) {
|
||||
if (!deviceList.contains(dev)) {
|
||||
deviceList.add(dev);
|
||||
}
|
||||
}
|
||||
|
||||
if (connected) {
|
||||
hintTextView.setText(R.string.tap_connected_device_for_app_mananger);
|
||||
} else if (!deviceList.isEmpty()) {
|
||||
hintTextView.setText(R.string.tap_a_device_to_connect);
|
||||
}
|
||||
}
|
||||
mGBDeviceAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,43 +1,67 @@
|
|||
/* 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.Activity;
|
||||
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.database.sqlite.SQLiteOpenHelper;
|
||||
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.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
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.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.model.ServiceCommand;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class DebugActivity extends Activity {
|
||||
public class DebugActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DebugActivity.class);
|
||||
|
||||
private Button sendSMSButton;
|
||||
private Button sendEmailButton;
|
||||
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;
|
||||
|
@ -46,15 +70,26 @@ public class DebugActivity extends Activity {
|
|||
private Button setMusicInfoButton;
|
||||
private Button setTimeButton;
|
||||
private Button rebootButton;
|
||||
private Button exportDBButton;
|
||||
private Button importDBButton;
|
||||
private EditText editContent;
|
||||
private Button HeartRateButton;
|
||||
private Button testNewFunctionalityButton;
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
private EditText editContent;
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent.getAction().equals(ControlCenter.ACTION_QUIT)) {
|
||||
finish();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -63,32 +98,35 @@ public class DebugActivity extends Activity {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_debug);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
registerReceiver(mReceiver, new IntentFilter(ControlCenter.ACTION_QUIT));
|
||||
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);
|
||||
sendSMSButton = (Button) findViewById(R.id.sendSMSButton);
|
||||
sendSMSButton.setOnClickListener(new View.OnClickListener() {
|
||||
|
||||
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();
|
||||
notificationSpec.sender = getResources().getText(R.string.app_name).toString();
|
||||
notificationSpec.body = editContent.getText().toString();
|
||||
notificationSpec.type = NotificationType.SMS;
|
||||
notificationSpec.id = -1;
|
||||
GBApplication.deviceService().onNotification(notificationSpec);
|
||||
}
|
||||
});
|
||||
sendEmailButton = (Button) findViewById(R.id.sendEmailButton);
|
||||
sendEmailButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
NotificationSpec notificationSpec = new NotificationSpec();
|
||||
notificationSpec.sender = getResources().getText(R.string.app_name).toString();
|
||||
notificationSpec.subject = editContent.getText().toString();
|
||||
notificationSpec.body = editContent.getText().toString();
|
||||
notificationSpec.type = NotificationType.EMAIL;
|
||||
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);
|
||||
}
|
||||
|
@ -98,20 +136,20 @@ public class DebugActivity extends Activity {
|
|||
incomingCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GBApplication.deviceService().onSetCallState(
|
||||
editContent.getText().toString(),
|
||||
null,
|
||||
ServiceCommand.CALL_INCOMING);
|
||||
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) {
|
||||
GBApplication.deviceService().onSetCallState(
|
||||
editContent.getText().toString(),
|
||||
null,
|
||||
ServiceCommand.CALL_OUTGOING);
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_OUTGOING;
|
||||
callSpec.number = editContent.getText().toString();
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -119,35 +157,18 @@ public class DebugActivity extends Activity {
|
|||
startCallButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GBApplication.deviceService().onSetCallState(
|
||||
null,
|
||||
null,
|
||||
ServiceCommand.CALL_START);
|
||||
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) {
|
||||
GBApplication.deviceService().onSetCallState(
|
||||
null,
|
||||
null,
|
||||
ServiceCommand.CALL_END);
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_END;
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -158,15 +179,38 @@ public class DebugActivity extends Activity {
|
|||
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) {
|
||||
GBApplication.deviceService().onSetMusicInfo(
|
||||
editContent.getText().toString() + "(artist)",
|
||||
editContent.getText().toString() + "(album)",
|
||||
editContent.getText().toString() + "(tracl)");
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -185,44 +229,18 @@ public class DebugActivity extends Activity {
|
|||
testNotification();
|
||||
}
|
||||
});
|
||||
|
||||
testNewFunctionalityButton = (Button) findViewById(R.id.testNewFunctionality);
|
||||
testNewFunctionalityButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
testNewFunctionality();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void exportDB() {
|
||||
DBHandler dbHandler = null;
|
||||
try {
|
||||
dbHandler = GBApplication.acquireDB();
|
||||
DBHelper helper = new DBHelper(this);
|
||||
File dir = FileUtils.getExternalFilesDir();
|
||||
File destFile = helper.exportDB(dbHandler.getHelper(), dir);
|
||||
GB.toast(this, "Exported to: " + destFile.getAbsolutePath(), Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Unable to export db", ex);
|
||||
Toast.makeText(this, "Error exporting DB: " + ex.getMessage(), Toast.LENGTH_LONG).show();
|
||||
} finally {
|
||||
if (dbHandler != null) {
|
||||
dbHandler.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importDB() {
|
||||
DBHandler dbHandler = null;
|
||||
try {
|
||||
dbHandler = GBApplication.acquireDB();
|
||||
DBHelper helper = new DBHelper(this);
|
||||
File dir = FileUtils.getExternalFilesDir();
|
||||
SQLiteOpenHelper sqLiteOpenHelper = dbHandler.getHelper();
|
||||
File sourceFile = new File(dir, sqLiteOpenHelper.getDatabaseName());
|
||||
helper.importDB(sqLiteOpenHelper, sourceFile);
|
||||
helper.validateDB(sqLiteOpenHelper);
|
||||
GB.toast(this, "Import successful.", Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch (Exception ex) {
|
||||
GB.toast(this, "Error importing DB: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
} finally {
|
||||
if (dbHandler != null) {
|
||||
dbHandler.release();
|
||||
}
|
||||
}
|
||||
private void testNewFunctionality() {
|
||||
GBApplication.deviceService().onTestNewFunction();
|
||||
}
|
||||
|
||||
private void testNotification() {
|
||||
|
@ -233,13 +251,30 @@ public class DebugActivity extends Activity {
|
|||
notificationIntent, 0);
|
||||
|
||||
NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this);
|
||||
ncomp.setContentTitle(getString(R.string.test_notification));
|
||||
ncomp.setContentText(getString(R.string.this_is_a_test_notification_from_gadgetbridge));
|
||||
ncomp.setTicker(getString(R.string.this_is_a_test_notification_from_gadgetbridge));
|
||||
ncomp.setSmallIcon(R.drawable.ic_notification);
|
||||
ncomp.setAutoCancel(true);
|
||||
ncomp.setContentIntent(pendingIntent);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -256,7 +291,11 @@ public class DebugActivity extends Activity {
|
|||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
||||
public interface DeviceSelectionCallback {
|
||||
void invoke(GBDevice device);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,47 @@
|
|||
/* 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;
|
||||
|
@ -23,35 +53,56 @@ 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;
|
||||
|
||||
public class DiscoveryActivity extends Activity implements AdapterView.OnItemClickListener {
|
||||
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 Handler handler = new Handler();
|
||||
private ScanCallback newLeScanCallback = null;
|
||||
|
||||
private BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
|
||||
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:
|
||||
discoveryStarted(Scanning.SCANNING_BT);
|
||||
if (isScanning != Scanning.SCANNING_BTLE && isScanning != Scanning.SCANNING_NEW_BTLE) {
|
||||
discoveryStarted(Scanning.SCANNING_BT);
|
||||
}
|
||||
break;
|
||||
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
|
||||
// continue with LE scan, if available
|
||||
if (isScanning == Scanning.SCANNING_BT) {
|
||||
startDiscovery(Scanning.SCANNING_BTLE);
|
||||
} else {
|
||||
discoveryFinished();
|
||||
}
|
||||
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);
|
||||
|
@ -64,13 +115,20 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
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 && device.getAddress().equals(bondingAddress)) {
|
||||
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) {
|
||||
LOG.info("Successfully bonded with: " + bondingAddress);
|
||||
finish();
|
||||
handleDeviceBonded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,14 +136,109 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
}
|
||||
};
|
||||
|
||||
private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
private Runnable stopRunnable = new Runnable() {
|
||||
|
||||
// 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();
|
||||
|
@ -94,15 +247,16 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
|
||||
private ProgressBar progressView;
|
||||
private BluetoothAdapter adapter;
|
||||
private ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
|
||||
private final ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
|
||||
private DeviceCandidateAdapter cadidateListAdapter;
|
||||
private Button startButton;
|
||||
private Scanning isScanning = Scanning.SCANNING_OFF;
|
||||
private String bondingAddress;
|
||||
private GBDeviceCandidate bondingDevice;
|
||||
|
||||
private enum Scanning {
|
||||
SCANNING_BT,
|
||||
SCANNING_BTLE,
|
||||
SCANNING_NEW_BTLE,
|
||||
SCANNING_OFF
|
||||
}
|
||||
|
||||
|
@ -131,6 +285,7 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
|
||||
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);
|
||||
|
@ -170,13 +325,44 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterReceiver(bluetoothReceiver);
|
||||
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) {
|
||||
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi);
|
||||
if (DeviceHelper.getInstance().isSupported(candidate)) {
|
||||
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
|
||||
|
@ -211,10 +397,16 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
} else {
|
||||
discoveryFinished();
|
||||
}
|
||||
} else if (what == Scanning.SCANNING_NEW_BTLE) {
|
||||
if (GB.supportsBluetoothLE()) {
|
||||
startNEWBTLEDiscovery();
|
||||
} else {
|
||||
discoveryFinished();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
discoveryFinished();
|
||||
Toast.makeText(this, "Enable Bluetooth to discover devices.", Toast.LENGTH_LONG).show();
|
||||
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,6 +426,8 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
stopBTDiscovery();
|
||||
} else if (wasScanning == Scanning.SCANNING_BTLE) {
|
||||
stopBTLEDiscovery();
|
||||
} else if (wasScanning == Scanning.SCANNING_NEW_BTLE) {
|
||||
stopNewBTLEDiscovery();
|
||||
}
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
}
|
||||
|
@ -247,9 +441,20 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
adapter.cancelDiscovery();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void stopNewBTLEDiscovery() {
|
||||
adapter.getBluetoothLeScanner().stopScan(newLeScanCallback);
|
||||
}
|
||||
|
||||
private void bluetoothStateChanged(int oldState, int newState) {
|
||||
discoveryFinished();
|
||||
startButton.setEnabled(newState == BluetoothAdapter.STATE_ON);
|
||||
if (newState == BluetoothAdapter.STATE_ON) {
|
||||
this.adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
startButton.setEnabled(true);
|
||||
} else {
|
||||
this.adapter = null;
|
||||
startButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void discoveryFinished() {
|
||||
|
@ -284,8 +489,15 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
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;
|
||||
}
|
||||
|
@ -293,6 +505,41 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
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);
|
||||
|
@ -307,6 +554,12 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
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;
|
||||
|
@ -323,17 +576,35 @@ public class DiscoveryActivity extends Activity implements AdapterView.OnItemCli
|
|||
|
||||
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_MAC_ADDRESS, deviceCandidate.getMacAddress());
|
||||
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());
|
||||
if (btDevice.createBond()) {
|
||||
// async, wait for bonding event to finish this activity
|
||||
bondingAddress = btDevice.getAddress();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,22 @@
|
|||
/* 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.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -29,14 +45,16 @@ 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 Activity implements InstallActivity {
|
||||
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;
|
||||
|
@ -45,13 +63,20 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
private InstallHandler installHandler;
|
||||
private boolean mayConnect;
|
||||
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
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 (action.equals(ControlCenter.ACTION_QUIT)) {
|
||||
finish();
|
||||
} else if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
|
||||
if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
|
||||
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (device != null) {
|
||||
refreshBusyState(device);
|
||||
|
@ -67,13 +92,13 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
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 ProgressBar mProgressBar;
|
||||
private ListView itemListView;
|
||||
private List<ItemWithDetails> mItems = new ArrayList<>();
|
||||
private ItemWithDetailsAdapter mItemAdapter;
|
||||
|
||||
private void refreshBusyState(GBDevice dev) {
|
||||
if (dev.isConnecting() || dev.isBusy()) {
|
||||
|
@ -89,7 +114,7 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
|
||||
private void connect() {
|
||||
mayConnect = false; // only do that once per #onCreate
|
||||
GBApplication.deviceService().connect(device != null ? device.getAddress() : null);
|
||||
GBApplication.deviceService().connect(device);
|
||||
}
|
||||
|
||||
private void validateInstallation() {
|
||||
|
@ -102,11 +127,18 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_appinstaller);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
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);
|
||||
|
@ -114,10 +146,14 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
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(ControlCenter.ACTION_QUIT);
|
||||
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
filter.addAction(GB.ACTION_DISPLAY_MESSAGE);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
installButton.setOnClickListener(new View.OnClickListener() {
|
||||
|
@ -130,6 +166,9 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
});
|
||||
|
||||
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));
|
||||
|
@ -145,6 +184,12 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
@ -176,6 +221,11 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
fwAppInstallTextView.setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getInfoText() {
|
||||
return fwAppInstallTextView.getText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInstallEnabled(boolean enable) {
|
||||
boolean enabled = device != null && device.isConnected() && enable;
|
||||
|
@ -195,4 +245,9 @@ public class FwAppInstallerActivity extends Activity implements InstallActivity
|
|||
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;
|
||||
}
|
|
@ -1,8 +1,26 @@
|
|||
/* 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);
|
||||
|
@ -10,4 +28,5 @@ public interface InstallActivity {
|
|||
void clearInstallItems();
|
||||
|
||||
void setInstallItem(ItemWithDetails item);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,19 +1,68 @@
|
|||
/* 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);
|
||||
|
@ -50,11 +99,20 @@ public class SettingsActivity extends AbstractSettingsActivity {
|
|||
}
|
||||
});
|
||||
|
||||
final Preference pebbleEmuAddr = findPreference("pebble_emu_addr");
|
||||
pebbleEmuAddr.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
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(ControlCenter.ACTION_REFRESH_DEVICELIST);
|
||||
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
|
||||
preference.setSummary(newVal.toString());
|
||||
return true;
|
||||
|
@ -62,11 +120,11 @@ public class SettingsActivity extends AbstractSettingsActivity {
|
|||
|
||||
});
|
||||
|
||||
final Preference pebbleEmuPort = findPreference("pebble_emu_port");
|
||||
pebbleEmuPort.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
pref = findPreference("pebble_emu_port");
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newVal) {
|
||||
Intent refreshIntent = new Intent(ControlCenter.ACTION_REFRESH_DEVICELIST);
|
||||
Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(refreshIntent);
|
||||
preference.setSummary(newVal.toString());
|
||||
return true;
|
||||
|
@ -74,6 +132,133 @@ public class SettingsActivity extends AbstractSettingsActivity {
|
|||
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
|
@ -100,17 +285,72 @@ public class SettingsActivity extends AbstractSettingsActivity {
|
|||
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[]{
|
||||
"audio_player",
|
||||
"notification_mode_calls",
|
||||
"notification_mode_sms",
|
||||
"notification_mode_k9mail",
|
||||
"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;
|
||||
}
|
||||
}
|
|
@ -1,19 +1,50 @@
|
|||
/* 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.graphics.Color;
|
||||
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;
|
||||
|
@ -28,12 +59,15 @@ 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;
|
||||
|
@ -59,29 +93,36 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
|||
* shift the date by one day.
|
||||
*/
|
||||
public abstract class AbstractChartFragment extends AbstractGBFragment {
|
||||
protected int ANIM_TIME = 350;
|
||||
protected final int ANIM_TIME = 250;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractChartFragment.class);
|
||||
|
||||
private final Set<String> mIntentFilterActions;
|
||||
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
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 Integer color;
|
||||
public final Integer color;
|
||||
|
||||
public ActivityConfig(int kind, String label, Integer color) {
|
||||
this.type = kind;
|
||||
|
@ -90,36 +131,40 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
}
|
||||
}
|
||||
|
||||
protected ActivityConfig akActivity = new ActivityConfig(ActivityKind.TYPE_ACTIVITY, "Activity", Color.rgb(89, 178, 44));
|
||||
protected ActivityConfig akLightSleep = new ActivityConfig(ActivityKind.TYPE_LIGHT_SLEEP, "Light Sleep", Color.rgb(182, 191, 255));
|
||||
protected ActivityConfig akDeepSleep = new ActivityConfig(ActivityKind.TYPE_DEEP_SLEEP, "Deep Sleep", Color.rgb(76, 90, 255));
|
||||
protected ActivityConfig akNotWorn = new ActivityConfig(ActivityKind.TYPE_NOT_WORN, "Not Worn", Color.rgb(84, 82, 84));
|
||||
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);
|
||||
}
|
||||
mIntentFilterActions.add(ChartsHost.DATE_NEXT);
|
||||
mIntentFilterActions.add(ChartsHost.DATE_PREV);
|
||||
mIntentFilterActions.add(ChartsHost.REFRESH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
initColors();
|
||||
init();
|
||||
|
||||
IntentFilter filter = new IntentFilter();
|
||||
for (String action : mIntentFilterActions) {
|
||||
|
@ -128,36 +173,49 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filter);
|
||||
}
|
||||
|
||||
protected void initColors() {
|
||||
BACKGROUND_COLOR = getResources().getColor(R.color.background_material_light);
|
||||
DESCRIPTION_COLOR = getResources().getColor(R.color.primarytext);
|
||||
CHART_TEXT_COLOR = getResources().getColor(R.color.secondarytext);
|
||||
LEGEND_TEXT_COLOR = getResources().getColor(R.color.primarytext);
|
||||
AK_ACTIVITY_COLOR = getResources().getColor(R.color.chart_activity_light);
|
||||
AK_DEEP_SLEEP_COLOR = getResources().getColor(R.color.chart_light_sleep_light);
|
||||
AK_LIGHT_SLEEP_COLOR = getResources().getColor(R.color.chart_deep_sleep_light);
|
||||
AK_NOT_WORN_COLOR = getResources().getColor(R.color.chart_not_worn_light);
|
||||
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;
|
||||
|
||||
akActivity.color = AK_ACTIVITY_COLOR;
|
||||
akLightSleep.color = AK_LIGHT_SLEEP_COLOR;
|
||||
akDeepSleep.color = AK_DEEP_SLEEP_COLOR;
|
||||
akNotWorn.color = AK_NOT_WORN_COLOR;
|
||||
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) {
|
||||
((ChartsHost) getHost()).setStartDate(date);
|
||||
getChartsHost().setStartDate(date);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected ChartsHost getChartsHost() {
|
||||
return (ChartsHost) getActivity();
|
||||
}
|
||||
|
||||
private void setEndDate(Date date) {
|
||||
((ChartsHost) getHost()).setEndDate(date);
|
||||
getChartsHost().setEndDate(date);
|
||||
}
|
||||
|
||||
public Date getStartDate() {
|
||||
return ((ChartsHost) getHost()).getStartDate();
|
||||
return getChartsHost().getStartDate();
|
||||
}
|
||||
|
||||
public Date getEndDate() {
|
||||
return ((ChartsHost) getHost()).getEndDate();
|
||||
return getChartsHost().getEndDate();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -169,11 +227,16 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
@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();
|
||||
|
@ -258,9 +321,9 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
return akActivity.color;
|
||||
}
|
||||
|
||||
protected SampleProvider getProvider(GBDevice device) {
|
||||
protected SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
return coordinator.getSampleProvider();
|
||||
return coordinator.getSampleProvider(device, db.getDaoSession());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -271,51 +334,48 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
* @param tsFrom
|
||||
* @param tsTo
|
||||
*/
|
||||
protected List<ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider provider = getProvider(device);
|
||||
return db.getAllActivitySamples(tsFrom, tsTo, provider);
|
||||
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);
|
||||
}
|
||||
|
||||
private int getTSLast24Hours(int tsTo) {
|
||||
return (tsTo) - (24 * 60 * 60); // -24 hours
|
||||
}
|
||||
|
||||
protected List<ActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider provider = getProvider(device);
|
||||
return db.getActivitySamples(tsFrom, tsTo, provider);
|
||||
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<ActivitySample> getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider provider = getProvider(device);
|
||||
return db.getSleepSamples(tsFrom, tsTo, provider);
|
||||
}
|
||||
|
||||
protected List<ActivitySample> getTestSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.clear();
|
||||
cal.set(2015, Calendar.JUNE, 10, 6, 40);
|
||||
// ignore provided date ranges
|
||||
tsTo = (int) ((cal.getTimeInMillis() / 1000));
|
||||
tsFrom = tsTo - (24 * 60 * 60);
|
||||
|
||||
SampleProvider provider = getProvider(device);
|
||||
return db.getAllActivitySamples(tsFrom, tsTo, provider);
|
||||
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.setHighlightEnabled(false);
|
||||
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);
|
||||
|
@ -333,19 +393,25 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
* #renderCharts
|
||||
*/
|
||||
protected void refresh() {
|
||||
if (((ChartsHost) getHost()).getDevice() != null) {
|
||||
mChartDirty = false;
|
||||
updateDateInfo(getStartDate(), getEndDate());
|
||||
createRefreshTask("Visualizing data", getActivity()).execute();
|
||||
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. #renderCharts will be automatically called after this method.
|
||||
* any UI access. #updateChartsInUIThread and #renderCharts will be automatically called after this method.
|
||||
*/
|
||||
protected abstract void refreshInBackground(DBHandler db, GBDevice device);
|
||||
protected abstract ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device);
|
||||
|
||||
/**
|
||||
* Triggers the actual (re-) rendering of the chart.
|
||||
|
@ -353,44 +419,47 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
*/
|
||||
protected abstract void renderCharts();
|
||||
|
||||
protected void refresh(GBDevice gbDevice, BarLineChartBase chart, List<ActivitySample> samples) {
|
||||
Calendar cal = GregorianCalendar.getInstance();
|
||||
cal.clear();
|
||||
Date date;
|
||||
String dateStringFrom = "";
|
||||
String dateStringTo = "";
|
||||
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) {
|
||||
float movement_divisor;
|
||||
boolean annotate = true;
|
||||
boolean use_steps_as_movement;
|
||||
SampleProvider provider = getProvider(gbDevice);
|
||||
|
||||
int last_type = ActivityKind.TYPE_UNKNOWN;
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
|
||||
SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm");
|
||||
|
||||
int numEntries = samples.size();
|
||||
List<String> xLabels = new ArrayList<>(numEntries);
|
||||
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(sample.getTimestamp() * 1000L); // make sure it's converted to long
|
||||
date = cal.getTime();
|
||||
dateStringFrom = dateFormat.format(date);
|
||||
} else if (i == samples.size() - 1) {
|
||||
cal.setTimeInMillis(sample.getTimestamp() * 1000L); // same here
|
||||
date = cal.getTime();
|
||||
dateStringTo = dateFormat.format(date);
|
||||
}
|
||||
// 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();
|
||||
|
||||
|
@ -416,14 +485,23 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
// value = ((float) movement) / movement_divisor;
|
||||
colors.add(akActivity.color);
|
||||
}
|
||||
activityEntries.add(createBarEntry(value, i));
|
||||
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(sample.getTimestamp() * 1000L);
|
||||
date = cal.getTime();
|
||||
String dateString = annotationDateFormat.format(date);
|
||||
xLabel = dateString;
|
||||
// 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
|
||||
|
@ -441,29 +519,39 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
// chart.getXAxis().addLimitLine(line);
|
||||
// }
|
||||
// }
|
||||
last_type = type;
|
||||
// last_type = type;
|
||||
}
|
||||
xLabels.add(xLabel);
|
||||
}
|
||||
|
||||
chart.getXAxis().setValues(xLabels);
|
||||
|
||||
BarDataSet activitySet = createActivitySet(activityEntries, colors, "Activity");
|
||||
|
||||
ArrayList<BarDataSet> dataSets = new ArrayList<>();
|
||||
dataSets.add(activitySet);
|
||||
|
||||
// create a data object with the datasets
|
||||
BarData data = new BarData(xLabels, dataSets);
|
||||
data.setGroupSpace(0);
|
||||
// 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);
|
||||
|
||||
chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo));
|
||||
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(?, ?);
|
||||
|
||||
setupLegend(chart);
|
||||
|
||||
chart.setData(data);
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -475,12 +563,16 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
* @param tsTo
|
||||
* @return
|
||||
*/
|
||||
protected abstract List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo);
|
||||
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 index) {
|
||||
return new BarEntry(value, index);
|
||||
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) {
|
||||
|
@ -497,6 +589,28 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
// 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;
|
||||
}
|
||||
|
||||
|
@ -539,20 +653,28 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
}
|
||||
|
||||
public class RefreshTask extends DBAccess {
|
||||
private ChartsData chartsData;
|
||||
|
||||
public RefreshTask(String task, Context context) {
|
||||
super(task, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInBackground(DBHandler db) {
|
||||
refreshInBackground(db, ((ChartsHost) getHost()).getDevice());
|
||||
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 = (FragmentActivity) getActivity();
|
||||
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");
|
||||
|
@ -560,6 +682,8 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
}
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -582,14 +706,52 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
|
||||
protected void updateDateInfo(Date from, Date to) {
|
||||
if (from.equals(to)) {
|
||||
((ChartsHost) getHost()).setDateInfo(DateTimeUtils.formatDate(from));
|
||||
getChartsHost().setDateInfo(DateTimeUtils.formatDate(from));
|
||||
} else {
|
||||
((ChartsHost) getHost()).setDateInfo(DateTimeUtils.formatDateRange(from, to));
|
||||
getChartsHost().setDateInfo(DateTimeUtils.formatDateRange(from, to));
|
||||
}
|
||||
}
|
||||
|
||||
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device) {
|
||||
return getSamples(db, device, getTSStart(), getTSEnd());
|
||||
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() {
|
||||
|
@ -603,4 +765,88 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
|
|||
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);
|
||||
}
|
|
@ -1,14 +1,43 @@
|
|||
/* 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;
|
||||
|
||||
public class ActivityAnalysis {
|
||||
public ActivityAmounts calculateActivityAmounts(List<ActivitySample> samples) {
|
||||
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);
|
||||
|
@ -17,7 +46,7 @@ public class ActivityAnalysis {
|
|||
ActivityAmount previousAmount = null;
|
||||
ActivitySample previousSample = null;
|
||||
for (ActivitySample sample : samples) {
|
||||
ActivityAmount amount = null;
|
||||
ActivityAmount amount;
|
||||
switch (sample.getKind()) {
|
||||
case ActivityKind.TYPE_DEEP_SLEEP:
|
||||
amount = deepSleep;
|
||||
|
@ -34,6 +63,11 @@ public class ActivityAnalysis {
|
|||
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()) {
|
||||
|
@ -43,8 +77,22 @@ public class ActivityAnalysis {
|
|||
previousAmount.addSeconds(sharedTimeDifference);
|
||||
amount.addSeconds(sharedTimeDifference);
|
||||
}
|
||||
} else {
|
||||
// nothing to do, we can only calculate when we have the next sample
|
||||
|
||||
// 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;
|
||||
|
@ -66,10 +114,13 @@ public class ActivityAnalysis {
|
|||
return result;
|
||||
}
|
||||
|
||||
public int calculateTotalSteps(List<ActivitySample> samples) {
|
||||
int calculateTotalSteps(List<? extends ActivitySample> samples) {
|
||||
int totalSteps = 0;
|
||||
for (ActivitySample sample : samples) {
|
||||
totalSteps += sample.getSteps();
|
||||
int steps = sample.getSteps();
|
||||
if (steps > 0) {
|
||||
totalSteps += sample.getSteps();
|
||||
}
|
||||
}
|
||||
return totalSteps;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
/* 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;
|
||||
|
@ -10,6 +27,7 @@ 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;
|
||||
|
||||
|
@ -20,6 +38,7 @@ 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;
|
||||
|
@ -54,7 +73,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
|
|||
|
||||
private void setupChart() {
|
||||
mChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mChart.setDescriptionColor(DESCRIPTION_COLOR);
|
||||
mChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
configureBarLineChartDefaults(mChart);
|
||||
|
||||
|
||||
|
@ -69,7 +88,8 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
|
|||
y.setDrawGridLines(false);
|
||||
// y.setDrawLabels(false);
|
||||
// TODO: make fixed max value optional
|
||||
y.setAxisMaxValue(1f);
|
||||
y.setAxisMaximum(1f);
|
||||
y.setAxisMinimum(0);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
|
@ -78,10 +98,12 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
|
|||
|
||||
YAxis yAxisRight = mChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(false);
|
||||
yAxisRight.setDrawLabels(false);
|
||||
yAxisRight.setDrawTopYLabelEntry(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();
|
||||
|
@ -103,33 +125,61 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void refreshInBackground(DBHandler db, GBDevice device) {
|
||||
List<ActivitySample> samples = getSamples(db, device);
|
||||
refresh(device, mChart, samples);
|
||||
|
||||
mChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
}
|
||||
|
||||
protected void renderCharts() {
|
||||
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
|
||||
}
|
||||
|
||||
protected void setupLegend(Chart chart) {
|
||||
List<Integer> legendColors = new ArrayList<>(4);
|
||||
List<String> legendLabels = new ArrayList<>(4);
|
||||
legendColors.add(akActivity.color);
|
||||
legendLabels.add(akActivity.label);
|
||||
legendColors.add(akLightSleep.color);
|
||||
legendLabels.add(akLightSleep.label);
|
||||
legendColors.add(akDeepSleep.color);
|
||||
legendLabels.add(akDeepSleep.label);
|
||||
legendColors.add(akNotWorn.color);
|
||||
legendLabels.add(akNotWorn.label);
|
||||
chart.getLegend().setCustom(legendColors, legendLabels);
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device);
|
||||
return refresh(device, samples);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
/* 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;
|
||||
|
@ -10,47 +28,80 @@ 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.ProgressBar;
|
||||
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.activities.ControlCenter;
|
||||
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 ProgressBar mProgressBar;
|
||||
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 BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
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 ControlCenter.ACTION_QUIT:
|
||||
finish();
|
||||
break;
|
||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
refreshBusyState(dev);
|
||||
|
@ -59,17 +110,19 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
}
|
||||
};
|
||||
private GBDevice mGBDevice;
|
||||
private ViewGroup dateBar;
|
||||
|
||||
private void refreshBusyState(GBDevice dev) {
|
||||
if (dev.isBusy()) {
|
||||
mProgressBar.setVisibility(View.VISIBLE);
|
||||
swipeLayout.setRefreshing(true);
|
||||
} else {
|
||||
boolean wasBusy = mProgressBar.getVisibility() != View.GONE;
|
||||
boolean wasBusy = swipeLayout.isRefreshing();
|
||||
swipeLayout.setRefreshing(false);
|
||||
if (wasBusy) {
|
||||
mProgressBar.setVisibility(View.GONE);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
|
||||
}
|
||||
}
|
||||
enableSwipeRefresh(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -80,7 +133,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
initDates();
|
||||
|
||||
IntentFilter filterLocal = new IntentFilter();
|
||||
filterLocal.addAction(ControlCenter.ACTION_QUIT);
|
||||
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
|
@ -91,11 +143,43 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
|
||||
viewPager.setAdapter(getPagerAdapter());
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
mProgressBar = (ProgressBar) findViewById(R.id.charts_progress);
|
||||
mPrevButton = (Button) findViewById(R.id.charts_previous);
|
||||
mPrevButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
|
@ -111,7 +195,15 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
}
|
||||
});
|
||||
|
||||
mDateControl = (TextView) findViewById(R.id.charts_text_date);
|
||||
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() {
|
||||
|
@ -162,6 +254,11 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -169,7 +266,7 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.charts_fetch_activity_data:
|
||||
GBApplication.deviceService().onFetchActivityData();
|
||||
fetchActivityData();
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
|
@ -178,6 +275,20 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
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);
|
||||
|
@ -188,13 +299,19 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
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 static class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
|
||||
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
|
||||
|
||||
public SectionsPagerAdapter(FragmentManager fm) {
|
||||
SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
|
@ -207,16 +324,44 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
|||
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 3 total pages.
|
||||
return 3;
|
||||
// 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 {
|
||||
}
|
|
@ -1,13 +1,31 @@
|
|||
/* 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 {
|
||||
static final String DATE_PREV = ChartsActivity.class.getName().concat(".date_prev");
|
||||
static final String DATE_NEXT = ChartsActivity.class.getName().concat(".date_next");
|
||||
static final String REFRESH = ChartsActivity.class.getName().concat(".refresh");
|
||||
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();
|
||||
|
||||
|
@ -20,4 +38,6 @@ public interface ChartsHost {
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,20 @@
|
|||
/* 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;
|
||||
|
@ -8,15 +25,18 @@ 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.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.formatter.ValueFormatter;
|
||||
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;
|
||||
|
@ -27,10 +47,12 @@ 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;
|
||||
|
||||
|
@ -38,7 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
|||
public class SleepChartFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class);
|
||||
|
||||
private BarLineChartBase mActivityChart;
|
||||
private CombinedChart mActivityChart;
|
||||
private PieChart mSleepAmountChart;
|
||||
|
||||
private int mSmartAlarmFrom = -1;
|
||||
|
@ -47,41 +69,61 @@ public class SleepChartFragment extends AbstractChartFragment {
|
|||
private int mSmartAlarmGoneOff = -1;
|
||||
|
||||
@Override
|
||||
protected void refreshInBackground(DBHandler db, GBDevice device) {
|
||||
List<ActivitySample> samples = getSamples(db, device);
|
||||
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
List<? extends ActivitySample> samples = getSamples(db, device);
|
||||
|
||||
refresh(device, mActivityChart, samples);
|
||||
refreshSleepAmounts(device, mSleepAmountChart, samples);
|
||||
MySleepChartsData mySleepChartsData = refreshSleepAmounts(device, samples);
|
||||
DefaultChartsData chartsData = refresh(device, samples);
|
||||
|
||||
return new MyChartsData(mySleepChartsData, chartsData);
|
||||
}
|
||||
|
||||
private void refreshSleepAmounts(GBDevice mGBDevice, PieChart pieChart, List<ActivitySample> samples) {
|
||||
private MySleepChartsData refreshSleepAmounts(GBDevice mGBDevice, List<? extends ActivitySample> samples) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
ActivityAmounts amounts = analysis.calculateActivityAmounts(samples);
|
||||
String totalSleep = DateTimeUtils.formatDurationHoursMinutes(amounts.getTotalSeconds(), TimeUnit.SECONDS);
|
||||
pieChart.setCenterText(totalSleep);
|
||||
PieData data = new PieData();
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
List<PieEntry> entries = new ArrayList<>();
|
||||
List<Integer> colors = new ArrayList<>();
|
||||
int index = 0;
|
||||
// int index = 0;
|
||||
long totalSeconds = 0;
|
||||
for (ActivityAmount amount : amounts.getAmounts()) {
|
||||
long value = amount.getTotalSeconds();
|
||||
entries.add(new Entry(amount.getTotalSeconds(), index++));
|
||||
colors.add(getColorFor(amount.getActivityKind()));
|
||||
data.addXValue(amount.getName(getActivity()));
|
||||
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 ValueFormatter() {
|
||||
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);
|
||||
pieChart.setData(data);
|
||||
|
||||
pieChart.getLegend().setEnabled(false);
|
||||
//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
|
||||
|
@ -94,7 +136,7 @@ public class SleepChartFragment extends AbstractChartFragment {
|
|||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
|
||||
|
||||
mActivityChart = (BarLineChartBase) rootView.findViewById(R.id.sleepchart);
|
||||
mActivityChart = (CombinedChart) rootView.findViewById(R.id.sleepchart);
|
||||
mSleepAmountChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
|
||||
|
||||
setupActivityChart();
|
||||
|
@ -123,15 +165,17 @@ public class SleepChartFragment extends AbstractChartFragment {
|
|||
|
||||
private void setupSleepAmountChart() {
|
||||
mSleepAmountChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mSleepAmountChart.setDescriptionColor(DESCRIPTION_COLOR);
|
||||
mSleepAmountChart.setDescription("");
|
||||
mSleepAmountChart.setNoDataTextDescription("");
|
||||
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.setDescriptionColor(DESCRIPTION_COLOR);
|
||||
mActivityChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
configureBarLineChartDefaults(mActivityChart);
|
||||
|
||||
XAxis x = mActivityChart.getXAxis();
|
||||
|
@ -145,7 +189,8 @@ public class SleepChartFragment extends AbstractChartFragment {
|
|||
y.setDrawGridLines(false);
|
||||
// y.setDrawLabels(false);
|
||||
// TODO: make fixed max value optional
|
||||
y.setAxisMaxValue(1f);
|
||||
y.setAxisMaximum(1f);
|
||||
y.setAxisMinimum(0);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
|
@ -154,30 +199,83 @@ public class SleepChartFragment extends AbstractChartFragment {
|
|||
|
||||
YAxis yAxisRight = mActivityChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(false);
|
||||
yAxisRight.setDrawLabels(false);
|
||||
yAxisRight.setDrawTopYLabelEntry(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<Integer> legendColors = new ArrayList<>(2);
|
||||
List<String> legendLabels = new ArrayList<>(2);
|
||||
legendColors.add(akLightSleep.color);
|
||||
legendLabels.add(akLightSleep.label);
|
||||
legendColors.add(akDeepSleep.color);
|
||||
legendLabels.add(akDeepSleep.label);
|
||||
chart.getLegend().setCustom(legendColors, legendLabels);
|
||||
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<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return super.getSleepSamples(db, device, tsFrom, tsTo);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
/* 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;
|
||||
|
@ -6,7 +22,7 @@ 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 final boolean isSleep(byte type) {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,223 +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 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.BarLineChartBase;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
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.Entry;
|
||||
import com.github.mikephil.charting.data.PieData;
|
||||
import com.github.mikephil.charting.data.PieDataSet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
|
||||
|
||||
public class WeekStepsChartFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(WeekStepsChartFragment.class);
|
||||
|
||||
private Locale mLocale;
|
||||
private int mTargetSteps = 10000;
|
||||
|
||||
private BarLineChartBase mWeekStepsChart;
|
||||
private PieChart mTodayStepsChart;
|
||||
|
||||
@Override
|
||||
protected void refreshInBackground(DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(((ChartsHost) getHost()).getEndDate());
|
||||
//NB: we could have omitted the day, but this way we can move things to the past easily
|
||||
refreshDaySteps(db, mTodayStepsChart, day, device);
|
||||
refreshWeekBeforeSteps(db, mWeekStepsChart, day, device);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
mWeekStepsChart.invalidate();
|
||||
mTodayStepsChart.invalidate();
|
||||
}
|
||||
|
||||
private void refreshWeekBeforeSteps(DBHandler db, BarLineChartBase barChart, Calendar day, GBDevice device) {
|
||||
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
|
||||
day = (Calendar) day.clone(); // do not modify the caller's argument
|
||||
day.add(Calendar.DATE, -7);
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
List<String> labels = new ArrayList<>();
|
||||
|
||||
for (int counter = 0; counter < 7; counter++) {
|
||||
entries.add(new BarEntry(analysis.calculateTotalSteps(getSamplesOfDay(db, day, device)), counter));
|
||||
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
|
||||
day.add(Calendar.DATE, 1);
|
||||
}
|
||||
|
||||
BarDataSet set = new BarDataSet(entries, "");
|
||||
set.setColor(akActivity.color);
|
||||
|
||||
BarData data = new BarData(labels, set);
|
||||
data.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);
|
||||
|
||||
LimitLine target = new LimitLine(mTargetSteps);
|
||||
barChart.getAxisLeft().getLimitLines().clear();
|
||||
barChart.getAxisLeft().addLimitLine(target);
|
||||
|
||||
setupLegend(barChart);
|
||||
barChart.setData(data);
|
||||
barChart.getLegend().setEnabled(false);
|
||||
}
|
||||
|
||||
private void refreshDaySteps(DBHandler db, PieChart pieChart, Calendar day, GBDevice device) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
|
||||
int totalSteps = analysis.calculateTotalSteps(getSamplesOfDay(db, day, device));
|
||||
|
||||
pieChart.setCenterText(NumberFormat.getNumberInstance(mLocale).format(totalSteps));
|
||||
PieData data = new PieData();
|
||||
List<Entry> entries = new ArrayList<>();
|
||||
List<Integer> colors = new ArrayList<>();
|
||||
|
||||
entries.add(new Entry(totalSteps, 0));
|
||||
colors.add(akActivity.color);
|
||||
//we don't want labels on the pie chart
|
||||
data.addXValue("");
|
||||
|
||||
if (totalSteps < mTargetSteps) {
|
||||
entries.add(new Entry((mTargetSteps - totalSteps), 1));
|
||||
colors.add(Color.GRAY);
|
||||
//we don't want labels on the pie chart
|
||||
data.addXValue("");
|
||||
}
|
||||
|
||||
PieDataSet set = new PieDataSet(entries, "");
|
||||
set.setColors(colors);
|
||||
data.setDataSet(set);
|
||||
//this hides the values (numeric) added to the set. These would be shown aside the strings set with addXValue above
|
||||
data.setDrawValues(false);
|
||||
pieChart.setData(data);
|
||||
|
||||
pieChart.getLegend().setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
mLocale = getResources().getConfiguration().locale;
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false);
|
||||
|
||||
GBDevice device = ((ChartsHost) getHost()).getDevice();
|
||||
if (device != null) {
|
||||
// TODO: eek, this is device specific!
|
||||
mTargetSteps = MiBandCoordinator.getFitnessGoal(device.getAddress());
|
||||
}
|
||||
|
||||
mWeekStepsChart = (BarLineChartBase) rootView.findViewById(R.id.sleepchart);
|
||||
mTodayStepsChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep);
|
||||
|
||||
setupWeekStepsChart();
|
||||
setupTodayStepsChart();
|
||||
|
||||
// refresh immediately instead of use refreshIfVisible(), for perceived performance
|
||||
refresh();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
private void setupTodayStepsChart() {
|
||||
mTodayStepsChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mTodayStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
|
||||
mTodayStepsChart.setDescription(getContext().getString(R.string.weeksteps_today_steps_description, mTargetSteps));
|
||||
mTodayStepsChart.setNoDataTextDescription("");
|
||||
mTodayStepsChart.setNoDataText("");
|
||||
}
|
||||
|
||||
private void setupWeekStepsChart() {
|
||||
mWeekStepsChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mWeekStepsChart.setDescriptionColor(DESCRIPTION_COLOR);
|
||||
mWeekStepsChart.setDescription("");
|
||||
|
||||
configureBarLineChartDefaults(mWeekStepsChart);
|
||||
|
||||
XAxis x = mWeekStepsChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
|
||||
YAxis y = mWeekStepsChart.getAxisLeft();
|
||||
y.setDrawGridLines(false);
|
||||
y.setDrawTopYLabelEntry(false);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
y.setEnabled(true);
|
||||
|
||||
YAxis yAxisRight = mWeekStepsChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setEnabled(false);
|
||||
yAxisRight.setDrawLabels(false);
|
||||
yAxisRight.setDrawTopYLabelEntry(false);
|
||||
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
}
|
||||
|
||||
protected void setupLegend(Chart chart) {
|
||||
List<Integer> legendColors = new ArrayList<>(1);
|
||||
List<String> legendLabels = new ArrayList<>(1);
|
||||
legendColors.add(akActivity.color);
|
||||
legendLabels.add(getContext().getString(R.string.chart_steps));
|
||||
chart.getLegend().setCustom(legendColors, legendLabels);
|
||||
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
}
|
||||
|
||||
private List<ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, 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);
|
||||
startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
|
||||
day.set(Calendar.HOUR_OF_DAY, 23);
|
||||
day.set(Calendar.MINUTE, 59);
|
||||
day.set(Calendar.SECOND, 59);
|
||||
endTs = (int) (day.getTimeInMillis() / 1000);
|
||||
|
||||
return getSamples(db, device, startTs, endTs);
|
||||
@Override
|
||||
String getPieDescription(int targetValue) {
|
||||
return getString(R.string.weeksteps_today_steps_description, String.valueOf(targetValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return super.getAllSamples(db, device, tsFrom, tsTo);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
/* 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;
|
||||
|
@ -15,6 +31,9 @@ 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;
|
||||
|
@ -42,17 +61,7 @@ public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
|
|||
String name = formatDeviceCandidate(device);
|
||||
deviceNameLabel.setText(name);
|
||||
deviceAddressLabel.setText(device.getMacAddress());
|
||||
|
||||
switch (device.getDeviceType()) {
|
||||
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);
|
||||
}
|
||||
deviceImageView.setImageResource(device.getDeviceType().getIcon());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,37 @@
|
|||
/* 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.graphics.Color;
|
||||
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.ArrayAdapter;
|
||||
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;
|
||||
|
@ -20,25 +39,23 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms;
|
|||
import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
|
||||
|
||||
public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
|
||||
/**
|
||||
* Adapter for displaying GBAlarm instances.
|
||||
*/
|
||||
public class GBAlarmListAdapter extends RecyclerView.Adapter<GBAlarmListAdapter.ViewHolder> {
|
||||
|
||||
|
||||
private final Context mContext;
|
||||
private ArrayList<GBAlarm> alarmList;
|
||||
|
||||
public GBAlarmListAdapter(Context context, ArrayList<GBAlarm> alarmList) {
|
||||
super(context, 0, alarmList);
|
||||
private List<GBAlarm> alarmList;
|
||||
|
||||
public GBAlarmListAdapter(Context context, List<GBAlarm> alarmList) {
|
||||
this.mContext = context;
|
||||
this.alarmList = alarmList;
|
||||
}
|
||||
|
||||
public GBAlarmListAdapter(Context context, Set<String> preferencesAlarmListSet) {
|
||||
super(context, 0, new ArrayList<GBAlarm>());
|
||||
|
||||
this.mContext = context;
|
||||
alarmList = new ArrayList<GBAlarm>();
|
||||
alarmList = new ArrayList<>();
|
||||
|
||||
for (String alarmString : preferencesAlarmListSet) {
|
||||
alarmList.add(new GBAlarm(alarmString));
|
||||
|
@ -47,18 +64,21 @@ public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
|
|||
Collections.sort(alarmList);
|
||||
}
|
||||
|
||||
public void setAlarmList(Set<String> preferencesAlarmListSet) {
|
||||
alarmList = new ArrayList<GBAlarm>();
|
||||
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 alarmList;
|
||||
return (ArrayList) alarmList;
|
||||
}
|
||||
|
||||
|
||||
|
@ -72,53 +92,26 @@ public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (alarmList != null) {
|
||||
return alarmList.size();
|
||||
}
|
||||
return 0;
|
||||
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 GBAlarm getItem(int position) {
|
||||
if (alarmList != null) {
|
||||
return alarmList.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public void onBindViewHolder(ViewHolder holder, final int position) {
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (alarmList != null) {
|
||||
return alarmList.get(position).getIndex();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
final GBAlarm alarm = alarmList.get(position);
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
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));
|
||||
|
||||
final GBAlarm alarm = getItem(position);
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) mContext
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
view = inflater.inflate(R.layout.alarm_item, parent, false);
|
||||
}
|
||||
|
||||
TextView alarmTime = (TextView) view.findViewById(R.id.alarm_item_time);
|
||||
Switch isEnabled = (Switch) view.findViewById(R.id.alarm_item_toggle);
|
||||
TextView isSmartWakeup = (TextView) view.findViewById(R.id.alarm_smart_wakeup);
|
||||
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_sunday), alarm.getRepetition(Alarm.ALARM_SUN));
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_monday), alarm.getRepetition(Alarm.ALARM_MON));
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_tuesday), alarm.getRepetition(Alarm.ALARM_TUE));
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_wednesday), alarm.getRepetition(Alarm.ALARM_WED));
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_thursday), alarm.getRepetition(Alarm.ALARM_THU));
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_friday), alarm.getRepetition(Alarm.ALARM_FRI));
|
||||
highlightDay((TextView) view.findViewById(R.id.alarm_item_saturday), alarm.getRepetition(Alarm.ALARM_SAT));
|
||||
|
||||
isEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
holder.isEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
alarm.setEnabled(isChecked);
|
||||
|
@ -126,28 +119,62 @@ public class GBAlarmListAdapter extends ArrayAdapter<GBAlarm> {
|
|||
}
|
||||
});
|
||||
|
||||
view.setOnClickListener(new View.OnClickListener() {
|
||||
holder.container.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
((ConfigureAlarms) mContext).configureAlarm(alarm);
|
||||
}
|
||||
});
|
||||
alarmTime.setText(alarm.getTime());
|
||||
isEnabled.setChecked(alarm.isEnabled());
|
||||
holder.alarmTime.setText(alarm.getTime());
|
||||
holder.isEnabled.setChecked(alarm.isEnabled());
|
||||
if (alarm.isSmartWakeup()) {
|
||||
isSmartWakeup.setVisibility(TextView.VISIBLE);
|
||||
holder.isSmartWakeup.setVisibility(TextView.VISIBLE);
|
||||
} else {
|
||||
isSmartWakeup.setVisibility(TextView.GONE);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void highlightDay(TextView view, boolean isOn) {
|
||||
if (isOn) {
|
||||
view.setTextColor(Color.BLUE);
|
||||
} else {
|
||||
view.setTextColor(Color.BLACK);
|
||||
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,93 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
|
||||
public class GBDeviceAdapter extends ArrayAdapter<GBDevice> {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public GBDeviceAdapter(Context context, List<GBDevice> deviceList) {
|
||||
super(context, 0, deviceList);
|
||||
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@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);
|
||||
TextView deviceInfoLabel = (TextView) view.findViewById(R.id.device_info);
|
||||
TextView batteryStatusLabel = (TextView) view.findViewById(R.id.battery_status);
|
||||
ImageView deviceImageView = (ImageView) view.findViewById(R.id.device_image);
|
||||
ProgressBar busyIndicator = (ProgressBar) view.findViewById(R.id.device_busy_indicator);
|
||||
|
||||
deviceNameLabel.setText(device.getName());
|
||||
deviceInfoLabel.setText(device.getInfoString());
|
||||
|
||||
if (device.isBusy()) {
|
||||
deviceStatusLabel.setText(device.getBusyTask());
|
||||
busyIndicator.setVisibility(View.VISIBLE);
|
||||
batteryStatusLabel.setVisibility(View.GONE);
|
||||
deviceInfoLabel.setVisibility(View.GONE);
|
||||
} else {
|
||||
deviceStatusLabel.setText(device.getStateString());
|
||||
busyIndicator.setVisibility(View.GONE);
|
||||
batteryStatusLabel.setVisibility(View.VISIBLE);
|
||||
deviceInfoLabel.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
short batteryLevel = device.getBatteryLevel();
|
||||
if (batteryLevel != GBDevice.BATTERY_UNKNOWN) {
|
||||
batteryStatusLabel.setText("BAT: " + device.getBatteryLevel() + "%");
|
||||
BatteryState batteryState = device.getBatteryState();
|
||||
if (BatteryState.BATTERY_LOW.equals(batteryState)) {
|
||||
batteryStatusLabel.setTextColor(Color.RED);
|
||||
} else {
|
||||
batteryStatusLabel.setTextColor(ContextCompat.getColor(getContext(), R.color.secondarytext));
|
||||
|
||||
if (BatteryState.BATTERY_CHARGING.equals(batteryState) ||
|
||||
BatteryState.BATTERY_CHARGING_FULL.equals(batteryState)) {
|
||||
batteryStatusLabel.append(" CHG");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
batteryStatusLabel.setText("");
|
||||
}
|
||||
|
||||
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,55 +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.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> {
|
||||
|
||||
public GBDeviceAppAdapter(Context context, List<GBDeviceApp> appList) {
|
||||
super(context, 0, appList);
|
||||
private final int mLayoutId;
|
||||
private final List<GBDeviceApp> appList;
|
||||
private final AbstractAppManagerFragment mParentFragment;
|
||||
|
||||
this.context = context;
|
||||
public List<GBDeviceApp> getAppList() {
|
||||
return 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.item_with_details, parent, false);
|
||||
}
|
||||
TextView deviceAppVersionAuthorLabel = (TextView) view.findViewById(R.id.item_details);
|
||||
TextView deviceAppNameLabel = (TextView) view.findViewById(R.id.item_name);
|
||||
ImageView deviceImageView = (ImageView) view.findViewById(R.id.item_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);
|
||||
|
||||
deviceAppVersionAuthorLabel.setText(getContext().getString(R.string.appversion_by_creator, deviceApp.getVersion(), deviceApp.getCreator()));
|
||||
deviceAppNameLabel.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/* 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;
|
||||
|
@ -13,9 +29,17 @@ 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);
|
||||
|
@ -23,6 +47,10 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
|
|||
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);
|
||||
|
@ -31,7 +59,18 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
|
|||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
|
||||
view = inflater.inflate(R.layout.item_with_details, parent, false);
|
||||
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);
|
||||
|
@ -43,4 +82,12 @@ public class ItemWithDetailsAdapter extends ArrayAdapter<ItemWithDetails> {
|
|||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setSize(int size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/* 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.contentprovider;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class PebbleContentProvider extends ContentProvider {
|
||||
|
||||
public static final int COLUMN_CONNECTED = 0;
|
||||
public static final int COLUMN_APPMSG_SUPPORT = 1;
|
||||
public static final int COLUMN_DATALOGGING_SUPPORT = 2;
|
||||
public static final int COLUMN_VERSION_MAJOR = 3;
|
||||
public static final int COLUMN_VERSION_MINOR = 4;
|
||||
public static final int COLUMN_VERSION_POINT = 5;
|
||||
public static final int COLUMN_VERSION_TAG = 6;
|
||||
|
||||
// this is only needed for the MatrixCursor constructor
|
||||
public static final String[] columnNames = new String[]{"0", "1", "2", "3", "4", "5", "6"};
|
||||
|
||||
static final String PROVIDER_NAME = "com.getpebble.android.provider";
|
||||
static final String URL = "content://" + PROVIDER_NAME + "/state";
|
||||
static final Uri CONTENT_URI = Uri.parse(URL);
|
||||
|
||||
private GBDevice mGBDevice = null;
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(GBDevice.ACTION_DEVICE_CHANGED)) {
|
||||
mGBDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
LocalBroadcastManager.getInstance(this.getContext()).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
if (uri.equals(CONTENT_URI)) {
|
||||
MatrixCursor mc = new MatrixCursor(columnNames);
|
||||
int connected = 0;
|
||||
int pebbleKit = 0;
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
if (prefs.getBoolean("pebble_enable_pebblekit", false)) {
|
||||
pebbleKit = 1;
|
||||
}
|
||||
String fwString = "unknown";
|
||||
if (mGBDevice != null && mGBDevice.getType() == DeviceType.PEBBLE && mGBDevice.isInitialized()) {
|
||||
connected = 1;
|
||||
fwString = mGBDevice.getFirmwareVersion();
|
||||
}
|
||||
mc.addRow(new Object[]{connected, pebbleKit, pebbleKit, 3, 8, 2, fwString});
|
||||
|
||||
return mc;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.schema.ActivityDBCreationScript;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.DATABASE_NAME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
|
||||
|
||||
public class ActivityDatabaseHandler extends SQLiteOpenHelper implements DBHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ActivityDatabaseHandler.class);
|
||||
|
||||
private static final int DATABASE_VERSION = 5;
|
||||
|
||||
public ActivityDatabaseHandler(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
try {
|
||||
ActivityDBCreationScript script = new ActivityDBCreationScript();
|
||||
script.createSchema(db);
|
||||
} catch (RuntimeException ex) {
|
||||
GB.toast("Error creating database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
try {
|
||||
for (int i = oldVersion + 1; i <= newVersion; i++) {
|
||||
DBUpdateScript updater = getUpdateScript(db, i);
|
||||
if (updater != null) {
|
||||
LOG.info("upgrading activity database to version " + i);
|
||||
updater.upgradeSchema(db);
|
||||
}
|
||||
}
|
||||
LOG.info("activity database is now at version " + newVersion);
|
||||
} catch (RuntimeException ex) {
|
||||
GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
|
||||
throw ex; // reject upgrade
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
try {
|
||||
for (int i = oldVersion; i >= newVersion; i--) {
|
||||
DBUpdateScript updater = getUpdateScript(db, i);
|
||||
if (updater != null) {
|
||||
LOG.info("downgrading activity database to version " + (i - 1));
|
||||
updater.downgradeSchema(db);
|
||||
}
|
||||
}
|
||||
LOG.info("activity database is now at version " + newVersion);
|
||||
} catch (RuntimeException ex) {
|
||||
GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
|
||||
throw ex; // reject downgrade
|
||||
}
|
||||
}
|
||||
|
||||
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
|
||||
try {
|
||||
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + ".schema.ActivityDBUpdate_" + version);
|
||||
return (DBUpdateScript) updateClass.newInstance();
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addGBActivitySample(ActivitySample sample) {
|
||||
try (SQLiteDatabase db = this.getWritableDatabase()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_TIMESTAMP, sample.getTimestamp());
|
||||
values.put(KEY_PROVIDER, sample.getProvider().getID());
|
||||
values.put(KEY_INTENSITY, sample.getRawIntensity());
|
||||
values.put(KEY_STEPS, sample.getSteps());
|
||||
values.put(KEY_TYPE, sample.getRawKind());
|
||||
|
||||
db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the a new sample to the database
|
||||
*
|
||||
* @param timestamp the timestamp of the same, second-based!
|
||||
* @param provider the SampleProvider ID
|
||||
* @param intensity the sample's raw intensity value
|
||||
* @param steps the sample's steps value
|
||||
* @param kind the raw activity kind of the sample
|
||||
*/
|
||||
@Override
|
||||
public void addGBActivitySample(int timestamp, byte provider, short intensity, short steps, byte kind) {
|
||||
if (intensity < 0) {
|
||||
LOG.error("negative intensity received, ignoring");
|
||||
intensity = 0;
|
||||
}
|
||||
if (steps < 0) {
|
||||
LOG.error("negative steps received, ignoring");
|
||||
steps = 0;
|
||||
}
|
||||
|
||||
try (SQLiteDatabase db = this.getWritableDatabase()) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(KEY_TIMESTAMP, timestamp);
|
||||
values.put(KEY_PROVIDER, provider);
|
||||
values.put(KEY_INTENSITY, intensity);
|
||||
values.put(KEY_STEPS, steps);
|
||||
values.put(KEY_TYPE, kind);
|
||||
|
||||
db.insert(TABLE_GBACTIVITYSAMPLES, null, values);
|
||||
}
|
||||
}
|
||||
|
||||
public ArrayList<ActivitySample> getSleepSamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_SLEEP, provider);
|
||||
}
|
||||
|
||||
public ArrayList<ActivitySample> getActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ACTIVITY, provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SQLiteOpenHelper getHelper() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
GBApplication.releaseDB();
|
||||
}
|
||||
|
||||
public ArrayList<ActivitySample> getAllActivitySamples(int timestamp_from, int timestamp_to, SampleProvider provider) {
|
||||
return getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available activity samples from between the two timestamps (inclusive), of the given
|
||||
* provided and type(s).
|
||||
*
|
||||
* @param timestamp_from
|
||||
* @param timestamp_to
|
||||
* @param activityTypes ORed combination of #TYPE_DEEP_SLEEP, #TYPE_LIGHT_SLEEP, #TYPE_ACTIVITY
|
||||
* @param provider the producer of the samples to be sought
|
||||
* @return
|
||||
*/
|
||||
private ArrayList<ActivitySample> getGBActivitySamples(int timestamp_from, int timestamp_to, int activityTypes, SampleProvider provider) {
|
||||
if (timestamp_to < 0) {
|
||||
throw new IllegalArgumentException("negative timestamp_to");
|
||||
}
|
||||
if (timestamp_from < 0) {
|
||||
throw new IllegalArgumentException("negative timestamp_from");
|
||||
}
|
||||
ArrayList<ActivitySample> samples = new ArrayList<ActivitySample>();
|
||||
final String where = "(provider=" + provider.getID() + " and timestamp>=" + timestamp_from + " and timestamp<=" + timestamp_to + getWhereClauseFor(activityTypes, provider) + ")";
|
||||
LOG.info("Activity query where: " + where);
|
||||
final String order = "timestamp";
|
||||
try (SQLiteDatabase db = this.getReadableDatabase()) {
|
||||
try (Cursor cursor = db.query(TABLE_GBACTIVITYSAMPLES, null, where, null, null, null, order)) {
|
||||
LOG.info("Activity query result: " + cursor.getCount() + " samples");
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
GBActivitySample sample = new GBActivitySample(
|
||||
provider,
|
||||
cursor.getInt(cursor.getColumnIndex(KEY_TIMESTAMP)),
|
||||
cursor.getShort(cursor.getColumnIndex(KEY_INTENSITY)),
|
||||
cursor.getShort(cursor.getColumnIndex(KEY_STEPS)),
|
||||
(byte) cursor.getShort(cursor.getColumnIndex(KEY_TYPE)));
|
||||
samples.add(sample);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
private String getWhereClauseFor(int activityTypes, SampleProvider provider) {
|
||||
if (activityTypes == ActivityKind.TYPE_ALL) {
|
||||
return ""; // no further restriction
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder(" and (");
|
||||
byte[] dbActivityTypes = ActivityKind.mapToDBActivityTypes(activityTypes, provider);
|
||||
for (int i = 0; i < dbActivityTypes.length; i++) {
|
||||
builder.append(" type=").append(dbActivityTypes[i]);
|
||||
if (i + 1 < dbActivityTypes.length) {
|
||||
builder.append(" or ");
|
||||
}
|
||||
}
|
||||
builder.append(')');
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
/* 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.database;
|
||||
|
||||
import android.content.Context;
|
||||
|
@ -26,16 +42,10 @@ public abstract class DBAccess extends AsyncTask {
|
|||
|
||||
@Override
|
||||
protected Object doInBackground(Object[] params) {
|
||||
DBHandler handler = null;
|
||||
try {
|
||||
handler = GBApplication.acquireDB();
|
||||
doInBackground(handler);
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
doInBackground(db);
|
||||
} catch (Exception e) {
|
||||
mError = e;
|
||||
} finally {
|
||||
if (handler != null) {
|
||||
handler.release();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.database;
|
||||
|
||||
public class DBConstants {
|
||||
public static final String DATABASE_NAME = "ActivityDatabase";
|
||||
|
||||
public static final String TABLE_GBACTIVITYSAMPLES = "GBActivitySamples";
|
||||
public static final String TABLE_STEPS_PER_DAY = "StepsPerDay";
|
||||
|
||||
public static final String KEY_TIMESTAMP = "timestamp";
|
||||
public static final String KEY_PROVIDER = "provider";
|
||||
public static final String KEY_INTENSITY = "intensity";
|
||||
public static final String KEY_STEPS = "steps";
|
||||
public static final String KEY_TYPE = "type";
|
||||
}
|
|
@ -1,30 +1,53 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, JohnnySun
|
||||
|
||||
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.database;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import java.util.List;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
|
||||
public interface DBHandler {
|
||||
public SQLiteOpenHelper getHelper();
|
||||
/**
|
||||
* Provides lowlevel access to the database.
|
||||
*/
|
||||
public interface DBHandler extends AutoCloseable {
|
||||
/**
|
||||
* Closes the database.
|
||||
*/
|
||||
void closeDb();
|
||||
|
||||
/**
|
||||
* Releases the DB handler. No access may be performed after calling this method.
|
||||
* Same as calling {@link GBApplication#releaseDB()}
|
||||
* Opens the database. Note that this is only possible after an explicit
|
||||
* #closeDb(). Initially the db is implicitly open.
|
||||
*/
|
||||
void release();
|
||||
void openDb();
|
||||
|
||||
List<ActivitySample> getAllActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
|
||||
SQLiteOpenHelper getHelper();
|
||||
|
||||
List<ActivitySample> getActivitySamples(int tsFrom, int tsTo, SampleProvider provider);
|
||||
/**
|
||||
* Releases the DB handler. No DB access will be possible before
|
||||
* #openDb() will be called.
|
||||
*/
|
||||
void close() throws Exception;
|
||||
|
||||
List<ActivitySample> getSleepSamples(int tsFrom, int tsTo, SampleProvider provider);
|
||||
SQLiteDatabase getDatabase();
|
||||
|
||||
void addGBActivitySample(int timestamp, byte provider, short intensity, short steps, byte kind);
|
||||
|
||||
SQLiteDatabase getWritableDatabase();
|
||||
DaoMaster getDaoMaster();
|
||||
DaoSession getDaoSession();
|
||||
}
|
||||
|
|
|
@ -1,60 +1,135 @@
|
|||
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, JohnnySun
|
||||
|
||||
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.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.greenrobot.dao.Property;
|
||||
import de.greenrobot.dao.query.Query;
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import de.greenrobot.dao.query.WhereCondition;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescription;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.ActivityDescriptionDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Tag;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.TagDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.UserAttributes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.UserDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Provides utiliy access to some common entities, so you won't need to use
|
||||
* their DAO classes.
|
||||
* <p/>
|
||||
* Maybe this code should actually be in the DAO classes themselves, but then
|
||||
* these should be under revision control instead of 100% generated at build time.
|
||||
*/
|
||||
public class DBHelper {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DBHelper.class);
|
||||
|
||||
private final Context context;
|
||||
|
||||
public DBHelper(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private String getClosedDBPath(SQLiteOpenHelper dbHandler) throws IllegalStateException {
|
||||
SQLiteDatabase db = dbHandler.getReadableDatabase();
|
||||
/**
|
||||
* Closes the database and returns its name.
|
||||
* Important: after calling this, you have to DBHandler#openDb() it again
|
||||
* to get it back to work.
|
||||
*
|
||||
* @param dbHandler
|
||||
* @return
|
||||
* @throws IllegalStateException
|
||||
*/
|
||||
private String getClosedDBPath(DBHandler dbHandler) throws IllegalStateException {
|
||||
SQLiteDatabase db = dbHandler.getDatabase();
|
||||
String path = db.getPath();
|
||||
db.close();
|
||||
dbHandler.closeDb();
|
||||
if (db.isOpen()) { // reference counted, so may still be open
|
||||
throw new IllegalStateException("Database must be closed");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public File exportDB(SQLiteOpenHelper dbHandler, File toDir) throws IllegalStateException, IOException {
|
||||
public File exportDB(DBHandler dbHandler, File toDir) throws IllegalStateException, IOException {
|
||||
String dbPath = getClosedDBPath(dbHandler);
|
||||
File sourceFile = new File(dbPath);
|
||||
File destFile = new File(toDir, sourceFile.getName());
|
||||
if (destFile.exists()) {
|
||||
File backup = new File(toDir, destFile.getName() + "_" + getDate());
|
||||
destFile.renameTo(backup);
|
||||
} else if (!toDir.exists()) {
|
||||
if (!toDir.mkdirs()) {
|
||||
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
|
||||
try {
|
||||
File sourceFile = new File(dbPath);
|
||||
File destFile = new File(toDir, sourceFile.getName());
|
||||
if (destFile.exists()) {
|
||||
File backup = new File(toDir, destFile.getName() + "_" + getDate());
|
||||
destFile.renameTo(backup);
|
||||
} else if (!toDir.exists()) {
|
||||
if (!toDir.mkdirs()) {
|
||||
throw new IOException("Unable to create directory: " + toDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileUtils.copyFile(sourceFile, destFile);
|
||||
return destFile;
|
||||
FileUtils.copyFile(sourceFile, destFile);
|
||||
return destFile;
|
||||
} finally {
|
||||
dbHandler.openDb();
|
||||
}
|
||||
}
|
||||
|
||||
private String getDate() {
|
||||
return new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date());
|
||||
}
|
||||
|
||||
public void importDB(SQLiteOpenHelper dbHandler, File fromFile) throws IllegalStateException, IOException {
|
||||
public void importDB(DBHandler dbHandler, File fromFile) throws IllegalStateException, IOException {
|
||||
String dbPath = getClosedDBPath(dbHandler);
|
||||
File toFile = new File(dbPath);
|
||||
FileUtils.copyFile(fromFile, toFile);
|
||||
try {
|
||||
File toFile = new File(dbPath);
|
||||
FileUtils.copyFile(fromFile, toFile);
|
||||
} finally {
|
||||
dbHandler.openDb();
|
||||
}
|
||||
}
|
||||
|
||||
public void validateDB(SQLiteOpenHelper dbHandler) throws IOException {
|
||||
|
@ -70,16 +145,416 @@ public class DBHelper {
|
|||
db.execSQL(statement);
|
||||
}
|
||||
|
||||
public boolean existsDB(String dbName) {
|
||||
File path = context.getDatabasePath(dbName);
|
||||
return path != null && path.exists();
|
||||
}
|
||||
|
||||
public static boolean existsColumn(String tableName, String columnName, SQLiteDatabase db) {
|
||||
try (Cursor res = db.rawQuery("PRAGMA table_info('" + tableName + "')", null)) {
|
||||
int index = res.getColumnIndex("name");
|
||||
if (index < 1) {
|
||||
return false; // something's really wrong
|
||||
}
|
||||
while (res.moveToNext()) {
|
||||
String cn = res.getString(index);
|
||||
if (columnName.equals(cn)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* WITHOUT ROWID is only available with sqlite 3.8.2, which is available
|
||||
* with Lollipop and later.
|
||||
*
|
||||
* @return the "WITHOUT ROWID" string or an empty string for pre-Lollipop devices
|
||||
*/
|
||||
@NonNull
|
||||
public static String getWithoutRowId() {
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
return " WITHOUT ROWID;";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the user entity in the database. If a user exists already, it will
|
||||
* be updated with the current preferences values. If no user exists yet, it will
|
||||
* be created in the database.
|
||||
*
|
||||
* Note: so far there is only ever a single user; there is no multi-user support yet
|
||||
* @param session
|
||||
* @return the User entity
|
||||
*/
|
||||
@NonNull
|
||||
public static User getUser(DaoSession session) {
|
||||
ActivityUser prefsUser = new ActivityUser();
|
||||
UserDao userDao = session.getUserDao();
|
||||
User user;
|
||||
List<User> users = userDao.loadAll();
|
||||
if (users.isEmpty()) {
|
||||
user = createUser(prefsUser, session);
|
||||
} else {
|
||||
user = users.get(0); // TODO: multiple users support?
|
||||
ensureUserUpToDate(user, prefsUser, session);
|
||||
}
|
||||
ensureUserAttributes(user, prefsUser, session);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static UserAttributes getUserAttributes(User user) {
|
||||
List<UserAttributes> list = user.getUserAttributesList();
|
||||
if (list.isEmpty()) {
|
||||
throw new IllegalStateException("user has no attributes");
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static User createUser(ActivityUser prefsUser, DaoSession session) {
|
||||
User user = new User();
|
||||
ensureUserUpToDate(user, prefsUser, session);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private static void ensureUserUpToDate(User user, ActivityUser prefsUser, DaoSession session) {
|
||||
if (!isUserUpToDate(user, prefsUser)) {
|
||||
user.setName(prefsUser.getName());
|
||||
user.setBirthday(prefsUser.getUserBirthday());
|
||||
user.setGender(prefsUser.getGender());
|
||||
|
||||
if (user.getId() == null) {
|
||||
session.getUserDao().insert(user);
|
||||
} else {
|
||||
session.getUserDao().update(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isUserUpToDate(User user, ActivityUser prefsUser) {
|
||||
if (!Objects.equals(user.getName(), prefsUser.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(user.getBirthday(), prefsUser.getUserBirthday())) {
|
||||
return false;
|
||||
}
|
||||
if (user.getGender() != prefsUser.getGender()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ensureUserAttributes(User user, ActivityUser prefsUser, DaoSession session) {
|
||||
List<UserAttributes> userAttributes = user.getUserAttributesList();
|
||||
UserAttributes[] previousUserAttributes = new UserAttributes[1];
|
||||
if (hasUpToDateUserAttributes(userAttributes, prefsUser, previousUserAttributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Calendar now = DateTimeUtils.getCalendarUTC();
|
||||
invalidateUserAttributes(previousUserAttributes[0], now, session);
|
||||
|
||||
UserAttributes attributes = new UserAttributes();
|
||||
attributes.setValidFromUTC(now.getTime());
|
||||
attributes.setHeightCM(prefsUser.getHeightCm());
|
||||
attributes.setWeightKG(prefsUser.getWeightKg());
|
||||
attributes.setSleepGoalHPD(prefsUser.getSleepDuration());
|
||||
attributes.setStepsGoalSPD(prefsUser.getStepsGoal());
|
||||
attributes.setUserId(user.getId());
|
||||
session.getUserAttributesDao().insert(attributes);
|
||||
|
||||
// sort order is important, so we re-fetch from the db
|
||||
// userAttributes.add(attributes);
|
||||
user.resetUserAttributesList();
|
||||
}
|
||||
|
||||
private static void invalidateUserAttributes(UserAttributes userAttributes, Calendar now, DaoSession session) {
|
||||
if (userAttributes != null) {
|
||||
Calendar invalid = (Calendar) now.clone();
|
||||
invalid.add(Calendar.MINUTE, -1);
|
||||
userAttributes.setValidToUTC(invalid.getTime());
|
||||
session.getUserAttributesDao().update(userAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasUpToDateUserAttributes(List<UserAttributes> userAttributes, ActivityUser prefsUser, UserAttributes[] outPreviousUserAttributes) {
|
||||
for (UserAttributes attr : userAttributes) {
|
||||
if (!isValidNow(attr)) {
|
||||
continue;
|
||||
}
|
||||
if (isEqual(attr, prefsUser)) {
|
||||
return true;
|
||||
} else {
|
||||
outPreviousUserAttributes[0] = attr;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: move this into db queries?
|
||||
private static boolean isValidNow(ValidByDate element) {
|
||||
Calendar cal = DateTimeUtils.getCalendarUTC();
|
||||
Date nowUTC = cal.getTime();
|
||||
return isValid(element, nowUTC);
|
||||
}
|
||||
|
||||
private static boolean isValid(ValidByDate element, Date nowUTC) {
|
||||
Date validFromUTC = element.getValidFromUTC();
|
||||
Date validToUTC = element.getValidToUTC();
|
||||
if (nowUTC.before(validFromUTC)) {
|
||||
return false;
|
||||
}
|
||||
if (validToUTC != null && nowUTC.after(validToUTC)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isEqual(UserAttributes attr, ActivityUser prefsUser) {
|
||||
if (prefsUser.getHeightCm() != attr.getHeightCM()) {
|
||||
LOG.info("user height changed to " + prefsUser.getHeightCm() + " from " + attr.getHeightCM());
|
||||
return false;
|
||||
}
|
||||
if (prefsUser.getWeightKg() != attr.getWeightKG()) {
|
||||
LOG.info("user changed to " + prefsUser.getWeightKg() + " from " + attr.getWeightKG());
|
||||
return false;
|
||||
}
|
||||
if (!Integer.valueOf(prefsUser.getSleepDuration()).equals(attr.getSleepGoalHPD())) {
|
||||
LOG.info("user sleep goal changed to " + prefsUser.getSleepDuration() + " from " + attr.getSleepGoalHPD());
|
||||
return false;
|
||||
}
|
||||
if (!Integer.valueOf(prefsUser.getStepsGoal()).equals(attr.getStepsGoalSPD())) {
|
||||
LOG.info("user steps goal changed to " + prefsUser.getStepsGoal() + " from " + attr.getStepsGoalSPD());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isEqual(DeviceAttributes attr, GBDevice gbDevice) {
|
||||
if (!Objects.equals(attr.getFirmwareVersion1(), gbDevice.getFirmwareVersion())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(attr.getFirmwareVersion2(), gbDevice.getFirmwareVersion2())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(attr.getVolatileIdentifier(), gbDevice.getVolatileAddress())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Device findDevice(GBDevice gbDevice, DaoSession session) {
|
||||
DeviceDao deviceDao = session.getDeviceDao();
|
||||
Query<Device> query = deviceDao.queryBuilder().where(DeviceDao.Properties.Identifier.eq(gbDevice.getAddress())).build();
|
||||
List<Device> devices = query.list();
|
||||
if (devices.size() > 0) {
|
||||
return devices.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all active (that is, not old, archived ones) from the database.
|
||||
* (currently the active handling is not available)
|
||||
* @param daoSession
|
||||
*/
|
||||
public static List<Device> getActiveDevices(DaoSession daoSession) {
|
||||
return daoSession.getDeviceDao().loadAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up in the database the Device entity corresponding to the GBDevice. If a device
|
||||
* exists already, it will be updated with the current preferences values. If no device exists
|
||||
* yet, it will be created in the database.
|
||||
*
|
||||
* @param session
|
||||
* @return the device entity corresponding to the given GBDevice
|
||||
*/
|
||||
public static Device getDevice(GBDevice gbDevice, DaoSession session) {
|
||||
Device device = findDevice(gbDevice, session);
|
||||
if (device == null) {
|
||||
device = createDevice(gbDevice, session);
|
||||
} else {
|
||||
ensureDeviceUpToDate(device, gbDevice, session);
|
||||
}
|
||||
if (gbDevice.isInitialized()) {
|
||||
ensureDeviceAttributes(device, gbDevice, session);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static DeviceAttributes getDeviceAttributes(Device device) {
|
||||
List<DeviceAttributes> list = device.getDeviceAttributesList();
|
||||
if (list.isEmpty()) {
|
||||
throw new IllegalStateException("device has no attributes");
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
|
||||
private static void ensureDeviceUpToDate(Device device, GBDevice gbDevice, DaoSession session) {
|
||||
if (!isDeviceUpToDate(device, gbDevice)) {
|
||||
device.setIdentifier(gbDevice.getAddress());
|
||||
device.setName(gbDevice.getName());
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
device.setManufacturer(coordinator.getManufacturer());
|
||||
device.setType(gbDevice.getType().getKey());
|
||||
device.setModel(gbDevice.getModel());
|
||||
|
||||
if (device.getId() == null) {
|
||||
session.getDeviceDao().insert(device);
|
||||
} else {
|
||||
session.getDeviceDao().update(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDeviceUpToDate(Device device, GBDevice gbDevice) {
|
||||
if (!Objects.equals(device.getIdentifier(), gbDevice.getAddress())) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(device.getName(), gbDevice.getName())) {
|
||||
return false;
|
||||
}
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
if (!Objects.equals(device.getManufacturer(), coordinator.getManufacturer())) {
|
||||
return false;
|
||||
}
|
||||
if (device.getType() != gbDevice.getType().getKey()) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(device.getModel(), gbDevice.getModel())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Device createDevice(GBDevice gbDevice, DaoSession session) {
|
||||
Device device = new Device();
|
||||
ensureDeviceUpToDate(device, gbDevice, session);
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
private static void ensureDeviceAttributes(Device device, GBDevice gbDevice, DaoSession session) {
|
||||
List<DeviceAttributes> deviceAttributes = device.getDeviceAttributesList();
|
||||
DeviceAttributes[] previousDeviceAttributes = new DeviceAttributes[1];
|
||||
if (hasUpToDateDeviceAttributes(deviceAttributes, gbDevice, previousDeviceAttributes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Calendar now = DateTimeUtils.getCalendarUTC();
|
||||
invalidateDeviceAttributes(previousDeviceAttributes[0], now, session);
|
||||
|
||||
DeviceAttributes attributes = new DeviceAttributes();
|
||||
attributes.setDeviceId(device.getId());
|
||||
attributes.setValidFromUTC(now.getTime());
|
||||
attributes.setFirmwareVersion1(gbDevice.getFirmwareVersion());
|
||||
attributes.setFirmwareVersion2(gbDevice.getFirmwareVersion2());
|
||||
attributes.setVolatileIdentifier(gbDevice.getVolatileAddress());
|
||||
DeviceAttributesDao attributesDao = session.getDeviceAttributesDao();
|
||||
attributesDao.insert(attributes);
|
||||
|
||||
// sort order is important, so we re-fetch from the db
|
||||
// deviceAttributes.add(attributes);
|
||||
device.resetDeviceAttributesList();
|
||||
}
|
||||
|
||||
private static void invalidateDeviceAttributes(DeviceAttributes deviceAttributes, Calendar now, DaoSession session) {
|
||||
if (deviceAttributes != null) {
|
||||
Calendar invalid = (Calendar) now.clone();
|
||||
invalid.add(Calendar.MINUTE, -1);
|
||||
deviceAttributes.setValidToUTC(invalid.getTime());
|
||||
session.getDeviceAttributesDao().update(deviceAttributes);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasUpToDateDeviceAttributes(List<DeviceAttributes> deviceAttributes, GBDevice gbDevice, DeviceAttributes[] outPreviousAttributes) {
|
||||
for (DeviceAttributes attr : deviceAttributes) {
|
||||
if (!isValidNow(attr)) {
|
||||
continue;
|
||||
}
|
||||
if (isEqual(attr, gbDevice)) {
|
||||
return true;
|
||||
} else {
|
||||
outPreviousAttributes[0] = attr;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static List<ActivityDescription> findActivityDecriptions(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
|
||||
Property tsFromProperty = ActivityDescriptionDao.Properties.TimestampFrom;
|
||||
Property tsToProperty = ActivityDescriptionDao.Properties.TimestampTo;
|
||||
Property userIdProperty = ActivityDescriptionDao.Properties.UserId;
|
||||
QueryBuilder<ActivityDescription> qb = session.getActivityDescriptionDao().queryBuilder();
|
||||
qb.where(userIdProperty.eq(user.getId()), isAtLeastPartiallyInRange(qb, tsFromProperty, tsToProperty, tsFrom, tsTo));
|
||||
List<ActivityDescription> descriptions = qb.build().list();
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a condition that matches when the range of the entity (tsFromProperty..tsToProperty)
|
||||
* is completely or partially inside the range tsFrom..tsTo.
|
||||
* @param qb the query builder to use
|
||||
* @param tsFromProperty the property indicating the start of the entity's range
|
||||
* @param tsToProperty the property indicating the end of the entity's range
|
||||
* @param tsFrom the timestamp indicating the start of the range to match
|
||||
* @param tsTo the timestamp indicating the end of the range to match
|
||||
* @param <T> the query builder's type parameter
|
||||
* @return the range WhereCondition
|
||||
*/
|
||||
private static <T> WhereCondition isAtLeastPartiallyInRange(QueryBuilder<T> qb, Property tsFromProperty, Property tsToProperty, int tsFrom, int tsTo) {
|
||||
return qb.and(tsFromProperty.lt(tsTo), tsToProperty.gt(tsFrom));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ActivityDescription createActivityDescription(@NonNull User user, int tsFrom, int tsTo, @NonNull DaoSession session) {
|
||||
ActivityDescription desc = new ActivityDescription();
|
||||
desc.setUser(user);
|
||||
desc.setTimestampFrom(tsFrom);
|
||||
desc.setTimestampTo(tsTo);
|
||||
session.getActivityDescriptionDao().insertOrReplace(desc);
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Tag getTag(@NonNull User user, @NonNull String name, @NonNull DaoSession session) {
|
||||
TagDao tagDao = session.getTagDao();
|
||||
QueryBuilder<Tag> qb = tagDao.queryBuilder();
|
||||
Query<Tag> query = qb.where(TagDao.Properties.UserId.eq(user.getId()), TagDao.Properties.Name.eq(name)).build();
|
||||
List<Tag> tags = query.list();
|
||||
if (tags.size() > 0) {
|
||||
return tags.get(0);
|
||||
}
|
||||
return createTag(user, name, null, session);
|
||||
}
|
||||
|
||||
static Tag createTag(@NonNull User user, @NonNull String name, @Nullable String description, @NonNull DaoSession session) {
|
||||
Tag tag = new Tag();
|
||||
tag.setUserId(user.getId());
|
||||
tag.setName(name);
|
||||
tag.setDescription(description);
|
||||
session.getTagDao().insertOrReplace(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static void clearSession() {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
session.clear();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Unable to acquire database to clear the session", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/* 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.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.schema.SchemaMigration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
|
||||
public class DBOpenHelper extends DaoMaster.OpenHelper {
|
||||
private final String updaterClassNamePrefix;
|
||||
private final Context context;
|
||||
|
||||
public DBOpenHelper(Context context, String dbName, SQLiteDatabase.CursorFactory factory) {
|
||||
super(context, dbName, factory);
|
||||
updaterClassNamePrefix = dbName + "Update_";
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
DaoMaster.createAllTables(db, true);
|
||||
new SchemaMigration(updaterClassNamePrefix).onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
DaoMaster.createAllTables(db, true);
|
||||
new SchemaMigration(updaterClassNamePrefix).onDowngrade(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return context;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
/* 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.database;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/* 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.database;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_INTENSITY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TYPE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
|
||||
|
||||
public class ActivityDBCreationScript {
|
||||
public void createSchema(SQLiteDatabase db) {
|
||||
String CREATE_GBACTIVITYSAMPLES_TABLE = "CREATE TABLE " + TABLE_GBACTIVITYSAMPLES + " ("
|
||||
+ KEY_TIMESTAMP + " INT,"
|
||||
+ KEY_PROVIDER + " TINYINT,"
|
||||
+ KEY_INTENSITY + " SMALLINT,"
|
||||
+ KEY_STEPS + " TINYINT,"
|
||||
+ KEY_TYPE + " TINYINT,"
|
||||
+ " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId();
|
||||
db.execSQL(CREATE_GBACTIVITYSAMPLES_TABLE);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_GBACTIVITYSAMPLES;
|
||||
|
||||
/**
|
||||
* Upgrade and downgrade with DB versions <= 5 is not supported.
|
||||
* Just recreates the default schema. Those GB versions may or may not
|
||||
* work with that, but this code will probably not create a DB for them
|
||||
* anyway.
|
||||
*/
|
||||
public class ActivityDBUpdate_4 extends ActivityDBCreationScript implements DBUpdateScript {
|
||||
@Override
|
||||
public void upgradeSchema(SQLiteDatabase db) {
|
||||
recreateSchema(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downgradeSchema(SQLiteDatabase db) {
|
||||
recreateSchema(db);
|
||||
}
|
||||
|
||||
private void recreateSchema(SQLiteDatabase db) {
|
||||
DBHelper.dropTable(TABLE_GBACTIVITYSAMPLES, db);
|
||||
createSchema(db);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package nodomain.freeyourgadget.gadgetbridge.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_PROVIDER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_STEPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.KEY_TIMESTAMP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.database.DBConstants.TABLE_STEPS_PER_DAY;
|
||||
|
||||
/**
|
||||
* Adds a table "STEPS_PER_DAY".
|
||||
*/
|
||||
public class ActivityDBUpdate_6 implements DBUpdateScript {
|
||||
@Override
|
||||
public void upgradeSchema(SQLiteDatabase db) {
|
||||
String CREATE_STEPS_PER_DAY_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_STEPS_PER_DAY + " ("
|
||||
+ KEY_TIMESTAMP + " INT,"
|
||||
+ KEY_PROVIDER + " TINYINT,"
|
||||
+ KEY_STEPS + " MEDIUMINT,"
|
||||
+ " PRIMARY KEY (" + KEY_TIMESTAMP + "," + KEY_PROVIDER + ") ON CONFLICT REPLACE)" + DBHelper.getWithoutRowId();
|
||||
db.execSQL(CREATE_STEPS_PER_DAY_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downgradeSchema(SQLiteDatabase db) {
|
||||
DBHelper.dropTable(TABLE_STEPS_PER_DAY, db);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/* 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.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleHealthActivitySampleDao;
|
||||
|
||||
/*
|
||||
* adds heart rate column to health table
|
||||
*/
|
||||
|
||||
public class GadgetbridgeUpdate_14 implements DBUpdateScript {
|
||||
@Override
|
||||
public void upgradeSchema(SQLiteDatabase db) {
|
||||
if (!DBHelper.existsColumn(PebbleHealthActivitySampleDao.TABLENAME, PebbleHealthActivitySampleDao.Properties.HeartRate.columnName, db)) {
|
||||
String ADD_COLUMN_HEART_RATE = "ALTER TABLE " + PebbleHealthActivitySampleDao.TABLENAME + " ADD COLUMN "
|
||||
+ PebbleHealthActivitySampleDao.Properties.HeartRate.columnName + " INTEGER NOT NULL DEFAULT 0;";
|
||||
db.execSQL(ADD_COLUMN_HEART_RATE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downgradeSchema(SQLiteDatabase db) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/* 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.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
||||
|
||||
/*
|
||||
* adds heart rate column to health table
|
||||
*/
|
||||
|
||||
public class GadgetbridgeUpdate_15 implements DBUpdateScript {
|
||||
@Override
|
||||
public void upgradeSchema(SQLiteDatabase db) {
|
||||
if (!DBHelper.existsColumn(DeviceAttributesDao.TABLENAME, DeviceAttributesDao.Properties.VolatileIdentifier.columnName, db)) {
|
||||
String ADD_COLUMN_VOLATILE_IDENTIFIER = "ALTER TABLE " + DeviceAttributesDao.TABLENAME + " ADD COLUMN "
|
||||
+ DeviceAttributesDao.Properties.VolatileIdentifier.columnName + " TEXT;";
|
||||
db.execSQL(ADD_COLUMN_VOLATILE_IDENTIFIER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downgradeSchema(SQLiteDatabase db) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* Copyright (C) 2017 protomors
|
||||
|
||||
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.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.No1F1ActivitySampleDao;
|
||||
|
||||
public class GadgetbridgeUpdate_17 implements DBUpdateScript {
|
||||
@Override
|
||||
public void upgradeSchema(SQLiteDatabase db) {
|
||||
if (!DBHelper.existsColumn(No1F1ActivitySampleDao.TABLENAME, No1F1ActivitySampleDao.Properties.HeartRate.columnName, db)) {
|
||||
String ADD_COLUMN_HEART_RATE = "ALTER TABLE " + No1F1ActivitySampleDao.TABLENAME + " ADD COLUMN "
|
||||
+ No1F1ActivitySampleDao.Properties.HeartRate.columnName + " INTEGER NOT NULL DEFAULT 0;";
|
||||
db.execSQL(ADD_COLUMN_HEART_RATE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downgradeSchema(SQLiteDatabase db) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/* 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.database.schema;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class SchemaMigration {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SchemaMigration.class);
|
||||
private final String classNamePrefix;
|
||||
|
||||
public SchemaMigration(String updaterClassNamePrefix) {
|
||||
classNamePrefix = updaterClassNamePrefix;
|
||||
}
|
||||
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
LOG.info("ActivityDatabase: schema upgrade requested from " + oldVersion + " to " + newVersion);
|
||||
try {
|
||||
for (int i = oldVersion + 1; i <= newVersion; i++) {
|
||||
DBUpdateScript updater = getUpdateScript(db, i);
|
||||
if (updater != null) {
|
||||
LOG.info("upgrading activity database to version " + i);
|
||||
updater.upgradeSchema(db);
|
||||
}
|
||||
}
|
||||
LOG.info("activity database is now at version " + newVersion);
|
||||
} catch (RuntimeException ex) {
|
||||
GB.toast("Error upgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
|
||||
throw ex; // reject upgrade
|
||||
}
|
||||
}
|
||||
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
LOG.info("ActivityDatabase: schema downgrade requested from " + oldVersion + " to " + newVersion);
|
||||
try {
|
||||
for (int i = oldVersion; i >= newVersion; i--) {
|
||||
DBUpdateScript updater = getUpdateScript(db, i);
|
||||
if (updater != null) {
|
||||
LOG.info("downgrading activity database to version " + (i - 1));
|
||||
updater.downgradeSchema(db);
|
||||
}
|
||||
}
|
||||
LOG.info("activity database is now at version " + newVersion);
|
||||
} catch (RuntimeException ex) {
|
||||
GB.toast("Error downgrading database.", Toast.LENGTH_SHORT, GB.ERROR, ex);
|
||||
throw ex; // reject downgrade
|
||||
}
|
||||
}
|
||||
|
||||
private DBUpdateScript getUpdateScript(SQLiteDatabase db, int version) {
|
||||
try {
|
||||
Class<?> updateClass = getClass().getClassLoader().loadClass(getClass().getPackage().getName() + "." + classNamePrefix + version);
|
||||
return (DBUpdateScript) updateClass.newInstance();
|
||||
} catch (ClassNotFoundException e) {
|
||||
return null;
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException("Error instantiating DBUpdate class for version " + version, e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
/* Copyright (C) 2015-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.deviceevents;
|
||||
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue