mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-02-16 18:40:46 +00:00
Compare commits
728 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99b53f9a6a | ||
|
|
8da563b280 | ||
|
|
d5b950e5cf | ||
|
|
c46b9acf6b | ||
|
|
d8e322bac9 | ||
|
|
44871626cf | ||
|
|
f661522b5d | ||
|
|
a454b0ab78 | ||
|
|
75bf84d0d9 | ||
|
|
c248994ed4 | ||
|
|
a7a34ec629 | ||
|
|
8d469c5d0a | ||
|
|
e6979d50b5 | ||
|
|
6e25b135a3 | ||
|
|
04e093a2c2 | ||
|
|
813f2edd95 | ||
|
|
337d753a33 | ||
|
|
1137c74f76 | ||
|
|
0c1451f757 | ||
|
|
5ab9ea48fd | ||
|
|
ffcb623040 | ||
|
|
bfe6929642 | ||
|
|
50d45011a8 | ||
|
|
2f7087d6d3 | ||
|
|
92b71588d0 | ||
|
|
4f3d018764 | ||
|
|
a22e33eeb9 | ||
|
|
6a939e7495 | ||
|
|
f8ba7cf190 | ||
|
|
d1e135db4d | ||
|
|
61a17afe69 | ||
|
|
47fd691884 | ||
|
|
0d171c6b28 | ||
|
|
f0c69d08b8 | ||
|
|
629bf5766d | ||
|
|
233bceeece | ||
|
|
6660ea7d91 | ||
|
|
4864b2ca45 | ||
|
|
92097b318d | ||
|
|
997898c99d | ||
|
|
1174e03885 | ||
|
|
ff0f54d541 | ||
|
|
814964a100 | ||
|
|
7e154292a9 | ||
|
|
0f9cba1053 | ||
|
|
a4e134589d | ||
|
|
cd80a94f28 | ||
|
|
57c645a291 | ||
|
|
0cba200207 | ||
|
|
81582d7343 | ||
|
|
04e561fd54 | ||
|
|
5efbb5229d | ||
|
|
541e43eb18 | ||
|
|
7e679ff4c6 | ||
|
|
486b4b4c4c | ||
|
|
7d76bf7868 | ||
|
|
db49077b9b | ||
|
|
16b845ab84 | ||
|
|
5c175fecf6 | ||
|
|
773976b265 | ||
|
|
80070bbdbe | ||
|
|
4c8d433b6c | ||
|
|
404f096d11 | ||
|
|
d2ac927cec | ||
|
|
5e3d59d3d7 | ||
|
|
9cd2ce1309 | ||
|
|
9ed49730d4 | ||
|
|
39ebb48f58 | ||
|
|
1c29c70fba | ||
|
|
6993051529 | ||
|
|
4930087c4d | ||
|
|
795f0a013b | ||
|
|
213414778e | ||
|
|
7eac0ccaf8 | ||
|
|
6adc9dcb2d | ||
|
|
be620908f9 | ||
|
|
e4edfdb043 | ||
|
|
3b5028d1a4 | ||
|
|
bc8c45bd59 | ||
|
|
63eb346a70 | ||
|
|
27ad691d23 | ||
|
|
747e920061 | ||
|
|
8d09f56a0e | ||
|
|
113a0e2c45 | ||
|
|
977215a098 | ||
|
|
a7e65b47f9 | ||
|
|
7126055ad6 | ||
|
|
3de9765eaa | ||
|
|
d4072eb295 | ||
|
|
cac2bdbb81 | ||
|
|
66f0aee3f8 | ||
|
|
b690dc5474 | ||
|
|
c2fbe6ad91 | ||
|
|
cf07c02398 | ||
|
|
42dc928ad5 | ||
|
|
11597f0aa7 | ||
|
|
cdcd4d48f2 | ||
|
|
a9af4e54a9 | ||
|
|
7eac609219 | ||
|
|
fa761debc4 | ||
|
|
62e175f069 | ||
|
|
d7d8c40565 | ||
|
|
64de13ab50 | ||
|
|
2f02939638 | ||
|
|
1d7c8697e9 | ||
|
|
7dea322bbd | ||
|
|
349ecb16ab | ||
|
|
a3867735c1 | ||
|
|
5b087e9f70 | ||
|
|
eed18223eb | ||
|
|
30d4d2a918 | ||
|
|
30f666c70e | ||
|
|
209fead0e8 | ||
|
|
5c6889bf6d | ||
|
|
7d24900756 | ||
|
|
79a75b9d19 | ||
|
|
29b64992bd | ||
|
|
c9b14540f2 | ||
|
|
546843a26c | ||
|
|
d03d260535 | ||
|
|
6946e3c7a2 | ||
|
|
b79d328961 | ||
|
|
c313797d93 | ||
|
|
c8cb8e1346 | ||
|
|
6a9f8da14e | ||
|
|
ff9260a0fd | ||
|
|
62bedb1609 | ||
|
|
a519723d44 | ||
|
|
36191781ed | ||
|
|
61b6a49669 | ||
|
|
e97845e46e | ||
|
|
6bba68207d | ||
|
|
0e17cccc06 | ||
|
|
918e922e40 | ||
|
|
a08854ddfd | ||
|
|
eb6f15c2b7 | ||
|
|
2cd9e31684 | ||
|
|
791d6624e2 | ||
|
|
af41021271 | ||
|
|
d726d939f4 | ||
|
|
748085e7bb | ||
|
|
d57d19174b | ||
|
|
efebe1828a | ||
|
|
06007e0597 | ||
|
|
3a868045d7 | ||
|
|
e0a7ff1880 | ||
|
|
88d43bbd40 | ||
|
|
30ff319b13 | ||
|
|
9a0f48b799 | ||
|
|
b52c8a1a8f | ||
|
|
3fde115670 | ||
|
|
b6f4d8ff1e | ||
|
|
a7d85a7dd5 | ||
|
|
9b238ab6c3 | ||
|
|
f82ee97c05 | ||
|
|
35fb96f9f4 | ||
|
|
37371906d5 | ||
|
|
83a9539f4b | ||
|
|
b214fe5301 | ||
|
|
57779b4e89 | ||
|
|
547932f8b2 | ||
|
|
762fa0fe2f | ||
|
|
9cedc57df2 | ||
|
|
ba81f8096a | ||
|
|
c4fa654166 | ||
|
|
8ac440b68b | ||
|
|
165386b941 | ||
|
|
3a7398f321 | ||
|
|
ebb1d0dfa2 | ||
|
|
1ca1ed5d20 | ||
|
|
b416bafb78 | ||
|
|
3a301b74a6 | ||
|
|
71d463f063 | ||
|
|
1fae816223 | ||
|
|
989d6fc169 | ||
|
|
381509b3a6 | ||
|
|
d8ae40376e | ||
|
|
4ea93f5e68 | ||
|
|
cd84c8f30e | ||
|
|
8d4cdca7c3 | ||
|
|
c0239c36fd | ||
|
|
9d9f729e42 | ||
|
|
6c5fe18b6e | ||
|
|
1994bf6522 | ||
|
|
31381e5664 | ||
|
|
fac1b1d7e5 | ||
|
|
40c406051c | ||
|
|
8bac873e67 | ||
|
|
a170e1efd7 | ||
|
|
17bffa8d78 | ||
|
|
289222749b | ||
|
|
81d84600d4 | ||
|
|
0b15fd582d | ||
|
|
cbe4a1cde6 | ||
|
|
89ef16c02e | ||
|
|
58b6ed8d00 | ||
|
|
7d01e1a7a4 | ||
|
|
ab769a1606 | ||
|
|
3ac9abbab1 | ||
|
|
288efd0726 | ||
|
|
d2d0ed65d6 | ||
|
|
e697ed72db | ||
|
|
b657c746be | ||
|
|
947f8db2d5 | ||
|
|
15857efd36 | ||
|
|
3fd0f20e10 | ||
|
|
a2e64fd7df | ||
|
|
a620dc7d0c | ||
|
|
9d7a28e408 | ||
|
|
3244344fc7 | ||
|
|
75057f2d39 | ||
|
|
bbec3402d9 | ||
|
|
dcf4dac8dd | ||
|
|
d98f484aaf | ||
|
|
0218a9ce14 | ||
|
|
0ec6dcd67e | ||
|
|
88f9b68db7 | ||
|
|
3c2fd32d1e | ||
|
|
6557cba307 | ||
|
|
ae6f797436 | ||
|
|
3442a64f4d | ||
|
|
37ddccde0c | ||
|
|
ffc59c6bd6 | ||
|
|
88f84a0c12 | ||
|
|
03ecf3e5ac | ||
|
|
617c8582b4 | ||
|
|
ef3b28295b | ||
|
|
3bcd2ee068 | ||
|
|
d4ff58b3ad | ||
|
|
c797318ece | ||
|
|
82387d23f8 | ||
|
|
766e629be5 | ||
|
|
b93aa42c0c | ||
|
|
36f132942f | ||
|
|
e4c251e7ee | ||
|
|
fb54bd5c78 | ||
|
|
8d4c86e113 | ||
|
|
7fafb8e0ff | ||
|
|
fbcbe09255 | ||
|
|
e336a4446a | ||
|
|
ffb35b2cdd | ||
|
|
2d0af6281c | ||
|
|
472a7f6c8a | ||
|
|
cd06559c66 | ||
|
|
d833933aaa | ||
|
|
dc3495d59b | ||
|
|
e3a2e40043 | ||
|
|
31e1fb743e | ||
|
|
bc59f11096 | ||
|
|
6d97775aa9 | ||
|
|
3fff34e08a | ||
|
|
15e856dccb | ||
|
|
07d04171c3 | ||
|
|
42bd93cb3a | ||
|
|
7d289f1134 | ||
|
|
214461e123 | ||
|
|
b0144a3256 | ||
|
|
3171256c6e | ||
|
|
5c69f6716c | ||
|
|
6264781539 | ||
|
|
0225f534d0 | ||
|
|
284a31737e | ||
|
|
b37a2dea57 | ||
|
|
5c865e7f36 | ||
|
|
04d9aea8c8 | ||
|
|
b6f52db9c3 | ||
|
|
99d2e40683 | ||
|
|
02c4ed2724 | ||
|
|
5f4aab8f94 | ||
|
|
ec65901003 | ||
|
|
915acee88d | ||
|
|
300d444f71 | ||
|
|
f37ab40c2f | ||
|
|
16e285d926 | ||
|
|
f2d122a275 | ||
|
|
bfa5a6349e | ||
|
|
a56689aea3 | ||
|
|
3a5ba820cb | ||
|
|
ec69fef36f | ||
|
|
ff38074f55 | ||
|
|
85d0ce0c40 | ||
|
|
777129ca90 | ||
|
|
06156c4d68 | ||
|
|
1c725b9dac | ||
|
|
f761ee52db | ||
|
|
05e8cfcc0a | ||
|
|
912925ef2c | ||
|
|
4deb881ec8 | ||
|
|
f55d6308ce | ||
|
|
44a3a141c0 | ||
|
|
37b5ba004c | ||
|
|
b774b47213 | ||
|
|
74dc00445e | ||
|
|
3b4563d5ea | ||
|
|
38669817b4 | ||
|
|
8f1d3ae04e | ||
|
|
74ed95871b | ||
|
|
cc5d67616c | ||
|
|
eed7f09e6f | ||
|
|
e3c1d23744 | ||
|
|
c4b1200b43 | ||
|
|
dff09f33a3 | ||
|
|
1f6b1dc2fe | ||
|
|
3f118dae93 | ||
|
|
91a30ff6fe | ||
|
|
5102669b06 | ||
|
|
2e2f09be00 | ||
|
|
c402103fe3 | ||
|
|
5e5df8abc8 | ||
|
|
d125eb7b16 | ||
|
|
a116858493 | ||
|
|
5f3b333e98 | ||
|
|
80a37855c7 | ||
|
|
5db1ec8ec0 | ||
|
|
8911c58e50 | ||
|
|
780a64694d | ||
|
|
3c5ea9c8c3 | ||
|
|
40d1436ce3 | ||
|
|
dbb02acd37 | ||
|
|
20c4eac4ef | ||
|
|
b9f1142af7 | ||
|
|
38a6a2b74a | ||
|
|
fd2421618a | ||
|
|
79a9ea7179 | ||
|
|
34a11c9262 | ||
|
|
84a9845c1d | ||
|
|
5b05220008 | ||
|
|
b2bd7257e1 | ||
|
|
46a998c113 | ||
|
|
60cd951774 | ||
|
|
d4f8d8f689 | ||
|
|
608a0ebb5b | ||
|
|
f01a15d182 | ||
|
|
0268b4f958 | ||
|
|
d71cf0eb98 | ||
|
|
10ab40f823 | ||
|
|
427edfa021 | ||
|
|
6f18831d5c | ||
|
|
a3db09f422 | ||
|
|
d185a05b1d | ||
|
|
78e575504a | ||
|
|
0a0be19b69 | ||
|
|
0792157e9d | ||
|
|
cdd0ecf0b7 | ||
|
|
1ac721a35b | ||
|
|
e49b1c92a2 | ||
|
|
db4295bf83 | ||
|
|
824c37f9d5 | ||
|
|
acf4426952 | ||
|
|
e8c50342ab | ||
|
|
598995de3b | ||
|
|
01cf0cc649 | ||
|
|
fa560f462f | ||
|
|
f6e40118a9 | ||
|
|
fe7148dbd4 | ||
|
|
60de065836 | ||
|
|
6f82f82abb | ||
|
|
42f18cb4ac | ||
|
|
1bbd0054c2 | ||
|
|
acdde37a3a | ||
|
|
ad40e12167 | ||
|
|
1b3322b5ee | ||
|
|
6340ec6c6d | ||
|
|
babd92c8c0 | ||
|
|
7f1fe5f520 | ||
|
|
01458770d2 | ||
|
|
8d05f044f5 | ||
|
|
f5680b59a5 | ||
|
|
0ecf86c7ed | ||
|
|
6789e8d497 | ||
|
|
7d0160d556 | ||
|
|
f6a0990432 | ||
|
|
5d6094df97 | ||
|
|
d98d4aeda2 | ||
|
|
852dcf5a2d | ||
|
|
82e5aa122d | ||
|
|
fe237d1da3 | ||
|
|
e199fcd2d9 | ||
|
|
d7c6f63592 | ||
|
|
4b9c6b149a | ||
|
|
d1e41e41a1 | ||
|
|
96dfe25a14 | ||
|
|
f76d78607a | ||
|
|
a96f688bb2 | ||
|
|
90a1e68c68 | ||
|
|
b287606106 | ||
|
|
a413185085 | ||
|
|
aa1b283570 | ||
|
|
f07c886711 | ||
|
|
e66b1ebec9 | ||
|
|
d06912e81a | ||
|
|
08bcd97594 | ||
|
|
49e51f5f6f | ||
|
|
4223a7fd30 | ||
|
|
6edd0ab540 | ||
|
|
ce7146175a | ||
|
|
3176a85f35 | ||
|
|
ad1c11bba5 | ||
|
|
ac640a6842 | ||
|
|
8962497a8c | ||
|
|
83141d3f91 | ||
|
|
55f2e89bbe | ||
|
|
3558655b72 | ||
|
|
44cbf8adc1 | ||
|
|
686490ba70 | ||
|
|
d0ecde1e16 | ||
|
|
9417908848 | ||
|
|
93b0073467 | ||
|
|
1434be262c | ||
|
|
75aabd6471 | ||
|
|
bafa2addd3 | ||
|
|
32b787e77c | ||
|
|
43b58b7a5e | ||
|
|
9ae1fe2696 | ||
|
|
6d0f34e2c4 | ||
|
|
f7d91b5107 | ||
|
|
a3c95480d8 | ||
|
|
732311c2a4 | ||
|
|
043c9a978e | ||
|
|
36b248be4b | ||
|
|
8e247ad9a6 | ||
|
|
a2de98c91a | ||
|
|
81d1e615bf | ||
|
|
244fae07ab | ||
|
|
e7d96f0ac2 | ||
|
|
4555b3c74c | ||
|
|
8c13186757 | ||
|
|
feafc4ef3c | ||
|
|
5c03295478 | ||
|
|
dc3a923041 | ||
|
|
eccba807bc | ||
|
|
35fa8f5bcc | ||
|
|
0380910588 | ||
|
|
e85bb4372e | ||
|
|
2c345cd6c2 | ||
|
|
b5c96cbb53 | ||
|
|
b21ee5ca31 | ||
|
|
9c7bff6c75 | ||
|
|
3d470d9aed | ||
|
|
b2a36c2c73 | ||
|
|
7978687bfc | ||
|
|
f612ec80e2 | ||
|
|
7df1a39fcb | ||
|
|
a539ac62ec | ||
|
|
fa52e5edc2 | ||
|
|
3ca681f050 | ||
|
|
8086c3d46b | ||
|
|
928fca843f | ||
|
|
25d74785d0 | ||
|
|
e12a8e7946 | ||
|
|
b14f2ce219 | ||
|
|
d31be3d64e | ||
|
|
0704f2aaf6 | ||
|
|
832e52ac74 | ||
|
|
f5444551b2 | ||
|
|
3143797b55 | ||
|
|
cc9b1aeaab | ||
|
|
3d177e97e4 | ||
|
|
6c3aaedc83 | ||
|
|
bf84ebef6d | ||
|
|
8991b29329 | ||
|
|
fa84575be5 | ||
|
|
0432d5725b | ||
|
|
8e7b144339 | ||
|
|
fc629db653 | ||
|
|
d5863e1bef | ||
|
|
c2c3a6b37c | ||
|
|
e701699dea | ||
|
|
17179bd027 | ||
|
|
b2f210700d | ||
|
|
f0e85c4c53 | ||
|
|
92f8425ace | ||
|
|
6ad001e8be | ||
|
|
b6e4d5528b | ||
|
|
0f0b83badc | ||
|
|
453fbb5f58 | ||
|
|
e7dc3a4c11 | ||
|
|
d68b2382cf | ||
|
|
1b5330323c | ||
|
|
8aba4888e1 | ||
|
|
1c3b9a3859 | ||
|
|
e8f04f5a3b | ||
|
|
56b814e877 | ||
|
|
628ccd39d6 | ||
|
|
59db3f9b62 | ||
|
|
416f922b56 | ||
|
|
b52a86e6cc | ||
|
|
e523b5069e | ||
|
|
e8ae8d9807 | ||
|
|
64e56a861d | ||
|
|
c1bcd09c9b | ||
|
|
574258804f | ||
|
|
21ea3d8a2b | ||
|
|
6de4288a85 | ||
|
|
a107b5e652 | ||
|
|
b02db2c182 | ||
|
|
f8a04cda7a | ||
|
|
226e8edefc | ||
|
|
9b90b30a1f | ||
|
|
2ed245b25a | ||
|
|
4b769839d0 | ||
|
|
239dd1d5a1 | ||
|
|
37509cce9b | ||
|
|
227c71549b | ||
|
|
a10d8334f3 | ||
|
|
f88c9904fb | ||
|
|
0fc61e52dd | ||
|
|
5e44c33bb6 | ||
|
|
df3655e958 | ||
|
|
fe43e13145 | ||
|
|
acd3aad8d9 | ||
|
|
811b4b4f22 | ||
|
|
7db3b9f401 | ||
|
|
a5a099cf43 | ||
|
|
ba605643bb | ||
|
|
96e98c1abb | ||
|
|
5de6f6ae2b | ||
|
|
0685722773 | ||
|
|
29df3b2859 | ||
|
|
fc6f859ced | ||
|
|
6b21a5416f | ||
|
|
74e7c8bbf1 | ||
|
|
757075b16a | ||
|
|
e8903c4d48 | ||
|
|
98262d16ee | ||
|
|
339506cf10 | ||
|
|
63bd5df09b | ||
|
|
32af2d0831 | ||
|
|
242b03d4b5 | ||
|
|
87a62666ac | ||
|
|
2dcf5486da | ||
|
|
60d3d8b3ae | ||
|
|
e9141d65fe | ||
|
|
aae591daec | ||
|
|
a5ca8a7472 | ||
|
|
36f8cc02cb | ||
|
|
55b9645651 | ||
|
|
d30ecbed5b | ||
|
|
0bbd27f04c | ||
|
|
3c53fb7403 | ||
|
|
7a81950819 | ||
|
|
74f212c702 | ||
|
|
36be943854 | ||
|
|
26a4fc75a5 | ||
|
|
a5ec5fc265 | ||
|
|
541ac44be4 | ||
|
|
117b555fcd | ||
|
|
a10cd04441 | ||
|
|
53dccbde2a | ||
|
|
56625dfe4b | ||
|
|
2eab5a3b7b | ||
|
|
f9e811862a | ||
|
|
25ccc3d0e1 | ||
|
|
8853bf0670 | ||
|
|
71fa3a824b | ||
|
|
56fd50834c | ||
|
|
48ba812cf6 | ||
|
|
019dc6d45f | ||
|
|
cbcb784a79 | ||
|
|
39fa0258ad | ||
|
|
d0dd5bfa8c | ||
|
|
b948c47618 | ||
|
|
18cae8ac53 | ||
|
|
0576231dfc | ||
|
|
6ad35a83dd | ||
|
|
33d4dfc745 | ||
|
|
f3bf63a668 | ||
|
|
2dbb7395a4 | ||
|
|
7c1eb80d62 | ||
|
|
f2bf093691 | ||
|
|
2f002bfa4a | ||
|
|
4a19038d54 | ||
|
|
15fb3dd92c | ||
|
|
e0982d3961 | ||
|
|
7fb2f15f54 | ||
|
|
f93dbb4116 | ||
|
|
bc34fe3a9f | ||
|
|
bbe49491c1 | ||
|
|
d5ccb80f26 | ||
|
|
50fd15379a | ||
|
|
ed479f1155 | ||
|
|
04db9ba714 | ||
|
|
6a973e3248 | ||
|
|
96d9e4977b | ||
|
|
5a3897f22a | ||
|
|
ceef00b79a | ||
|
|
94ee24ea11 | ||
|
|
1a201f2e94 | ||
|
|
e0c6d41d4b | ||
|
|
44a0ae86d2 | ||
|
|
06822ad385 | ||
|
|
3be52280ba | ||
|
|
5142f978cf | ||
|
|
667ffd4dfd | ||
|
|
17626f1853 | ||
|
|
5c79567a2c | ||
|
|
0f5fd9af62 | ||
|
|
99643537d1 | ||
|
|
47650386e0 | ||
|
|
aa3fc34646 | ||
|
|
92f5f1ac71 | ||
|
|
eb739f73c7 | ||
|
|
20a646106b | ||
|
|
0dc14517cd | ||
|
|
04713c007b | ||
|
|
1cac7660b8 | ||
|
|
edb286f9af | ||
|
|
fb15ff99ca | ||
|
|
a455e75e37 | ||
|
|
2b452e51f9 | ||
|
|
9d2b6f8854 | ||
|
|
3be10a1b59 | ||
|
|
01950c25a8 | ||
|
|
7ad1ebd0e8 | ||
|
|
ee01a8b5a0 | ||
|
|
23c54f6813 | ||
|
|
ceef4510fb | ||
|
|
042a6b943e | ||
|
|
e114b73654 | ||
|
|
da0a505978 | ||
|
|
cb6d4a385c | ||
|
|
2806aee0fc | ||
|
|
52736f5162 | ||
|
|
6d45ad7fe8 | ||
|
|
2fc53644bc | ||
|
|
b33eaec493 | ||
|
|
63d6f3ac78 | ||
|
|
fd4caac013 | ||
|
|
ada875cdb0 | ||
|
|
49ddfa573d | ||
|
|
b58ac367ee | ||
|
|
cf62b4ed95 | ||
|
|
b05c62e141 | ||
|
|
095556106c | ||
|
|
5cdd72a45c | ||
|
|
5d84f8af43 | ||
|
|
d9483d9214 | ||
|
|
250475830f | ||
|
|
b8a0a823e0 | ||
|
|
6a54d669a3 | ||
|
|
62559c4e66 | ||
|
|
e04ecaaf7a | ||
|
|
fa4706c95f | ||
|
|
7067c0e02e | ||
|
|
cc71ce6180 | ||
|
|
f409a3583c | ||
|
|
ac7504e017 | ||
|
|
345bd3f7c1 | ||
|
|
2e2960ec69 | ||
|
|
e93b103d1e | ||
|
|
22977a4c5b | ||
|
|
7da5d5322b | ||
|
|
49e2c40ba4 | ||
|
|
8041a004c2 | ||
|
|
db62d78e04 | ||
|
|
bd79318b1e | ||
|
|
2736bd9165 | ||
|
|
b6bd48584f | ||
|
|
7b4f3c975a | ||
|
|
b165fadc55 | ||
|
|
274e0d0557 | ||
|
|
7594e51a18 | ||
|
|
bf22819b53 | ||
|
|
3dea4b15e0 | ||
|
|
5836b3292b | ||
|
|
a8fd49a234 | ||
|
|
006ad72eb2 | ||
|
|
dc254e1ee5 | ||
|
|
b0d31a4d35 | ||
|
|
24155feea4 | ||
|
|
db0a4e35c6 | ||
|
|
68ef98d346 | ||
|
|
f23bb9fac1 | ||
|
|
d20dde0b6d | ||
|
|
f76b30d109 | ||
|
|
ee1a047cde | ||
|
|
4c533fedfd | ||
|
|
f8ab7b8e13 | ||
|
|
46c5eaf0e1 | ||
|
|
e7e73aa1d2 | ||
|
|
394221f3df | ||
|
|
7d2647f830 | ||
|
|
563c90a8c4 | ||
|
|
0e0352fdd6 | ||
|
|
d6a8db97d8 | ||
|
|
05f8fa21de | ||
|
|
ab8779086b | ||
|
|
ed8305b199 | ||
|
|
1def825c7f | ||
|
|
3c9b5d3b17 | ||
|
|
3c2dd88fd3 | ||
|
|
0e21d5e166 | ||
|
|
8c221bd786 | ||
|
|
3b1fcdfb10 | ||
|
|
9bb91e1085 | ||
|
|
98bee122fe | ||
|
|
6aaa9a83a6 | ||
|
|
2eaea8ce7c | ||
|
|
f5ded03b9b | ||
|
|
f509a4b3ab | ||
|
|
3f46485382 | ||
|
|
2c5e6c0788 | ||
|
|
a7d4a04ac2 | ||
|
|
d199c1b6c4 | ||
|
|
5606ed1308 | ||
|
|
a301575dd7 | ||
|
|
cb8eab443c | ||
|
|
51c85a1b10 | ||
|
|
74ea87676e | ||
|
|
9d1510f14d | ||
|
|
62ea92335d | ||
|
|
d1e2822b92 | ||
|
|
533cb747df | ||
|
|
33a0f9c97f | ||
|
|
ef9a442718 | ||
|
|
1d6b5a35bd | ||
|
|
1ff6ee14ac | ||
|
|
d2e51e97c0 | ||
|
|
9f94465979 | ||
|
|
d83526ff5c | ||
|
|
1d6b7e1b2e | ||
|
|
1c9458d056 | ||
|
|
4e29f2ae8b | ||
|
|
69321636b5 | ||
|
|
d190b254bd | ||
|
|
005a96f3d3 | ||
|
|
e39e0910a1 | ||
|
|
56a6cee8f2 |
8
.github/no-response.yml
vendored
Normal file
8
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||
|
||||
daysUntilClose: 7
|
||||
responseRequiredLabel: 'need more info'
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there was no response to a
|
||||
request for more information from the issue opener. Please leave a comment or
|
||||
open a new issue if you have additional information related to this issue.
|
||||
14
.github/stale.yml
vendored
Normal file
14
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||
|
||||
daysUntilStale: 90
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- accepted
|
||||
- bug
|
||||
- enhancement
|
||||
- meta
|
||||
staleLabel: stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
closeComment: false
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
#built application files
|
||||
# built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
*.aab
|
||||
output.json
|
||||
out/
|
||||
|
||||
# files for the dex VM
|
||||
*.dex
|
||||
@@ -30,6 +33,8 @@ Thumbs.db
|
||||
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||
.gradle
|
||||
build/
|
||||
*.iml
|
||||
|
||||
# Compiled JNI libraries folder
|
||||
**/jniLibs
|
||||
**/jniLibs
|
||||
app/.externalNativeBuild/
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "moonlight-common"]
|
||||
path = moonlight-common
|
||||
url = https://github.com/moonlight-stream/moonlight-common.git
|
||||
@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
|
||||
|
||||
local i = 0
|
||||
local data_start = -1
|
||||
while i < buf:len do
|
||||
while i < buf:len() do
|
||||
-- Make sure we have a potential start sequence and type
|
||||
if buf:len() - i < 5 then
|
||||
-- We need more data
|
||||
|
||||
30
README.md
30
README.md
@@ -1,44 +1,42 @@
|
||||
#Moonlight
|
||||
# Moonlight Android
|
||||
|
||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||
|
||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||
whether in your own home or over the internet.
|
||||
|
||||
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||
|
||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
|
||||
|
||||
##Features
|
||||
## Features
|
||||
|
||||
* Streams any of your games from your PC to your Android device
|
||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||
* Automatically finds GameStream-compatible PCs on your network
|
||||
|
||||
##Installation
|
||||
## Installation
|
||||
|
||||
* Download and install Moonlight for Android from
|
||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||
|
||||
##Requirements
|
||||
## Requirements
|
||||
|
||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
||||
* Android device running 4.1 (Jelly Bean) or higher
|
||||
* High-end wireless router (802.11n dual-band recommended)
|
||||
|
||||
##Usage
|
||||
## Usage
|
||||
|
||||
* Turn on GameStream in the GFE settings
|
||||
* If you are connecting from outside the same network, turn on internet
|
||||
streaming
|
||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||
* Accept the pairing confirmation on your PC
|
||||
* Accept the pairing confirmation on your PC and add the PIN if needed
|
||||
* Tap your PC again to view the list of apps to stream
|
||||
* Play games!
|
||||
|
||||
##Contribute
|
||||
## Contribute
|
||||
|
||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||
|
||||
@@ -46,7 +44,13 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
|
||||
##Authors
|
||||
## Building
|
||||
* Install Android Studio and the Android NDK
|
||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
||||
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||
* Build the APK using Android Studio
|
||||
|
||||
## Authors
|
||||
|
||||
* [Cameron Gutman](https://github.com/cgutman)
|
||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||
|
||||
120
app/app.iml
120
app/app.iml
@@ -1,120 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="android-gradle" name="Android-Gradle">
|
||||
<configuration>
|
||||
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="android" name="Android">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
||||
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
|
||||
<afterSyncTasks>
|
||||
<task>generateNonRootDebugAndroidTestSources</task>
|
||||
<task>generateNonRootDebugSources</task>
|
||||
</afterSyncTasks>
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
|
||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
|
||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||
<orderEntry type="library" exported="" name="jmdns-3.4.2" level="project" />
|
||||
<orderEntry type="library" exported="" name="okhttp-2.4.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="jcodec-0.1.9-patched" level="project" />
|
||||
<orderEntry type="library" exported="" name="okio-1.5.0" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
135
app/build.gradle
135
app/build.gradle
@@ -1,75 +1,122 @@
|
||||
import com.android.builder.model.ProductFlavor
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.2"
|
||||
buildToolsVersion '28.0.3'
|
||||
compileSdkVersion 28
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
targetSdkVersion 28
|
||||
|
||||
versionName "3.1.13"
|
||||
versionCode = 72
|
||||
versionName "7.3"
|
||||
versionCode = 193
|
||||
}
|
||||
|
||||
flavorDimensions "root"
|
||||
|
||||
productFlavors {
|
||||
root {
|
||||
// Android O has native mouse capture, so don't show the rooted
|
||||
// version to devices running O on the Play Store.
|
||||
maxSdkVersion 25
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
arguments "PRODUCT_FLAVOR=root"
|
||||
}
|
||||
}
|
||||
|
||||
applicationId "com.limelight.root"
|
||||
dimension "root"
|
||||
}
|
||||
|
||||
nonRoot {
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
arguments "PRODUCT_FLAVOR=nonRoot"
|
||||
}
|
||||
}
|
||||
|
||||
applicationId "com.limelight"
|
||||
dimension "root"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
bundle {
|
||||
language {
|
||||
// Avoid splitting by language, since we allow users
|
||||
// to manually switch language in settings.
|
||||
enableSplit = false
|
||||
}
|
||||
density {
|
||||
// FIXME: This should not be neccessary but we get
|
||||
// weird crashes due to missing drawable resources
|
||||
// when this split is enabled.
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
|
||||
minifyEnabled true
|
||||
useProguard false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
// To whomever is releasing/using an APK in release mode with
|
||||
// Moonlight's official application ID, please stop. I see every
|
||||
// single one of your crashes in my Play Console and it makes
|
||||
// Moonlight's reliability look worse and makes it more difficult
|
||||
// to distinguish real crashes from your crashy VR app. Seriously,
|
||||
// 44 of the *same* native crash in 72 hours and a few each of
|
||||
// several other crashes.
|
||||
//
|
||||
// This is technically not your fault. I would have hoped Google
|
||||
// would validate the signature of the APK before attributing
|
||||
// the crash to it. I asked their Play Store support about this
|
||||
// and they said they don't and don't have plans to, so that sucks.
|
||||
//
|
||||
// In any case, it's bad form to release an APK using someone
|
||||
// else's application ID. There is no legitimate reason, that
|
||||
// anyone would need to comment out the following line, except me
|
||||
// when I release an official signed Moonlight build. If you feel
|
||||
// like doing so would solve something, I can tell you it will not.
|
||||
// You can't upgrade an app while retaining data without having the
|
||||
// same signature as the official version. Nor can you post it on
|
||||
// the Play Store, since that application ID is already taken.
|
||||
// Reputable APK hosting websites similarly validate the signature
|
||||
// is consistent with the Play Store and won't allow an APK that
|
||||
// isn't signed the same as the original.
|
||||
//
|
||||
// I wish any and all people using Moonlight as the basis of other
|
||||
// cool projects the best of luck with their efforts. All I ask
|
||||
// is to please change the applicationId before you publish.
|
||||
//
|
||||
// TL;DR: Leave the following line alone!
|
||||
applicationIdSuffix ".unofficial"
|
||||
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets.main.jni.srcDirs = []
|
||||
|
||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
||||
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
|
||||
Properties properties = new Properties()
|
||||
properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
def ndkDir = properties.getProperty('ndk.dir')
|
||||
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
commandLine "$ndkDir\\ndk-build.cmd",
|
||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "src/main/jni/Android.mk"
|
||||
}
|
||||
else {
|
||||
commandLine "$ndkDir/ndk-build",
|
||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
compileTask -> compileTask.dependsOn ndkBuild
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
|
||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
|
||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
||||
implementation 'org.jcodec:jcodec:0.2.3'
|
||||
|
||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
|
||||
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
|
||||
|
||||
compile files('libs/jmdns-3.4.2.jar')
|
||||
compile files('libs/limelight-common.jar')
|
||||
compile files('libs/tinyrtsp.jar')
|
||||
compile files('libs/jcodec-0.1.9-patched.jar')
|
||||
implementation project(':moonlight-common')
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
app/proguard-rules.pro
vendored
Normal file
28
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Don't obfuscate code
|
||||
-dontobfuscate
|
||||
|
||||
# Our code
|
||||
-keep class com.limelight.binding.input.evdev.* {*;}
|
||||
|
||||
# Moonlight common
|
||||
-keep class com.limelight.nvstream.jni.* {*;}
|
||||
|
||||
# Okio
|
||||
-keep class sun.misc.Unsafe {*;}
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
|
||||
# BouncyCastle
|
||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.* {*;}
|
||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.util.* {*;}
|
||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.* {*;}
|
||||
-keep class org.bouncycastle.jcajce.provider.digest.** {*;}
|
||||
-keep class org.bouncycastle.jcajce.provider.symmetric.** {*;}
|
||||
-keep class org.bouncycastle.jcajce.spec.* {*;}
|
||||
-keep class org.bouncycastle.jce.** {*;}
|
||||
-dontwarn javax.naming.**
|
||||
|
||||
# jMDNS
|
||||
-dontwarn javax.jmdns.impl.DNSCache
|
||||
-dontwarn org.slf4j.**
|
||||
7
app/src/debug/res/values/strings.xml
Normal file
7
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_label" translatable="false">Moonlight (Debug)</string>
|
||||
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
|
||||
|
||||
</resources>
|
||||
@@ -4,78 +4,119 @@
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<!-- Disable legacy input emulation on ChromeOS -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.type.pc"
|
||||
android:required="false"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:theme="@style/AppTheme" >
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:isGame="true"
|
||||
android:banner="@drawable/atv_banner"
|
||||
android:appCategory="game"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!-- Samsung multi-window support -->
|
||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
|
||||
<!-- Launcher for traditional devices -->
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
|
||||
in each activity even though it is implied by targeting API 24+ -->
|
||||
|
||||
<activity
|
||||
android:name=".PcView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.APP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Launcher for Android TV devices -->
|
||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||
<activity
|
||||
android:name=".PcViewTv"
|
||||
android:logo="@drawable/atv_banner"
|
||||
android:icon="@drawable/atv_banner"
|
||||
android:name=".ShortcutTrampoline"
|
||||
android:noHistory="true"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AppView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".preferences.StreamSettings"
|
||||
android:label="Streaming Settings" >
|
||||
android:resizeableActivity="true"
|
||||
android:label="Streaming Settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".preferences.AddComputerManually"
|
||||
android:label="Add Computer Manually" >
|
||||
android:resizeableActivity="true"
|
||||
android:label="Add Computer Manually">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
||||
<activity
|
||||
android:name=".Game"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/StreamTheme"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:noHistory="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:resizeableActivity="true"
|
||||
android:launchMode="singleTask"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/StreamTheme">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.AppView" />
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".discovery.DiscoveryService"
|
||||
android:label="mDNS PC Auto-Discovery Service" />
|
||||
@@ -85,6 +126,15 @@
|
||||
<service
|
||||
android:name=".binding.input.driver.UsbDriverService"
|
||||
android:label="Usb Driver Service" />
|
||||
|
||||
<activity
|
||||
android:name=".HelpActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-web.png
Normal file
BIN
app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -1,9 +1,8 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
@@ -11,12 +10,14 @@ import com.limelight.grid.AppGridAdapter;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.AdapterFragment;
|
||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
@@ -25,7 +26,9 @@ import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.ContextMenu;
|
||||
@@ -36,13 +39,17 @@ import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private AppGridAdapter appGridAdapter;
|
||||
private String uuidString;
|
||||
private ShortcutHelper shortcutHelper;
|
||||
|
||||
private ComputerDetails computer;
|
||||
private ComputerManagerService.ApplistPoller poller;
|
||||
@@ -50,11 +57,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private String lastRawApplist;
|
||||
private int lastRunningAppId;
|
||||
private boolean suspendGridUpdates;
|
||||
private boolean inForeground;
|
||||
|
||||
private final static int START_OR_RESUME_ID = 1;
|
||||
private final static int QUIT_ID = 2;
|
||||
private final static int CANCEL_ID = 3;
|
||||
private final static int START_WTIH_QUIT = 4;
|
||||
private final static int START_WITH_QUIT = 4;
|
||||
private final static int VIEW_DETAILS_ID = 5;
|
||||
private final static int CREATE_SHORTCUT_ID = 6;
|
||||
|
||||
public final static String NAME_EXTRA = "Name";
|
||||
public final static String UUID_EXTRA = "UUID";
|
||||
@@ -72,32 +82,57 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Wait for the binder to be ready
|
||||
localBinder.waitForReady();
|
||||
|
||||
// Now make the binder visible
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Get the computer object
|
||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||
computer = localBinder.getComputer(uuidString);
|
||||
if (computer == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||
computer, managerBinder.getUniqueId());
|
||||
computer, localBinder.getUniqueId());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Now make the binder visible. We must do this after appGridAdapter
|
||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
||||
// touching the appGridAdapter prior to initialization.
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Load the app grid with cached data (if possible).
|
||||
// This must be done _before_ startComputerUpdates()
|
||||
// so the initial serverinfo response can update the running
|
||||
// icon.
|
||||
populateAppGridWithCache();
|
||||
|
||||
// Start updates
|
||||
startComputerUpdates();
|
||||
|
||||
// Load the app grid with cached data (if possible)
|
||||
populateAppGridWithCache();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFinishing() || isChangingConfigurations()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
// Despite my best efforts to catch all conditions that could
|
||||
// cause the activity to be destroyed when we try to commit
|
||||
// I haven't been able to, so we have this try-catch block.
|
||||
try {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
@@ -108,20 +143,21 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
};
|
||||
|
||||
private void startComputerUpdates() {
|
||||
if (managerBinder == null) {
|
||||
// Don't start polling if we're not bound or in the foreground
|
||||
if (managerBinder == null || !inForeground) {
|
||||
return;
|
||||
}
|
||||
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
public void notifyComputerUpdated(ComputerDetails details) {
|
||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||
// Do nothing if updates are suspended
|
||||
if (suspendGridUpdates) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,6 +175,24 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close immediately if the PC is no longer paired
|
||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
|
||||
AppView.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Disable shortcuts referencing this PC for now
|
||||
shortcutHelper.disableShortcut(details.uuid,
|
||||
getResources().getString(R.string.scut_not_paired));
|
||||
|
||||
// Display a toast to the user and quit the activity
|
||||
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// App list is the same or empty
|
||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||
|
||||
@@ -157,12 +211,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
try {
|
||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||
updateUiWithServerinfo(details);
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -190,12 +247,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(locale);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
// Assume we're in the foreground when created to avoid a race
|
||||
// between binding to CMS and onResume()
|
||||
inForeground = true;
|
||||
|
||||
shortcutHelper = new ShortcutHelper(this);
|
||||
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
setContentView(R.layout.activity_app_view);
|
||||
|
||||
@@ -203,10 +261,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||
|
||||
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
||||
TextView label = (TextView) findViewById(R.id.appListText);
|
||||
setTitle(labelText);
|
||||
label.setText(labelText);
|
||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||
|
||||
TextView label = findViewById(R.id.appListText);
|
||||
setTitle(computerName);
|
||||
label.setText(computerName);
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||
shortcutHelper.reportShortcutUsed(uuidString);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
@@ -220,7 +283,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||
updateUiWithAppList(applist);
|
||||
LimeLog.info("Loaded applist from cache");
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
if (lastRawApplist != null) {
|
||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||
e.printStackTrace();
|
||||
@@ -252,6 +315,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Display a decoder crash notification if we've returned after a crash
|
||||
UiHelper.showDecoderCrashDialog(this);
|
||||
|
||||
inForeground = true;
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
@@ -259,38 +326,41 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
inForeground = false;
|
||||
stopComputerUpdates();
|
||||
}
|
||||
|
||||
private int getRunningAppId() {
|
||||
int runningAppId = -1;
|
||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||
if (app.app.getIsRunning()) {
|
||||
runningAppId = app.app.getAppId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return runningAppId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||
int runningAppId = getRunningAppId();
|
||||
if (runningAppId != -1) {
|
||||
if (runningAppId == selectedApp.app.getAppId()) {
|
||||
if (lastRunningAppId != 0) {
|
||||
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||
}
|
||||
else {
|
||||
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||
}
|
||||
}
|
||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Only add an option to create shortcut if box art is loaded
|
||||
// and when we're in grid-mode (not list-mode).
|
||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||
if (appImageView != null) {
|
||||
// We have a grid ImageView, so we must be in grid-mode
|
||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
||||
if (drawable != null && drawable.getBitmap() != null) {
|
||||
// We have a bitmap loaded too
|
||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -302,7 +372,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||
switch (item.getItemId()) {
|
||||
case START_WTIH_QUIT:
|
||||
case START_WITH_QUIT:
|
||||
// Display a confirmation dialog first
|
||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||
@Override
|
||||
@@ -323,8 +393,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void run() {
|
||||
suspendGridUpdates = true;
|
||||
ServerHelper.doQuit(AppView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
ServerHelper.doQuit(AppView.this, computer,
|
||||
app.app, managerBinder, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -342,6 +411,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
case CANCEL_ID:
|
||||
return true;
|
||||
|
||||
case VIEW_DETAILS_ID:
|
||||
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
||||
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
|
||||
return true;
|
||||
|
||||
case CREATE_SHORTCUT_ID:
|
||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
||||
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
|
||||
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
|
||||
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
@@ -358,19 +440,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||
|
||||
// There can only be one or zero apps running.
|
||||
if (existingApp.app.getIsRunning() &&
|
||||
if (existingApp.isRunning &&
|
||||
existingApp.app.getAppId() == details.runningGameId) {
|
||||
// This app was running and still is, so we're done now
|
||||
return;
|
||||
}
|
||||
else if (existingApp.app.getAppId() == details.runningGameId) {
|
||||
// This app wasn't running but now is
|
||||
existingApp.app.setIsRunning(true);
|
||||
existingApp.isRunning = true;
|
||||
updated = true;
|
||||
}
|
||||
else if (existingApp.app.getIsRunning()) {
|
||||
else if (existingApp.isRunning) {
|
||||
// This app was running but now isn't
|
||||
existingApp.app.setIsRunning(false);
|
||||
existingApp.isRunning = false;
|
||||
updated = true;
|
||||
}
|
||||
else {
|
||||
@@ -400,10 +482,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||
// Found the app; update its properties
|
||||
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||
existingApp.app.setIsRunning(app.getIsRunning());
|
||||
updated = true;
|
||||
}
|
||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||
existingApp.app.setAppName(app.getAppName());
|
||||
updated = true;
|
||||
@@ -473,7 +551,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||
|
||||
// Only open the context menu if something is running, otherwise start it
|
||||
if (getRunningAppId() != -1) {
|
||||
if (lastRunningAppId != 0) {
|
||||
openContextMenu(arg1);
|
||||
} else {
|
||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||
@@ -486,6 +564,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
public class AppObject {
|
||||
public final NvApp app;
|
||||
public boolean isRunning;
|
||||
|
||||
public AppObject(NvApp app) {
|
||||
if (app == null) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
73
app/src/main/java/com/limelight/HelpActivity.java
Normal file
73
app/src/main/java/com/limelight/HelpActivity.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package com.limelight;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
public class HelpActivity extends Activity {
|
||||
|
||||
private SpinnerDialog loadingDialog;
|
||||
private WebView webView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
webView = new WebView(this);
|
||||
setContentView(webView);
|
||||
|
||||
// These allow the user to zoom the page
|
||||
webView.getSettings().setBuiltInZoomControls(true);
|
||||
webView.getSettings().setDisplayZoomControls(false);
|
||||
|
||||
// This sets the view to display the whole page by default
|
||||
webView.getSettings().setUseWideViewPort(true);
|
||||
webView.getSettings().setLoadWithOverviewMode(true);
|
||||
|
||||
// This allows the links to places on the same page to work
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
if (loadingDialog == null) {
|
||||
loadingDialog = SpinnerDialog.displayDialog(HelpActivity.this,
|
||||
getResources().getString(R.string.help_loading_title),
|
||||
getResources().getString(R.string.help_loading_msg), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
if (loadingDialog != null) {
|
||||
loadingDialog.dismiss();
|
||||
loadingDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
||||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
||||
}
|
||||
});
|
||||
|
||||
webView.loadUrl(getIntent().getData().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Back goes back through the WebView history
|
||||
// until no more history remains
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
}
|
||||
else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@ package com.limelight;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
@@ -18,20 +16,26 @@ import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||
import com.limelight.preferences.AddComputerManually;
|
||||
import com.limelight.preferences.GlPreferences;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.preferences.StreamSettings;
|
||||
import com.limelight.ui.AdapterFragment;
|
||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.HelpLauncher;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
@@ -49,11 +53,17 @@ import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
private RelativeLayout noPcFoundLayout;
|
||||
private PcGridAdapter pcGridAdapter;
|
||||
private ShortcutHelper shortcutHelper;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private boolean freezeUpdates, runningPolling, hasResumed;
|
||||
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||
@@ -83,11 +93,19 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Reinitialize views just in case orientation changed
|
||||
initializeViews();
|
||||
// Only reinitialize views if completeOnCreate() was called
|
||||
// before this callback. If it was not, completeOnCreate() will
|
||||
// handle initializing views with the config change accounted for.
|
||||
// This is not prone to races because both callbacks are invoked
|
||||
// in the main thread.
|
||||
if (completeOnCreateCalled) {
|
||||
// Reinitialize views just in case orientation changed
|
||||
initializeViews();
|
||||
}
|
||||
}
|
||||
|
||||
private final static int APP_LIST_ID = 1;
|
||||
@@ -97,6 +115,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
private final static int DELETE_ID = 5;
|
||||
private final static int RESUME_ID = 6;
|
||||
private final static int QUIT_ID = 7;
|
||||
private final static int VIEW_DETAILS_ID = 8;
|
||||
|
||||
private void initializeViews() {
|
||||
setContentView(R.layout.activity_pc_view);
|
||||
@@ -107,8 +126,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||
|
||||
// Setup the list view
|
||||
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
||||
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
||||
ImageButton settingsButton = findViewById(R.id.settingsButton);
|
||||
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
|
||||
ImageButton helpButton = findViewById(R.id.helpButton);
|
||||
|
||||
settingsButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
@@ -123,12 +143,18 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
helpButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
HelpLauncher.launchSetupGuide(PcView.this);
|
||||
}
|
||||
});
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
|
||||
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
|
||||
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
|
||||
if (pcGridAdapter.getCount() == 0) {
|
||||
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
@@ -142,12 +168,55 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(locale);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
// Assume we're in the foreground when created to avoid a race
|
||||
// between binding to CMS and onResume()
|
||||
inForeground = true;
|
||||
|
||||
// Create a GLSurfaceView to fetch GLRenderer unless we have
|
||||
// a cached result already.
|
||||
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
|
||||
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
|
||||
GLSurfaceView surfaceView = new GLSurfaceView(this);
|
||||
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
|
||||
// Save the GLRenderer string so we don't need to do this next time
|
||||
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
|
||||
glPrefs.savedFingerprint = Build.FINGERPRINT;
|
||||
glPrefs.writePreferences();
|
||||
|
||||
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
|
||||
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
completeOnCreate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl10) {
|
||||
}
|
||||
});
|
||||
setContentView(surfaceView);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
|
||||
completeOnCreate();
|
||||
}
|
||||
}
|
||||
|
||||
private void completeOnCreate() {
|
||||
completeOnCreateCalled = true;
|
||||
|
||||
shortcutHelper = new ShortcutHelper(this);
|
||||
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||
@@ -161,11 +230,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void startComputerUpdates() {
|
||||
if (managerBinder != null) {
|
||||
if (runningPolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow polling to start if we're bound to CMS, polling is not already running,
|
||||
// and our activity is in the foreground.
|
||||
if (managerBinder != null && !runningPolling && inForeground) {
|
||||
freezeUpdates = false;
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
@@ -215,7 +282,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
hasResumed = true;
|
||||
// Display a decoder crash notification if we've returned after a crash
|
||||
UiHelper.showDecoderCrashDialog(this);
|
||||
|
||||
inForeground = true;
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
@@ -223,7 +293,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
hasResumed = false;
|
||||
inForeground = false;
|
||||
stopComputerUpdates(false);
|
||||
}
|
||||
|
||||
@@ -245,8 +315,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||
|
||||
// Inflate the context menu
|
||||
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
|
||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
if (computer.details.state == ComputerDetails.State.OFFLINE ||
|
||||
computer.details.state == ComputerDetails.State.UNKNOWN) {
|
||||
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
|
||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||
}
|
||||
@@ -266,19 +336,20 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// it with delete which actually work
|
||||
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||
}
|
||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
||||
// We don't want to start computer updates again, so we need to keep track of whether we're paused.
|
||||
if (hasResumed) {
|
||||
startComputerUpdates();
|
||||
}
|
||||
// startComputerUpdates() manages this and won't actual start polling until the activity
|
||||
// returns to the foreground.
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
private void doPair(final ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -302,23 +373,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Stop updates and wait while pairing
|
||||
stopComputerUpdates(true);
|
||||
|
||||
InetAddress addr = null;
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
addr = computer.localIp;
|
||||
}
|
||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||
addr = computer.remoteIp;
|
||||
}
|
||||
else {
|
||||
LimeLog.warning("Unknown reachability - using local IP");
|
||||
addr = computer.localIp;
|
||||
}
|
||||
|
||||
httpConn = new NvHTTP(addr,
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
if (httpConn.getPairState() == PairState.PAIRED) {
|
||||
// Don't display any toast, but open the app list
|
||||
message = null;
|
||||
success = true;
|
||||
@@ -330,17 +389,29 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||
|
||||
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
PairingManager pm = httpConn.getPairingManager();
|
||||
|
||||
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.FAILED) {
|
||||
else if (pairState == PairState.FAILED) {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
|
||||
message = getResources().getString(R.string.pair_already_in_progress);
|
||||
}
|
||||
else if (pairState == PairState.PAIRED) {
|
||||
// Just navigate to the app view without displaying a toast
|
||||
message = null;
|
||||
success = true;
|
||||
|
||||
// Pin this certificate for later HTTPS use
|
||||
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
|
||||
|
||||
// Invalidate reachability information after pairing to force
|
||||
// a refresh before reading pair state again
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
}
|
||||
else {
|
||||
// Should be no other values
|
||||
@@ -351,7 +422,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
message = e.getMessage();
|
||||
}
|
||||
@@ -368,14 +439,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
if (toastSuccess) {
|
||||
// Open the app list after a successful pairing attemp
|
||||
// Open the app list after a successful pairing attempt
|
||||
doAppList(computer);
|
||||
}
|
||||
else {
|
||||
// Start polling again if we're still in the foreground
|
||||
startComputerUpdates();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start polling again
|
||||
startComputerUpdates();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -391,7 +463,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -415,7 +486,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doUnpair(final ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -431,21 +503,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
InetAddress addr = null;
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
addr = computer.localIp;
|
||||
}
|
||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||
addr = computer.remoteIp;
|
||||
}
|
||||
else {
|
||||
LimeLog.warning("Unknown reachability - using local IP");
|
||||
addr = computer.localIp;
|
||||
}
|
||||
|
||||
httpConn = new NvHTTP(addr,
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(),
|
||||
PlatformBinding.getDeviceName(),
|
||||
computer.serverCert,
|
||||
PlatformBinding.getCryptoProvider(PcView.this));
|
||||
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
|
||||
httpConn.unpair();
|
||||
@@ -463,8 +523,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
message = getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
message = e.getMessage();
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
final String toastMessage = message;
|
||||
@@ -479,7 +540,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void doAppList(ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.state == ComputerDetails.State.OFFLINE) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@@ -490,7 +551,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
Intent i = new Intent(this, AppView.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computer.name);
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
|
||||
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
|
||||
startActivity(i);
|
||||
}
|
||||
|
||||
@@ -512,12 +573,21 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
return true;
|
||||
|
||||
case DELETE_ID:
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
if (ActivityManager.isUserAMonkey()) {
|
||||
LimeLog.info("Ignoring delete PC request from monkey");
|
||||
return true;
|
||||
}
|
||||
managerBinder.removeComputer(computer.details.name);
|
||||
removeComputer(computer.details);
|
||||
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (managerBinder == null) {
|
||||
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
managerBinder.removeComputer(computer.details.name);
|
||||
removeComputer(computer.details);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case APP_LIST_ID:
|
||||
@@ -530,7 +600,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
return true;
|
||||
}
|
||||
|
||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
||||
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
|
||||
return true;
|
||||
|
||||
case QUIT_ID:
|
||||
@@ -543,13 +613,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ServerHelper.doQuit(PcView.this,
|
||||
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||
new NvApp("app", 0), managerBinder, null);
|
||||
ServerHelper.doQuit(PcView.this, computer.details,
|
||||
new NvApp("app", 0, false), managerBinder, null);
|
||||
}
|
||||
}, null);
|
||||
return true;
|
||||
|
||||
case VIEW_DETAILS_ID:
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
@@ -560,6 +633,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||
|
||||
if (details.equals(computer.details)) {
|
||||
// Disable or delete shortcuts referencing this PC
|
||||
shortcutHelper.disableShortcut(details.uuid,
|
||||
getResources().getString(R.string.scut_deleted_pc));
|
||||
|
||||
pcGridAdapter.removeComputer(computer);
|
||||
pcGridAdapter.notifyDataSetChanged();
|
||||
|
||||
@@ -586,6 +663,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
// Add a launcher shortcut for this PC
|
||||
if (details.pairState == PairState.PAIRED) {
|
||||
shortcutHelper.createAppViewShortcut(details.uuid, details, false);
|
||||
}
|
||||
|
||||
if (existingEntry != null) {
|
||||
// Replace the information in the existing entry
|
||||
existingEntry.details = details;
|
||||
@@ -617,8 +699,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
|
||||
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
||||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
|
||||
computer.details.state == ComputerDetails.State.OFFLINE) {
|
||||
// Open the context menu if a PC is offline or refreshing
|
||||
openContextMenu(arg1);
|
||||
} else if (computer.details.pairState != PairState.PAIRED) {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.limelight;
|
||||
|
||||
/* This is a dummy class to allow for a separate icon
|
||||
* and launcher for TV.
|
||||
*/
|
||||
public class PcViewTv extends PcView {}
|
||||
275
app/src/main/java/com/limelight/ShortcutTrampoline.java
Normal file
275
app/src/main/java/com/limelight/ShortcutTrampoline.java
Normal file
@@ -0,0 +1,275 @@
|
||||
package com.limelight;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ShortcutTrampoline extends Activity {
|
||||
private String uuidString;
|
||||
private String appIdString;
|
||||
private ArrayList<Intent> intentStack = new ArrayList<>();
|
||||
|
||||
private ComputerDetails computer;
|
||||
private SpinnerDialog blockingLoadSpinner;
|
||||
|
||||
public final static String APP_ID_EXTRA = "AppId";
|
||||
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||
|
||||
// Wait in a separate thread to avoid stalling the UI
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Wait for the binder to be ready
|
||||
localBinder.waitForReady();
|
||||
|
||||
// Now make the binder visible
|
||||
managerBinder = localBinder;
|
||||
|
||||
// Get the computer object
|
||||
computer = managerBinder.getComputer(uuidString);
|
||||
|
||||
if (computer == null) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_pc_not_found),
|
||||
true);
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
if (managerBinder != null) {
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Force CMS to repoll this machine
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
|
||||
// Start polling
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (details.state != ComputerDetails.State.UNKNOWN) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Stop showing the spinner
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
// If the managerBinder was destroyed before this callback,
|
||||
// just finish the activity.
|
||||
if (managerBinder == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
|
||||
|
||||
// Launch game if provided app ID, otherwise launch app view
|
||||
if (appIdString != null && appIdString.length() > 0) {
|
||||
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
|
||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
|
||||
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
} else {
|
||||
// Create the start intent immediately, so we can safely unbind the managerBinder
|
||||
// below before we return.
|
||||
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
|
||||
|
||||
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
intentStack.add(startIntent);
|
||||
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
}
|
||||
}, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Close this activity
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Add the PC view at the back (and clear the task)
|
||||
Intent i;
|
||||
i = new Intent(ShortcutTrampoline.this, PcView.class);
|
||||
i.setAction(Intent.ACTION_MAIN);
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intentStack.add(i);
|
||||
|
||||
// Take this intent's data and create an intent to start the app view
|
||||
i = new Intent(getIntent());
|
||||
i.setClass(ShortcutTrampoline.this, AppView.class);
|
||||
intentStack.add(i);
|
||||
|
||||
// If a game is running, we'll make the stream the top level activity
|
||||
if (details.runningGameId != 0) {
|
||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
||||
new NvApp("app", details.runningGameId, false), details, managerBinder));
|
||||
}
|
||||
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
}
|
||||
|
||||
}
|
||||
else if (details.state == ComputerDetails.State.OFFLINE) {
|
||||
// Computer offline - display an error dialog
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.error_pc_offline),
|
||||
true);
|
||||
} else if (details.pairState != PairingManager.PairState.PAIRED) {
|
||||
// Computer not paired - display an error dialog
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_not_paired),
|
||||
true);
|
||||
}
|
||||
|
||||
// We don't want any more callbacks from now on, so go ahead
|
||||
// and unbind from the service
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
managerBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
protected boolean validateInput() {
|
||||
// Validate UUID
|
||||
if (uuidString == null) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_invalid_uuid),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
UUID.fromString(uuidString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_invalid_uuid),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate App ID (if provided)
|
||||
if (appIdString != null && !appIdString.isEmpty()) {
|
||||
try {
|
||||
Integer.parseInt(appIdString);
|
||||
} catch (NumberFormatException ex) {
|
||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.scut_invalid_app_id),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
||||
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
|
||||
|
||||
if (validateInput()) {
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
|
||||
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
||||
getResources().getString(R.string.applist_connect_msg), true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
Dialog.closeDialogs();
|
||||
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,86 @@
|
||||
package com.limelight.binding.audio;
|
||||
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||
import com.limelight.nvstream.jni.MoonBridge;
|
||||
|
||||
public class AndroidAudioRenderer implements AudioRenderer {
|
||||
|
||||
private AudioTrack track;
|
||||
|
||||
@Override
|
||||
public boolean streamInitialized(int channelCount, int channelMask, int samplesPerFrame, int sampleRate) {
|
||||
int channelConfig;
|
||||
int bufferSize;
|
||||
int bytesPerFrame = (samplesPerFrame * 2);
|
||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
48000,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
}
|
||||
else {
|
||||
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME);
|
||||
AudioFormat format = new AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setSampleRate(48000)
|
||||
.setChannelMask(channelConfig)
|
||||
.build();
|
||||
|
||||
switch (channelCount)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// Use FLAG_LOW_LATENCY on L through N
|
||||
if (lowLatency) {
|
||||
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
||||
.setAudioFormat(format)
|
||||
.setAudioAttributes(attributesBuilder.build())
|
||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||
.setBufferSizeInBytes(bufferSize);
|
||||
|
||||
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
||||
if (lowLatency) {
|
||||
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
||||
}
|
||||
|
||||
return trackBuilder.build();
|
||||
}
|
||||
else {
|
||||
return new AudioTrack(attributesBuilder.build(),
|
||||
format,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int setup(int audioConfiguration) {
|
||||
int channelConfig;
|
||||
int bytesPerFrame;
|
||||
|
||||
switch (audioConfiguration)
|
||||
{
|
||||
case 1:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||
break;
|
||||
case 2:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
break;
|
||||
case 4:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||
break;
|
||||
case 6:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
break;
|
||||
default:
|
||||
LimeLog.severe("Decoder returned unhandled channel count");
|
||||
return false;
|
||||
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
bytesPerFrame = 2 * 240 * 2;
|
||||
break;
|
||||
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
bytesPerFrame = 6 * 240 * 2;
|
||||
break;
|
||||
default:
|
||||
LimeLog.severe("Decoder returned unhandled channel count");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We're not supposed to request less than the minimum
|
||||
@@ -41,62 +88,111 @@ public class AndroidAudioRenderer implements AudioRenderer {
|
||||
// do this on many devices and it lowers audio latency.
|
||||
// We'll try the small buffer size first and if it fails,
|
||||
// use the recommended larger buffer size.
|
||||
try {
|
||||
// Buffer two frames of audio if possible
|
||||
bufferSize = bytesPerFrame * 2;
|
||||
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
track.play();
|
||||
} catch (Exception e) {
|
||||
// Try to release the AudioTrack if we got far enough
|
||||
try {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
boolean lowLatency;
|
||||
int bufferSize;
|
||||
|
||||
// Now try the larger buffer size
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||
// We will try:
|
||||
// 1) Small buffer, low latency mode
|
||||
// 2) Large buffer, low latency mode
|
||||
// 3) Small buffer, standard mode
|
||||
// 4) Large buffer, standard mode
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
case 1:
|
||||
lowLatency = true;
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
lowLatency = false;
|
||||
break;
|
||||
default:
|
||||
// Unreachable
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
switch (i) {
|
||||
case 0:
|
||||
case 2:
|
||||
bufferSize = bytesPerFrame * 2;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
case 3:
|
||||
// Try the larger buffer size
|
||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT),
|
||||
bytesPerFrame * 2);
|
||||
bytesPerFrame * 2);
|
||||
|
||||
// Round to next frame
|
||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||
// Round to next frame
|
||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||
break;
|
||||
default:
|
||||
// Unreachable
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
sampleRate,
|
||||
channelConfig,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
track.play();
|
||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
||||
track.play();
|
||||
|
||||
// Successfully created working AudioTrack. We're done here.
|
||||
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
// Try to release the AudioTrack if we got far enough
|
||||
e.printStackTrace();
|
||||
try {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
track = null;
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||
track.write(audioData, offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void streamClosing() {
|
||||
if (track != null) {
|
||||
track.release();
|
||||
if (track == null) {
|
||||
// Couldn't create any audio track for playback
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playDecodedAudio(short[] audioData) {
|
||||
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
|
||||
if (MoonBridge.getPendingAudioFrames() < 8) {
|
||||
// This will block until the write is completed. That can cause a backlog
|
||||
// of pending audio data, so we do the above check to be able to bound
|
||||
// latency at 40 ms in that situation.
|
||||
track.write(audioData, 0, audioData.length);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void stop() {}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Immediately drop all pending data
|
||||
track.pause();
|
||||
track.flush();
|
||||
|
||||
track.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Provider;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
@@ -54,10 +53,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
private static final Object globalCryptoLock = new Object();
|
||||
|
||||
static {
|
||||
// Install the Bouncy Castle provider
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
private static final Provider bcProvider = new BouncyCastleProvider();
|
||||
|
||||
public AndroidCryptoProvider(Context c) {
|
||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||
@@ -96,10 +92,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
pemCertBytes = certBytes;
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||
} catch (CertificateException e) {
|
||||
// May happen if the cert is corrupt
|
||||
@@ -113,10 +109,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
// May happen if the key is corrupt
|
||||
LimeLog.warning("Corrupted key");
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -129,17 +121,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e1) {
|
||||
// Should never happen
|
||||
e1.printStackTrace();
|
||||
return false;
|
||||
} catch (NoSuchProviderException e) {
|
||||
// Should never happen
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
@@ -160,13 +148,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
try {
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||
} catch (Exception e) {
|
||||
// Nothing should go wrong here
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
LimeLog.info("Generated a new key pair");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,15 +2,12 @@ package com.limelight.binding.input;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.KeycodeTranslator;
|
||||
|
||||
/**
|
||||
* Class to translate a Android key code into the codes GFE is expecting
|
||||
* @author Diego Waxemberg
|
||||
* @author Cameron Gutman
|
||||
*/
|
||||
public class KeyboardTranslator extends KeycodeTranslator {
|
||||
public class KeyboardTranslator {
|
||||
|
||||
/**
|
||||
* GFE's prefix for every key code
|
||||
@@ -21,22 +18,15 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
public static final int VK_9 = 57;
|
||||
public static final int VK_A = 65;
|
||||
public static final int VK_Z = 90;
|
||||
public static final int VK_ALT = 18;
|
||||
public static final int VK_NUMPAD0 = 96;
|
||||
public static final int VK_BACK_SLASH = 92;
|
||||
public static final int VK_CAPS_LOCK = 20;
|
||||
public static final int VK_CLEAR = 12;
|
||||
public static final int VK_COMMA = 44;
|
||||
public static final int VK_CONTROL = 17;
|
||||
public static final int VK_BACK_SPACE = 8;
|
||||
public static final int VK_EQUALS = 61;
|
||||
public static final int VK_ESCAPE = 27;
|
||||
public static final int VK_F1 = 112;
|
||||
public static final int VK_PERIOD = 46;
|
||||
public static final int VK_INSERT = 155;
|
||||
public static final int VK_OPEN_BRACKET = 91;
|
||||
public static final int VK_WINDOWS = 524;
|
||||
public static final int VK_MINUS = 45;
|
||||
public static final int VK_END = 35;
|
||||
public static final int VK_HOME = 36;
|
||||
public static final int VK_NUM_LOCK = 144;
|
||||
@@ -46,7 +36,6 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
public static final int VK_CLOSE_BRACKET = 93;
|
||||
public static final int VK_SCROLL_LOCK = 145;
|
||||
public static final int VK_SEMICOLON = 59;
|
||||
public static final int VK_SHIFT = 16;
|
||||
public static final int VK_SLASH = 47;
|
||||
public static final int VK_SPACE = 32;
|
||||
public static final int VK_PRINTSCREEN = 154;
|
||||
@@ -58,28 +47,32 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
public static final int VK_BACK_QUOTE = 192;
|
||||
public static final int VK_QUOTE = 222;
|
||||
public static final int VK_PAUSE = 19;
|
||||
|
||||
/**
|
||||
* Constructs a new translator for the specified connection
|
||||
* @param conn the connection to which the translated codes are sent
|
||||
*/
|
||||
public KeyboardTranslator(NvConnection conn) {
|
||||
super(conn);
|
||||
|
||||
public static boolean needsShift(int keycode) {
|
||||
switch (keycode)
|
||||
{
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Translates the given keycode and returns the GFE keycode
|
||||
* @param keycode the code to be translated
|
||||
* @return a GFE keycode for the given keycode
|
||||
*/
|
||||
@Override
|
||||
public short translate(int keycode) {
|
||||
public static short translate(int keycode) {
|
||||
int translated;
|
||||
|
||||
/* There seems to be no clean mapping between Android key codes
|
||||
* and what Nvidia sends over the wire. If someone finds one,
|
||||
* I'll happily delete this code :)
|
||||
*/
|
||||
// This is a poor man's mapping between Android key codes
|
||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||
keycode <= KeyEvent.KEYCODE_9) {
|
||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||
@@ -99,8 +92,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
else {
|
||||
switch (keycode) {
|
||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||
translated = 0xA4;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||
translated = VK_ALT;
|
||||
translated = 0xA5;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_BACKSLASH:
|
||||
@@ -120,8 +116,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||
translated = 0xA2;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||
translated = VK_CONTROL;
|
||||
translated = 0xA3;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_DEL:
|
||||
@@ -131,7 +130,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
translated = 0x0d;
|
||||
break;
|
||||
|
||||
|
||||
case KeyEvent.KEYCODE_PLUS:
|
||||
case KeyEvent.KEYCODE_EQUALS:
|
||||
translated = 0xbb;
|
||||
break;
|
||||
@@ -141,23 +141,25 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||
// Nvidia maps period to delete
|
||||
translated = VK_PERIOD;
|
||||
translated = 0x2e;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_INSERT:
|
||||
translated = -1;
|
||||
translated = 0x2d;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||
translated = 0xdb;
|
||||
break;
|
||||
|
||||
|
||||
case KeyEvent.KEYCODE_META_LEFT:
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = VK_WINDOWS;
|
||||
translated = 0x5b;
|
||||
break;
|
||||
|
||||
|
||||
case KeyEvent.KEYCODE_META_RIGHT:
|
||||
translated = 0x5c;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_MINUS:
|
||||
translated = 0xbd;
|
||||
break;
|
||||
@@ -199,8 +201,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||
translated = 0xA0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||
translated = VK_SHIFT;
|
||||
translated = 0xA1;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_SLASH:
|
||||
@@ -247,7 +252,39 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
||||
case KeyEvent.KEYCODE_BREAK:
|
||||
translated = VK_PAUSE;
|
||||
break;
|
||||
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
||||
translated = 0x6F;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
||||
translated = 0x6A;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
||||
translated = 0x6D;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
||||
translated = 0x6B;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
||||
translated = 0x6E;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_AT:
|
||||
translated = 2 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_POUND:
|
||||
translated = 3 + VK_0;
|
||||
break;
|
||||
|
||||
case KeyEvent.KEYCODE_STAR:
|
||||
translated = 8 + VK_0;
|
||||
break;
|
||||
|
||||
default:
|
||||
System.out.println("No key for "+keycode);
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
|
||||
@@ -17,23 +19,27 @@ public class TouchContext {
|
||||
private boolean confirmedDrag;
|
||||
private Timer dragTimer;
|
||||
private double distanceMoved;
|
||||
private double xFactor, yFactor;
|
||||
|
||||
private final NvConnection conn;
|
||||
private final int actionIndex;
|
||||
private final double xFactor;
|
||||
private final double yFactor;
|
||||
private final int referenceWidth;
|
||||
private final int referenceHeight;
|
||||
private final View targetView;
|
||||
|
||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||
|
||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||
public TouchContext(NvConnection conn, int actionIndex,
|
||||
int referenceWidth, int referenceHeight, View view)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
this.xFactor = xFactor;
|
||||
this.yFactor = yFactor;
|
||||
this.referenceWidth = referenceWidth;
|
||||
this.referenceHeight = referenceHeight;
|
||||
this.targetView = view;
|
||||
}
|
||||
|
||||
public int getActionIndex()
|
||||
@@ -68,6 +74,10 @@ public class TouchContext {
|
||||
|
||||
public boolean touchDownEvent(int eventX, int eventY)
|
||||
{
|
||||
// Get the view dimensions to scale inputs on this touch
|
||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||
|
||||
originalTouchX = lastTouchX = eventX;
|
||||
originalTouchY = lastTouchY = eventY;
|
||||
originalTouchTime = System.currentTimeMillis();
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
private View targetView;
|
||||
|
||||
public AndroidNativePointerCaptureProvider(View targetView) {
|
||||
this.targetView = targetView;
|
||||
}
|
||||
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
super.enableCapture();
|
||||
targetView.requestPointerCapture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.disableCapture();
|
||||
targetView.releasePointerCapture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCapturingActive() {
|
||||
return targetView.hasPointerCapture();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
float x = event.getX();
|
||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||
x += event.getHistoricalX(i);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
float y = event.getY();
|
||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||
y += event.getHistoricalY(i);
|
||||
}
|
||||
return y;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||
private ViewGroup rootViewGroup;
|
||||
private Context context;
|
||||
|
||||
public AndroidPointerIconCaptureProvider(Activity activity) {
|
||||
this.context = activity;
|
||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||
}
|
||||
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||
}
|
||||
|
||||
private void setPointerIconOnAllViews(PointerIcon icon) {
|
||||
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
||||
View view = rootViewGroup.getChildAt(i);
|
||||
view.setPointerIcon(icon);
|
||||
}
|
||||
rootViewGroup.setPointerIcon(icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
super.enableCapture();
|
||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.disableCapture();
|
||||
setPointerIconOnAllViews(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
||||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.LimelightBuildProps;
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
|
||||
import com.limelight.binding.input.evdev.EvdevListener;
|
||||
|
||||
public class InputCaptureManager {
|
||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Android O+ native mouse capture");
|
||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
||||
}
|
||||
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
||||
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
||||
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||
return new ShieldCaptureProvider(activity);
|
||||
}
|
||||
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Evdev mouse capture");
|
||||
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
|
||||
}
|
||||
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
||||
// Android N's native capture can't capture over system UI elements
|
||||
// so we want to only use it if there's no other option.
|
||||
LimeLog.info("Using Android N+ pointer hiding");
|
||||
return new AndroidPointerIconCaptureProvider(activity);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Mouse capture not available");
|
||||
return new NullCaptureProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
public abstract class InputCaptureProvider {
|
||||
protected boolean isCapturing;
|
||||
|
||||
public void enableCapture() {
|
||||
isCapturing = true;
|
||||
}
|
||||
public void disableCapture() {
|
||||
isCapturing = false;
|
||||
}
|
||||
|
||||
public void destroy() {}
|
||||
|
||||
public boolean isCapturingEnabled() {
|
||||
return isCapturing;
|
||||
}
|
||||
|
||||
public boolean isCapturingActive() {
|
||||
return isCapturing;
|
||||
}
|
||||
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
|
||||
public class NullCaptureProvider extends InputCaptureProvider {}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
||||
// mode without having to grab the input device (which requires root). The data comes in the form
|
||||
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
||||
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
||||
//
|
||||
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
||||
|
||||
public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||
private static boolean nvExtensionSupported;
|
||||
private static Method methodSetCursorVisibility;
|
||||
private static int AXIS_RELATIVE_X;
|
||||
private static int AXIS_RELATIVE_Y;
|
||||
|
||||
private Context context;
|
||||
|
||||
static {
|
||||
try {
|
||||
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
||||
|
||||
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
||||
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
||||
|
||||
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
||||
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
||||
|
||||
nvExtensionSupported = true;
|
||||
} catch (Exception e) {
|
||||
nvExtensionSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
public ShieldCaptureProvider(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return nvExtensionSupported;
|
||||
}
|
||||
|
||||
private boolean setCursorVisibility(boolean visible) {
|
||||
try {
|
||||
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
||||
return true;
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
super.enableCapture();
|
||||
setCursorVisibility(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
super.disableCapture();
|
||||
setCursorVisibility(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_X);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
public abstract class AbstractController {
|
||||
|
||||
private final int deviceId;
|
||||
|
||||
private UsbDriverListener listener;
|
||||
|
||||
protected short buttonFlags;
|
||||
protected float leftTrigger, rightTrigger;
|
||||
protected float rightStickX, rightStickY;
|
||||
protected float leftStickX, leftStickY;
|
||||
|
||||
public int getControllerId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
protected void setButtonFlag(int buttonFlag, int data) {
|
||||
if (data != 0) {
|
||||
buttonFlags |= buttonFlag;
|
||||
}
|
||||
else {
|
||||
buttonFlags &= ~buttonFlag;
|
||||
}
|
||||
}
|
||||
|
||||
protected void reportInput() {
|
||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||
}
|
||||
|
||||
public abstract boolean start();
|
||||
public abstract void stop();
|
||||
|
||||
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||
this.deviceId = deviceId;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||
|
||||
protected void notifyDeviceRemoved() {
|
||||
listener.deviceRemoved(this);
|
||||
}
|
||||
|
||||
protected void notifyDeviceAdded() {
|
||||
listener.deviceAdded(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public abstract class AbstractXboxController extends AbstractController {
|
||||
protected final UsbDevice device;
|
||||
protected final UsbDeviceConnection connection;
|
||||
|
||||
private Thread inputThread;
|
||||
private boolean stopped;
|
||||
|
||||
protected UsbEndpoint inEndpt, outEndpt;
|
||||
|
||||
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(deviceId, listener);
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private Thread createInputThread() {
|
||||
return new Thread() {
|
||||
public void run() {
|
||||
while (!isInterrupted() && !stopped) {
|
||||
byte[] buffer = new byte[64];
|
||||
|
||||
int res;
|
||||
|
||||
//
|
||||
// There's no way that I can tell to determine if a device has failed
|
||||
// or if the timeout has simply expired. We'll check how long the transfer
|
||||
// took to fail and assume the device failed if it happened before the timeout
|
||||
// expired.
|
||||
//
|
||||
|
||||
do {
|
||||
// Read the next input state packet
|
||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||
|
||||
// If we get a zero length response, treat it as an error
|
||||
if (res == 0) {
|
||||
res = -1;
|
||||
}
|
||||
|
||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||
LimeLog.warning("Detected device I/O error");
|
||||
AbstractXboxController.this.stop();
|
||||
break;
|
||||
}
|
||||
} while (res == -1 && !isInterrupted() && !stopped);
|
||||
|
||||
if (res == -1 || stopped) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
||||
// Report input if handleRead() returns true
|
||||
reportInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
// Force claim all interfaces
|
||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||
UsbInterface iface = device.getInterface(i);
|
||||
|
||||
if (!connection.claimInterface(iface, true)) {
|
||||
LimeLog.warning("Failed to claim interfaces");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the endpoints
|
||||
UsbInterface iface = device.getInterface(0);
|
||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||
if (inEndpt != null) {
|
||||
LimeLog.warning("Found duplicate IN endpoint");
|
||||
return false;
|
||||
}
|
||||
inEndpt = endpt;
|
||||
}
|
||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||
if (outEndpt != null) {
|
||||
LimeLog.warning("Found duplicate OUT endpoint");
|
||||
return false;
|
||||
}
|
||||
outEndpt = endpt;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present
|
||||
if (inEndpt == null || outEndpt == null) {
|
||||
LimeLog.warning("Missing required endpoint");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run the init function
|
||||
if (!doInit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Report that we're added _before_ starting the input thread
|
||||
notifyDeviceAdded();
|
||||
|
||||
// Start listening for controller input
|
||||
inputThread = createInputThread();
|
||||
inputThread.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopped = true;
|
||||
|
||||
// Cancel any rumble effects
|
||||
rumble((short)0, (short)0);
|
||||
|
||||
// Stop the input thread
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
inputThread = null;
|
||||
}
|
||||
|
||||
// Close the USB connection
|
||||
connection.close();
|
||||
|
||||
// Report the device removed
|
||||
notifyDeviceRemoved();
|
||||
}
|
||||
|
||||
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||
protected abstract boolean doInit();
|
||||
}
|
||||
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
|
||||
float rightStickX, float rightStickY,
|
||||
float leftTrigger, float rightTrigger);
|
||||
|
||||
void deviceRemoved(int controllerId);
|
||||
void deviceAdded(int controllerId);
|
||||
void deviceRemoved(AbstractController controller);
|
||||
void deviceAdded(AbstractController controller);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,15 @@ import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.view.InputDevice;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.R;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -20,14 +28,15 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
"com.limelight.USB_PERMISSION";
|
||||
|
||||
private UsbManager usbManager;
|
||||
private PreferenceConfiguration prefConfig;
|
||||
|
||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||
|
||||
private final ArrayList<XboxOneController> controllers = new ArrayList<>();
|
||||
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
||||
|
||||
private UsbDriverListener listener;
|
||||
private static int nextDeviceId;
|
||||
private int nextDeviceId;
|
||||
|
||||
@Override
|
||||
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
||||
@@ -38,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceRemoved(int controllerId) {
|
||||
public void deviceRemoved(AbstractController controller) {
|
||||
// Remove the the controller from our list (if not removed already)
|
||||
for (XboxOneController controller : controllers) {
|
||||
if (controller.getControllerId() == controllerId) {
|
||||
controllers.remove(controller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
controllers.remove(controller);
|
||||
|
||||
// Call through to the client's listener
|
||||
if (listener != null) {
|
||||
listener.deviceRemoved(controllerId);
|
||||
listener.deviceRemoved(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deviceAdded(int controllerId) {
|
||||
public void deviceAdded(AbstractController controller) {
|
||||
// Call through to the client's listener
|
||||
if (listener != null) {
|
||||
listener.deviceAdded(controllerId);
|
||||
listener.deviceAdded(controller);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,14 +72,26 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Initial attachment broadcast
|
||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
|
||||
// Continue the state machine
|
||||
handleUsbDeviceState(device);
|
||||
// shouldClaimDevice() looks at the kernel's enumerated input
|
||||
// devices to make its decision about whether to prompt to take
|
||||
// control of the device. The kernel bringing up the input stack
|
||||
// may race with this callback and cause us to prompt when the
|
||||
// kernel is capable of running the device. Let's post a delayed
|
||||
// message to process this state change to allow the kernel
|
||||
// some time to bring up the stack.
|
||||
new Handler().postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Continue the state machine
|
||||
handleUsbDeviceState(device);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
// Subsequent permission dialog completion intent
|
||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
|
||||
// If we got this far, we've already found we're able to handle this device
|
||||
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||
@@ -91,8 +107,8 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Report all controllerMap that already exist
|
||||
if (listener != null) {
|
||||
for (XboxOneController controller : controllers) {
|
||||
listener.deviceAdded(controller.getControllerId());
|
||||
for (AbstractController controller : controllers) {
|
||||
listener.deviceAdded(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,19 +116,46 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
private void handleUsbDeviceState(UsbDevice device) {
|
||||
// Are we able to operate it?
|
||||
if (XboxOneController.canClaimDevice(device)) {
|
||||
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
||||
// Do we have permission yet?
|
||||
if (!usbManager.hasPermission(device)) {
|
||||
// Let's ask for permission
|
||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||
try {
|
||||
// This function is not documented as throwing any exceptions (denying access
|
||||
// is indicated by calling the PendingIntent with a false result). However,
|
||||
// Samsung Knox has some policies which block this request, but rather than
|
||||
// just returning a false result or returning 0 enumerated devices,
|
||||
// they throw an undocumented SecurityException from this call, crashing
|
||||
// the whole app. :(
|
||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
||||
} catch (SecurityException e) {
|
||||
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the device
|
||||
UsbDeviceConnection connection = usbManager.openDevice(device);
|
||||
if (connection == null) {
|
||||
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to initialize it
|
||||
XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||
|
||||
AbstractController controller;
|
||||
|
||||
if (XboxOneController.canClaimDevice(device)) {
|
||||
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||
}
|
||||
else if (Xbox360Controller.canClaimDevice(device)) {
|
||||
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
||||
}
|
||||
else {
|
||||
// Unreachable
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the controller
|
||||
if (!controller.start()) {
|
||||
connection.close();
|
||||
return;
|
||||
@@ -123,9 +166,42 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isRecognizedInputDevice(UsbDevice device) {
|
||||
// On KitKat and later, we can determine if this VID and PID combo
|
||||
// matches an existing input device and defer to the built-in controller
|
||||
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
for (int id : InputDevice.getDeviceIds()) {
|
||||
InputDevice inputDev = InputDevice.getDevice(id);
|
||||
if (inputDev == null) {
|
||||
// Device was removed while looping
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||
inputDev.getProductId() == device.getProductId()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||
// if we know the kernel isn't already driving this device.
|
||||
return XboxOneController.canClaimDevice(device) ||
|
||||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||
this.prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||
|
||||
// Register for USB attach broadcasts and permission completions
|
||||
IntentFilter filter = new IntentFilter();
|
||||
@@ -135,7 +211,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Enumerate existing devices
|
||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||
if (XboxOneController.canClaimDevice(dev)) {
|
||||
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
|
||||
// Start the process of claiming this device
|
||||
handleUsbDeviceState(dev);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class Xbox360Controller extends AbstractXboxController {
|
||||
private static final int XB360_IFACE_SUBCLASS = 93;
|
||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||
|
||||
private static final int[] SUPPORTED_VENDORS = {
|
||||
0x0079, // GPD Win 2
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
0x056e, // Elecom
|
||||
0x06a3, // Saitek
|
||||
0x0738, // Mad Catz
|
||||
0x07ff, // Mad Catz
|
||||
0x0e6f, // Unknown
|
||||
0x0f0d, // Hori
|
||||
0x1038, // SteelSeries
|
||||
0x11c9, // Nacon
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
0x146b, // BigBen
|
||||
0x1532, // Razer Sabertooth
|
||||
0x15e4, // Numark
|
||||
0x162e, // Joytech
|
||||
0x1689, // Razer Onza
|
||||
0x1bad, // Harmonix
|
||||
0x24c6, // PowerA
|
||||
};
|
||||
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (device.getVendorId() == supportedVid &&
|
||||
device.getInterfaceCount() >= 1 &&
|
||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(device, connection, deviceId, listener);
|
||||
}
|
||||
|
||||
private int unsignByte(byte b) {
|
||||
if (b < 0) {
|
||||
return b + 256;
|
||||
}
|
||||
else {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleRead(ByteBuffer buffer) {
|
||||
if (buffer.limit() < 14) {
|
||||
LimeLog.severe("Read too small: "+buffer.limit());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip first short
|
||||
buffer.position(buffer.position() + 2);
|
||||
|
||||
// DPAD
|
||||
byte b = buffer.get();
|
||||
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
||||
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
||||
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
||||
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
||||
|
||||
// Start/Select
|
||||
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
||||
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
||||
|
||||
// LS/RS
|
||||
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||
|
||||
// ABXY buttons
|
||||
b = buffer.get();
|
||||
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||
|
||||
// LB/RB
|
||||
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
||||
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
||||
|
||||
// Xbox button
|
||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
||||
|
||||
// Triggers
|
||||
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||
|
||||
// Left stick
|
||||
leftStickX = buffer.getShort() / 32767.0f;
|
||||
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||
|
||||
// Right stick
|
||||
rightStickX = buffer.getShort() / 32767.0f;
|
||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||
|
||||
// Return true to send input
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean sendLedCommand(byte command) {
|
||||
byte[] commandBuffer = {0x01, 0x03, command};
|
||||
|
||||
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
||||
if (res != commandBuffer.length) {
|
||||
LimeLog.warning("LED set transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doInit() {
|
||||
// Turn the LED on corresponding to our device ID
|
||||
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
||||
|
||||
// No need to fail init if the LED command fails
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||
byte[] data = {
|
||||
0x00, 0x08, 0x00,
|
||||
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
|
||||
0x00, 0x00, 0x00
|
||||
};
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Rumble transfer failed: "+res);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,60 +3,55 @@ package com.limelight.binding.input.driver;
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class XboxOneController {
|
||||
private final UsbDevice device;
|
||||
private final UsbDeviceConnection connection;
|
||||
private final int deviceId;
|
||||
public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
private Thread inputThread;
|
||||
private UsbDriverListener listener;
|
||||
private boolean stopped;
|
||||
|
||||
private short buttonFlags;
|
||||
private float leftTrigger, rightTrigger;
|
||||
private float rightStickX, rightStickY;
|
||||
private float leftStickX, leftStickY;
|
||||
|
||||
private static final int MICROSOFT_VID = 0x045e;
|
||||
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||
private static final int XB1_IFACE_PROTOCOL = 208;
|
||||
|
||||
// FIXME: odata_serial
|
||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
private static final int[] SUPPORTED_VENDORS = {
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0e6f, // Unknown
|
||||
0x0f0d, // Hori
|
||||
0x1532, // Razer Wildcat
|
||||
0x24c6, // PowerA
|
||||
};
|
||||
|
||||
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
||||
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
||||
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
||||
private static final byte[] PDP_INIT2 = {0x06, 0x20, 0x00, 0x02, 0x01, 0x00};
|
||||
private static final byte[] RUMBLE_INIT1 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
||||
0x1D, 0x1D, (byte)0xFF, 0x00, 0x00};
|
||||
private static final byte[] RUMBLE_INIT2 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
private static InitPacket[] INIT_PKTS = {
|
||||
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
||||
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
||||
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT1),
|
||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT1),
|
||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT2),
|
||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT2),
|
||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
|
||||
};
|
||||
|
||||
private byte seqNum = 0;
|
||||
|
||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
this.deviceId = deviceId;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public int getControllerId() {
|
||||
return this.deviceId;
|
||||
}
|
||||
|
||||
private void setButtonFlag(int buttonFlag, int data) {
|
||||
if (data != 0) {
|
||||
buttonFlags |= buttonFlag;
|
||||
}
|
||||
else {
|
||||
buttonFlags &= ~buttonFlag;
|
||||
}
|
||||
}
|
||||
|
||||
private void reportInput() {
|
||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||
super(device, connection, deviceId, listener);
|
||||
}
|
||||
|
||||
private void processButtons(ByteBuffer buffer) {
|
||||
@@ -90,144 +85,105 @@ public class XboxOneController {
|
||||
|
||||
rightStickX = buffer.getShort() / 32767.0f;
|
||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||
|
||||
reportInput();
|
||||
}
|
||||
|
||||
private void processPacket(ByteBuffer buffer) {
|
||||
private void ackModeReport(byte seqNum) {
|
||||
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleRead(ByteBuffer buffer) {
|
||||
switch (buffer.get())
|
||||
{
|
||||
case 0x20:
|
||||
buffer.position(buffer.position()+3);
|
||||
processButtons(buffer);
|
||||
break;
|
||||
return true;
|
||||
|
||||
case 0x07:
|
||||
buffer.position(buffer.position() + 3);
|
||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||
reportInput();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void startInputThread(final UsbEndpoint inEndpt) {
|
||||
inputThread = new Thread() {
|
||||
public void run() {
|
||||
while (!isInterrupted() && !stopped) {
|
||||
byte[] buffer = new byte[64];
|
||||
|
||||
int res;
|
||||
|
||||
//
|
||||
// There's no way that I can tell to determine if a device has failed
|
||||
// or if the timeout has simply expired. We'll check how long the transfer
|
||||
// took to fail and assume the device failed if it happened before the timeout
|
||||
// expired.
|
||||
//
|
||||
|
||||
do {
|
||||
// Read the next input state packet
|
||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||
LimeLog.warning("Detected device I/O error");
|
||||
XboxOneController.this.stop();
|
||||
break;
|
||||
}
|
||||
} while (res == -1 && !isInterrupted() && !stopped);
|
||||
|
||||
if (res == -1 || stopped) {
|
||||
break;
|
||||
}
|
||||
|
||||
processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
|
||||
// The Xbox One S controller needs acks for mode reports otherwise
|
||||
// it retransmits them forever.
|
||||
if (buffer.get() == 0x30) {
|
||||
ackModeReport(buffer.get());
|
||||
buffer.position(buffer.position() + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
inputThread.setName("Xbox One Controller - Input Thread");
|
||||
inputThread.start();
|
||||
else {
|
||||
buffer.position(buffer.position() + 2);
|
||||
}
|
||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
// Force claim all interfaces
|
||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||
UsbInterface iface = device.getInterface(i);
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (device.getVendorId() == supportedVid &&
|
||||
device.getInterfaceCount() >= 1 &&
|
||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!connection.claimInterface(iface, true)) {
|
||||
LimeLog.warning("Failed to claim interfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doInit() {
|
||||
// Send all applicable init packets
|
||||
for (InitPacket pkt : INIT_PKTS) {
|
||||
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
|
||||
|
||||
// Populate sequence number
|
||||
data[2] = seqNum++;
|
||||
|
||||
// Send the initialization packet
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Initialization transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the endpoints
|
||||
UsbEndpoint outEndpt = null;
|
||||
UsbEndpoint inEndpt = null;
|
||||
UsbInterface iface = device.getInterface(0);
|
||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||
if (inEndpt != null) {
|
||||
LimeLog.warning("Found duplicate IN endpoint");
|
||||
return false;
|
||||
}
|
||||
inEndpt = endpt;
|
||||
}
|
||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||
if (outEndpt != null) {
|
||||
LimeLog.warning("Found duplicate OUT endpoint");
|
||||
return false;
|
||||
}
|
||||
outEndpt = endpt;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present
|
||||
if (inEndpt == null || outEndpt == null) {
|
||||
LimeLog.warning("Missing required endpoint");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the initialization packet
|
||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||
if (res != XB1_INIT_DATA.length) {
|
||||
LimeLog.warning("Initialization transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for controller input
|
||||
startInputThread(inEndpt);
|
||||
|
||||
// Report this device added via the listener
|
||||
listener.deviceAdded(deviceId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (stopped) {
|
||||
return;
|
||||
@Override
|
||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
||||
byte[] data = {
|
||||
0x09, 0x00, seqNum++, 0x09, 0x00,
|
||||
0x0F, 0x00, 0x00,
|
||||
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
|
||||
(byte)0xFF, 0x00, (byte)0xFF
|
||||
};
|
||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
||||
if (res != data.length) {
|
||||
LimeLog.warning("Rumble transfer failed: "+res);
|
||||
}
|
||||
|
||||
stopped = true;
|
||||
|
||||
// Stop the input thread
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
inputThread = null;
|
||||
}
|
||||
|
||||
// Report the device removed
|
||||
listener.deviceRemoved(deviceId);
|
||||
|
||||
// Close the USB connection
|
||||
connection.close();
|
||||
}
|
||||
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
return device.getVendorId() == MICROSOFT_VID &&
|
||||
device.getInterfaceCount() >= 1 &&
|
||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
|
||||
private static class InitPacket {
|
||||
final int vendorId;
|
||||
final int productId;
|
||||
final byte[] data;
|
||||
|
||||
InitPacket(int vendorId, int productId, byte[] data) {
|
||||
this.vendorId = vendorId;
|
||||
this.productId = productId;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.limelight.LimelightBuildProps;
|
||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||
|
||||
public class EvdevCaptureProviderShim {
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return LimelightBuildProps.ROOT_BUILD;
|
||||
}
|
||||
|
||||
// We need to construct our capture provider using reflection because it isn't included in non-root builds
|
||||
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
||||
try {
|
||||
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
|
||||
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class EvdevHandler {
|
||||
|
||||
private final String absolutePath;
|
||||
private final EvdevListener listener;
|
||||
private boolean shutdown = false;
|
||||
private int fd = -1;
|
||||
|
||||
private final Thread handlerThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// All the finally blocks here make this code look like a mess
|
||||
// but it's important that we get this right to avoid causing
|
||||
// system-wide input problems.
|
||||
|
||||
// Open the /dev/input/eventX file
|
||||
fd = EvdevReader.open(absolutePath);
|
||||
if (fd == -1) {
|
||||
LimeLog.warning("Unable to open "+absolutePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if it's a mouse or keyboard, but not a gamepad
|
||||
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||
EvdevReader.isGamepad(fd)) {
|
||||
// We only handle keyboards and mice
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab it for ourselves
|
||||
if (!EvdevReader.grab(fd)) {
|
||||
LimeLog.warning("Unable to grab "+absolutePath);
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||
|
||||
try {
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
byte deltaScroll = 0;
|
||||
|
||||
while (!isInterrupted() && !shutdown) {
|
||||
EvdevEvent event = EvdevReader.read(fd, buffer);
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.type)
|
||||
{
|
||||
case EvdevEvent.EV_SYN:
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
listener.mouseMove(deltaX, deltaY);
|
||||
deltaX = deltaY = 0;
|
||||
}
|
||||
if (deltaScroll != 0) {
|
||||
listener.mouseScroll(deltaScroll);
|
||||
deltaScroll = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_REL:
|
||||
switch (event.code)
|
||||
{
|
||||
case EvdevEvent.REL_X:
|
||||
deltaX = event.value;
|
||||
break;
|
||||
case EvdevEvent.REL_Y:
|
||||
deltaY = event.value;
|
||||
break;
|
||||
case EvdevEvent.REL_WHEEL:
|
||||
deltaScroll = (byte) event.value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_KEY:
|
||||
switch (event.code)
|
||||
{
|
||||
case EvdevEvent.BTN_LEFT:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||
event.value != 0);
|
||||
break;
|
||||
case EvdevEvent.BTN_MIDDLE:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||
event.value != 0);
|
||||
break;
|
||||
case EvdevEvent.BTN_RIGHT:
|
||||
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||
event.value != 0);
|
||||
break;
|
||||
|
||||
case EvdevEvent.BTN_SIDE:
|
||||
case EvdevEvent.BTN_EXTRA:
|
||||
case EvdevEvent.BTN_FORWARD:
|
||||
case EvdevEvent.BTN_BACK:
|
||||
case EvdevEvent.BTN_TASK:
|
||||
// Other unhandled mouse buttons
|
||||
break;
|
||||
|
||||
default:
|
||||
// We got some unrecognized button. This means
|
||||
// someone is trying to use the other device in this
|
||||
// "combination" input device. We'll try to handle
|
||||
// it via keyboard, but we're not going to disconnect
|
||||
// if we can't
|
||||
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||
if (keyCode != 0) {
|
||||
listener.keyboardEvent(event.value != 0, keyCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case EvdevEvent.EV_MSC:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Release our grab
|
||||
EvdevReader.ungrab(fd);
|
||||
}
|
||||
} finally {
|
||||
// Close the file
|
||||
EvdevReader.close(fd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
||||
this.absolutePath = absolutePath;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
handlerThread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Close the fd. It doesn't matter if this races
|
||||
// with the handler thread. We'll close this out from
|
||||
// under the thread to wake it up
|
||||
if (fd != -1) {
|
||||
EvdevReader.close(fd);
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
handlerThread.interrupt();
|
||||
|
||||
try {
|
||||
handlerThread.join();
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
public void notifyDeleted() {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
public interface EvdevListener {
|
||||
public static final int BUTTON_LEFT = 1;
|
||||
public static final int BUTTON_MIDDLE = 2;
|
||||
public static final int BUTTON_RIGHT = 3;
|
||||
int BUTTON_LEFT = 1;
|
||||
int BUTTON_MIDDLE = 2;
|
||||
int BUTTON_RIGHT = 3;
|
||||
int BUTTON_X1 = 4;
|
||||
int BUTTON_X2 = 5;
|
||||
|
||||
public void mouseMove(int deltaX, int deltaY);
|
||||
public void mouseButtonEvent(int buttonId, boolean down);
|
||||
public void mouseScroll(byte amount);
|
||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||
void mouseMove(int deltaX, int deltaY);
|
||||
void mouseButtonEvent(int buttonId, boolean down);
|
||||
void mouseScroll(byte amount);
|
||||
void keyboardEvent(boolean buttonDown, short keyCode);
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class EvdevReader {
|
||||
static {
|
||||
System.loadLibrary("evdev_reader");
|
||||
}
|
||||
|
||||
public static void patchSeLinuxPolicies() {
|
||||
//
|
||||
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
|
||||
// We should probably do something clever with a separate daemon and talk via a localhost
|
||||
// socket. We don't return the SELinux policies back to default after we're done which I feel
|
||||
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
|
||||
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
|
||||
// normally be able to do with SELinux enforcing on Lollipop.
|
||||
//
|
||||
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
|
||||
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
|
||||
// 4.4 and later to do live SELinux policy changes.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
EvdevShell shell = EvdevShell.getInstance();
|
||||
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
|
||||
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
||||
}
|
||||
}
|
||||
|
||||
// Requires root to chmod /dev/input/eventX
|
||||
public static void setPermissions(String[] files, int octalPermissions) {
|
||||
EvdevShell shell = EvdevShell.getInstance();
|
||||
|
||||
for (String file : files) {
|
||||
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the fd to be passed to other function or -1 on error
|
||||
public static native int open(String fileName);
|
||||
|
||||
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||
public static native boolean grab(int fd);
|
||||
public static native boolean ungrab(int fd);
|
||||
|
||||
// Used for checking device capabilities
|
||||
public static native boolean hasRelAxis(int fd, short axis);
|
||||
public static native boolean hasAbsAxis(int fd, short axis);
|
||||
public static native boolean hasKey(int fd, short key);
|
||||
|
||||
public static boolean isMouse(int fd) {
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||
}
|
||||
|
||||
public static boolean isAlphaKeyboard(int fd) {
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||
}
|
||||
|
||||
public static boolean isGamepad(int fd) {
|
||||
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||
}
|
||||
|
||||
// Returns the bytes read or -1 on error
|
||||
private static native int read(int fd, byte[] buffer);
|
||||
|
||||
// Takes a byte buffer to use to read the output into.
|
||||
// This buffer MUST be in native byte order and at least
|
||||
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||
int bytesRead = read(fd, buffer.array());
|
||||
if (bytesRead < 0) {
|
||||
LimeLog.warning("Failed to read: "+bytesRead);
|
||||
return null;
|
||||
}
|
||||
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||
LimeLog.warning("Short read: "+bytesRead);
|
||||
return null;
|
||||
}
|
||||
|
||||
buffer.limit(bytesRead);
|
||||
buffer.rewind();
|
||||
|
||||
// Throw away the time stamp
|
||||
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||
buffer.getLong();
|
||||
buffer.getLong();
|
||||
} else {
|
||||
buffer.getInt();
|
||||
buffer.getInt();
|
||||
}
|
||||
|
||||
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||
}
|
||||
|
||||
// Closes the fd from open()
|
||||
public static native int close(int fd);
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.UUID;
|
||||
|
||||
public class EvdevShell {
|
||||
private OutputStream stdin;
|
||||
private InputStream stdout;
|
||||
private Process shell;
|
||||
private final String uuidString = UUID.randomUUID().toString();
|
||||
|
||||
private static final EvdevShell globalShell = new EvdevShell();
|
||||
|
||||
public static EvdevShell getInstance() {
|
||||
return globalShell;
|
||||
}
|
||||
|
||||
public void startShell() {
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
|
||||
try {
|
||||
// Redirect stderr to stdout
|
||||
builder.redirectErrorStream(true);
|
||||
shell = builder.start();
|
||||
|
||||
stdin = shell.getOutputStream();
|
||||
stdout = shell.getInputStream();
|
||||
} catch (IOException e) {
|
||||
// This is unexpected
|
||||
e.printStackTrace();
|
||||
|
||||
// Kill the shell if it spawned
|
||||
if (stdin != null) {
|
||||
try {
|
||||
stdin.close();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
} finally {
|
||||
stdin = null;
|
||||
}
|
||||
}
|
||||
if (stdout != null) {
|
||||
try {
|
||||
stdout.close();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
} finally {
|
||||
stdout = null;
|
||||
}
|
||||
}
|
||||
if (shell != null) {
|
||||
shell.destroy();
|
||||
shell = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void runCommand(String command) {
|
||||
if (shell == null) {
|
||||
// Shell never started
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Write the command followed by an echo with our UUID
|
||||
stdin.write((command+'\n').getBytes("UTF-8"));
|
||||
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
|
||||
stdin.flush();
|
||||
|
||||
// This is the only command in flight so we can use a scanner
|
||||
// without worrying about it eating too many characters
|
||||
Scanner scanner = new Scanner(stdout);
|
||||
while (scanner.hasNext()) {
|
||||
if (scanner.next().contains(uuidString)) {
|
||||
// Our command ran
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void stopShell() throws InterruptedException {
|
||||
boolean exitWritten = false;
|
||||
|
||||
if (shell == null) {
|
||||
// Shell never started
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
stdin.write("exit\n".getBytes("UTF-8"));
|
||||
exitWritten = true;
|
||||
} catch (IOException e) {
|
||||
// We'll destroy the process without
|
||||
// waiting for it to terminate since
|
||||
// we don't know whether our exit command made it
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (exitWritten) {
|
||||
try {
|
||||
shell.waitFor();
|
||||
} finally {
|
||||
shell.destroy();
|
||||
}
|
||||
}
|
||||
else {
|
||||
shell.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import android.os.FileObserver;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class EvdevWatcher {
|
||||
private static final String PATH = "/dev/input";
|
||||
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||
|
||||
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||
private boolean shutdown = false;
|
||||
private boolean init = false;
|
||||
private boolean ungrabbed = false;
|
||||
private EvdevListener listener;
|
||||
private Thread startThread;
|
||||
|
||||
private static boolean patchedSeLinuxPolicies = false;
|
||||
|
||||
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||
@Override
|
||||
public void onEvent(int event, String fileName) {
|
||||
if (fileName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (handlers) {
|
||||
if (shutdown) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((event & FileObserver.CREATE) != 0) {
|
||||
LimeLog.info("Starting evdev handler for "+fileName);
|
||||
|
||||
if (!init) {
|
||||
// If this a real new device, update permissions again so we can read it
|
||||
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
|
||||
}
|
||||
|
||||
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||
|
||||
// If we're ungrabbed now, don't start the handler
|
||||
if (!ungrabbed) {
|
||||
handler.start();
|
||||
}
|
||||
|
||||
handlers.put(fileName, handler);
|
||||
}
|
||||
|
||||
if ((event & FileObserver.DELETE) != 0) {
|
||||
LimeLog.info("Halting evdev handler for "+fileName);
|
||||
|
||||
EvdevHandler handler = handlers.remove(fileName);
|
||||
if (handler != null) {
|
||||
handler.notifyDeleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevWatcher(EvdevListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
private File[] rundownWithPermissionsChange(int newPermissions) {
|
||||
// Rundown existing files
|
||||
File devInputDir = new File(PATH);
|
||||
File[] files = devInputDir.listFiles();
|
||||
if (files == null) {
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
// Set desired permissions
|
||||
String[] filePaths = new String[files.length];
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
filePaths[i] = files[i].getAbsolutePath();
|
||||
}
|
||||
EvdevReader.setPermissions(filePaths, newPermissions);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
public void ungrabAll() {
|
||||
synchronized (handlers) {
|
||||
// Note that we're ungrabbed for now
|
||||
ungrabbed = true;
|
||||
|
||||
// Stop all handlers
|
||||
for (EvdevHandler handler : handlers.values()) {
|
||||
handler.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void regrabAll() {
|
||||
synchronized (handlers) {
|
||||
// We're regrabbing everything now
|
||||
ungrabbed = false;
|
||||
|
||||
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
|
||||
// We need to recreate each entry since we can't reuse a stopped one
|
||||
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
|
||||
entry.getValue().start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
startThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Initialize the root shell
|
||||
EvdevShell.getInstance().startShell();
|
||||
|
||||
// Patch SELinux policies (if needed)
|
||||
if (!patchedSeLinuxPolicies) {
|
||||
EvdevReader.patchSeLinuxPolicies();
|
||||
patchedSeLinuxPolicies = true;
|
||||
}
|
||||
|
||||
// List all files and allow us access
|
||||
File[] files = rundownWithPermissionsChange(0666);
|
||||
|
||||
init = true;
|
||||
for (File f : files) {
|
||||
observer.onEvent(FileObserver.CREATE, f.getName());
|
||||
}
|
||||
|
||||
// Done with initial onEvent calls
|
||||
init = false;
|
||||
|
||||
// Start watching for new files
|
||||
observer.startWatching();
|
||||
|
||||
synchronized (startThread) {
|
||||
// Wait to be awoken again by shutdown()
|
||||
try {
|
||||
startThread.wait();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
|
||||
// Giveup eventX permissions
|
||||
rundownWithPermissionsChange(0660);
|
||||
|
||||
// Kill the root shell
|
||||
try {
|
||||
EvdevShell.getInstance().stopShell();
|
||||
} catch (InterruptedException e) {}
|
||||
}
|
||||
};
|
||||
startThread.start();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
// Let start thread cleanup on it's own sweet time
|
||||
synchronized (startThread) {
|
||||
startThread.notify();
|
||||
}
|
||||
|
||||
// Stop the observer
|
||||
observer.stopWatching();
|
||||
|
||||
synchronized (handlers) {
|
||||
// Stop creating new handlers
|
||||
shutdown = true;
|
||||
|
||||
// If we've already ungrabbed, there's nothing else to do
|
||||
if (ungrabbed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop all handlers
|
||||
for (EvdevHandler handler : handlers.values()) {
|
||||
handler.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
||||
*/
|
||||
public class AnalogStick extends VirtualControllerElement {
|
||||
|
||||
/**
|
||||
* outer radius size in percent of the ui element
|
||||
*/
|
||||
public static final int SIZE_RADIUS_COMPLETE = 90;
|
||||
/**
|
||||
* analog stick size in percent of the ui element
|
||||
*/
|
||||
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
||||
/**
|
||||
* dead zone size in percent of the ui element
|
||||
*/
|
||||
public static final int SIZE_RADIUS_DEADZONE = 90;
|
||||
/**
|
||||
* time frame for a double click
|
||||
*/
|
||||
public final static long timeoutDoubleClick = 350;
|
||||
|
||||
/**
|
||||
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
||||
*/
|
||||
public final static long timeoutDeadzone = 150;
|
||||
|
||||
/**
|
||||
* Listener interface to update registered observers.
|
||||
*/
|
||||
public interface AnalogStickListener {
|
||||
|
||||
/**
|
||||
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
||||
*
|
||||
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
||||
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
||||
*/
|
||||
void onMovement(float x, float y);
|
||||
|
||||
/**
|
||||
* onClick event will be fired on click on the analog stick
|
||||
*/
|
||||
void onClick();
|
||||
|
||||
/**
|
||||
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
||||
* stick.
|
||||
*/
|
||||
void onDoubleClick();
|
||||
|
||||
/**
|
||||
* onRevoke event will be fired on unpress of the analog stick.
|
||||
*/
|
||||
void onRevoke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Movement states of the analog sick.
|
||||
*/
|
||||
private enum STICK_STATE {
|
||||
NO_MOVEMENT,
|
||||
MOVED_IN_DEAD_ZONE,
|
||||
MOVED_ACTIVE
|
||||
}
|
||||
|
||||
/**
|
||||
* Click type states.
|
||||
*/
|
||||
private enum CLICK_STATE {
|
||||
SINGLE,
|
||||
DOUBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* configuration if the analog stick should be displayed as circle or square
|
||||
*/
|
||||
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
||||
|
||||
/**
|
||||
* outer radius, this size will be automatically updated on resize
|
||||
*/
|
||||
private float radius_complete = 0;
|
||||
/**
|
||||
* analog stick radius, this size will be automatically updated on resize
|
||||
*/
|
||||
private float radius_analog_stick = 0;
|
||||
/**
|
||||
* dead zone radius, this size will be automatically updated on resize
|
||||
*/
|
||||
private float radius_dead_zone = 0;
|
||||
|
||||
/**
|
||||
* horizontal position in relation to the center of the element
|
||||
*/
|
||||
private float relative_x = 0;
|
||||
/**
|
||||
* vertical position in relation to the center of the element
|
||||
*/
|
||||
private float relative_y = 0;
|
||||
|
||||
|
||||
private double movement_radius = 0;
|
||||
private double movement_angle = 0;
|
||||
|
||||
private float position_stick_x = 0;
|
||||
private float position_stick_y = 0;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
||||
|
||||
private List<AnalogStickListener> listeners = new ArrayList<>();
|
||||
private long timeLastClick = 0;
|
||||
|
||||
private static double getMovementRadius(float x, float y) {
|
||||
return Math.sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
private static double getAngle(float way_x, float way_y) {
|
||||
// prevent divisions by zero for corner cases
|
||||
if (way_x == 0) {
|
||||
return way_y < 0 ? Math.PI : 0;
|
||||
} else if (way_y == 0) {
|
||||
if (way_x > 0) {
|
||||
return Math.PI * 3 / 2;
|
||||
} else if (way_x < 0) {
|
||||
return Math.PI * 1 / 2;
|
||||
}
|
||||
}
|
||||
// return correct calculated angle for each quadrant
|
||||
if (way_x > 0) {
|
||||
if (way_y < 0) {
|
||||
// first quadrant
|
||||
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
||||
} else {
|
||||
// second quadrant
|
||||
return Math.PI + Math.atan((double) (way_x / way_y));
|
||||
}
|
||||
} else {
|
||||
if (way_y > 0) {
|
||||
// third quadrant
|
||||
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
||||
} else {
|
||||
// fourth quadrant
|
||||
return 0 + Math.atan((double) (-way_x / -way_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AnalogStick(VirtualController controller, Context context, int elementId) {
|
||||
super(controller, context, elementId);
|
||||
// reset stick position
|
||||
position_stick_x = getWidth() / 2;
|
||||
position_stick_y = getHeight() / 2;
|
||||
}
|
||||
|
||||
public void addAnalogStickListener(AnalogStickListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
private void notifyOnMovement(float x, float y) {
|
||||
_DBG("movement x: " + x + " movement y: " + y);
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onMovement(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnClick() {
|
||||
_DBG("click");
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnDoubleClick() {
|
||||
_DBG("double click");
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onDoubleClick();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnRevoke() {
|
||||
_DBG("revoke");
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onRevoke();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
// calculate new radius sizes depending
|
||||
radius_complete = getPercent(getCorrectWidth() / 2, 100);
|
||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onElementDraw(Canvas canvas) {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
// draw outer circle
|
||||
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
||||
paint.setColor(getDefaultColor());
|
||||
} else {
|
||||
paint.setColor(pressedColor);
|
||||
}
|
||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
||||
|
||||
paint.setColor(getDefaultColor());
|
||||
// draw dead zone
|
||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
||||
|
||||
// draw stick depending on state
|
||||
switch (stick_state) {
|
||||
case NO_MOVEMENT: {
|
||||
paint.setColor(getDefaultColor());
|
||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
||||
break;
|
||||
}
|
||||
case MOVED_IN_DEAD_ZONE:
|
||||
case MOVED_ACTIVE: {
|
||||
paint.setColor(pressedColor);
|
||||
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePosition() {
|
||||
// get 100% way
|
||||
float complete = radius_complete - radius_analog_stick;
|
||||
|
||||
// calculate relative way
|
||||
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||
|
||||
// update positions
|
||||
position_stick_x = getWidth() / 2 - correlated_x;
|
||||
position_stick_y = getHeight() / 2 - correlated_y;
|
||||
|
||||
// Stay active even if we're back in the deadzone because we know the user is actively
|
||||
// giving analog stick input and we don't want to snap back into the deadzone.
|
||||
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||
// them to make precise movements.
|
||||
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||
movement_radius > radius_dead_zone) ?
|
||||
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||
|
||||
// trigger move event if state active
|
||||
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
||||
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onElementTouchEvent(MotionEvent event) {
|
||||
// save last click state
|
||||
CLICK_STATE lastClickState = click_state;
|
||||
|
||||
// get absolute way for each axis
|
||||
relative_x = -(getWidth() / 2 - event.getX());
|
||||
relative_y = -(getHeight() / 2 - event.getY());
|
||||
|
||||
// get radius and angel of movement from center
|
||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||
movement_angle = getAngle(relative_x, relative_y);
|
||||
|
||||
// chop radius if out of outer circle and already pressed
|
||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||
// not pressed already, so ignore event from outer circle
|
||||
if (!isPressed()) {
|
||||
return false;
|
||||
}
|
||||
movement_radius = radius_complete - radius_analog_stick;
|
||||
}
|
||||
|
||||
// handle event depending on action
|
||||
switch (event.getActionMasked()) {
|
||||
// down event (touch event)
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
// set to dead zoned, will be corrected in update position if necessary
|
||||
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||
// check for double click
|
||||
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
||||
click_state = CLICK_STATE.DOUBLE;
|
||||
notifyOnDoubleClick();
|
||||
} else {
|
||||
click_state = CLICK_STATE.SINGLE;
|
||||
notifyOnClick();
|
||||
}
|
||||
// reset last click timestamp
|
||||
timeLastClick = System.currentTimeMillis();
|
||||
// set item pressed and update
|
||||
setPressed(true);
|
||||
break;
|
||||
}
|
||||
// up event (revoke touch)
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
setPressed(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPressed()) {
|
||||
// when is pressed calculate new positions (will trigger movement if necessary)
|
||||
updatePosition();
|
||||
} else {
|
||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
notifyOnRevoke();
|
||||
|
||||
// not longer pressed reset analog stick
|
||||
notifyOnMovement(0, 0);
|
||||
}
|
||||
// refresh view
|
||||
invalidate();
|
||||
// accept the touch event
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This is a digital button on screen element. It is used to get click and double click user input.
|
||||
*/
|
||||
public class DigitalButton extends VirtualControllerElement {
|
||||
|
||||
/**
|
||||
* Listener interface to update registered observers.
|
||||
*/
|
||||
public interface DigitalButtonListener {
|
||||
|
||||
/**
|
||||
* onClick event will be fired on button click.
|
||||
*/
|
||||
void onClick();
|
||||
|
||||
/**
|
||||
* onLongClick event will be fired on button long click.
|
||||
*/
|
||||
void onLongClick();
|
||||
|
||||
/**
|
||||
* onRelease event will be fired on button unpress.
|
||||
*/
|
||||
void onRelease();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private class TimerLongClickTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
onLongClickCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
||||
private String text = "";
|
||||
private int icon = -1;
|
||||
private long timerLongClickTimeout = 3000;
|
||||
private Timer timerLongClick = null;
|
||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private int layer;
|
||||
private DigitalButton movingButton = null;
|
||||
|
||||
boolean inRange(float x, float y) {
|
||||
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
|
||||
(this.getY() < y && this.getY() + this.getHeight() > y);
|
||||
}
|
||||
|
||||
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
|
||||
// check if the movement happened in the same layer
|
||||
if (movingButton.layer != this.layer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// save current pressed state
|
||||
boolean wasPressed = isPressed();
|
||||
|
||||
// check if the movement directly happened on the button
|
||||
if ((this.movingButton == null || movingButton == this.movingButton)
|
||||
&& this.inRange(x, y)) {
|
||||
// set button pressed state depending on moving button pressed state
|
||||
if (this.isPressed() != movingButton.isPressed()) {
|
||||
this.setPressed(movingButton.isPressed());
|
||||
}
|
||||
}
|
||||
// check if the movement is outside of the range and the movement button
|
||||
// is the saved moving button
|
||||
else if (movingButton == this.movingButton) {
|
||||
this.setPressed(false);
|
||||
}
|
||||
|
||||
// check if a change occurred
|
||||
if (wasPressed != isPressed()) {
|
||||
if (isPressed()) {
|
||||
// is pressed set moving button and emit click event
|
||||
this.movingButton = movingButton;
|
||||
onClickCallback();
|
||||
} else {
|
||||
// no longer pressed reset moving button and emit release event
|
||||
this.movingButton = null;
|
||||
onReleaseCallback();
|
||||
}
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkMovementForAllButtons(float x, float y) {
|
||||
for (VirtualControllerElement element : virtualController.getElements()) {
|
||||
if (element != this && element instanceof DigitalButton) {
|
||||
((DigitalButton) element).checkMovement(x, y, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
|
||||
super(controller, context, elementId);
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setIcon(int id) {
|
||||
this.icon = id;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onElementDraw(Canvas canvas) {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setTextSize(getPercent(getWidth(), 30));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
||||
|
||||
if (icon != -1) {
|
||||
Drawable d = getResources().getDrawable(icon);
|
||||
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
||||
d.draw(canvas);
|
||||
} else {
|
||||
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
||||
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickCallback() {
|
||||
_DBG("clicked");
|
||||
// notify listeners
|
||||
for (DigitalButtonListener listener : listeners) {
|
||||
listener.onClick();
|
||||
}
|
||||
|
||||
timerLongClick = new Timer();
|
||||
longClickTimerTask = new TimerLongClickTimerTask();
|
||||
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
||||
}
|
||||
|
||||
private void onLongClickCallback() {
|
||||
_DBG("long click");
|
||||
// notify listeners
|
||||
for (DigitalButtonListener listener : listeners) {
|
||||
listener.onLongClick();
|
||||
}
|
||||
}
|
||||
|
||||
private void onReleaseCallback() {
|
||||
_DBG("released");
|
||||
// notify listeners
|
||||
for (DigitalButtonListener listener : listeners) {
|
||||
listener.onRelease();
|
||||
}
|
||||
|
||||
// We may be called for a release without a prior click
|
||||
if (timerLongClick != null) {
|
||||
timerLongClick.cancel();
|
||||
}
|
||||
if (longClickTimerTask != null) {
|
||||
longClickTimerTask.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onElementTouchEvent(MotionEvent event) {
|
||||
// get masked (not specific to a pointer) action
|
||||
float x = getX() + event.getX();
|
||||
float y = getY() + event.getY();
|
||||
int action = event.getActionMasked();
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
movingButton = null;
|
||||
setPressed(true);
|
||||
onClickCallback();
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
checkMovementForAllButtons(x, y);
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
setPressed(false);
|
||||
onReleaseCallback();
|
||||
|
||||
checkMovementForAllButtons(x, y);
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DigitalPad extends VirtualControllerElement {
|
||||
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
|
||||
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
|
||||
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
|
||||
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
||||
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
||||
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
||||
List<DigitalPadListener> listeners = new ArrayList<>();
|
||||
|
||||
private static final int DPAD_MARGIN = 5;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
public DigitalPad(VirtualController controller, Context context) {
|
||||
super(controller, context, EID_DPAD);
|
||||
}
|
||||
|
||||
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onElementDraw(Canvas canvas) {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||
// draw no direction rect
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(getDefaultColor());
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
||||
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
||||
paint
|
||||
);
|
||||
}
|
||||
|
||||
// draw left rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||
paint
|
||||
);
|
||||
|
||||
|
||||
// draw up rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw right rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw down rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw left up line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||
paint
|
||||
);
|
||||
|
||||
// draw up right line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw right down line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
||||
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw down left line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
||||
paint
|
||||
);
|
||||
}
|
||||
|
||||
private void newDirectionCallback(int direction) {
|
||||
_DBG("direction: " + direction);
|
||||
|
||||
// notify listeners
|
||||
for (DigitalPadListener listener : listeners) {
|
||||
listener.onDirectionChange(direction);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onElementTouchEvent(MotionEvent event) {
|
||||
// get masked (not specific to a pointer) action
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
direction = 0;
|
||||
|
||||
if (event.getX() < getPercent(getWidth(), 33)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
||||
}
|
||||
if (event.getX() > getPercent(getWidth(), 66)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
||||
}
|
||||
if (event.getY() > getPercent(getHeight(), 66)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
||||
}
|
||||
if (event.getY() < getPercent(getHeight(), 33)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_UP;
|
||||
}
|
||||
newDirectionCallback(direction);
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
direction = 0;
|
||||
newDirectionCallback(direction);
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public interface DigitalPadListener {
|
||||
void onDirectionChange(int direction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
public class LeftAnalogStick extends AnalogStick {
|
||||
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||
super(controller, context, EID_LS);
|
||||
|
||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||
@Override
|
||||
public void onMovement(float x, float y) {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.leftStickX = (short) (x * 0x7FFE);
|
||||
inputContext.leftStickY = (short) (y * 0x7FFE);
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoke() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class LeftTrigger extends DigitalButton {
|
||||
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||
super(controller, EID_LT, layer, context);
|
||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||
@Override
|
||||
public void onClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.leftTrigger = (byte) 0xFF;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.leftTrigger = (byte) 0x00;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
public class RightAnalogStick extends AnalogStick {
|
||||
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||
super(controller, context, EID_RS);
|
||||
|
||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||
@Override
|
||||
public void onMovement(float x, float y) {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.rightStickX = (short) (x * 0x7FFE);
|
||||
inputContext.rightStickY = (short) (y * 0x7FFE);
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoke() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class RightTrigger extends DigitalButton {
|
||||
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||
super(controller, EID_RT, layer, context);
|
||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||
@Override
|
||||
public void onClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.rightTrigger = (byte) 0xFF;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.rightTrigger = (byte) 0x00;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.input.ControllerHandler;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class VirtualController {
|
||||
public class ControllerInputContext {
|
||||
public short inputMap = 0x0000;
|
||||
public byte leftTrigger = 0x00;
|
||||
public byte rightTrigger = 0x00;
|
||||
public short rightStickX = 0x0000;
|
||||
public short rightStickY = 0x0000;
|
||||
public short leftStickX = 0x0000;
|
||||
public short leftStickY = 0x0000;
|
||||
}
|
||||
|
||||
public enum ControllerMode {
|
||||
Active,
|
||||
Configuration
|
||||
}
|
||||
|
||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
|
||||
private ControllerHandler controllerHandler;
|
||||
private Context context = null;
|
||||
|
||||
private FrameLayout frame_layout = null;
|
||||
private RelativeLayout relative_layout = null;
|
||||
|
||||
private Timer retransmitTimer;
|
||||
|
||||
ControllerMode currentMode = ControllerMode.Active;
|
||||
ControllerInputContext inputContext = new ControllerInputContext();
|
||||
|
||||
private Button buttonConfigure = null;
|
||||
|
||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||
|
||||
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
||||
this.controllerHandler = controllerHandler;
|
||||
this.frame_layout = layout;
|
||||
this.context = context;
|
||||
|
||||
relative_layout = new RelativeLayout(context);
|
||||
|
||||
frame_layout.addView(relative_layout);
|
||||
|
||||
buttonConfigure = new Button(context);
|
||||
buttonConfigure.setAlpha(0.25f);
|
||||
buttonConfigure.setFocusable(false);
|
||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String message;
|
||||
|
||||
if (currentMode == ControllerMode.Configuration) {
|
||||
currentMode = ControllerMode.Active;
|
||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||
message = "Exiting configuration mode";
|
||||
} else {
|
||||
currentMode = ControllerMode.Configuration;
|
||||
message = "Entering configuration mode";
|
||||
}
|
||||
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
|
||||
relative_layout.invalidate();
|
||||
|
||||
for (VirtualControllerElement element : elements) {
|
||||
element.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
retransmitTimer.cancel();
|
||||
relative_layout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
relative_layout.setVisibility(View.VISIBLE);
|
||||
|
||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
||||
// very shortly after another. This can be critical if an axis zeroing packet
|
||||
// is lost and causes an analog stick to get stuck. To avoid this, we send
|
||||
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
|
||||
retransmitTimer = new Timer("OSC timer", true);
|
||||
retransmitTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendControllerInputContext();
|
||||
}
|
||||
}, 100, 100);
|
||||
}
|
||||
|
||||
public void removeElements() {
|
||||
for (VirtualControllerElement element : elements) {
|
||||
relative_layout.removeView(element);
|
||||
}
|
||||
elements.clear();
|
||||
}
|
||||
|
||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||
elements.add(element);
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||
layoutParams.setMargins(x, y, 0, 0);
|
||||
|
||||
relative_layout.addView(element, layoutParams);
|
||||
}
|
||||
|
||||
public List<VirtualControllerElement> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
private static final void _DBG(String text) {
|
||||
if (_PRINT_DEBUG_INFORMATION) {
|
||||
System.out.println("VirtualController: " + text);
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshLayout() {
|
||||
relative_layout.removeAllViews();
|
||||
removeElements();
|
||||
|
||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||
|
||||
int buttonSize = (int)(screen.heightPixels*0.06f);
|
||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
|
||||
params.leftMargin = 15;
|
||||
params.topMargin = 15;
|
||||
relative_layout.addView(buttonConfigure, params);
|
||||
|
||||
// Start with the default layout
|
||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||
|
||||
// Apply user preferences onto the default layout
|
||||
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
|
||||
}
|
||||
|
||||
public ControllerMode getControllerMode() {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
public ControllerInputContext getControllerInputContext() {
|
||||
return inputContext;
|
||||
}
|
||||
|
||||
void sendControllerInputContext() {
|
||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
|
||||
if (controllerHandler != null) {
|
||||
controllerHandler.reportOscState(
|
||||
inputContext.inputMap,
|
||||
inputContext.leftStickX,
|
||||
inputContext.leftStickY,
|
||||
inputContext.rightStickX,
|
||||
inputContext.rightStickY,
|
||||
inputContext.leftTrigger,
|
||||
inputContext.rightTrigger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class VirtualControllerConfigurationLoader {
|
||||
public static final String OSC_PREFERENCE = "OSC";
|
||||
|
||||
private static int getPercent(
|
||||
int percent,
|
||||
int total) {
|
||||
return (int) (((float) total / (float) 100) * (float) percent);
|
||||
}
|
||||
|
||||
private static DigitalPad createDigitalPad(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
|
||||
DigitalPad digitalPad = new DigitalPad(controller, context);
|
||||
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
||||
@Override
|
||||
public void onDirectionChange(int direction) {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
|
||||
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
return;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
}
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
|
||||
return digitalPad;
|
||||
}
|
||||
|
||||
private static DigitalButton createDigitalButton(
|
||||
final int elementId,
|
||||
final int keyShort,
|
||||
final int keyLong,
|
||||
final int layer,
|
||||
final String text,
|
||||
final int icon,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
|
||||
button.setText(text);
|
||||
button.setIcon(icon);
|
||||
|
||||
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||
@Override
|
||||
public void onClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= keyShort;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= keyLong;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap &= ~keyShort;
|
||||
inputContext.inputMap &= ~keyLong;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private static DigitalButton createLeftTrigger(
|
||||
final int layer,
|
||||
final String text,
|
||||
final int icon,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
||||
button.setText(text);
|
||||
button.setIcon(icon);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static DigitalButton createRightTrigger(
|
||||
final int layer,
|
||||
final String text,
|
||||
final int icon,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
RightTrigger button = new RightTrigger(controller, layer, context);
|
||||
button.setText(text);
|
||||
button.setIcon(icon);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static AnalogStick createLeftStick(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
return new LeftAnalogStick(controller, context);
|
||||
}
|
||||
|
||||
private static AnalogStick createRightStick(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
return new RightAnalogStick(controller, context);
|
||||
}
|
||||
|
||||
private static final int BUTTON_BASE_X = 65;
|
||||
private static final int BUTTON_BASE_Y = 5;
|
||||
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
||||
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
||||
|
||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||
|
||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||
|
||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||
|
||||
if (!config.onlyL3R3)
|
||||
{
|
||||
controller.addElement(createDigitalPad(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(30, screen.widthPixels),
|
||||
getPercent(40, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_A,
|
||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_B,
|
||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_X,
|
||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_Y,
|
||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftTrigger(
|
||||
0, "LT", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createRightTrigger(
|
||||
0, "RT", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_LB,
|
||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_RB,
|
||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftStick(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createRightStick(controller, context),
|
||||
getPercent(55, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_BACK,
|
||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(90, screen.heightPixels),
|
||||
getPercent(10, screen.widthPixels),
|
||||
getPercent(10, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_START,
|
||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
|
||||
getPercent(90, screen.heightPixels),
|
||||
getPercent(10, screen.widthPixels),
|
||||
getPercent(10, screen.heightPixels)
|
||||
);
|
||||
}
|
||||
else {
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_LSB,
|
||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
||||
getPercent(2, screen.widthPixels),
|
||||
getPercent(80, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
VirtualControllerElement.EID_RSB,
|
||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
||||
getPercent(89, screen.widthPixels),
|
||||
getPercent(80, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveProfile(final VirtualController controller,
|
||||
final Context context) {
|
||||
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
|
||||
|
||||
for (VirtualControllerElement element : controller.getElements()) {
|
||||
String prefKey = ""+element.elementId;
|
||||
try {
|
||||
prefEditor.putString(prefKey, element.getConfiguration().toString());
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
prefEditor.apply();
|
||||
}
|
||||
|
||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
||||
|
||||
for (VirtualControllerElement element : controller.getElements()) {
|
||||
String prefKey = ""+element.elementId;
|
||||
|
||||
String jsonConfig = pref.getString(prefKey, null);
|
||||
if (jsonConfig != null) {
|
||||
try {
|
||||
element.loadConfiguration(new JSONObject(jsonConfig));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
// Remove the corrupt element from the preferences
|
||||
pref.edit().remove(prefKey).apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public abstract class VirtualControllerElement extends View {
|
||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
|
||||
public static final int EID_DPAD = 1;
|
||||
public static final int EID_LT = 2;
|
||||
public static final int EID_RT = 3;
|
||||
public static final int EID_LB = 4;
|
||||
public static final int EID_RB = 5;
|
||||
public static final int EID_A = 6;
|
||||
public static final int EID_B = 7;
|
||||
public static final int EID_X = 8;
|
||||
public static final int EID_Y = 9;
|
||||
public static final int EID_BACK = 10;
|
||||
public static final int EID_START = 11;
|
||||
public static final int EID_LS = 12;
|
||||
public static final int EID_RS = 13;
|
||||
public static final int EID_LSB = 14;
|
||||
public static final int EID_RSB = 15;
|
||||
|
||||
protected VirtualController virtualController;
|
||||
protected final int elementId;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private int normalColor = 0xF0888888;
|
||||
protected int pressedColor = 0xF00000FF;
|
||||
private int configNormalColor = 0xF0FF0000;
|
||||
private int configSelectedColor = 0xF000FF00;
|
||||
|
||||
protected int startSize_x;
|
||||
protected int startSize_y;
|
||||
|
||||
float position_pressed_x = 0;
|
||||
float position_pressed_y = 0;
|
||||
|
||||
private enum Mode {
|
||||
Normal,
|
||||
Resize,
|
||||
Move
|
||||
}
|
||||
|
||||
private Mode currentMode = Mode.Normal;
|
||||
|
||||
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
|
||||
super(context);
|
||||
|
||||
this.virtualController = controller;
|
||||
this.elementId = elementId;
|
||||
}
|
||||
|
||||
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||
int newPos_x = (int) getX() + x - pressed_x;
|
||||
int newPos_y = (int) getY() + y - pressed_y;
|
||||
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
||||
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
||||
layoutParams.rightMargin = 0;
|
||||
layoutParams.bottomMargin = 0;
|
||||
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
int newHeight = height + (startSize_y - pressed_y);
|
||||
int newWidth = width + (startSize_x - pressed_x);
|
||||
|
||||
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
||||
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
||||
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
onElementDraw(canvas);
|
||||
|
||||
if (currentMode != Mode.Normal) {
|
||||
paint.setColor(configSelectedColor);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
|
||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
||||
paint);
|
||||
}
|
||||
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
/*
|
||||
protected void actionShowNormalColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
normalColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
|
||||
protected void actionShowPressedColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
*/
|
||||
|
||||
protected void actionEnableMove() {
|
||||
currentMode = Mode.Move;
|
||||
}
|
||||
|
||||
protected void actionEnableResize() {
|
||||
currentMode = Mode.Resize;
|
||||
}
|
||||
|
||||
protected void actionCancel() {
|
||||
currentMode = Mode.Normal;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected int getDefaultColor() {
|
||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||
configNormalColor : normalColor;
|
||||
}
|
||||
|
||||
protected int getDefaultStrokeWidth() {
|
||||
DisplayMetrics screen = getResources().getDisplayMetrics();
|
||||
return (int)(screen.heightPixels*0.004f);
|
||||
}
|
||||
|
||||
protected void showConfigurationDialog() {
|
||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||
|
||||
alertBuilder.setTitle("Configuration");
|
||||
|
||||
CharSequence functions[] = new CharSequence[]{
|
||||
"Move",
|
||||
"Resize",
|
||||
/*election
|
||||
"Set n
|
||||
Disable color sormal color",
|
||||
"Set pressed color",
|
||||
*/
|
||||
"Cancel"
|
||||
};
|
||||
|
||||
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0: { // move
|
||||
actionEnableMove();
|
||||
break;
|
||||
}
|
||||
case 1: { // resize
|
||||
actionEnableResize();
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case 2: { // set default color
|
||||
actionShowNormalColorChooser();
|
||||
break;
|
||||
}
|
||||
case 3: { // set pressed color
|
||||
actionShowPressedColorChooser();
|
||||
break;
|
||||
}
|
||||
*/
|
||||
default: { // cancel
|
||||
actionCancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
AlertDialog alert = alertBuilder.create();
|
||||
// show menu
|
||||
alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
||||
return onElementTouchEvent(event);
|
||||
}
|
||||
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
position_pressed_x = event.getX();
|
||||
position_pressed_y = event.getY();
|
||||
startSize_x = getWidth();
|
||||
startSize_y = getHeight();
|
||||
|
||||
actionEnableMove();
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
switch (currentMode) {
|
||||
case Move: {
|
||||
moveElement(
|
||||
(int) position_pressed_x,
|
||||
(int) position_pressed_y,
|
||||
(int) event.getX(),
|
||||
(int) event.getY());
|
||||
break;
|
||||
}
|
||||
case Resize: {
|
||||
resizeElement(
|
||||
(int) position_pressed_x,
|
||||
(int) position_pressed_y,
|
||||
(int) event.getX(),
|
||||
(int) event.getY());
|
||||
break;
|
||||
}
|
||||
case Normal: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
actionCancel();
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract protected void onElementDraw(Canvas canvas);
|
||||
|
||||
abstract public boolean onElementTouchEvent(MotionEvent event);
|
||||
|
||||
protected static final void _DBG(String text) {
|
||||
if (_PRINT_DEBUG_INFORMATION) {
|
||||
System.out.println(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void setColors(int normalColor, int pressedColor) {
|
||||
this.normalColor = normalColor;
|
||||
this.pressedColor = pressedColor;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected final float getPercent(float value, float percent) {
|
||||
return value / 100 * percent;
|
||||
}
|
||||
|
||||
protected final int getCorrectWidth() {
|
||||
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||
}
|
||||
|
||||
|
||||
public JSONObject getConfiguration() throws JSONException {
|
||||
JSONObject configuration = new JSONObject();
|
||||
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
configuration.put("LEFT", layoutParams.leftMargin);
|
||||
configuration.put("TOP", layoutParams.topMargin);
|
||||
configuration.put("WIDTH", layoutParams.width);
|
||||
configuration.put("HEIGHT", layoutParams.height);
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
layoutParams.leftMargin = configuration.getInt("LEFT");
|
||||
layoutParams.topMargin = configuration.getInt("TOP");
|
||||
layoutParams.width = configuration.getInt("WIDTH");
|
||||
layoutParams.height = configuration.getInt("HEIGHT");
|
||||
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Build;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||
|
||||
@SuppressWarnings("EmptyCatchBlock")
|
||||
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
|
||||
private Thread rendererThread, decoderThread;
|
||||
private int targetFps;
|
||||
|
||||
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||
private ByteBuffer decoderBuffer;
|
||||
|
||||
// Only sleep if the difference is above this value
|
||||
private static final int WAIT_CEILING_MS = 5;
|
||||
|
||||
private static final int LOW_PERF = 1;
|
||||
private static final int MED_PERF = 2;
|
||||
private static final int HIGH_PERF = 3;
|
||||
|
||||
private int totalFrames;
|
||||
private long totalTimeMs;
|
||||
|
||||
private final int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private int findOptimalPerformanceLevel() {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||
for (;;) {
|
||||
int ch = br.read();
|
||||
if (ch == -1)
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
// Here we're doing very simple heuristics based on CPU model
|
||||
String cpuInfoStr = cpuInfo.toString();
|
||||
|
||||
// We order them from greatest to least for proper detection
|
||||
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
||||
// TODO Make this better (only even kind of works on ARM)
|
||||
if (Build.FINGERPRINT.contains("generic")) {
|
||||
// Emulator
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc0f")) {
|
||||
// Cortex-A15
|
||||
return MED_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc09")) {
|
||||
// Cortex-A9
|
||||
return LOW_PERF;
|
||||
}
|
||||
else if (cpuInfoStr.contains("0xc07")) {
|
||||
// Cortex-A7
|
||||
return LOW_PERF;
|
||||
}
|
||||
else {
|
||||
// Didn't have anything we're looking for
|
||||
return MED_PERF;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
if (br != null) {
|
||||
try {
|
||||
br.close();
|
||||
} catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't read cpuinfo, so assume medium
|
||||
return MED_PERF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.targetFps = redrawRate;
|
||||
|
||||
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||
int threadCount;
|
||||
|
||||
int avcFlags = 0;
|
||||
switch (perfLevel) {
|
||||
case HIGH_PERF:
|
||||
// Single threaded low latency decode is ideal but hard to acheive
|
||||
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
|
||||
threadCount = 1;
|
||||
break;
|
||||
|
||||
case LOW_PERF:
|
||||
// Disable the loop filter for performance reasons
|
||||
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
|
||||
|
||||
// Use plenty of threads to try to utilize the CPU as best we can
|
||||
threadCount = cpuCount - 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
case MED_PERF:
|
||||
avcFlags = AvcDecoder.BILINEAR_FILTERING;
|
||||
|
||||
// Only use 2 threads to minimize frame processing latency
|
||||
threadCount = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the user wants quality, we'll remove the low IQ flags
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||
// Make sure the loop filter is enabled
|
||||
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
||||
|
||||
// Disable the non-compliant speed optimizations
|
||||
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
||||
|
||||
LimeLog.info("Using high quality decoding");
|
||||
}
|
||||
|
||||
SurfaceHolder sh = (SurfaceHolder)renderTarget;
|
||||
sh.setFormat(PixelFormat.RGBX_8888);
|
||||
|
||||
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
||||
if (err != 0) {
|
||||
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||
}
|
||||
|
||||
if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||
|
||||
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start(final VideoDepacketizer depacketizer) {
|
||||
decoderThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
DecodeUnit du;
|
||||
while (!isInterrupted()) {
|
||||
try {
|
||||
du = depacketizer.takeNextDecodeUnit();
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
|
||||
submitDecodeUnit(du);
|
||||
depacketizer.freeDecodeUnit(du);
|
||||
}
|
||||
}
|
||||
};
|
||||
decoderThread.setName("Video - Decoder (CPU)");
|
||||
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
|
||||
decoderThread.start();
|
||||
|
||||
rendererThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
long nextFrameTime = MediaCodecHelper.getMonotonicMillis();
|
||||
while (!isInterrupted())
|
||||
{
|
||||
long diff = nextFrameTime - MediaCodecHelper.getMonotonicMillis();
|
||||
|
||||
if (diff > WAIT_CEILING_MS) {
|
||||
try {
|
||||
Thread.sleep(diff - WAIT_CEILING_MS);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||
AvcDecoder.redraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (CPU)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
private long computePresentationTimeMs(int frameRate) {
|
||||
return MediaCodecHelper.getMonotonicMillis() + (1000 / frameRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
rendererThread.interrupt();
|
||||
decoderThread.interrupt();
|
||||
|
||||
try {
|
||||
rendererThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
try {
|
||||
decoderThread.join();
|
||||
} catch (InterruptedException e) { }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
AvcDecoder.destroy();
|
||||
}
|
||||
|
||||
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||
byte[] data;
|
||||
|
||||
// Use the reserved decoder buffer if this decode unit will fit
|
||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||
decoderBuffer.clear();
|
||||
|
||||
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
||||
bbd != null; bbd = bbd.nextDescriptor) {
|
||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||
}
|
||||
|
||||
data = decoderBuffer.array();
|
||||
}
|
||||
else {
|
||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||
|
||||
int offset = 0;
|
||||
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
||||
bbd != null; bbd = bbd.nextDescriptor) {
|
||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||
offset += bbd.length;
|
||||
}
|
||||
}
|
||||
|
||||
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||
if (success) {
|
||||
long timeAfterDecode = MediaCodecHelper.getMonotonicMillis();
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
totalTimeMs += delta;
|
||||
totalFrames++;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (totalFrames == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(totalTimeMs / totalFrames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
return "CPU decoding";
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import com.limelight.nvstream.av.DecodeUnit;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||
|
||||
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
|
||||
private EnhancedDecoderRenderer decoderRenderer;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (decoderRenderer != null) {
|
||||
decoderRenderer.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
if (decoderRenderer == null) {
|
||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||
}
|
||||
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||
}
|
||||
|
||||
public void initializeWithFlags(int drFlags) {
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||
}
|
||||
else {
|
||||
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isHardwareAccelerated() {
|
||||
if (decoderRenderer == null) {
|
||||
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||
}
|
||||
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start(VideoDepacketizer depacketizer) {
|
||||
return decoderRenderer.start(depacketizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
decoderRenderer.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return decoderRenderer.getCapabilities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void directSubmitDecodeUnit(DecodeUnit du) {
|
||||
decoderRenderer.directSubmitDecodeUnit(du);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getAverageDecoderLatency();
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getAverageEndToEndLatency();
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDecoderName() {
|
||||
if (decoderRenderer != null) {
|
||||
return decoderRenderer.getDecoderName();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
public interface CrashListener {
|
||||
void notifyCrash(Exception e);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
|
||||
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
|
||||
public abstract String getDecoderName();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,13 @@ import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ConfigurationInfo;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
@@ -24,13 +28,21 @@ public class MediaCodecHelper {
|
||||
|
||||
private static final List<String> blacklistedDecoderPrefixes;
|
||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
||||
private static final List<String> deprioritizedHevcDecoders;
|
||||
private static final List<String> baselineProfileHackPrefixes;
|
||||
private static final List<String> directSubmitPrefixes;
|
||||
private static final List<String> constrainedHighProfilePrefixes;
|
||||
private static final List<String> whitelistedHevcDecoders;
|
||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
||||
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
||||
|
||||
private static boolean isLowEndSnapdragon = false;
|
||||
private static boolean initialized = false;
|
||||
|
||||
static {
|
||||
directSubmitPrefixes = new LinkedList<String>();
|
||||
directSubmitPrefixes = new LinkedList<>();
|
||||
|
||||
// These decoders have low enough input buffer latency that they
|
||||
// can be directly invoked from the receive thread
|
||||
@@ -41,40 +53,238 @@ public class MediaCodecHelper {
|
||||
directSubmitPrefixes.add("omx.brcm");
|
||||
directSubmitPrefixes.add("omx.TI");
|
||||
directSubmitPrefixes.add("omx.arc");
|
||||
directSubmitPrefixes.add("omx.nvidia");
|
||||
}
|
||||
|
||||
static {
|
||||
refFrameInvalidationAvcPrefixes = new LinkedList<>();
|
||||
refFrameInvalidationHevcPrefixes = new LinkedList<>();
|
||||
|
||||
// Qualcomm and NVIDIA may be added at runtime
|
||||
}
|
||||
|
||||
static {
|
||||
preferredDecoders = new LinkedList<String>();
|
||||
preferredDecoders = new LinkedList<>();
|
||||
}
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||
|
||||
// Software decoders that don't support H264 high profile
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
// Blacklist software decoders that don't support H264 high profile,
|
||||
// but exclude the official AOSP and CrOS emulator from this restriction.
|
||||
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
}
|
||||
|
||||
// Never use ffmpeg decoders since they're software decoders
|
||||
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
||||
|
||||
// Force these decoders disabled because:
|
||||
// 1) They are software decoders, so the performance is terrible
|
||||
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
|
||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
|
||||
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
|
||||
}
|
||||
|
||||
static {
|
||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||
// If a decoder qualifies for reference frame invalidation,
|
||||
// these entries will be ignored for those decoders.
|
||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||
|
||||
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||
baselineProfileHackPrefixes = new LinkedList<>();
|
||||
baselineProfileHackPrefixes.add("omx.intel");
|
||||
|
||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
||||
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
||||
// if adaptive playback was enabled so let's avoid it to be safe.
|
||||
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
||||
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
||||
// on some Android TV devices with H.265 only.
|
||||
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
||||
|
||||
constrainedHighProfilePrefixes = new LinkedList<String>();
|
||||
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||
constrainedHighProfilePrefixes.add("omx.intel");
|
||||
}
|
||||
|
||||
static {
|
||||
whitelistedHevcDecoders = new LinkedList<>();
|
||||
|
||||
// Allow software HEVC decoding in the official AOSP emulator
|
||||
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
|
||||
whitelistedHevcDecoders.add("omx.google");
|
||||
}
|
||||
|
||||
// Exynos seems to be the only HEVC decoder that works reliably
|
||||
whitelistedHevcDecoders.add("omx.exynos");
|
||||
|
||||
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
|
||||
// For some reason, other X1 implementations require bitstream fixups.
|
||||
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
|
||||
whitelistedHevcDecoders.add("omx.nvidia");
|
||||
}
|
||||
else {
|
||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
||||
}
|
||||
|
||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
|
||||
// to actually be tested.
|
||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||
whitelistedHevcDecoders.add("omx.mtk");
|
||||
whitelistedHevcDecoders.add("omx.amlogic");
|
||||
}
|
||||
|
||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||
// their AVC decoders), but haven't been tested enough
|
||||
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||
//whitelistedHevcDecoders.add("omx.rk");
|
||||
|
||||
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||
}
|
||||
|
||||
static {
|
||||
deprioritizedHevcDecoders = new LinkedList<>();
|
||||
|
||||
// These are decoders that work but aren't used by default for various reasons.
|
||||
|
||||
// Qualcomm is currently the only decoders in this group.
|
||||
}
|
||||
|
||||
static {
|
||||
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
// We see a bunch of crashes on MediaTek Android TVs running
|
||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
||||
// these devices and hope they fix it in Pie.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPowerVR(String glRenderer) {
|
||||
return glRenderer.toLowerCase().contains("powervr");
|
||||
}
|
||||
|
||||
private static String getAdrenoVersionString(String glRenderer) {
|
||||
glRenderer = glRenderer.toLowerCase().trim();
|
||||
|
||||
if (!glRenderer.contains("adreno")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
||||
|
||||
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String modelNumber = matcher.group(2);
|
||||
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
||||
return modelNumber;
|
||||
}
|
||||
|
||||
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||
if (modelNumber == null) {
|
||||
// Not an Adreno GPU
|
||||
return false;
|
||||
}
|
||||
|
||||
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
||||
return modelNumber.charAt(1) == '0';
|
||||
}
|
||||
|
||||
// This is a workaround for some broken devices that report
|
||||
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
||||
// An example of such a device is the Huawei Honor 5x with the
|
||||
// Snapdragon 616 SoC (Adreno 405).
|
||||
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
||||
if (modelNumber == null) {
|
||||
// Not an Adreno GPU
|
||||
return false;
|
||||
}
|
||||
|
||||
// Snapdragon 4xx and higher support GLES 3.1
|
||||
return modelNumber.charAt(0) >= '4';
|
||||
}
|
||||
|
||||
public static void initialize(Context context, String glRenderer) {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
ActivityManager activityManager =
|
||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
||||
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
||||
|
||||
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
||||
|
||||
// Tegra K1 and later can do reference frame invalidation properly
|
||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
||||
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
||||
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
||||
|
||||
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
||||
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
||||
|
||||
// Prior to M, we were tricking the decoder into using baseline profile, which
|
||||
// won't support RFI properly.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
||||
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
||||
}
|
||||
}
|
||||
|
||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
||||
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
||||
// 3xx - bad
|
||||
// 4xx - good
|
||||
//
|
||||
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
||||
// (see comment on isGLES31SnapdragonRenderer).
|
||||
//
|
||||
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
||||
// We prefer reference frame invalidation support (which is only doable on AVC on
|
||||
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
||||
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
|
||||
// HEVC anyway.
|
||||
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
|
||||
deprioritizedHevcDecoders.add("omx.qcom");
|
||||
}
|
||||
else {
|
||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
||||
}
|
||||
|
||||
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
||||
// PowerVR GPUs have good HEVC support.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
|
||||
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
|
||||
whitelistedHevcDecoders.add("omx.mtk");
|
||||
|
||||
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
|
||||
// required to make it work adds a huge amount of latency.
|
||||
LimeLog.info("Added omx.mtk to RFI list for HEVC");
|
||||
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||
if (!initialized) {
|
||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
||||
}
|
||||
|
||||
for (String badPrefix : decoderList) {
|
||||
if (decoderName.length() >= badPrefix.length()) {
|
||||
String prefix = decoderName.substring(0, badPrefix.length());
|
||||
@@ -90,20 +300,15 @@ public class MediaCodecHelper {
|
||||
public static long getMonotonicMillis() {
|
||||
return System.nanoTime() / 1000000L;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
/*
|
||||
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||
|
||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
||||
// Possibly enable adaptive playback on KitKat and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
|
||||
LimeLog.info("Decoder blacklisted for adaptive playback");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||
@@ -115,31 +320,98 @@ public class MediaCodecHelper {
|
||||
} catch (Exception e) {
|
||||
// Tolerate buggy codecs
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean decoderNeedsConstrainedHighProfile(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
||||
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
public static boolean decoderCanDirectSubmit(String decoderName) {
|
||||
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
||||
}
|
||||
|
||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
|
||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
|
||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
|
||||
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
||||
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
||||
if (videoHeight > 720 && isLowEndSnapdragon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This device seems to crash constantly at 720p, so try disabling
|
||||
// RFI to see if we can get that under control.
|
||||
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
||||
}
|
||||
|
||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
||||
// TODO: Shield Tablet K1/LTE?
|
||||
//
|
||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
||||
// whether the performance is good enough to use for streaming, but they're
|
||||
// using the same omx.nvidia.h265.decode name as the Shield TV which has a
|
||||
// fully accelerated HEVC pipeline. AFAIK, the only K1 device with this
|
||||
// partially accelerated HEVC decoder is the Shield Tablet, so I'll
|
||||
// check for it here.
|
||||
//
|
||||
// TODO: Temporarily disabled with NVIDIA HEVC support
|
||||
/*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) {
|
||||
return false;
|
||||
}*/
|
||||
|
||||
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
|
||||
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
|
||||
// so I'm restricting HEVC usage to Lollipop and higher.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// Software decoders are terrible and we never want to use them.
|
||||
// We want to catch decoders like:
|
||||
// OMX.qcom.video.decoder.hevcswvdec
|
||||
// OMX.SEC.hevc.sw.dec
|
||||
//
|
||||
if (decoderName.contains("sw")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Some devices have HEVC decoders that we prefer not to use
|
||||
// typically because it can't support reference frame invalidation.
|
||||
// However, we will use it for HDR and for streaming over mobile networks
|
||||
// since it works fine otherwise.
|
||||
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
||||
LimeLog.info("Selected deprioritized decoder");
|
||||
return true;
|
||||
}
|
||||
|
||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("NewApi")
|
||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
@@ -180,6 +452,10 @@ public class MediaCodecHelper {
|
||||
// This is a different algorithm than the other findXXXDecoder functions,
|
||||
// because we want to evaluate the decoders in our list's order
|
||||
// rather than MediaCodecList's order
|
||||
|
||||
if (!initialized) {
|
||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
||||
}
|
||||
|
||||
for (String preferredDecoder : preferredDecoders) {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
@@ -199,7 +475,7 @@ public class MediaCodecHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findFirstDecoder() {
|
||||
public static MediaCodecInfo findFirstDecoder(String mimeType) {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
@@ -212,9 +488,9 @@ public class MediaCodecHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a decoder that supports H.264
|
||||
// Find a decoder that supports the specified video format
|
||||
for (String mime : codecInfo.getSupportedTypes()) {
|
||||
if (mime.equalsIgnoreCase("video/avc")) {
|
||||
if (mime.equalsIgnoreCase(mimeType)) {
|
||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
||||
return codecInfo;
|
||||
}
|
||||
@@ -224,7 +500,7 @@ public class MediaCodecHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findProbableSafeDecoder() {
|
||||
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
|
||||
// First look for a preferred decoder by name
|
||||
MediaCodecInfo info = findPreferredDecoder();
|
||||
if (info != null) {
|
||||
@@ -234,12 +510,12 @@ public class MediaCodecHelper {
|
||||
// Now look for decoders we know are safe
|
||||
try {
|
||||
// If this function completes, it will determine if the decoder is safe
|
||||
return findKnownSafeDecoder();
|
||||
return findKnownSafeDecoder(mimeType, requiredProfile);
|
||||
} catch (Exception e) {
|
||||
// Some buggy devices seem to throw exceptions
|
||||
// from getCapabilitiesForType() so we'll just assume
|
||||
// they're okay and go with the first one we find
|
||||
return findFirstDecoder();
|
||||
return findFirstDecoder(mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +523,7 @@ public class MediaCodecHelper {
|
||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||
// and we want to be sure all callers are handling this possibility
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
@@ -260,21 +536,26 @@ public class MediaCodecHelper {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a decoder that supports H.264 high profile
|
||||
// Find a decoder that supports the requested video format
|
||||
for (String mime : codecInfo.getSupportedTypes()) {
|
||||
if (mime.equalsIgnoreCase("video/avc")) {
|
||||
if (mime.equalsIgnoreCase(mimeType)) {
|
||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
||||
|
||||
|
||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
|
||||
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
|
||||
LimeLog.info("Selected decoder: "+codecInfo.getName());
|
||||
return codecInfo;
|
||||
|
||||
if (requiredProfile != -1) {
|
||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||
if (profile.profile == requiredProfile) {
|
||||
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
|
||||
return codecInfo;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
|
||||
}
|
||||
else {
|
||||
return codecInfo;
|
||||
}
|
||||
|
||||
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +573,7 @@ public class MediaCodecHelper {
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
|
||||
return cpuInfo.toString();
|
||||
} finally {
|
||||
br.close();
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import android.content.ContentValues;
|
||||
@@ -17,13 +18,15 @@ import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
public class ComputerDatabaseManager {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||
private static final String MAC_COLUMN_NAME = "Mac";
|
||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||
|
||||
private SQLiteDatabase computerDb;
|
||||
|
||||
@@ -36,79 +39,101 @@ public class ComputerDatabaseManager {
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||
}
|
||||
initializeDb();
|
||||
initializeDb(c);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
computerDb.close();
|
||||
}
|
||||
|
||||
private void initializeDb() {
|
||||
private void initializeDb(Context c) {
|
||||
// Add cert column to the table if not present
|
||||
try {
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
} catch (SQLiteException e) {}
|
||||
|
||||
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||
computerDb.execSQL(String.format((Locale)null,
|
||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
||||
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||
|
||||
// Move all computers from the old DB (if any) to the new one
|
||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||
for (ComputerDetails computer : oldComputers) {
|
||||
updateComputer(computer);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||
}
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
||||
try {
|
||||
if (details.serverCert != null) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
|
||||
}
|
||||
else {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
||||
e.printStackTrace();
|
||||
}
|
||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
|
||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.uuid = c.getString(0);
|
||||
details.name = c.getString(1);
|
||||
details.localAddress = c.getString(2);
|
||||
details.remoteAddress = c.getString(3);
|
||||
details.manualAddress = c.getString(4);
|
||||
details.macAddress = c.getString(5);
|
||||
|
||||
try {
|
||||
byte[] derCertData = c.getBlob(6);
|
||||
|
||||
if (derCertData != null) {
|
||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
||||
}
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
public List<ComputerDetails> getAllComputers() {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
|
||||
// If a field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||
details.macAddress == null) {
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
@@ -117,49 +142,19 @@ public class ComputerDatabaseManager {
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByName(String name) {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
public ComputerDetails getComputerByUUID(String uuid) {
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
c.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
details.name = c.getString(0);
|
||||
|
||||
String uuidStr = c.getString(1);
|
||||
try {
|
||||
details.uuid = UUID.fromString(uuidStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||
} catch (UnknownHostException e) {
|
||||
// We'll delete this entry
|
||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||
}
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
c.close();
|
||||
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
|
||||
// If a field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||
details.macAddress == null) {
|
||||
// If a critical field is corrupt or missing, delete the database entry
|
||||
if (details.uuid == null) {
|
||||
deleteComputer(details.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
public interface ComputerManagerListener {
|
||||
public void notifyComputerUpdated(ComputerDetails details);
|
||||
void notifyComputerUpdated(ComputerDetails details);
|
||||
}
|
||||
|
||||
@@ -3,23 +3,25 @@ package com.limelight.computers;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.StringReader;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.discovery.DiscoveryService;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.mdns.MdnsComputer;
|
||||
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
@@ -35,8 +37,11 @@ public class ComputerManagerService extends Service {
|
||||
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
private static final int FAST_POLL_TIMEOUT = 500;
|
||||
private static final int FAST_POLL_TIMEOUT = 1000;
|
||||
private static final int OFFLINE_POLL_TRIES = 5;
|
||||
private static final int INITIAL_POLL_TRIES = 2;
|
||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||
private static final int POLL_DATA_TTL_MS = 30000;
|
||||
|
||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
|
||||
@@ -44,7 +49,7 @@ public class ComputerManagerService extends Service {
|
||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
|
||||
private IdentityManager idManager;
|
||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
|
||||
private ComputerManagerListener listener = null;
|
||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private boolean pollingActive = false;
|
||||
@@ -75,19 +80,21 @@ public class ComputerManagerService extends Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int pollTriesBeforeOffline = details.state == ComputerDetails.State.UNKNOWN ?
|
||||
INITIAL_POLL_TRIES : OFFLINE_POLL_TRIES;
|
||||
|
||||
activePolls.incrementAndGet();
|
||||
|
||||
// Poll the machine
|
||||
try {
|
||||
if (!pollComputer(details)) {
|
||||
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
||||
if (!newPc && offlineCount < pollTriesBeforeOffline) {
|
||||
// Return without calling the listener
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
|
||||
details.state = ComputerDetails.State.OFFLINE;
|
||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
releaseLocalDatabaseReference();
|
||||
@@ -98,17 +105,27 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
if (!newPc) {
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (dbManager.getComputerByName(details.name) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
|
||||
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (!newPc && existingComputer == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
}
|
||||
|
||||
dbManager.updateComputer(details);
|
||||
// If we already have an entry for this computer in the DB, we must
|
||||
// combine the existing data with this new data (which may be partially available
|
||||
// due to detecting the PC via mDNS) without the saved external address. If we
|
||||
// write to the DB without doing this first, we can overwrite our existing data.
|
||||
if (existingComputer != null) {
|
||||
existingComputer.update(details);
|
||||
dbManager.updateComputer(existingComputer);
|
||||
}
|
||||
else {
|
||||
dbManager.updateComputer(details);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't call the listener if this is a failed lookup of a new PC
|
||||
@@ -126,7 +143,7 @@ public class ComputerManagerService extends Service {
|
||||
public void run() {
|
||||
|
||||
int offlineCount = 0;
|
||||
while (!isInterrupted() && pollingActive) {
|
||||
while (!isInterrupted() && pollingActive && tuple.thread == this) {
|
||||
try {
|
||||
// Only allow one request to the machine at a time
|
||||
synchronized (tuple.networkLock) {
|
||||
@@ -135,6 +152,7 @@ public class ComputerManagerService extends Service {
|
||||
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
||||
offlineCount++;
|
||||
} else {
|
||||
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
|
||||
offlineCount = 0;
|
||||
}
|
||||
}
|
||||
@@ -147,7 +165,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setName("Polling thread for " + tuple.computer.localIp.getHostAddress());
|
||||
t.setName("Polling thread for " + tuple.computer.name);
|
||||
return t;
|
||||
}
|
||||
|
||||
@@ -164,11 +182,17 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Enforce the poll data TTL
|
||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
}
|
||||
|
||||
// Report this computer initially
|
||||
listener.notifyComputerUpdated(tuple.computer);
|
||||
|
||||
// This polling thread might already be there
|
||||
if (tuple.thread == null) {
|
||||
// Report this computer initially
|
||||
listener.notifyComputerUpdated(tuple.computer);
|
||||
|
||||
tuple.thread = createPollingThread(tuple);
|
||||
tuple.thread.start();
|
||||
}
|
||||
@@ -196,8 +220,8 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
|
||||
}
|
||||
|
||||
public void removeComputer(String name) {
|
||||
@@ -217,7 +241,7 @@ public class ComputerManagerService extends Service {
|
||||
return idManager.getUniqueId();
|
||||
}
|
||||
|
||||
public ComputerDetails getComputer(UUID uuid) {
|
||||
public ComputerDetails getComputer(String uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
@@ -228,12 +252,28 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void invalidateStateForComputer(String uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
// We need the network lock to prevent a concurrent poll
|
||||
// from wiping this change out
|
||||
synchronized (tuple.networkLock) {
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// Stop mDNS autodiscovery
|
||||
discoveryBinder.stopDiscovery();
|
||||
if (discoveryBinder != null) {
|
||||
// Stop mDNS autodiscovery
|
||||
discoveryBinder.stopDiscovery();
|
||||
}
|
||||
|
||||
// Stop polling
|
||||
pollingActive = false;
|
||||
@@ -258,7 +298,7 @@ public class ComputerManagerService extends Service {
|
||||
@Override
|
||||
public void notifyComputerAdded(MdnsComputer computer) {
|
||||
// Kick off a serverinfo poll on this machine
|
||||
addComputerBlocking(computer.getAddress());
|
||||
addComputerBlocking(computer.getAddress().getHostAddress(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -279,10 +319,8 @@ public class ComputerManagerService extends Service {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Check if this is the same computer
|
||||
if (tuple.computer.uuid.equals(details.uuid)) {
|
||||
// Update details anyway in case this machine has been re-added by IP
|
||||
// after not being reachable by our existing information
|
||||
tuple.computer.localIp = details.localIp;
|
||||
tuple.computer.remoteIp = details.remoteIp;
|
||||
// Update the saved computer with potentially new details
|
||||
tuple.computer.update(details);
|
||||
|
||||
// Start a polling thread if polling is active
|
||||
if (pollingActive && tuple.thread == null) {
|
||||
@@ -307,15 +345,43 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
|
||||
if (manuallyAdded) {
|
||||
// Add PC UI
|
||||
fakeDetails.manualAddress = addr;
|
||||
}
|
||||
else {
|
||||
// mDNS
|
||||
fakeDetails.localAddress = addr;
|
||||
|
||||
// Since we're on the same network, we can use STUN to find
|
||||
// our WAN address, which is also very likely the WAN address
|
||||
// of the PC. We can use this later to connect remotely.
|
||||
fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
|
||||
}
|
||||
|
||||
// Block while we try to fill the details
|
||||
try {
|
||||
runPoll(fakeDetails, true, 0);
|
||||
// We cannot use runPoll() here because it will attempt to persist the state of the machine
|
||||
// in the database, which would be bad because we don't have our pinned cert loaded yet.
|
||||
if (pollComputer(fakeDetails)) {
|
||||
// See if we have record of this PC to pull its pinned cert
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
|
||||
fakeDetails.serverCert = tuple.computer.serverCert;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Poll again, possibly with the pinned cert, to get accurate pairing information.
|
||||
// This will insert the host into the database too.
|
||||
runPoll(fakeDetails, true, 0);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -329,6 +395,10 @@ public class ComputerManagerService extends Service {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (!manuallyAdded) {
|
||||
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -348,6 +418,7 @@ public class ComputerManagerService extends Service {
|
||||
if (tuple.thread != null) {
|
||||
// Interrupt the thread on this entry
|
||||
tuple.thread.interrupt();
|
||||
tuple.thread = null;
|
||||
}
|
||||
pollingTuples.remove(tuple);
|
||||
break;
|
||||
@@ -373,38 +444,51 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
|
||||
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
|
||||
// Fast poll this address first to determine if we can connect at the TCP layer
|
||||
if (!fastPollIp(ipAddr)) {
|
||||
if (!fastPollIp(address)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
|
||||
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
ComputerDetails newDetails = http.getComputerDetails();
|
||||
|
||||
// Check if this is the PC we expected
|
||||
if (details.uuid != null && newDetails.uuid != null &&
|
||||
!details.uuid.equals(newDetails.uuid)) {
|
||||
if (newDetails.uuid == null) {
|
||||
LimeLog.severe("Polling returned no UUID!");
|
||||
return null;
|
||||
}
|
||||
// details.uuid can be null on initial PC add
|
||||
else if (details.uuid != null && !details.uuid.equals(newDetails.uuid)) {
|
||||
// We got the wrong PC!
|
||||
LimeLog.info("Polling returned the wrong PC!");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the new active address
|
||||
newDetails.activeAddress = address;
|
||||
|
||||
return newDetails;
|
||||
} catch (Exception e) {
|
||||
} catch (XmlPullParserException | IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Just try to establish a TCP connection to speculatively detect a running
|
||||
// GFE server
|
||||
private boolean fastPollIp(InetAddress addr) {
|
||||
private boolean fastPollIp(String address) {
|
||||
if (address == null) {
|
||||
// Don't bother if our address is null
|
||||
return false;
|
||||
}
|
||||
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||
s.close();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
@@ -412,11 +496,11 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
||||
private void startFastPollThread(final String address, final boolean[] info) {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean pollRes = fastPollIp(addr);
|
||||
boolean pollRes = fastPollIp(address);
|
||||
|
||||
synchronized (info) {
|
||||
info[0] = true; // Done
|
||||
@@ -426,16 +510,18 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setName("Fast Poll - "+addr.getHostAddress());
|
||||
t.setName("Fast Poll - "+address);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
||||
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException {
|
||||
final boolean[] remoteInfo = new boolean[2];
|
||||
final boolean[] localInfo = new boolean[2];
|
||||
final boolean[] manualInfo = new boolean[2];
|
||||
|
||||
startFastPollThread(local, localInfo);
|
||||
startFastPollThread(remote, remoteInfo);
|
||||
startFastPollThread(localAddress, localInfo);
|
||||
startFastPollThread(remoteAddress, remoteInfo);
|
||||
startFastPollThread(manualAddress, manualInfo);
|
||||
|
||||
// Check local first
|
||||
synchronized (localInfo) {
|
||||
@@ -444,125 +530,78 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
if (localInfo[1]) {
|
||||
return ComputerDetails.Reachability.LOCAL;
|
||||
return localAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remote
|
||||
// Now manual
|
||||
synchronized (manualInfo) {
|
||||
while (!manualInfo[0]) {
|
||||
manualInfo.wait(500);
|
||||
}
|
||||
|
||||
if (manualInfo[1]) {
|
||||
return manualAddress;
|
||||
}
|
||||
}
|
||||
|
||||
// And finally, remote
|
||||
synchronized (remoteInfo) {
|
||||
while (!remoteInfo[0]) {
|
||||
remoteInfo.wait(500);
|
||||
}
|
||||
|
||||
if (remoteInfo[1]) {
|
||||
return ComputerDetails.Reachability.REMOTE;
|
||||
return remoteAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
|
||||
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
|
||||
ComputerDetails polledDetails;
|
||||
ComputerDetails.Reachability reachability;
|
||||
|
||||
// If the local address is routable across the Internet,
|
||||
// always consider this PC remote to be conservative
|
||||
if (details.localIp.equals(details.remoteIp)) {
|
||||
reachability = ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else {
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||
|
||||
if (localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localIp);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteIp);
|
||||
}
|
||||
|
||||
InetAddress reachableAddr = null;
|
||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||
// Failed, so let's try the fallback
|
||||
if (!localFirst) {
|
||||
polledDetails = tryPollIp(details, details.localIp);
|
||||
}
|
||||
else {
|
||||
polledDetails = tryPollIp(details, details.remoteIp);
|
||||
}
|
||||
|
||||
if (polledDetails != null) {
|
||||
// The fallback poll worked
|
||||
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
|
||||
}
|
||||
}
|
||||
else if (polledDetails != null) {
|
||||
reachableAddr = localFirst ? details.localIp : details.remoteIp;
|
||||
}
|
||||
|
||||
if (reachableAddr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (polledDetails.remoteIp.equals(reachableAddr)) {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
else if (polledDetails.localIp.equals(reachableAddr)) {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
|
||||
return new ReachabilityTuple(polledDetails, reachableAddr);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||
ReachabilityTuple initialReachTuple = pollForReachability(details);
|
||||
if (initialReachTuple == null) {
|
||||
ComputerDetails polledDetails;
|
||||
|
||||
// Do a TCP-level connection to the HTTP server to see if it's listening.
|
||||
// Do not write this address to details.activeAddress because:
|
||||
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
|
||||
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
|
||||
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")");
|
||||
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress);
|
||||
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
|
||||
|
||||
// If no connection could be established to either IP address, there's nothing we can do
|
||||
if (candidateAddress == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
// Neither IP address reported in the serverinfo response was the one we used.
|
||||
// Poll again to see if we can contact this machine on either of its reported addresses.
|
||||
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
|
||||
if (confirmationReachTuple == null) {
|
||||
// Neither of those seem to work, so we'll hold onto the address that did work
|
||||
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
|
||||
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
|
||||
}
|
||||
else {
|
||||
// We got it on one of the returned addresses; replace the original reach tuple
|
||||
// with the new one
|
||||
initialReachTuple = confirmationReachTuple;
|
||||
// Try using the active address from fast-poll
|
||||
polledDetails = tryPollIp(details, candidateAddress);
|
||||
if (polledDetails == null) {
|
||||
// If that failed, try all unique addresses except what we've
|
||||
// already tried
|
||||
HashSet<String> uniqueAddresses = new HashSet<>();
|
||||
uniqueAddresses.add(details.localAddress);
|
||||
uniqueAddresses.add(details.remoteAddress);
|
||||
uniqueAddresses.add(details.manualAddress);
|
||||
for (String addr : uniqueAddresses) {
|
||||
if (addr == null || addr.equals(candidateAddress)) {
|
||||
continue;
|
||||
}
|
||||
polledDetails = tryPollIp(details, addr);
|
||||
if (polledDetails != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the old MAC address
|
||||
String savedMacAddress = details.macAddress;
|
||||
|
||||
// If we got here, it's reachable
|
||||
details.update(initialReachTuple.computer);
|
||||
|
||||
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
||||
details.macAddress = savedMacAddress;
|
||||
if (polledDetails != null) {
|
||||
details.update(polledDetails);
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -661,11 +700,11 @@ public class ComputerManagerService extends Service {
|
||||
thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
int emptyAppListResponses = 0;
|
||||
do {
|
||||
InetAddress selectedAddr;
|
||||
|
||||
// Can't poll if it's not online
|
||||
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||
// Can't poll if it's not online or paired
|
||||
if (computer.state != ComputerDetails.State.ONLINE ||
|
||||
computer.pairState != PairingManager.PairState.PAIRED) {
|
||||
if (listener != null) {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
@@ -677,19 +716,12 @@ public class ComputerManagerService extends Service {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
selectedAddr = computer.localIp;
|
||||
}
|
||||
else {
|
||||
selectedAddr = computer.remoteIp;
|
||||
}
|
||||
|
||||
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
PollingTuple tuple = getPollingTuple(computer);
|
||||
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
|
||||
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
String appList;
|
||||
if (tuple != null) {
|
||||
// If we're polling this machine too, grab the network lock
|
||||
@@ -705,11 +737,19 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||
if (list.isEmpty()) {
|
||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||
|
||||
// The app list might actually be empty, so if we get an empty response a few times
|
||||
// in a row, we'll go ahead and believe it.
|
||||
emptyAppListResponses++;
|
||||
}
|
||||
if (!appList.isEmpty() &&
|
||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||
// Open the cache file
|
||||
OutputStream cacheOut = null;
|
||||
try {
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
|
||||
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -721,6 +761,11 @@ public class ComputerManagerService extends Service {
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
// Reset empty count if it wasn't empty this time
|
||||
if (!list.isEmpty()) {
|
||||
emptyAppListResponses = 0;
|
||||
}
|
||||
|
||||
// Update the computer
|
||||
computer.rawAppList = appList;
|
||||
receivedAppList = true;
|
||||
@@ -731,8 +776,8 @@ public class ComputerManagerService extends Service {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||
else if (appList.isEmpty()) {
|
||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
@@ -742,6 +787,7 @@ public class ComputerManagerService extends Service {
|
||||
} while (waitPollingDelay());
|
||||
}
|
||||
};
|
||||
thread.setName("App list polling thread for " + computer.name);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
@@ -761,6 +807,7 @@ class PollingTuple {
|
||||
public Thread thread;
|
||||
public final ComputerDetails computer;
|
||||
public final Object networkLock;
|
||||
public long lastSuccessfulPollMs;
|
||||
|
||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||
this.computer = computer;
|
||||
@@ -770,10 +817,10 @@ class PollingTuple {
|
||||
}
|
||||
|
||||
class ReachabilityTuple {
|
||||
public final InetAddress reachableAddress;
|
||||
public final String reachableAddress;
|
||||
public final ComputerDetails computer;
|
||||
|
||||
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
|
||||
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
|
||||
this.computer = computer;
|
||||
this.reachableAddress = reachableAddress;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class LegacyDatabaseReader {
|
||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||
|
||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
||||
|
||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
details.name = c.getString(0);
|
||||
details.uuid = c.getString(1);
|
||||
|
||||
// An earlier schema defined addresses as byte blobs. We'll
|
||||
// gracefully migrate those to strings so we can store DNS names
|
||||
// too. To disambiguate, we'll need to prefix them with a string
|
||||
// greater than the allowable IP address length.
|
||||
try {
|
||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy local address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(2);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted local address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
||||
LimeLog.warning("DB: Legacy remote address for " + details.name);
|
||||
} catch (UnknownHostException e) {
|
||||
// This is probably a hostname/address with the prefix string
|
||||
String stringData = c.getString(3);
|
||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
||||
} else {
|
||||
LimeLog.severe("DB: Corrupted remote address for " + details.name);
|
||||
}
|
||||
}
|
||||
|
||||
// On older versions of Moonlight, this is typically where manual addresses got stored,
|
||||
// so let's initialize it just to be safe.
|
||||
details.manualAddress = details.remoteAddress;
|
||||
|
||||
details.macAddress = c.getString(4);
|
||||
|
||||
// This signifies we don't have dynamic state (like pair state)
|
||||
details.state = ComputerDetails.State.UNKNOWN;
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
|
||||
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = getComputerFromCursor(c);
|
||||
|
||||
// If a critical field is corrupt or missing, skip the database entry
|
||||
if (details.uuid == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
computerList.add(details);
|
||||
}
|
||||
|
||||
c.close();
|
||||
|
||||
return computerList;
|
||||
}
|
||||
|
||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
||||
SQLiteDatabase computerDb = null;
|
||||
try {
|
||||
// Open the existing database
|
||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
return getAllComputers(computerDb);
|
||||
} catch (SQLiteException e) {
|
||||
return new LinkedList<ComputerDetails>();
|
||||
} finally {
|
||||
// Close and delete the old DB
|
||||
if (computerDb != null) {
|
||||
computerDb.close();
|
||||
}
|
||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class DiscoveryService extends Service {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||
multicastLock.setReferenceCounted(false);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.AppView;
|
||||
@@ -14,8 +14,6 @@ import com.limelight.grid.assets.MemoryAssetLoader;
|
||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
@@ -28,7 +26,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
private final CachedAppAssetLoader loader;
|
||||
|
||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
||||
|
||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||
int dp;
|
||||
@@ -47,15 +45,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
}
|
||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = (int) scalingDivisor;
|
||||
|
||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(),
|
||||
new DiskAssetLoader(context.getCacheDir()),
|
||||
BitmapFactory.decodeResource(activity.getResources(),
|
||||
R.drawable.image_loading, options));
|
||||
new DiskAssetLoader(context));
|
||||
}
|
||||
|
||||
public void cancelQueuedOperations() {
|
||||
@@ -68,7 +61,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||
@Override
|
||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
|
||||
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -86,9 +79,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
itemList.remove(app);
|
||||
}
|
||||
|
||||
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
||||
// Let the cached asset loader handle it
|
||||
loader.populateImageView(obj.app, imgView);
|
||||
loader.populateImageView(obj.app, imgView, prgView);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,9 +97,9 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
|
||||
@Override
|
||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||
if (obj.app.getIsRunning()) {
|
||||
if (obj.isRunning) {
|
||||
// Show the play button overlay
|
||||
overlayView.setImageResource(R.drawable.play);
|
||||
overlayView.setImageResource(R.drawable.ic_play);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.R;
|
||||
@@ -14,15 +15,13 @@ import java.util.ArrayList;
|
||||
|
||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
protected final Context context;
|
||||
protected final int defaultImageRes;
|
||||
protected final int layoutId;
|
||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected final ArrayList<T> itemList = new ArrayList<>();
|
||||
protected final LayoutInflater inflater;
|
||||
|
||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||
public GenericGridAdapter(Context context, int layoutId) {
|
||||
this.context = context;
|
||||
this.layoutId = layoutId;
|
||||
this.defaultImageRes = defaultImageRes;
|
||||
|
||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
@@ -46,7 +45,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
return i;
|
||||
}
|
||||
|
||||
public abstract boolean populateImageView(ImageView imgView, T obj);
|
||||
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
|
||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||
|
||||
@@ -56,13 +55,14 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||
}
|
||||
|
||||
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||
ImageView imgView = convertView.findViewById(R.id.grid_image);
|
||||
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
||||
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
||||
|
||||
if (imgView != null) {
|
||||
if (!populateImageView(imgView, itemList.get(i))) {
|
||||
imgView.setImageResource(defaultImageRes);
|
||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||
imgView.setImageBitmap(null);
|
||||
}
|
||||
}
|
||||
if (!populateTextView(txtView, itemList.get(i))) {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.PcView;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@@ -14,7 +17,7 @@ import java.util.Comparator;
|
||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
|
||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
|
||||
}
|
||||
|
||||
public void addComputer(PcView.ComputerObject computer) {
|
||||
@@ -26,7 +29,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
||||
@Override
|
||||
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
||||
return lhs.details.name.compareTo(rhs.details.name);
|
||||
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -36,21 +39,28 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||
imgView.setAlpha(1.0f);
|
||||
}
|
||||
else {
|
||||
imgView.setAlpha(0.4f);
|
||||
}
|
||||
|
||||
// Return false to use the default drawable
|
||||
return false;
|
||||
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
|
||||
prgView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
imgView.setImageResource(R.drawable.ic_computer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||
txtView.setAlpha(1.0f);
|
||||
}
|
||||
else {
|
||||
@@ -63,13 +73,19 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
|
||||
@Override
|
||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
// Still refreshing this PC so display the overlay
|
||||
overlayView.setImageResource(R.drawable.image_loading);
|
||||
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
||||
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
||||
overlayView.setAlpha(0.4f);
|
||||
return true;
|
||||
}
|
||||
// We must check if the status is exactly online and unpaired
|
||||
// to avoid colliding with the loading spinner when status is unknown
|
||||
else if (obj.details.state == ComputerDetails.State.ONLINE &&
|
||||
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
||||
overlayView.setImageResource(R.drawable.ic_lock);
|
||||
overlayView.setAlpha(1.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No overlay
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
@@ -53,13 +55,13 @@ public class CachedAppAssetLoader {
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
||||
DiskAssetLoader diskLoader) {
|
||||
this.computer = computer;
|
||||
this.scalingDivider = scalingDivider;
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
this.placeholderBitmap = placeholderBitmap;
|
||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
public void cancelBackgroundLoads() {
|
||||
@@ -130,12 +132,14 @@ public class CachedAppAssetLoader {
|
||||
|
||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||
private final WeakReference<ImageView> imageViewRef;
|
||||
private final WeakReference<ProgressBar> progressViewRef;
|
||||
private final boolean diskOnly;
|
||||
|
||||
private LoaderTuple tuple;
|
||||
|
||||
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
||||
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
||||
this.imageViewRef = new WeakReference<>(imageView);
|
||||
this.progressViewRef = new WeakReference<>(prgView);
|
||||
this.diskOnly = diskOnly;
|
||||
}
|
||||
|
||||
@@ -143,8 +147,8 @@ public class CachedAppAssetLoader {
|
||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||
tuple = params[0];
|
||||
|
||||
// Check whether it has been cancelled or the image view is gone
|
||||
if (isCancelled() || imageViewRef.get() == null) {
|
||||
// Check whether it has been cancelled or the views are gone
|
||||
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -177,11 +181,17 @@ public class CachedAppAssetLoader {
|
||||
|
||||
// If the current loader task for this view isn't us, do nothing
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
final ProgressBar prgView = progressViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Now display the progress bar since we have to hit the network
|
||||
if (prgView != null) {
|
||||
prgView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Set off another loader task on the network executor
|
||||
LoaderTask task = new LoaderTask(imageView, false);
|
||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||
imageView.setAlpha(1.0f);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
task.executeOnExecutor(networkExecutor, tuple);
|
||||
}
|
||||
@@ -195,14 +205,20 @@ public class CachedAppAssetLoader {
|
||||
}
|
||||
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
final ProgressBar prgView = progressViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Set the bitmap
|
||||
if (bitmap != null) {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
// Hide the progress bar
|
||||
if (prgView != null) {
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Show the view
|
||||
imageView.setAlpha(1.0f);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +229,7 @@ public class CachedAppAssetLoader {
|
||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||
LoaderTask loaderTask) {
|
||||
super(res, bitmap);
|
||||
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
||||
loaderTaskReference = new WeakReference<>(loaderTask);
|
||||
}
|
||||
|
||||
public LoaderTask getLoaderTask() {
|
||||
@@ -280,34 +296,38 @@ public class CachedAppAssetLoader {
|
||||
});
|
||||
}
|
||||
|
||||
public void populateImageView(NvApp app, ImageView view) {
|
||||
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
||||
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||
|
||||
// If there's already a task in progress for this view,
|
||||
// cancel it. If the task is already loading the same image,
|
||||
// we return and let that load finish.
|
||||
if (!cancelPendingLoad(tuple, view)) {
|
||||
return;
|
||||
if (!cancelPendingLoad(tuple, imgView)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hide the progress bar always on initial load
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
|
||||
// First, try the memory cache in the current context
|
||||
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||
if (bmp != null) {
|
||||
// Show the bitmap immediately
|
||||
view.setAlpha(1.0f);
|
||||
view.setImageBitmap(bmp);
|
||||
return;
|
||||
imgView.setVisibility(View.VISIBLE);
|
||||
imgView.setImageBitmap(bmp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's not in memory, create an async task to load it. This task will be attached
|
||||
// via AsyncDrawable to this view.
|
||||
final LoaderTask task = new LoaderTask(view, true);
|
||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
|
||||
view.setAlpha(0.0f);
|
||||
view.setImageDrawable(asyncDrawable);
|
||||
final LoaderTask task = new LoaderTask(imgView, prgView, true);
|
||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||
imgView.setVisibility(View.INVISIBLE);
|
||||
imgView.setImageDrawable(asyncDrawable);
|
||||
|
||||
// Run the task on our foreground executor
|
||||
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||
return false;
|
||||
}
|
||||
|
||||
public class LoaderTuple {
|
||||
|
||||
@@ -1,58 +1,133 @@
|
||||
package com.limelight.grid.assets;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.ImageDecoder;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class DiskAssetLoader {
|
||||
// 5 MB
|
||||
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
// Standard box art is 300x400
|
||||
private static final int STANDARD_ASSET_WIDTH = 300;
|
||||
private static final int STANDARD_ASSET_HEIGHT = 400;
|
||||
|
||||
private final boolean isLowRamDevice;
|
||||
private final File cacheDir;
|
||||
|
||||
public DiskAssetLoader(File cacheDir) {
|
||||
this.cacheDir = cacheDir;
|
||||
public DiskAssetLoader(Context context) {
|
||||
this.cacheDir = context.getCacheDir();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
this.isLowRamDevice =
|
||||
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
|
||||
}
|
||||
else {
|
||||
// Use conservative low RAM behavior on very old devices
|
||||
this.isLowRamDevice = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||
InputStream in = null;
|
||||
Bitmap bmp = null;
|
||||
try {
|
||||
// Make sure the cached asset doesn't exceed the maximum size
|
||||
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
return null;
|
||||
}
|
||||
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
||||
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||
// Raw height and width of image
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
|
||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = sampleSize;
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||
} catch (IOException ignored) {
|
||||
} finally {
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {}
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
|
||||
final int halfHeight = height / 2;
|
||||
final int halfWidth = width / 2;
|
||||
|
||||
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
||||
// height and width larger than the requested height and width.
|
||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (bmp != null) {
|
||||
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
|
||||
// Don't bother with anything if it doesn't exist
|
||||
if (!file.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make sure the cached asset doesn't exceed the maximum size
|
||||
if (file.length() > MAX_ASSET_SIZE) {
|
||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
||||
file.delete();
|
||||
return null;
|
||||
}
|
||||
|
||||
Bitmap bmp;
|
||||
|
||||
// For OSes prior to P, we have to use the ugly BitmapFactory API
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
// Lookup bounds of the downloaded image
|
||||
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
||||
decodeOnlyOptions.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
|
||||
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
|
||||
// Dimensions set to -1 on error. Return value always null.
|
||||
return null;
|
||||
}
|
||||
|
||||
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
|
||||
|
||||
// Load the image scaled to the appropriate size
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
|
||||
STANDARD_ASSET_WIDTH / sampleSize,
|
||||
STANDARD_ASSET_HEIGHT / sampleSize);
|
||||
if (isLowRamDevice) {
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
||||
options.inDither = true;
|
||||
}
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
options.inPreferredConfig = Bitmap.Config.HARDWARE;
|
||||
}
|
||||
|
||||
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
if (bmp != null) {
|
||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// On P, we can get a bitmap back in one step with ImageDecoder
|
||||
try {
|
||||
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
||||
@Override
|
||||
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
||||
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
||||
if (isLowRamDevice) {
|
||||
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return bmp;
|
||||
@@ -62,7 +137,7 @@ public class DiskAssetLoader {
|
||||
OutputStream out = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
@@ -76,7 +151,7 @@ public class DiskAssetLoader {
|
||||
|
||||
if (!success) {
|
||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class MemoryAssetLoader {
|
||||
};
|
||||
|
||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||
}
|
||||
|
||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
|
||||
@@ -4,12 +4,11 @@ import android.content.Context;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class NetworkAssetLoader {
|
||||
private final Context context;
|
||||
@@ -21,10 +20,10 @@ public class NetworkAssetLoader {
|
||||
}
|
||||
|
||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||
|
||||
InputStream in = null;
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
|
||||
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
|
||||
in = http.getBoxArt(tuple.app);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
@@ -37,13 +36,4 @@ public class NetworkAssetLoader {
|
||||
|
||||
return in;
|
||||
}
|
||||
|
||||
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
return computer.localIp;
|
||||
}
|
||||
else {
|
||||
return computer.remoteIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.limelight.nvstream.av.video.cpu;
|
||||
|
||||
public class AvcDecoder {
|
||||
static {
|
||||
// FFMPEG dependencies
|
||||
System.loadLibrary("avutil-52");
|
||||
System.loadLibrary("swresample-0");
|
||||
System.loadLibrary("swscale-2");
|
||||
System.loadLibrary("avcodec-55");
|
||||
System.loadLibrary("avformat-55");
|
||||
|
||||
System.loadLibrary("nv_avc_dec");
|
||||
}
|
||||
|
||||
/** Disables the deblocking filter at the cost of image quality */
|
||||
public static final int DISABLE_LOOP_FILTER = 0x1;
|
||||
/** Uses the low latency decode flag (disables multithreading) */
|
||||
public static final int LOW_LATENCY_DECODE = 0x2;
|
||||
/** Threads process each slice, rather than each frame */
|
||||
public static final int SLICE_THREADING = 0x4;
|
||||
/** Uses nonstandard speedup tricks */
|
||||
public static final int FAST_DECODE = 0x8;
|
||||
/** Uses bilinear filtering instead of bicubic */
|
||||
public static final int BILINEAR_FILTERING = 0x10;
|
||||
/** Uses a faster bilinear filtering with lower image quality */
|
||||
public static final int FAST_BILINEAR_FILTERING = 0x20;
|
||||
/** Disables color conversion (output is NV21) */
|
||||
public static final int NO_COLOR_CONVERSION = 0x40;
|
||||
|
||||
public static native int init(int width, int height, int perflvl, int threadcount);
|
||||
public static native void destroy();
|
||||
|
||||
// Rendering API when NO_COLOR_CONVERSION == 0
|
||||
public static native boolean setRenderTarget(Object androidSurface);
|
||||
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
|
||||
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
|
||||
public static native boolean redraw();
|
||||
|
||||
// Rendering API when NO_COLOR_CONVERSION == 1
|
||||
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
|
||||
|
||||
public static native int getInputPaddingSize();
|
||||
public static native int decode(byte[] indata, int inoff, int inlen);
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Locale;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
@@ -17,10 +21,10 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.TextView;
|
||||
@@ -29,7 +33,7 @@ import android.widget.Toast;
|
||||
public class AddComputerManually extends Activity {
|
||||
private TextView hostText;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
||||
private Thread addThread;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||
@@ -43,42 +47,90 @@ public class AddComputerManually extends Activity {
|
||||
}
|
||||
};
|
||||
|
||||
private boolean isWrongSubnetSiteLocalAddress(String address) {
|
||||
try {
|
||||
InetAddress targetAddress = InetAddress.getByName(address);
|
||||
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have a site-local address. Look for a matching local interface.
|
||||
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
||||
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
||||
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
|
||||
// Skip non-site-local or non-IPv4 addresses
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] targetAddrBytes = targetAddress.getAddress();
|
||||
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
|
||||
|
||||
// Compare prefix to ensure it's the same
|
||||
boolean addressMatches = true;
|
||||
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
|
||||
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
|
||||
addressMatches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (addressMatches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Couldn't find a matching interface
|
||||
return true;
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doAddPc(String host) {
|
||||
String msg;
|
||||
boolean finish = false;
|
||||
boolean wrongSiteLocal = false;
|
||||
boolean success;
|
||||
|
||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||
getResources().getString(R.string.msg_add_pc), false);
|
||||
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(host);
|
||||
|
||||
if (!managerBinder.addComputerBlocking(addr)){
|
||||
msg = getResources().getString(R.string.addpc_fail);
|
||||
}
|
||||
else {
|
||||
msg = getResources().getString(R.string.addpc_success);
|
||||
finish = true;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||
success = managerBinder.addComputerBlocking(host, true);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
||||
e.printStackTrace();
|
||||
success = false;
|
||||
}
|
||||
if (!success){
|
||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
final boolean toastFinish = finish;
|
||||
final String toastMsg = msg;
|
||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||
if (wrongSiteLocal) {
|
||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
||||
}
|
||||
else if (!success) {
|
||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
||||
}
|
||||
else {
|
||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
|
||||
|
||||
if (toastFinish && !isFinishing()) {
|
||||
if (!isFinishing()) {
|
||||
// Close the activity
|
||||
AddComputerManually.this.finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void startAddThread() {
|
||||
@@ -136,18 +188,13 @@ public class AddComputerManually extends Activity {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(locale);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
setContentView(R.layout.activity_add_computer_manually);
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||
this.hostText = findViewById(R.id.hostTextView);
|
||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
@@ -156,12 +203,7 @@ public class AddComputerManually extends Activity {
|
||||
(keyEvent != null &&
|
||||
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||
if (hostText.getText().length() == 0) {
|
||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
computersToAdd.add(hostText.getText().toString().trim());
|
||||
return handleDoneEvent();
|
||||
}
|
||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
||||
// This is how the Fire TV dismisses the keyboard
|
||||
@@ -174,8 +216,28 @@ public class AddComputerManually extends Activity {
|
||||
}
|
||||
});
|
||||
|
||||
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleDoneEvent();
|
||||
}
|
||||
});
|
||||
|
||||
// Bind to the ComputerManager service
|
||||
bindService(new Intent(AddComputerManually.this,
|
||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
// Returns true if the event should be eaten
|
||||
private boolean handleDoneEvent() {
|
||||
String hostAddress = hostText.getText().toString().trim();
|
||||
|
||||
if (hostAddress.length() == 0) {
|
||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
computersToAdd.add(hostAddress);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Build;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
|
||||
|
||||
public class ConfirmDeleteOscPreference extends DialogPreference {
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public ConfirmDeleteOscPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
|
||||
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class GlPreferences {
|
||||
private static final String PREF_NAME = "GlPreferences";
|
||||
|
||||
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
|
||||
private static final String GL_RENDERER_PREF_STRING = "Renderer";
|
||||
|
||||
private SharedPreferences prefs;
|
||||
public String glRenderer;
|
||||
public String savedFingerprint;
|
||||
|
||||
private GlPreferences(SharedPreferences prefs) {
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
public static GlPreferences readPreferences(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
|
||||
GlPreferences glPrefs = new GlPreferences(prefs);
|
||||
|
||||
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
|
||||
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
|
||||
|
||||
return glPrefs;
|
||||
}
|
||||
|
||||
public boolean writePreferences() {
|
||||
return prefs.edit()
|
||||
.putString(GL_RENDERER_PREF_STRING, glRenderer)
|
||||
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
private static final String DECODER_PREF_STRING = "list_decoders";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
|
||||
|
||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
||||
static final String FPS_PREF_STRING = "list_fps";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
||||
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
|
||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||
@@ -19,16 +23,23 @@ public class PreferenceConfiguration {
|
||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
||||
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
||||
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
||||
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final String DEFAULT_DECODER = "auto";
|
||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
static final String DEFAULT_RESOLUTION = "720p";
|
||||
static final String DEFAULT_FPS = "60";
|
||||
private static final boolean DEFAULT_STRETCH = false;
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
@@ -37,36 +48,130 @@ public class PreferenceConfiguration {
|
||||
public static final String DEFAULT_LANGUAGE = "default";
|
||||
private static final boolean DEFAULT_LIST_MODE = false;
|
||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
||||
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||
private static final boolean ONLY_L3_R3_DEFAULT = false;
|
||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
|
||||
|
||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||
public static final int AUTOSELECT_DECODER = 0;
|
||||
public static final int FORCE_SOFTWARE_DECODER = 1;
|
||||
public static final int FORCE_H265_ON = -1;
|
||||
public static final int AUTOSELECT_H265 = 0;
|
||||
public static final int FORCE_H265_OFF = 1;
|
||||
|
||||
public int width, height, fps;
|
||||
public int bitrate;
|
||||
public int decoder;
|
||||
public int videoFormat;
|
||||
public int deadzonePercentage;
|
||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||
public String language;
|
||||
public boolean listMode, smallIconMode, multiController, usbDriver;
|
||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||
public boolean onscreenController;
|
||||
public boolean onlyL3R3;
|
||||
public boolean disableFrameDrop;
|
||||
public boolean enableHdr;
|
||||
public boolean enablePip;
|
||||
public boolean bindAllUsb;
|
||||
public boolean mouseEmulation;
|
||||
public boolean mouseNavButtons;
|
||||
public boolean unlockFps;
|
||||
public boolean vibrateOsc;
|
||||
public boolean vibrateFallbackToDevice;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
private static int getHeightFromResolutionString(String resString) {
|
||||
if (resString.equalsIgnoreCase("360p")) {
|
||||
return 360;
|
||||
}
|
||||
else if (resFpsString.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
else if (resString.equalsIgnoreCase("480p")) {
|
||||
return 480;
|
||||
}
|
||||
else if (resFpsString.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
else if (resString.equalsIgnoreCase("720p")) {
|
||||
return 720;
|
||||
}
|
||||
else if (resFpsString.equals("1080p60")) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
else if (resString.equalsIgnoreCase("1080p")) {
|
||||
return 1080;
|
||||
}
|
||||
else if (resString.equalsIgnoreCase("1440p")) {
|
||||
return 1440;
|
||||
}
|
||||
else if (resString.equalsIgnoreCase("4K")) {
|
||||
return 2160;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
// Should be unreachable
|
||||
return 720;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getWidthFromResolutionString(String resString) {
|
||||
int height = getHeightFromResolutionString(resString);
|
||||
if (height == 480) {
|
||||
// This isn't an exact 16:9 resolution
|
||||
return 854;
|
||||
}
|
||||
else {
|
||||
return (height * 16) / 9;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getResolutionString(int width, int height) {
|
||||
switch (height) {
|
||||
case 360:
|
||||
return "360p";
|
||||
case 480:
|
||||
return "480p";
|
||||
default:
|
||||
case 720:
|
||||
return "720p";
|
||||
case 1080:
|
||||
return "1080p";
|
||||
case 1440:
|
||||
return "1440p";
|
||||
case 2160:
|
||||
return "4K";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(String resString, String fpsString) {
|
||||
int width = getWidthFromResolutionString(resString);
|
||||
int height = getHeightFromResolutionString(resString);
|
||||
int fps = Integer.parseInt(fpsString);
|
||||
|
||||
// This table prefers 16:10 resolutions because they are
|
||||
// only slightly more pixels than the 16:9 equivalents, so
|
||||
// we don't want to bump those 16:10 resolutions up to the
|
||||
// next 16:9 slot.
|
||||
//
|
||||
// This logic is shamelessly stolen from Moonlight Qt:
|
||||
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
||||
|
||||
if (width * height <= 640 * 360) {
|
||||
return (int)(1000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 854 * 480) {
|
||||
return (int)(1500 * (fps / 30.0));
|
||||
}
|
||||
// This covers 1280x720 and 1280x800 too
|
||||
else if (width * height <= 1366 * 768) {
|
||||
return (int)(5000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 1920 * 1200) {
|
||||
return (int)(10000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 2560 * 1600) {
|
||||
return (int)(20000 * (fps / 30.0));
|
||||
}
|
||||
else /* if (width * height <= 3840 * 2160) */ {
|
||||
return (int)(40000 * (fps / 30.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,67 +192,125 @@ public class PreferenceConfiguration {
|
||||
}
|
||||
|
||||
// Use small mode on anything smaller than a 7" tablet
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
||||
return getDefaultBitrate(
|
||||
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
||||
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||
}
|
||||
|
||||
private static int getDecoderValue(Context context) {
|
||||
private static int getVideoFormatValue(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
|
||||
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
|
||||
if (str.equals("auto")) {
|
||||
return AUTOSELECT_DECODER;
|
||||
return AUTOSELECT_H265;
|
||||
}
|
||||
else if (str.equals("software")) {
|
||||
return FORCE_SOFTWARE_DECODER;
|
||||
else if (str.equals("forceh265")) {
|
||||
return FORCE_H265_ON;
|
||||
}
|
||||
else if (str.equals("hardware")) {
|
||||
return FORCE_HARDWARE_DECODER;
|
||||
else if (str.equals("neverh265")) {
|
||||
return FORCE_H265_OFF;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return AUTOSELECT_DECODER;
|
||||
return AUTOSELECT_H265;
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetStreamingSettings(Context context) {
|
||||
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit()
|
||||
.remove(BITRATE_PREF_STRING)
|
||||
.remove(BITRATE_PREF_OLD_STRING)
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.remove(RESOLUTION_PREF_STRING)
|
||||
.remove(FPS_PREF_STRING)
|
||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
||||
.remove(ENABLE_HDR_PREF_STRING)
|
||||
.remove(UNLOCK_FPS_STRING)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public static PreferenceConfiguration readPreferences(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
// Migrate legacy preferences to the new locations
|
||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
||||
if (str != null) {
|
||||
if (str.equals("360p30")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("360p60")) {
|
||||
config.width = 640;
|
||||
config.height = 360;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("720p30")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
config.fps = 60;
|
||||
}
|
||||
else if (str.equals("4K30")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 30;
|
||||
}
|
||||
else if (str.equals("4K60")) {
|
||||
config.width = 3840;
|
||||
config.height = 2160;
|
||||
config.fps = 60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
||||
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
|
||||
.putString(FPS_PREF_STRING, ""+config.fps)
|
||||
.apply();
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
// Use the new preference location
|
||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
||||
}
|
||||
|
||||
config.decoder = getDecoderValue(context);
|
||||
// This must happen after the preferences migration to ensure the preferences are populated
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
||||
if (config.bitrate == 0) {
|
||||
config.bitrate = getDefaultBitrate(context);
|
||||
}
|
||||
|
||||
config.videoFormat = getVideoFormatValue(context);
|
||||
|
||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||
|
||||
@@ -161,7 +324,19 @@ public class PreferenceConfiguration {
|
||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import android.widget.TextView;
|
||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||
public class SeekBarPreference extends DialogPreference
|
||||
{
|
||||
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||
private static final String ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
|
||||
|
||||
private SeekBar seekBar;
|
||||
private TextView valueText;
|
||||
@@ -27,6 +28,7 @@ public class SeekBarPreference extends DialogPreference
|
||||
private final int defaultValue;
|
||||
private final int maxValue;
|
||||
private final int minValue;
|
||||
private final int stepSize;
|
||||
private int currentValue;
|
||||
|
||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||
@@ -34,27 +36,28 @@ public class SeekBarPreference extends DialogPreference
|
||||
this.context = context;
|
||||
|
||||
// Read the message from XML
|
||||
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
|
||||
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
|
||||
if (dialogMessageId == 0) {
|
||||
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
|
||||
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
|
||||
}
|
||||
else {
|
||||
dialogMessage = context.getString(dialogMessageId);
|
||||
}
|
||||
|
||||
// Get the suffix for the number displayed in the dialog
|
||||
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
|
||||
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
|
||||
if (suffixId == 0) {
|
||||
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
|
||||
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
|
||||
}
|
||||
else {
|
||||
suffix = context.getString(suffixId);
|
||||
}
|
||||
|
||||
// Get default, min, and max seekbar values
|
||||
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||
minValue = 1;
|
||||
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
||||
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
||||
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,6 +92,12 @@ public class SeekBarPreference extends DialogPreference
|
||||
return;
|
||||
}
|
||||
|
||||
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
|
||||
if (roundedValue != value) {
|
||||
seekBar.setProgress(roundedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
String t = String.valueOf(value);
|
||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||
}
|
||||
|
||||
@@ -2,38 +2,45 @@ package com.limelight.preferences;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.os.Handler;
|
||||
import android.preference.ListPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.util.Range;
|
||||
import android.view.Display;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.PcView;
|
||||
import com.limelight.R;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class StreamSettings extends Activity {
|
||||
private PreferenceConfiguration previousPrefs;
|
||||
|
||||
void reloadSettings() {
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
||||
|
||||
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
config.locale = new Locale(previousPrefs.language);
|
||||
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||
}
|
||||
UiHelper.setLocale(this);
|
||||
|
||||
setContentView(R.layout.activity_stream_settings);
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
reloadSettings();
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
}
|
||||
@@ -55,26 +62,320 @@ public class StreamSettings extends Activity {
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
|
||||
private void setValue(String preferenceKey, String value) {
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
pref.setValue(value);
|
||||
}
|
||||
|
||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
||||
int matchingCount = 0;
|
||||
|
||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
||||
|
||||
// Count the number of matching entries we'll be removing
|
||||
for (CharSequence seq : pref.getEntryValues()) {
|
||||
if (seq.toString().equalsIgnoreCase(value)) {
|
||||
matchingCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the new arrays
|
||||
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
|
||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
||||
int outIndex = 0;
|
||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
||||
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
||||
// Skip matching values
|
||||
continue;
|
||||
}
|
||||
|
||||
entries[outIndex] = pref.getEntries()[i];
|
||||
entryValues[outIndex] = pref.getEntryValues()[i];
|
||||
outIndex++;
|
||||
}
|
||||
|
||||
if (pref.getValue().equalsIgnoreCase(value)) {
|
||||
onMatched.run();
|
||||
}
|
||||
|
||||
// Update the preference with the new list
|
||||
pref.setEntries(entries);
|
||||
pref.setEntryValues(entryValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
||||
if (res == null) {
|
||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
||||
}
|
||||
if (fps == null) {
|
||||
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
||||
.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
PreferenceScreen screen = getPreferenceScreen();
|
||||
|
||||
// hide on-screen controls category on non touch screen devices
|
||||
if (!getActivity().getPackageManager().
|
||||
hasSystemFeature("android.hardware.touchscreen")) {
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_onscreen_controls");
|
||||
screen.removePreference(category);
|
||||
}
|
||||
|
||||
// Remove PiP mode on devices pre-Oreo
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_basic_settings");
|
||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
||||
}
|
||||
|
||||
int maxSupportedFps = 0;
|
||||
|
||||
// Hide non-supported resolution/FPS combinations
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||
|
||||
int maxSupportedResW = 0;
|
||||
|
||||
// Always allow resolutions that are smaller or equal to the active
|
||||
// display resolution because decoders can report total non-sense to us.
|
||||
// For example, a p201 device reports:
|
||||
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
|
||||
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
||||
// AVC supported width range: 64 - 384
|
||||
// HEVC supported width range: 64 - 544
|
||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||
// Some devices report their dimensions in the portrait orientation
|
||||
// where height > width. Normalize these to the conventional width > height
|
||||
// arrangement before we process them.
|
||||
|
||||
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
||||
|
||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
||||
maxSupportedResW = 3840;
|
||||
}
|
||||
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
||||
maxSupportedResW = 2560;
|
||||
}
|
||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
||||
maxSupportedResW = 1920;
|
||||
}
|
||||
|
||||
if (candidate.getRefreshRate() > maxSupportedFps) {
|
||||
maxSupportedFps = (int)candidate.getRefreshRate();
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called to do runtime initialization before calling functions that evaluate
|
||||
// decoder lists.
|
||||
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
||||
|
||||
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
|
||||
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
||||
|
||||
if (avcDecoder != null) {
|
||||
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
||||
|
||||
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
|
||||
|
||||
// If 720p is not reported as supported, ignore all results from this API
|
||||
if (avcWidthRange.contains(1280)) {
|
||||
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
||||
maxSupportedResW = 3840;
|
||||
}
|
||||
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
||||
maxSupportedResW = 1920;
|
||||
}
|
||||
else if (maxSupportedResW < 1280) {
|
||||
maxSupportedResW = 1280;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hevcDecoder != null) {
|
||||
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
||||
|
||||
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
|
||||
|
||||
// If 720p is not reported as supported, ignore all results from this API
|
||||
if (hevcWidthRange.contains(1280)) {
|
||||
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
||||
maxSupportedResW = 3840;
|
||||
}
|
||||
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
||||
maxSupportedResW = 1920;
|
||||
}
|
||||
else if (maxSupportedResW < 1280) {
|
||||
maxSupportedResW = 1280;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
||||
|
||||
if (maxSupportedResW != 0) {
|
||||
if (maxSupportedResW < 3840) {
|
||||
// 4K is unsupported
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 2560) {
|
||||
// 1440p is unsupported
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedResW < 1920) {
|
||||
// 1080p is unsupported
|
||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 720p
|
||||
}
|
||||
}
|
||||
|
||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
||||
// We give some extra room in case the FPS is rounded down
|
||||
if (maxSupportedFps < 118) {
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (maxSupportedFps < 88) {
|
||||
// 1080p is unsupported
|
||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
|
||||
resetBitrateToDefault(prefs, null, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Never remove 30 FPS or 60 FPS
|
||||
}
|
||||
|
||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
||||
// that the unlock FPS option relies on to not massively increase latency.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_basic_settings");
|
||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
||||
}
|
||||
else {
|
||||
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
// HACK: We need to let the preference change succeed before reinitializing to ensure
|
||||
// it's reflected in the new layout.
|
||||
final Handler h = new Handler();
|
||||
h.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Ensure the activity is still open when this timeout expires
|
||||
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
|
||||
if (settingsActivity != null) {
|
||||
settingsActivity.reloadSettings();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove HDR preference for devices below Nougat
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
LimeLog.info("Excluding HDR toggle based on OS");
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
||||
}
|
||||
else {
|
||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
||||
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
||||
|
||||
// We must now ensure our display is compatible with HDR10
|
||||
boolean foundHdr10 = false;
|
||||
if (hdrCaps != null) {
|
||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
||||
foundHdr10 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundHdr10) {
|
||||
LimeLog.info("Excluding HDR toggle based on display capabilities");
|
||||
PreferenceCategory category =
|
||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
||||
}
|
||||
}
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||
.apply();
|
||||
resetBitrateToDefault(prefs, valueStr, null);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
resetBitrateToDefault(prefs, null, valueStr);
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
|
||||
@@ -3,6 +3,6 @@ package com.limelight.ui;
|
||||
import android.widget.AbsListView;
|
||||
|
||||
public interface AdapterFragmentCallbacks {
|
||||
public int getAdapterFragmentLayoutId();
|
||||
public void receiveAbsListView(AbsListView gridView);
|
||||
int getAdapterFragmentLayoutId();
|
||||
void receiveAbsListView(AbsListView gridView);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
public interface GameGestures {
|
||||
public void showKeyboard();
|
||||
void showKeyboard();
|
||||
}
|
||||
|
||||
86
app/src/main/java/com/limelight/ui/StreamView.java
Normal file
86
app/src/main/java/com/limelight/ui/StreamView.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
public class StreamView extends SurfaceView {
|
||||
private double desiredAspectRatio;
|
||||
private InputCallbacks inputCallbacks;
|
||||
|
||||
public void setDesiredAspectRatio(double aspectRatio) {
|
||||
this.desiredAspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
public void setInputCallbacks(InputCallbacks callbacks) {
|
||||
this.inputCallbacks = callbacks;
|
||||
}
|
||||
|
||||
public StreamView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public StreamView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||
if (desiredAspectRatio == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
int measuredHeight, measuredWidth;
|
||||
if (widthSize > heightSize * desiredAspectRatio) {
|
||||
measuredHeight = heightSize;
|
||||
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||
} else {
|
||||
measuredWidth = widthSize;
|
||||
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||
}
|
||||
|
||||
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
||||
// This callbacks allows us to override dumb IME behavior like when
|
||||
// Samsung's default keyboard consumes Shift+Space.
|
||||
if (inputCallbacks != null) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (inputCallbacks.handleKeyDown(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
if (inputCallbacks.handleKeyUp(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
public interface InputCallbacks {
|
||||
boolean handleKeyUp(KeyEvent event);
|
||||
boolean handleKeyDown(KeyEvent event);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
|
||||
public class CacheHelper {
|
||||
private static File openPath(boolean createPath, File root, String... path) {
|
||||
public static File openPath(boolean createPath, File root, String... path) {
|
||||
File f = root;
|
||||
for (int i = 0; i < path.length; i++) {
|
||||
String component = path[i];
|
||||
|
||||
@@ -5,23 +5,26 @@ import java.util.ArrayList;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
public class Dialog implements Runnable {
|
||||
private final String title;
|
||||
private final String message;
|
||||
private final Activity activity;
|
||||
private final boolean endAfterDismiss;
|
||||
private final Runnable runOnDismiss;
|
||||
|
||||
private AlertDialog alert;
|
||||
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||
|
||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
||||
{
|
||||
this.activity = activity;
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.endAfterDismiss = endAfterDismiss;
|
||||
this.runOnDismiss = runOnDismiss;
|
||||
}
|
||||
|
||||
public static void closeDialogs()
|
||||
@@ -37,9 +40,21 @@ public class Dialog implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
|
||||
{
|
||||
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
||||
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (endAfterDismiss) {
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
||||
{
|
||||
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,18 +70,39 @@ public class Dialog implements Runnable {
|
||||
alert.setCancelable(false);
|
||||
alert.setCanceledOnTouchOutside(false);
|
||||
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
alert.dismiss();
|
||||
}
|
||||
|
||||
if (endAfterDismiss) {
|
||||
activity.finish();
|
||||
}
|
||||
runOnDismiss.run();
|
||||
}
|
||||
});
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
alert.dismiss();
|
||||
}
|
||||
|
||||
runOnDismiss.run();
|
||||
|
||||
HelpLauncher.launchTroubleshooting(activity);
|
||||
}
|
||||
});
|
||||
alert.setOnShowListener(new DialogInterface.OnShowListener(){
|
||||
|
||||
@Override
|
||||
public void onShow(DialogInterface dialog) {
|
||||
// Set focus to the OK button by default
|
||||
Button button = alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setFocusable(true);
|
||||
button.setFocusableInTouchMode(true);
|
||||
button.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.add(this);
|
||||
|
||||
66
app/src/main/java/com/limelight/utils/HelpLauncher.java
Normal file
66
app/src/main/java/com/limelight/utils/HelpLauncher.java
Normal file
@@ -0,0 +1,66 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.limelight.HelpActivity;
|
||||
|
||||
public class HelpLauncher {
|
||||
|
||||
private static boolean isKnownBrowser(Context context, Intent i) {
|
||||
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
if (resolvedActivity == null) {
|
||||
// No browser
|
||||
return false;
|
||||
}
|
||||
|
||||
String name = resolvedActivity.activityInfo.name;
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
name = name.toLowerCase();
|
||||
return name.contains("chrome") || name.contains("firefox");
|
||||
}
|
||||
|
||||
private static void launchUrl(Context context, String url) {
|
||||
// Try to launch the default browser
|
||||
try {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
|
||||
// Several Android TV devices will lie and say they do have a browser
|
||||
// even though the OS just shows an error dialog if we try to use it. We need to
|
||||
// be a bit more clever on these devices and detect if the browser is a legitimate
|
||||
// browser or just a fake error message activity.
|
||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
||||
isKnownBrowser(context, i)) {
|
||||
context.startActivity(i);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// This is only supposed to throw ActivityNotFoundException but
|
||||
// it can (at least) also throw SecurityException if a user's default
|
||||
// browser is not exported. We'll catch everything to workaround this.
|
||||
|
||||
// Fall through
|
||||
}
|
||||
|
||||
// This platform has no browser (possibly a leanback device)
|
||||
// We'll launch our WebView activity
|
||||
Intent i = new Intent(context, HelpActivity.class);
|
||||
i.setData(Uri.parse(url));
|
||||
context.startActivity(i);
|
||||
}
|
||||
|
||||
public static void launchSetupGuide(Context context) {
|
||||
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||
}
|
||||
|
||||
public static void launchTroubleshooting(Context context) {
|
||||
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
}
|
||||
}
|
||||
@@ -13,32 +13,50 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.InetAddress;
|
||||
import java.io.IOException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
|
||||
public class ServerHelper {
|
||||
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||
computer.localIp : computer.remoteIp;
|
||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||
return computer.activeAddress;
|
||||
}
|
||||
|
||||
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
Intent intent = new Intent(parent, Game.class);
|
||||
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
|
||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
|
||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||
try {
|
||||
if (computer.serverCert != null) {
|
||||
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
||||
}
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
Intent intent = new Intent(parent, Game.class);
|
||||
intent.putExtra(Game.EXTRA_HOST,
|
||||
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||
parent.startActivity(intent);
|
||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
||||
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||
}
|
||||
|
||||
public static void doQuit(final Activity parent,
|
||||
final InetAddress address,
|
||||
final ComputerDetails computer,
|
||||
final NvApp app,
|
||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||
final Runnable onComplete) {
|
||||
@@ -49,8 +67,8 @@ public class ServerHelper {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
httpConn = new NvHTTP(address,
|
||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
|
||||
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
|
||||
if (httpConn.quitApp()) {
|
||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||
} else {
|
||||
@@ -69,8 +87,9 @@ public class ServerHelper {
|
||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||
} catch (FileNotFoundException e) {
|
||||
message = parent.getResources().getString(R.string.error_404);
|
||||
} catch (Exception e) {
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
message = e.getMessage();
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (onComplete != null) {
|
||||
onComplete.run();
|
||||
|
||||
171
app/src/main/java/com/limelight/utils/ShortcutHelper.java
Normal file
171
app/src/main/java/com/limelight/utils/ShortcutHelper.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.ShortcutTrampoline;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ShortcutHelper {
|
||||
|
||||
private final ShortcutManager sm;
|
||||
private final Context context;
|
||||
|
||||
public ShortcutHelper(Context context) {
|
||||
this.context = context;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
sm = context.getSystemService(ShortcutManager.class);
|
||||
}
|
||||
else {
|
||||
sm = null;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private void reapShortcutsForDynamicAdd() {
|
||||
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
||||
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
||||
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
||||
for (ShortcutInfo scut : dynamicShortcuts) {
|
||||
if (maxRankShortcut.getRank() < scut.getRank()) {
|
||||
maxRankShortcut = scut;
|
||||
}
|
||||
}
|
||||
sm.removeDynamicShortcuts(Collections.singletonList(maxRankShortcut.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private List<ShortcutInfo> getAllShortcuts() {
|
||||
LinkedList<ShortcutInfo> list = new LinkedList<>();
|
||||
list.addAll(sm.getDynamicShortcuts());
|
||||
list.addAll(sm.getPinnedShortcuts());
|
||||
return list;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private ShortcutInfo getInfoForId(String id) {
|
||||
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||
|
||||
for (ShortcutInfo info : shortcuts) {
|
||||
if (info.getId().equals(id)) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private boolean isExistingDynamicShortcut(String id) {
|
||||
for (ShortcutInfo si : sm.getDynamicShortcuts()) {
|
||||
if (si.getId().equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reportShortcutUsed(String id) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
if (getInfoForId(id) != null) {
|
||||
sm.reportShortcutUsed(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||
.setIntent(i)
|
||||
.setShortLabel(computerName)
|
||||
.setLongLabel(computerName)
|
||||
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||
.build();
|
||||
|
||||
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||
if (existingSinfo != null) {
|
||||
// Update in place
|
||||
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||
sm.enableShortcuts(Collections.singletonList(id));
|
||||
}
|
||||
|
||||
// Reap shortcuts to make space for this if it's new
|
||||
// NOTE: This CAN'T be an else on the above if, because it's
|
||||
// possible that we have an existing shortcut but it's not a dynamic one.
|
||||
if (!isExistingDynamicShortcut(id)) {
|
||||
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||
// to interact with this PC.
|
||||
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
||||
reapShortcutsForDynamicAdd();
|
||||
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||
createAppViewShortcut(id, details.name, details.uuid, forceAdd);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
|
||||
if (sm.isRequestPinShortcutSupported()) {
|
||||
Icon appIcon;
|
||||
Intent i = new Intent(context, ShortcutTrampoline.class);
|
||||
|
||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
if (iconBits != null) {
|
||||
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
|
||||
} else {
|
||||
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
|
||||
}
|
||||
|
||||
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
|
||||
.setIntent(i)
|
||||
.setShortLabel(appName + " (" + computerName + ")")
|
||||
.setIcon(appIcon)
|
||||
.build();
|
||||
|
||||
return sm.requestPinShortcut(sInfo, null);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
|
||||
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
|
||||
}
|
||||
|
||||
public void disableShortcut(String id, CharSequence reason) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
if (getInfoForId(id) != null) {
|
||||
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||
private ProgressDialog progress;
|
||||
private final boolean finish;
|
||||
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
|
||||
|
||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
|
||||
@@ -5,10 +5,17 @@ import android.app.AlertDialog;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class UiHelper {
|
||||
|
||||
@@ -16,6 +23,28 @@ public class UiHelper {
|
||||
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||
|
||||
public static void setLocale(Activity activity)
|
||||
{
|
||||
String locale = PreferenceConfiguration.readPreferences(activity).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(activity.getResources().getConfiguration());
|
||||
|
||||
// Some locales include both language and country which must be separated
|
||||
// before calling the Locale constructor.
|
||||
if (locale.contains("-"))
|
||||
{
|
||||
config.locale = new Locale(locale.substring(0, locale.indexOf('-')),
|
||||
locale.substring(locale.indexOf('-') + 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
config.locale = new Locale(locale);
|
||||
}
|
||||
|
||||
activity.getResources().updateConfiguration(config, activity.getResources().getDisplayMetrics());
|
||||
}
|
||||
}
|
||||
|
||||
public static void notifyNewRootView(Activity activity)
|
||||
{
|
||||
View rootView = activity.findViewById(android.R.id.content);
|
||||
@@ -31,6 +60,54 @@ public class UiHelper {
|
||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||
horizontalPaddingPixels, verticalPaddingPixels);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// Allow this non-streaming activity to layout under notches.
|
||||
//
|
||||
// We should NOT do this for the Game activity unless
|
||||
// the user specifically opts in, because it can obscure
|
||||
// parts of the streaming surface.
|
||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
}
|
||||
}
|
||||
|
||||
public static void showDecoderCrashDialog(Activity activity) {
|
||||
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
|
||||
final int crashCount = prefs.getInt("CrashCount", 0);
|
||||
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
||||
|
||||
// Remember the last crash count we notified at, so we don't
|
||||
// display the crash dialog every time the app is started until
|
||||
// they stream again
|
||||
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
||||
if (crashCount % 3 == 0) {
|
||||
// At 3 consecutive crashes, we'll forcefully reset their settings
|
||||
PreferenceConfiguration.resetStreamingSettings(activity);
|
||||
Dialog.displayDialog(activity,
|
||||
activity.getResources().getString(R.string.title_decoding_reset),
|
||||
activity.getResources().getString(R.string.message_decoding_reset),
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Mark notification as acknowledged on dismissal
|
||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
Dialog.displayDialog(activity,
|
||||
activity.getResources().getString(R.string.title_decoding_error),
|
||||
activity.getResources().getString(R.string.message_decoding_error),
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Mark notification as acknowledged on dismissal
|
||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||
@@ -59,4 +136,32 @@ public class UiHelper {
|
||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, final Runnable onYes, final Runnable onNo) {
|
||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which){
|
||||
case DialogInterface.BUTTON_POSITIVE:
|
||||
if (onYes != null) {
|
||||
onYes.run();
|
||||
}
|
||||
break;
|
||||
|
||||
case DialogInterface.BUTTON_NEGATIVE:
|
||||
if (onNo != null) {
|
||||
onNo.run();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||
builder.setMessage(parent.getResources().getString(R.string.delete_pc_msg))
|
||||
.setTitle(computer.name)
|
||||
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
47
app/src/main/java/com/limelight/utils/Vector2d.java
Normal file
47
app/src/main/java/com/limelight/utils/Vector2d.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
public class Vector2d {
|
||||
private float x;
|
||||
private float y;
|
||||
private double magnitude;
|
||||
|
||||
public static final Vector2d ZERO = new Vector2d();
|
||||
|
||||
public Vector2d() {
|
||||
initialize(0, 0);
|
||||
}
|
||||
|
||||
public void initialize(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
||||
}
|
||||
|
||||
public double getMagnitude() {
|
||||
return magnitude;
|
||||
}
|
||||
|
||||
public void getNormalized(Vector2d vector) {
|
||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
||||
}
|
||||
|
||||
public void scalarMultiply(double factor) {
|
||||
initialize((float)(x * factor), (float)(y * factor));
|
||||
}
|
||||
|
||||
public void setX(float x) {
|
||||
initialize(x, this.y);
|
||||
}
|
||||
|
||||
public void setY(float y) {
|
||||
initialize(this.x, y);
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
# Application.mk for Limelight
|
||||
# Application.mk for Moonlight
|
||||
|
||||
# Our minimum version is Android 4.1
|
||||
APP_PLATFORM := android-16
|
||||
|
||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
|
||||
|
||||
# We want an optimized build
|
||||
APP_OPTIM := release
|
||||
|
||||
@@ -5,9 +5,32 @@ include $(call all-subdir-makefiles)
|
||||
|
||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := evdev_reader
|
||||
LOCAL_SRC_FILES := evdev_reader.c
|
||||
LOCAL_LDLIBS := -llog
|
||||
# Only build evdev_reader for the rooted APK flavor
|
||||
ifeq (root,$(PRODUCT_FLAVOR))
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := evdev_reader
|
||||
LOCAL_SRC_FILES := evdev_reader.c
|
||||
LOCAL_LDLIBS := -llog
|
||||
|
||||
|
||||
# This next portion of the makefile is mostly copied from build-executable.mk but
|
||||
# creates a binary with the libXXX.so form so the APK will install and drop
|
||||
# the binary correctly.
|
||||
|
||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||
LOCAL_MAKEFILE := $(local-makefile)
|
||||
|
||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
||||
$(call check-LOCAL_MODULE_FILENAME)
|
||||
|
||||
# we are building target objects
|
||||
my := TARGET_
|
||||
|
||||
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
||||
$(call handle-module-built)
|
||||
|
||||
LOCAL_MODULE_CLASS := EXECUTABLE
|
||||
include $(BUILD_SYSTEM)/build-module.mk
|
||||
endif
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
|
||||
@@ -1,118 +1,412 @@
|
||||
#include <stdlib.h>
|
||||
#include <jni.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
#include <errno.h>
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
|
||||
const char *path;
|
||||
|
||||
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
||||
|
||||
return open(path, O_RDWR);
|
||||
}
|
||||
#define EVDEV_MAX_EVENT_SIZE 24
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
||||
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
||||
}
|
||||
#define REL_X 0x00
|
||||
#define REL_Y 0x01
|
||||
#define KEY_Q 16
|
||||
#define BTN_LEFT 0x110
|
||||
#define BTN_GAMEPAD 0x130
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
||||
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
||||
}
|
||||
struct DeviceEntry {
|
||||
struct DeviceEntry *next;
|
||||
pthread_t thread;
|
||||
int fd;
|
||||
char devName[128];
|
||||
};
|
||||
|
||||
// has*() and friends are based on Android's EventHub.cpp
|
||||
static struct DeviceEntry *DeviceListHead;
|
||||
static int grabbing = 1;
|
||||
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t SocketSendLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int sock;
|
||||
|
||||
// This is a small executable that runs in a root shell. It reads input
|
||||
// devices and writes the evdev output packets to a socket. This allows
|
||||
// Moonlight to read input devices without having to muck with changing
|
||||
// device permissions or modifying SELinux policy (which is prevented in
|
||||
// Marshmallow anyway).
|
||||
|
||||
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||
static int hasRelAxis(int fd, short axis) {
|
||||
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
||||
|
||||
|
||||
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
||||
|
||||
|
||||
return test_bit(axis, relBitmask);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||
unsigned char absBitmask[(ABS_MAX + 1) / 8];
|
||||
|
||||
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
|
||||
|
||||
return test_bit(axis, absBitmask);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
|
||||
static int hasKey(int fd, short key) {
|
||||
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
||||
|
||||
|
||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||
|
||||
|
||||
return test_bit(key, keyBitmask);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
||||
jint ret;
|
||||
jbyte *data;
|
||||
int pollres;
|
||||
static void outputEvdevData(char *data, int dataSize) {
|
||||
char packetBuffer[EVDEV_MAX_EVENT_SIZE + sizeof(dataSize)];
|
||||
|
||||
// Copy the full packet into our buffer
|
||||
memcpy(packetBuffer, &dataSize, sizeof(dataSize));
|
||||
memcpy(&packetBuffer[sizeof(dataSize)], data, dataSize);
|
||||
|
||||
// Lock to prevent other threads from sending at the same time
|
||||
pthread_mutex_lock(&SocketSendLock);
|
||||
send(sock, packetBuffer, dataSize + sizeof(dataSize), 0);
|
||||
pthread_mutex_unlock(&SocketSendLock);
|
||||
}
|
||||
|
||||
void* pollThreadFunc(void* context) {
|
||||
struct DeviceEntry *device = context;
|
||||
struct pollfd pollinfo;
|
||||
|
||||
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
||||
if (data == NULL) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Failed to get byte array");
|
||||
int pollres, ret;
|
||||
char data[EVDEV_MAX_EVENT_SIZE];
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
|
||||
|
||||
if (grabbing) {
|
||||
// Exclusively grab the input device (required to make the Android cursor disappear)
|
||||
if (ioctl(device->fd, EVIOCGRAB, 1) < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"EVIOCGRAB failed for %s: %d", device->devName, errno);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
do {
|
||||
// Unwait every 250 ms to return to caller if the fd is closed
|
||||
pollinfo.fd = device->fd;
|
||||
pollinfo.events = POLLIN;
|
||||
pollinfo.revents = 0;
|
||||
pollres = poll(&pollinfo, 1, 250);
|
||||
}
|
||||
while (pollres == 0);
|
||||
|
||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||
// We'll have data available now
|
||||
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"read() failed: %d", errno);
|
||||
goto cleanup;
|
||||
}
|
||||
else if (ret == 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"read() graceful EOF");
|
||||
goto cleanup;
|
||||
}
|
||||
else if (grabbing) {
|
||||
// Write out the data to our client
|
||||
outputEvdevData(data, ret);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (pollres < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"poll() failed: %d", errno);
|
||||
}
|
||||
else {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Unexpected revents: %d", pollinfo.revents);
|
||||
}
|
||||
|
||||
// Terminate this thread
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Closing /dev/input/%s", device->devName);
|
||||
|
||||
// Remove the context from the linked list
|
||||
{
|
||||
struct DeviceEntry *lastEntry;
|
||||
|
||||
// Lock the device list
|
||||
pthread_mutex_lock(&DeviceListLock);
|
||||
|
||||
if (DeviceListHead == device) {
|
||||
DeviceListHead = device->next;
|
||||
}
|
||||
else {
|
||||
lastEntry = DeviceListHead;
|
||||
while (lastEntry->next != NULL) {
|
||||
if (lastEntry->next == device) {
|
||||
lastEntry->next = device->next;
|
||||
break;
|
||||
}
|
||||
|
||||
lastEntry = lastEntry->next;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock device list
|
||||
pthread_mutex_unlock(&DeviceListLock);
|
||||
}
|
||||
|
||||
// Free the context
|
||||
ioctl(device->fd, EVIOCGRAB, 0);
|
||||
close(device->fd);
|
||||
free(device);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int precheckDeviceForPolling(int fd) {
|
||||
int isMouse;
|
||||
int isKeyboard;
|
||||
int isGamepad;
|
||||
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
isMouse = hasRelAxis(fd, REL_X) &&
|
||||
hasRelAxis(fd, REL_Y) &&
|
||||
hasKey(fd, BTN_LEFT);
|
||||
|
||||
// This is the same check that Android does in EventHub.cpp
|
||||
isKeyboard = hasKey(fd, KEY_Q);
|
||||
|
||||
isGamepad = hasKey(fd, BTN_GAMEPAD);
|
||||
|
||||
// We only handle keyboards and mice that aren't gamepads
|
||||
return (isMouse || isKeyboard) && !isGamepad;
|
||||
}
|
||||
|
||||
static void startPollForDevice(char* deviceName) {
|
||||
struct DeviceEntry *currentEntry;
|
||||
char fullPath[256];
|
||||
int fd;
|
||||
|
||||
// Lock the device list
|
||||
pthread_mutex_lock(&DeviceListLock);
|
||||
|
||||
// Check if the device is already being polled
|
||||
currentEntry = DeviceListHead;
|
||||
while (currentEntry != NULL) {
|
||||
if (strcmp(currentEntry->devName, deviceName) == 0) {
|
||||
// Already polling this device
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
currentEntry = currentEntry->next;
|
||||
}
|
||||
|
||||
// Open the device
|
||||
sprintf(fullPath, "/dev/input/%s", deviceName);
|
||||
fd = open(fullPath, O_RDWR);
|
||||
if (fd < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open %s: %d", fullPath, errno);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Allocate a context
|
||||
currentEntry = malloc(sizeof(*currentEntry));
|
||||
if (currentEntry == NULL) {
|
||||
close(fd);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Populate context
|
||||
currentEntry->fd = fd;
|
||||
strcpy(currentEntry->devName, deviceName);
|
||||
|
||||
// Check if we support polling this device
|
||||
if (!precheckDeviceForPolling(fd)) {
|
||||
// Nope, get out
|
||||
free(currentEntry);
|
||||
close(fd);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Start the polling thread
|
||||
if (pthread_create(¤tEntry->thread, NULL, pollThreadFunc, currentEntry) != 0) {
|
||||
free(currentEntry);
|
||||
close(fd);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
// Queue this onto the device list
|
||||
currentEntry->next = DeviceListHead;
|
||||
DeviceListHead = currentEntry;
|
||||
|
||||
unlock:
|
||||
// Unlock and return
|
||||
pthread_mutex_unlock(&DeviceListLock);
|
||||
}
|
||||
|
||||
static int enumerateDevices(void) {
|
||||
DIR *inputDir;
|
||||
struct dirent *dirEnt;
|
||||
|
||||
inputDir = opendir("/dev/input");
|
||||
if (!inputDir) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open /dev/input: %d", errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Unwait every 250 ms to return to caller if the fd is closed
|
||||
pollinfo.fd = fd;
|
||||
pollinfo.events = POLLIN;
|
||||
pollinfo.revents = 0;
|
||||
pollres = poll(&pollinfo, 1, 250);
|
||||
}
|
||||
while (pollres == 0);
|
||||
|
||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||
// We'll have data available now
|
||||
ret = read(fd, data, sizeof(struct input_event));
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"read() failed: %d", errno);
|
||||
// Start polling each device in /dev/input
|
||||
while ((dirEnt = readdir(inputDir)) != NULL) {
|
||||
if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0) {
|
||||
// Skip these virtual directories
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strstr(dirEnt->d_name, "event") == NULL) {
|
||||
// Skip non-event devices
|
||||
continue;
|
||||
}
|
||||
|
||||
startPollForDevice(dirEnt->d_name);
|
||||
}
|
||||
else {
|
||||
// There must have been a failure
|
||||
ret = -1;
|
||||
|
||||
if (pollres < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"poll() failed: %d", errno);
|
||||
|
||||
closedir(inputDir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int connectSocket(int port) {
|
||||
struct sockaddr_in saddr;
|
||||
int ret;
|
||||
int val;
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "socket() failed: %d", errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&saddr, 0, sizeof(saddr));
|
||||
saddr.sin_family = AF_INET;
|
||||
saddr.sin_port = htons(port);
|
||||
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
ret = connect(sock, (struct sockaddr*)&saddr, sizeof(saddr));
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "connect() failed: %d", errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
val = 1;
|
||||
ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "setsockopt() failed: %d", errno);
|
||||
// We can continue anyways
|
||||
}
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Connection established to port %d", port);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define UNGRAB_REQ 1
|
||||
#define REGRAB_REQ 2
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int ret;
|
||||
int pollres;
|
||||
struct pollfd pollinfo;
|
||||
int port;
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
|
||||
|
||||
port = atoi(argv[1]);
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
|
||||
|
||||
// Connect to the app's socket
|
||||
ret = connectSocket(port);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Perform initial enumeration
|
||||
ret = enumerateDevices();
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Wait for requests from the client
|
||||
for (;;) {
|
||||
unsigned char requestId;
|
||||
|
||||
do {
|
||||
// Every second we poll again for new devices if
|
||||
// we haven't received any new events
|
||||
pollinfo.fd = sock;
|
||||
pollinfo.events = POLLIN;
|
||||
pollinfo.revents = 0;
|
||||
pollres = poll(&pollinfo, 1, 1000);
|
||||
if (pollres == 0) {
|
||||
// Timeout, re-enumerate devices
|
||||
enumerateDevices();
|
||||
}
|
||||
}
|
||||
while (pollres == 0);
|
||||
|
||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||
// We'll have data available now
|
||||
ret = recv(sock, &requestId, sizeof(requestId), 0);
|
||||
if (ret < sizeof(requestId)) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
|
||||
return errno;
|
||||
}
|
||||
|
||||
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
||||
return requestId;
|
||||
}
|
||||
|
||||
{
|
||||
struct DeviceEntry *currentEntry;
|
||||
|
||||
pthread_mutex_lock(&DeviceListLock);
|
||||
|
||||
// Update state for future devices
|
||||
grabbing = (requestId == REGRAB_REQ);
|
||||
|
||||
// Carry out the requested action on each device
|
||||
currentEntry = DeviceListHead;
|
||||
while (currentEntry != NULL) {
|
||||
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
||||
currentEntry = currentEntry->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&DeviceListLock);
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "New grab status is: %s",
|
||||
grabbing ? "enabled" : "disabled");
|
||||
}
|
||||
}
|
||||
else {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Unexpected revents: %d", pollinfo.revents);
|
||||
// Terminate this thread
|
||||
if (pollres < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Socket recv poll() failed: %d", errno);
|
||||
}
|
||||
else {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Socket poll unexpected revents: %d", pollinfo.revents);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
|
||||
return close(fd);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# Android.mk for Limelight's H264 decoder
|
||||
MY_LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(call all-subdir-makefiles)
|
||||
|
||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := nv_avc_dec
|
||||
LOCAL_SRC_FILES := nv_avc_dec.c nv_avc_dec_jni.c
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/$(TARGET_ARCH_ABI)/include
|
||||
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
|
||||
|
||||
# Link to ffmpeg libraries
|
||||
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libwsresample
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
@@ -1,31 +0,0 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libavcodec
|
||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavcodec-55.so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libavformat
|
||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavformat-55.so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libswscale
|
||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswscale-2.so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libavutil
|
||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavutil-52.so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE:= libwsresample
|
||||
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* This file is part of FFmpeg.
|
||||
*
|
||||
* FFmpeg is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* FFmpeg 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with FFmpeg; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef AVCODEC_AVFFT_H
|
||||
#define AVCODEC_AVFFT_H
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @ingroup lavc_fft
|
||||
* FFT functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup lavc_fft FFT functions
|
||||
* @ingroup lavc_misc
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
typedef float FFTSample;
|
||||
|
||||
typedef struct FFTComplex {
|
||||
FFTSample re, im;
|
||||
} FFTComplex;
|
||||
|
||||
typedef struct FFTContext FFTContext;
|
||||
|
||||
/**
|
||||
* Set up a complex FFT.
|
||||
* @param nbits log2 of the length of the input array
|
||||
* @param inverse if 0 perform the forward transform, if 1 perform the inverse
|
||||
*/
|
||||
FFTContext *av_fft_init(int nbits, int inverse);
|
||||
|
||||
/**
|
||||
* Do the permutation needed BEFORE calling ff_fft_calc().
|
||||
*/
|
||||
void av_fft_permute(FFTContext *s, FFTComplex *z);
|
||||
|
||||
/**
|
||||
* Do a complex FFT with the parameters defined in av_fft_init(). The
|
||||
* input data must be permuted before. No 1.0/sqrt(n) normalization is done.
|
||||
*/
|
||||
void av_fft_calc(FFTContext *s, FFTComplex *z);
|
||||
|
||||
void av_fft_end(FFTContext *s);
|
||||
|
||||
FFTContext *av_mdct_init(int nbits, int inverse, double scale);
|
||||
void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
|
||||
void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
|
||||
void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
|
||||
void av_mdct_end(FFTContext *s);
|
||||
|
||||
/* Real Discrete Fourier Transform */
|
||||
|
||||
enum RDFTransformType {
|
||||
DFT_R2C,
|
||||
IDFT_C2R,
|
||||
IDFT_R2C,
|
||||
DFT_C2R,
|
||||
};
|
||||
|
||||
typedef struct RDFTContext RDFTContext;
|
||||
|
||||
/**
|
||||
* Set up a real FFT.
|
||||
* @param nbits log2 of the length of the input array
|
||||
* @param trans the type of transform
|
||||
*/
|
||||
RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
|
||||
void av_rdft_calc(RDFTContext *s, FFTSample *data);
|
||||
void av_rdft_end(RDFTContext *s);
|
||||
|
||||
/* Discrete Cosine Transform */
|
||||
|
||||
typedef struct DCTContext DCTContext;
|
||||
|
||||
enum DCTTransformType {
|
||||
DCT_II = 0,
|
||||
DCT_III,
|
||||
DCT_I,
|
||||
DST_I,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up DCT.
|
||||
* @param nbits size of the input array:
|
||||
* (1 << nbits) for DCT-II, DCT-III and DST-I
|
||||
* (1 << nbits) + 1 for DCT-I
|
||||
*
|
||||
* @note the first element of the input of DST-I is ignored
|
||||
*/
|
||||
DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
|
||||
void av_dct_calc(DCTContext *s, FFTSample *data);
|
||||
void av_dct_end (DCTContext *s);
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#endif /* AVCODEC_AVFFT_H */
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* DXVA2 HW acceleration
|
||||
*
|
||||
* copyright (c) 2009 Laurent Aimar
|
||||
*
|
||||
* This file is part of FFmpeg.
|
||||
*
|
||||
* FFmpeg is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* FFmpeg 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with FFmpeg; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef AVCODEC_DXVA_H
|
||||
#define AVCODEC_DXVA_H
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @ingroup lavc_codec_hwaccel_dxva2
|
||||
* Public libavcodec DXVA2 header.
|
||||
*/
|
||||
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0600
|
||||
#undef _WIN32_WINNT
|
||||
#endif
|
||||
|
||||
#if !defined(_WIN32_WINNT)
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <d3d9.h>
|
||||
#include <dxva2api.h>
|
||||
|
||||
/**
|
||||
* @defgroup lavc_codec_hwaccel_dxva2 DXVA2
|
||||
* @ingroup lavc_codec_hwaccel
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
|
||||
|
||||
/**
|
||||
* This structure is used to provides the necessary configurations and data
|
||||
* to the DXVA2 FFmpeg HWAccel implementation.
|
||||
*
|
||||
* The application must make it available as AVCodecContext.hwaccel_context.
|
||||
*/
|
||||
struct dxva_context {
|
||||
/**
|
||||
* DXVA2 decoder object
|
||||
*/
|
||||
IDirectXVideoDecoder *decoder;
|
||||
|
||||
/**
|
||||
* DXVA2 configuration used to create the decoder
|
||||
*/
|
||||
const DXVA2_ConfigPictureDecode *cfg;
|
||||
|
||||
/**
|
||||
* The number of surface in the surface array
|
||||
*/
|
||||
unsigned surface_count;
|
||||
|
||||
/**
|
||||
* The array of Direct3D surfaces used to create the decoder
|
||||
*/
|
||||
LPDIRECT3DSURFACE9 *surface;
|
||||
|
||||
/**
|
||||
* A bit field configuring the workarounds needed for using the decoder
|
||||
*/
|
||||
uint64_t workaround;
|
||||
|
||||
/**
|
||||
* Private to the FFmpeg AVHWAccel implementation
|
||||
*/
|
||||
unsigned report_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
#endif /* AVCODEC_DXVA_H */
|
||||
@@ -1,397 +0,0 @@
|
||||
/*
|
||||
* This file is part of FFmpeg.
|
||||
*
|
||||
* FFmpeg is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* FFmpeg 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with FFmpeg; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
#ifndef AVCODEC_OLD_CODEC_IDS_H
|
||||
#define AVCODEC_OLD_CODEC_IDS_H
|
||||
|
||||
#include "libavutil/common.h"
|
||||
|
||||
/*
|
||||
* This header exists to prevent new codec IDs from being accidentally added to
|
||||
* the deprecated list.
|
||||
* Do not include it directly. It will be removed on next major bump
|
||||
*
|
||||
* Do not add new items to this list. Use the AVCodecID enum instead.
|
||||
*/
|
||||
|
||||
CODEC_ID_NONE = AV_CODEC_ID_NONE,
|
||||
|
||||
/* video codecs */
|
||||
CODEC_ID_MPEG1VIDEO,
|
||||
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
||||
CODEC_ID_MPEG2VIDEO_XVMC,
|
||||
CODEC_ID_H261,
|
||||
CODEC_ID_H263,
|
||||
CODEC_ID_RV10,
|
||||
CODEC_ID_RV20,
|
||||
CODEC_ID_MJPEG,
|
||||
CODEC_ID_MJPEGB,
|
||||
CODEC_ID_LJPEG,
|
||||
CODEC_ID_SP5X,
|
||||
CODEC_ID_JPEGLS,
|
||||
CODEC_ID_MPEG4,
|
||||
CODEC_ID_RAWVIDEO,
|
||||
CODEC_ID_MSMPEG4V1,
|
||||
CODEC_ID_MSMPEG4V2,
|
||||
CODEC_ID_MSMPEG4V3,
|
||||
CODEC_ID_WMV1,
|
||||
CODEC_ID_WMV2,
|
||||
CODEC_ID_H263P,
|
||||
CODEC_ID_H263I,
|
||||
CODEC_ID_FLV1,
|
||||
CODEC_ID_SVQ1,
|
||||
CODEC_ID_SVQ3,
|
||||
CODEC_ID_DVVIDEO,
|
||||
CODEC_ID_HUFFYUV,
|
||||
CODEC_ID_CYUV,
|
||||
CODEC_ID_H264,
|
||||
CODEC_ID_INDEO3,
|
||||
CODEC_ID_VP3,
|
||||
CODEC_ID_THEORA,
|
||||
CODEC_ID_ASV1,
|
||||
CODEC_ID_ASV2,
|
||||
CODEC_ID_FFV1,
|
||||
CODEC_ID_4XM,
|
||||
CODEC_ID_VCR1,
|
||||
CODEC_ID_CLJR,
|
||||
CODEC_ID_MDEC,
|
||||
CODEC_ID_ROQ,
|
||||
CODEC_ID_INTERPLAY_VIDEO,
|
||||
CODEC_ID_XAN_WC3,
|
||||
CODEC_ID_XAN_WC4,
|
||||
CODEC_ID_RPZA,
|
||||
CODEC_ID_CINEPAK,
|
||||
CODEC_ID_WS_VQA,
|
||||
CODEC_ID_MSRLE,
|
||||
CODEC_ID_MSVIDEO1,
|
||||
CODEC_ID_IDCIN,
|
||||
CODEC_ID_8BPS,
|
||||
CODEC_ID_SMC,
|
||||
CODEC_ID_FLIC,
|
||||
CODEC_ID_TRUEMOTION1,
|
||||
CODEC_ID_VMDVIDEO,
|
||||
CODEC_ID_MSZH,
|
||||
CODEC_ID_ZLIB,
|
||||
CODEC_ID_QTRLE,
|
||||
CODEC_ID_TSCC,
|
||||
CODEC_ID_ULTI,
|
||||
CODEC_ID_QDRAW,
|
||||
CODEC_ID_VIXL,
|
||||
CODEC_ID_QPEG,
|
||||
CODEC_ID_PNG,
|
||||
CODEC_ID_PPM,
|
||||
CODEC_ID_PBM,
|
||||
CODEC_ID_PGM,
|
||||
CODEC_ID_PGMYUV,
|
||||
CODEC_ID_PAM,
|
||||
CODEC_ID_FFVHUFF,
|
||||
CODEC_ID_RV30,
|
||||
CODEC_ID_RV40,
|
||||
CODEC_ID_VC1,
|
||||
CODEC_ID_WMV3,
|
||||
CODEC_ID_LOCO,
|
||||
CODEC_ID_WNV1,
|
||||
CODEC_ID_AASC,
|
||||
CODEC_ID_INDEO2,
|
||||
CODEC_ID_FRAPS,
|
||||
CODEC_ID_TRUEMOTION2,
|
||||
CODEC_ID_BMP,
|
||||
CODEC_ID_CSCD,
|
||||
CODEC_ID_MMVIDEO,
|
||||
CODEC_ID_ZMBV,
|
||||
CODEC_ID_AVS,
|
||||
CODEC_ID_SMACKVIDEO,
|
||||
CODEC_ID_NUV,
|
||||
CODEC_ID_KMVC,
|
||||
CODEC_ID_FLASHSV,
|
||||
CODEC_ID_CAVS,
|
||||
CODEC_ID_JPEG2000,
|
||||
CODEC_ID_VMNC,
|
||||
CODEC_ID_VP5,
|
||||
CODEC_ID_VP6,
|
||||
CODEC_ID_VP6F,
|
||||
CODEC_ID_TARGA,
|
||||
CODEC_ID_DSICINVIDEO,
|
||||
CODEC_ID_TIERTEXSEQVIDEO,
|
||||
CODEC_ID_TIFF,
|
||||
CODEC_ID_GIF,
|
||||
CODEC_ID_DXA,
|
||||
CODEC_ID_DNXHD,
|
||||
CODEC_ID_THP,
|
||||
CODEC_ID_SGI,
|
||||
CODEC_ID_C93,
|
||||
CODEC_ID_BETHSOFTVID,
|
||||
CODEC_ID_PTX,
|
||||
CODEC_ID_TXD,
|
||||
CODEC_ID_VP6A,
|
||||
CODEC_ID_AMV,
|
||||
CODEC_ID_VB,
|
||||
CODEC_ID_PCX,
|
||||
CODEC_ID_SUNRAST,
|
||||
CODEC_ID_INDEO4,
|
||||
CODEC_ID_INDEO5,
|
||||
CODEC_ID_MIMIC,
|
||||
CODEC_ID_RL2,
|
||||
CODEC_ID_ESCAPE124,
|
||||
CODEC_ID_DIRAC,
|
||||
CODEC_ID_BFI,
|
||||
CODEC_ID_CMV,
|
||||
CODEC_ID_MOTIONPIXELS,
|
||||
CODEC_ID_TGV,
|
||||
CODEC_ID_TGQ,
|
||||
CODEC_ID_TQI,
|
||||
CODEC_ID_AURA,
|
||||
CODEC_ID_AURA2,
|
||||
CODEC_ID_V210X,
|
||||
CODEC_ID_TMV,
|
||||
CODEC_ID_V210,
|
||||
CODEC_ID_DPX,
|
||||
CODEC_ID_MAD,
|
||||
CODEC_ID_FRWU,
|
||||
CODEC_ID_FLASHSV2,
|
||||
CODEC_ID_CDGRAPHICS,
|
||||
CODEC_ID_R210,
|
||||
CODEC_ID_ANM,
|
||||
CODEC_ID_BINKVIDEO,
|
||||
CODEC_ID_IFF_ILBM,
|
||||
CODEC_ID_IFF_BYTERUN1,
|
||||
CODEC_ID_KGV1,
|
||||
CODEC_ID_YOP,
|
||||
CODEC_ID_VP8,
|
||||
CODEC_ID_PICTOR,
|
||||
CODEC_ID_ANSI,
|
||||
CODEC_ID_A64_MULTI,
|
||||
CODEC_ID_A64_MULTI5,
|
||||
CODEC_ID_R10K,
|
||||
CODEC_ID_MXPEG,
|
||||
CODEC_ID_LAGARITH,
|
||||
CODEC_ID_PRORES,
|
||||
CODEC_ID_JV,
|
||||
CODEC_ID_DFA,
|
||||
CODEC_ID_WMV3IMAGE,
|
||||
CODEC_ID_VC1IMAGE,
|
||||
CODEC_ID_UTVIDEO,
|
||||
CODEC_ID_BMV_VIDEO,
|
||||
CODEC_ID_VBLE,
|
||||
CODEC_ID_DXTORY,
|
||||
CODEC_ID_V410,
|
||||
CODEC_ID_XWD,
|
||||
CODEC_ID_CDXL,
|
||||
CODEC_ID_XBM,
|
||||
CODEC_ID_ZEROCODEC,
|
||||
CODEC_ID_MSS1,
|
||||
CODEC_ID_MSA1,
|
||||
CODEC_ID_TSCC2,
|
||||
CODEC_ID_MTS2,
|
||||
CODEC_ID_CLLC,
|
||||
CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
|
||||
CODEC_ID_ESCAPE130 = MKBETAG('E','1','3','0'),
|
||||
CODEC_ID_EXR = MKBETAG('0','E','X','R'),
|
||||
CODEC_ID_AVRP = MKBETAG('A','V','R','P'),
|
||||
|
||||
CODEC_ID_G2M = MKBETAG( 0 ,'G','2','M'),
|
||||
CODEC_ID_AVUI = MKBETAG('A','V','U','I'),
|
||||
CODEC_ID_AYUV = MKBETAG('A','Y','U','V'),
|
||||
CODEC_ID_V308 = MKBETAG('V','3','0','8'),
|
||||
CODEC_ID_V408 = MKBETAG('V','4','0','8'),
|
||||
CODEC_ID_YUV4 = MKBETAG('Y','U','V','4'),
|
||||
CODEC_ID_SANM = MKBETAG('S','A','N','M'),
|
||||
CODEC_ID_PAF_VIDEO = MKBETAG('P','A','F','V'),
|
||||
CODEC_ID_SNOW = AV_CODEC_ID_SNOW,
|
||||
|
||||
/* various PCM "codecs" */
|
||||
CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
||||
CODEC_ID_PCM_S16LE = 0x10000,
|
||||
CODEC_ID_PCM_S16BE,
|
||||
CODEC_ID_PCM_U16LE,
|
||||
CODEC_ID_PCM_U16BE,
|
||||
CODEC_ID_PCM_S8,
|
||||
CODEC_ID_PCM_U8,
|
||||
CODEC_ID_PCM_MULAW,
|
||||
CODEC_ID_PCM_ALAW,
|
||||
CODEC_ID_PCM_S32LE,
|
||||
CODEC_ID_PCM_S32BE,
|
||||
CODEC_ID_PCM_U32LE,
|
||||
CODEC_ID_PCM_U32BE,
|
||||
CODEC_ID_PCM_S24LE,
|
||||
CODEC_ID_PCM_S24BE,
|
||||
CODEC_ID_PCM_U24LE,
|
||||
CODEC_ID_PCM_U24BE,
|
||||
CODEC_ID_PCM_S24DAUD,
|
||||
CODEC_ID_PCM_ZORK,
|
||||
CODEC_ID_PCM_S16LE_PLANAR,
|
||||
CODEC_ID_PCM_DVD,
|
||||
CODEC_ID_PCM_F32BE,
|
||||
CODEC_ID_PCM_F32LE,
|
||||
CODEC_ID_PCM_F64BE,
|
||||
CODEC_ID_PCM_F64LE,
|
||||
CODEC_ID_PCM_BLURAY,
|
||||
CODEC_ID_PCM_LXF,
|
||||
CODEC_ID_S302M,
|
||||
CODEC_ID_PCM_S8_PLANAR,
|
||||
|
||||
/* various ADPCM codecs */
|
||||
CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
||||
CODEC_ID_ADPCM_IMA_WAV,
|
||||
CODEC_ID_ADPCM_IMA_DK3,
|
||||
CODEC_ID_ADPCM_IMA_DK4,
|
||||
CODEC_ID_ADPCM_IMA_WS,
|
||||
CODEC_ID_ADPCM_IMA_SMJPEG,
|
||||
CODEC_ID_ADPCM_MS,
|
||||
CODEC_ID_ADPCM_4XM,
|
||||
CODEC_ID_ADPCM_XA,
|
||||
CODEC_ID_ADPCM_ADX,
|
||||
CODEC_ID_ADPCM_EA,
|
||||
CODEC_ID_ADPCM_G726,
|
||||
CODEC_ID_ADPCM_CT,
|
||||
CODEC_ID_ADPCM_SWF,
|
||||
CODEC_ID_ADPCM_YAMAHA,
|
||||
CODEC_ID_ADPCM_SBPRO_4,
|
||||
CODEC_ID_ADPCM_SBPRO_3,
|
||||
CODEC_ID_ADPCM_SBPRO_2,
|
||||
CODEC_ID_ADPCM_THP,
|
||||
CODEC_ID_ADPCM_IMA_AMV,
|
||||
CODEC_ID_ADPCM_EA_R1,
|
||||
CODEC_ID_ADPCM_EA_R3,
|
||||
CODEC_ID_ADPCM_EA_R2,
|
||||
CODEC_ID_ADPCM_IMA_EA_SEAD,
|
||||
CODEC_ID_ADPCM_IMA_EA_EACS,
|
||||
CODEC_ID_ADPCM_EA_XAS,
|
||||
CODEC_ID_ADPCM_EA_MAXIS_XA,
|
||||
CODEC_ID_ADPCM_IMA_ISS,
|
||||
CODEC_ID_ADPCM_G722,
|
||||
CODEC_ID_ADPCM_IMA_APC,
|
||||
CODEC_ID_VIMA = MKBETAG('V','I','M','A'),
|
||||
|
||||
/* AMR */
|
||||
CODEC_ID_AMR_NB = 0x12000,
|
||||
CODEC_ID_AMR_WB,
|
||||
|
||||
/* RealAudio codecs*/
|
||||
CODEC_ID_RA_144 = 0x13000,
|
||||
CODEC_ID_RA_288,
|
||||
|
||||
/* various DPCM codecs */
|
||||
CODEC_ID_ROQ_DPCM = 0x14000,
|
||||
CODEC_ID_INTERPLAY_DPCM,
|
||||
CODEC_ID_XAN_DPCM,
|
||||
CODEC_ID_SOL_DPCM,
|
||||
|
||||
/* audio codecs */
|
||||
CODEC_ID_MP2 = 0x15000,
|
||||
CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
|
||||
CODEC_ID_AAC,
|
||||
CODEC_ID_AC3,
|
||||
CODEC_ID_DTS,
|
||||
CODEC_ID_VORBIS,
|
||||
CODEC_ID_DVAUDIO,
|
||||
CODEC_ID_WMAV1,
|
||||
CODEC_ID_WMAV2,
|
||||
CODEC_ID_MACE3,
|
||||
CODEC_ID_MACE6,
|
||||
CODEC_ID_VMDAUDIO,
|
||||
CODEC_ID_FLAC,
|
||||
CODEC_ID_MP3ADU,
|
||||
CODEC_ID_MP3ON4,
|
||||
CODEC_ID_SHORTEN,
|
||||
CODEC_ID_ALAC,
|
||||
CODEC_ID_WESTWOOD_SND1,
|
||||
CODEC_ID_GSM, ///< as in Berlin toast format
|
||||
CODEC_ID_QDM2,
|
||||
CODEC_ID_COOK,
|
||||
CODEC_ID_TRUESPEECH,
|
||||
CODEC_ID_TTA,
|
||||
CODEC_ID_SMACKAUDIO,
|
||||
CODEC_ID_QCELP,
|
||||
CODEC_ID_WAVPACK,
|
||||
CODEC_ID_DSICINAUDIO,
|
||||
CODEC_ID_IMC,
|
||||
CODEC_ID_MUSEPACK7,
|
||||
CODEC_ID_MLP,
|
||||
CODEC_ID_GSM_MS, /* as found in WAV */
|
||||
CODEC_ID_ATRAC3,
|
||||
CODEC_ID_VOXWARE,
|
||||
CODEC_ID_APE,
|
||||
CODEC_ID_NELLYMOSER,
|
||||
CODEC_ID_MUSEPACK8,
|
||||
CODEC_ID_SPEEX,
|
||||
CODEC_ID_WMAVOICE,
|
||||
CODEC_ID_WMAPRO,
|
||||
CODEC_ID_WMALOSSLESS,
|
||||
CODEC_ID_ATRAC3P,
|
||||
CODEC_ID_EAC3,
|
||||
CODEC_ID_SIPR,
|
||||
CODEC_ID_MP1,
|
||||
CODEC_ID_TWINVQ,
|
||||
CODEC_ID_TRUEHD,
|
||||
CODEC_ID_MP4ALS,
|
||||
CODEC_ID_ATRAC1,
|
||||
CODEC_ID_BINKAUDIO_RDFT,
|
||||
CODEC_ID_BINKAUDIO_DCT,
|
||||
CODEC_ID_AAC_LATM,
|
||||
CODEC_ID_QDMC,
|
||||
CODEC_ID_CELT,
|
||||
CODEC_ID_G723_1,
|
||||
CODEC_ID_G729,
|
||||
CODEC_ID_8SVX_EXP,
|
||||
CODEC_ID_8SVX_FIB,
|
||||
CODEC_ID_BMV_AUDIO,
|
||||
CODEC_ID_RALF,
|
||||
CODEC_ID_IAC,
|
||||
CODEC_ID_ILBC,
|
||||
CODEC_ID_FFWAVESYNTH = MKBETAG('F','F','W','S'),
|
||||
CODEC_ID_SONIC = MKBETAG('S','O','N','C'),
|
||||
CODEC_ID_SONIC_LS = MKBETAG('S','O','N','L'),
|
||||
CODEC_ID_PAF_AUDIO = MKBETAG('P','A','F','A'),
|
||||
CODEC_ID_OPUS = MKBETAG('O','P','U','S'),
|
||||
|
||||
/* subtitle codecs */
|
||||
CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
|
||||
CODEC_ID_DVD_SUBTITLE = 0x17000,
|
||||
CODEC_ID_DVB_SUBTITLE,
|
||||
CODEC_ID_TEXT, ///< raw UTF-8 text
|
||||
CODEC_ID_XSUB,
|
||||
CODEC_ID_SSA,
|
||||
CODEC_ID_MOV_TEXT,
|
||||
CODEC_ID_HDMV_PGS_SUBTITLE,
|
||||
CODEC_ID_DVB_TELETEXT,
|
||||
CODEC_ID_SRT,
|
||||
CODEC_ID_MICRODVD = MKBETAG('m','D','V','D'),
|
||||
CODEC_ID_EIA_608 = MKBETAG('c','6','0','8'),
|
||||
CODEC_ID_JACOSUB = MKBETAG('J','S','U','B'),
|
||||
CODEC_ID_SAMI = MKBETAG('S','A','M','I'),
|
||||
CODEC_ID_REALTEXT = MKBETAG('R','T','X','T'),
|
||||
CODEC_ID_SUBVIEWER = MKBETAG('S','u','b','V'),
|
||||
|
||||
/* other specific kind of codecs (generally used for attachments) */
|
||||
CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
|
||||
CODEC_ID_TTF = 0x18000,
|
||||
CODEC_ID_BINTEXT = MKBETAG('B','T','X','T'),
|
||||
CODEC_ID_XBIN = MKBETAG('X','B','I','N'),
|
||||
CODEC_ID_IDF = MKBETAG( 0 ,'I','D','F'),
|
||||
CODEC_ID_OTF = MKBETAG( 0 ,'O','T','F'),
|
||||
|
||||
CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
|
||||
|
||||
CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
|
||||
* stream (only used by libavformat) */
|
||||
CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
|
||||
* stream (only used by libavformat) */
|
||||
CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
|
||||
|
||||
#endif /* AVCODEC_OLD_CODEC_IDS_H */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user