Compare commits
1760 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b0f485b41 | |||
| 2be2c95212 | |||
| e7aeeb8bd5 | |||
| 73df93f86a | |||
| 9cd4d5e2aa | |||
| c3b81554f4 | |||
| 6f79c52fc5 | |||
| 29bc3e022b | |||
| 7d03203d83 | |||
| 11dde835d1 | |||
| 52c47c288c | |||
| 63072aa8e1 | |||
| 4cca3ac922 | |||
| 604bc1ec11 | |||
| 5d7fbf3195 | |||
| 8c56e6f0d4 | |||
| 2069be7932 | |||
| 9c1c2991a9 | |||
| 81dabf2713 | |||
| 27520cb77e | |||
| f555d3dae0 | |||
| 70f1a2cacb | |||
| 7f15aaa2e5 | |||
| e5726205c4 | |||
| 07fabc0663 | |||
| 800f97ae85 | |||
| 3ee5b284e1 | |||
| c0389f0da9 | |||
| a7a4d7ded5 | |||
| 87cd974b79 | |||
| 7faaac31ff | |||
| 7386eb2a78 | |||
| 49a1524f4f | |||
| c957b8b06b | |||
| a3a6e14d80 | |||
| 7231f5468b | |||
| 4dfb0d7220 | |||
| 2f4f53b048 | |||
| b6e8389544 | |||
| d113878613 | |||
| f7ed7e06db | |||
| 977a1d4a3c | |||
| eefc08db47 | |||
| ab2b1663d3 | |||
| 04b8a718e3 | |||
| 37cf260ba6 | |||
| 8f91fe4cd1 | |||
| 9246ad412f | |||
| 1ccbbdd4fb | |||
| 16cf37994d | |||
| 01e84624c2 | |||
| 939cd7cf70 | |||
| 4b11603035 | |||
| ca18b6b052 | |||
| 3d0d19e561 | |||
| ae463a8735 | |||
| 7e797829ae | |||
| 431ed6bc5d | |||
| e9bb711c42 | |||
| 623bc5c156 | |||
| cfefef4619 | |||
| 4a9a881c1f | |||
| 13a06d585c | |||
| 1c8ad64da0 | |||
| 1d8925de57 | |||
| 0eb7e779b8 | |||
| a4b86eefe2 | |||
| 902a58bc70 | |||
| a34a44f29a | |||
| 454fe80172 | |||
| 81b6a8a311 | |||
| 3011a5bad7 | |||
| dcb7be3acd | |||
| 68a6b510b1 | |||
| dca3e89303 | |||
| bae6fef588 | |||
| 37f65e43a5 | |||
| 8c910101c7 | |||
| 112d9c41eb | |||
| c91d1097f6 | |||
| 105c2c9eef | |||
| b754d2de28 | |||
| f6425c7ec6 | |||
| e690c9b8c8 | |||
| f87cbac77c | |||
| 150bd313cf | |||
| bc90cb894c | |||
| c51a75a681 | |||
| 68aa9bd12d | |||
| 1fb6bf4d70 | |||
| b4df3658f1 | |||
| 4efb4d9b24 | |||
| d61e893731 | |||
| 951e44728e | |||
| 8dcdf73222 | |||
| 44c3b0af57 | |||
| 2b295400ac | |||
| aa8d8e93d2 | |||
| 89be7eac0e | |||
| f3847b932b | |||
| 5e4f37532c | |||
| f3f5ca74a3 | |||
| e50b7076a1 | |||
| 36ab5aa1b6 | |||
| a0a2b299d9 | |||
| 14d354fc29 | |||
| 342515f916 | |||
| 5f5944c237 | |||
| c025432ad6 | |||
| 171a6437fe | |||
| 11b3648fac | |||
| d1fae89d6d | |||
| 5c06848fe9 | |||
| b50e506e58 | |||
| 59fafa163d | |||
| 22d84b5763 | |||
| 6d186892a8 | |||
| 88d6143897 | |||
| b729fba75e | |||
| c0d3f9fa48 | |||
| af5e7a0e33 | |||
| 371d96ea65 | |||
| e9e332ff85 | |||
| e133ac2815 | |||
| 1dba5d147e | |||
| 1616c0b022 | |||
| bcee2cf0e3 | |||
| 3e7ddab0e9 | |||
| 5da0177356 | |||
| 7e21638811 | |||
| db5b7ab867 | |||
| 3bcc1c84bb | |||
| d46053f8d6 | |||
| 00a5fed9e9 | |||
| b6315a715a | |||
| 0da8303468 | |||
| c821c4684f | |||
| 6bae33f822 | |||
| 08d4ab67a6 | |||
| 62203d2f21 | |||
| 4968dcc558 | |||
| 6d66d1371f | |||
| b87ca71103 | |||
| c251cd2e8f | |||
| 593616d2d9 | |||
| a2fc62a4a8 | |||
| fd457c5dea | |||
| 64e1ba500c | |||
| 235a0635be | |||
| 61f8fa7c5a | |||
| 7c3c107381 | |||
| 555477751f | |||
| 9364f43c52 | |||
| 0be3169c2c | |||
| 5199d90505 | |||
| 5b5277bf3f | |||
| ad3614c58e | |||
| 9401ecc9fb | |||
| 1711e5e1a4 | |||
| f28f9bc65f | |||
| 8eb4014f01 | |||
| df0d7952db | |||
| 77d1770063 | |||
| f433bfdc02 | |||
| f75b6f9b80 | |||
| 621df9996d | |||
| 6c29503db9 | |||
| 304a02e2ec | |||
| 7aea7ed8c6 | |||
| e5ab3baa7b | |||
| 41b73f7cd9 | |||
| 38da42caf3 | |||
| 424d71fa13 | |||
| dbc9d78002 | |||
| b7ef8f54b7 | |||
| bea7cab0c3 | |||
| 352b6f7dd9 | |||
| 8665fe364f | |||
| 7d023c8865 | |||
| 503d4b970c | |||
| 6b07072a08 | |||
| 2b02af5d98 | |||
| 613c068523 | |||
| 0b0181f35c | |||
| c873bae3e4 | |||
| 7397a97a9e | |||
| b567db9ab7 | |||
| 3440f54598 | |||
| d533b25b29 | |||
| 72290bd725 | |||
| fdd4c0bbe1 | |||
| 0ac83e1cf7 | |||
| e27129fc48 | |||
| d54fdc9f5f | |||
| dc984e8679 | |||
| ee46906376 | |||
| 1d76536e31 | |||
| dc97adc7a1 | |||
| a1c659b7b8 | |||
| 9997f164b9 | |||
| 2f7ac67cb0 | |||
| 27f0fd63b3 | |||
| 9f56fdfbb9 | |||
| 83b66b19de | |||
| 9a4f85e752 | |||
| d00824d49c | |||
| ba0171221c | |||
| 68040394fb | |||
| 6fa1c35521 | |||
| 7a3fbd8dae | |||
| 329ee1a0bc | |||
| 11908e07bf | |||
| fd53122cb3 | |||
| d9c0830198 | |||
| d0aafb3814 | |||
| 40a3cc2ecb | |||
| 4469013bb5 | |||
| 78393932d0 | |||
| dbc2491151 | |||
| 936834e396 | |||
| 01eb7a2b64 | |||
| 252285e4f7 | |||
| df7333b8d0 | |||
| cf98ec2c41 | |||
| 0afda10bcb | |||
| 754773420f | |||
| 71aadfa2f5 | |||
| f7bfa63145 | |||
| 6574a0aab2 | |||
| 5d4988969e | |||
| 5121eb1852 | |||
| 004aeef2a7 | |||
| aa65a0312a | |||
| a1b58ab2fc | |||
| d32c0e32d0 | |||
| 9cd71e2855 | |||
| 1308a4ed80 | |||
| deb78e1c64 | |||
| 9aec6b1d31 | |||
| 97702b8861 | |||
| 832e7197c5 | |||
| 26b992726c | |||
| 1cb3588841 | |||
| b461d546d6 | |||
| b7810d6eb6 | |||
| 6fb3a8e57d | |||
| b521c784bc | |||
| 8e1641af5f | |||
| c0aac01d33 | |||
| 4f8b0adcbb | |||
| d7bdfb4db9 | |||
| 393a4c9c8a | |||
| 99b53f9a6a | |||
| 8da563b280 | |||
| 5cca5cd352 | |||
| d48e964d05 | |||
| ad94978f98 | |||
| d5b950e5cf | |||
| c46b9acf6b | |||
| 1d65baa981 | |||
| d8e322bac9 | |||
| 44871626cf | |||
| f661522b5d | |||
| 8b7287a5d6 | |||
| a454b0ab78 | |||
| c4ae049091 | |||
| 75bf84d0d9 | |||
| c248994ed4 | |||
| 0e4e3d80d2 | |||
| 59b2449cdc | |||
| a7a34ec629 | |||
| 8d469c5d0a | |||
| e6979d50b5 | |||
| 640255efb2 | |||
| 6e25b135a3 | |||
| 04e093a2c2 | |||
| 1bffa6bf41 | |||
| 69b175573d | |||
| 0eaff9d328 | |||
| 44905ed774 | |||
| 813f2edd95 | |||
| 337d753a33 | |||
| 1137c74f76 | |||
| 0c1451f757 | |||
| 5ab9ea48fd | |||
| ffcb623040 | |||
| bfe6929642 | |||
| 50d45011a8 | |||
| 2f7087d6d3 | |||
| 92b71588d0 | |||
| 4f3d018764 | |||
| a22e33eeb9 | |||
| 10e0a262f7 | |||
| 6a939e7495 | |||
| 422e703c2f | |||
| f8ba7cf190 | |||
| 27191da45e | |||
| d1e135db4d | |||
| 61a17afe69 | |||
| 47fd691884 | |||
| 0d171c6b28 | |||
| f0c69d08b8 | |||
| 629bf5766d | |||
| 233bceeece | |||
| 24926c75f1 | |||
| 6660ea7d91 | |||
| 4864b2ca45 | |||
| 92097b318d | |||
| 997898c99d | |||
| 1174e03885 | |||
| ff0f54d541 | |||
| a83f6f2ef7 | |||
| 814964a100 | |||
| 7e154292a9 | |||
| 0220dd921a | |||
| 0f9cba1053 | |||
| 05e4792d6f | |||
| c9b5c00756 | |||
| a4e134589d | |||
| cd80a94f28 | |||
| 57c645a291 | |||
| 6f35b991b7 | |||
| 0cba200207 | |||
| 81582d7343 | |||
| 04e561fd54 | |||
| a7023f52aa | |||
| 5efbb5229d | |||
| 541e43eb18 | |||
| 7e679ff4c6 | |||
| 486b4b4c4c | |||
| 752b204be8 | |||
| 7d76bf7868 | |||
| 564e7c71a6 | |||
| db49077b9b | |||
| 67f01fbdca | |||
| 16b845ab84 | |||
| 5c175fecf6 | |||
| 773976b265 | |||
| 80070bbdbe | |||
| 4c8d433b6c | |||
| 404f096d11 | |||
| d2ac927cec | |||
| 5e3d59d3d7 | |||
| 9cd2ce1309 | |||
| 9ed49730d4 | |||
| 39ebb48f58 | |||
| 1c29c70fba | |||
| 6993051529 | |||
| 4930087c4d | |||
| 795f0a013b | |||
| 213414778e | |||
| 7eac0ccaf8 | |||
| 02b74fbbc5 | |||
| 6adc9dcb2d | |||
| be620908f9 | |||
| efcfcf88db | |||
| e4edfdb043 | |||
| 3b5028d1a4 | |||
| bc8c45bd59 | |||
| 63eb346a70 | |||
| 68028242b4 | |||
| 27ad691d23 | |||
| 747e920061 | |||
| bae3b4a6e8 | |||
| 8d09f56a0e | |||
| 113a0e2c45 | |||
| 977215a098 | |||
| a7e65b47f9 | |||
| 7126055ad6 | |||
| 3de9765eaa | |||
| d4072eb295 | |||
| cac2bdbb81 | |||
| 66f0aee3f8 | |||
| b690dc5474 | |||
| 514e0ca2c9 | |||
| c9cf485025 | |||
| c2fbe6ad91 | |||
| cf07c02398 | |||
| 42dc928ad5 | |||
| 11597f0aa7 | |||
| cdcd4d48f2 | |||
| a9af4e54a9 | |||
| 7eac609219 | |||
| fa761debc4 | |||
| 62e175f069 | |||
| d7d8c40565 | |||
| 64de13ab50 | |||
| 2f02939638 | |||
| 1d7c8697e9 | |||
| 16c4b2532d | |||
| 7dea322bbd | |||
| a64db9d86f | |||
| 349ecb16ab | |||
| a3867735c1 | |||
| 5b087e9f70 | |||
| 14d9f77e4e | |||
| eed18223eb | |||
| 30d4d2a918 | |||
| 40c3db0214 | |||
| 30f666c70e | |||
| 209fead0e8 | |||
| 5c6889bf6d | |||
| 7d24900756 | |||
| f7c5039912 | |||
| 79a75b9d19 | |||
| 3c37c89db8 | |||
| 29b64992bd | |||
| c9b14540f2 | |||
| 6995a27126 | |||
| 546843a26c | |||
| d03d260535 | |||
| 6946e3c7a2 | |||
| b79d328961 | |||
| c313797d93 | |||
| c8cb8e1346 | |||
| f61540c099 | |||
| 6a9f8da14e | |||
| 6a6bd9fb0b | |||
| ff9260a0fd | |||
| 62bedb1609 | |||
| a519723d44 | |||
| c2a16a9b4a | |||
| 36191781ed | |||
| 61b6a49669 | |||
| e97845e46e | |||
| 6bba68207d | |||
| 0e17cccc06 | |||
| 918e922e40 | |||
| a08854ddfd | |||
| eb6f15c2b7 | |||
| 2cd9e31684 | |||
| 791d6624e2 | |||
| af41021271 | |||
| d726d939f4 | |||
| 748085e7bb | |||
| b64c84a5c3 | |||
| d57d19174b | |||
| efebe1828a | |||
| 06007e0597 | |||
| 42a502eff1 | |||
| 3a868045d7 | |||
| e0a7ff1880 | |||
| 88d43bbd40 | |||
| 30ff319b13 | |||
| 9a0f48b799 | |||
| b52c8a1a8f | |||
| 3fde115670 | |||
| b6f4d8ff1e | |||
| 46d72b912c | |||
| 722f397819 | |||
| a7d85a7dd5 | |||
| 9b238ab6c3 | |||
| f82ee97c05 | |||
| 8f169b976b | |||
| 35fb96f9f4 | |||
| 37371906d5 | |||
| 53452d22c0 | |||
| 9d4e9631bc | |||
| 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 | |||
| 5942545b9c | |||
| 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 | |||
| 035a62856d | |||
| 37ddccde0c | |||
| ffc59c6bd6 | |||
| 636c1ceb26 | |||
| 88f84a0c12 | |||
| 03ecf3e5ac | |||
| 617c8582b4 | |||
| ef3b28295b | |||
| 37cde22a55 | |||
| 3bcd2ee068 | |||
| d4ff58b3ad | |||
| c797318ece | |||
| 82387d23f8 | |||
| 772835689d | |||
| 766e629be5 | |||
| b93aa42c0c | |||
| 36f132942f | |||
| 7dcc689014 | |||
| e4c251e7ee | |||
| fb54bd5c78 | |||
| 8d4c86e113 | |||
| 90981a6643 | |||
| de05a5b446 | |||
| f644436aeb | |||
| 7a8166ec09 | |||
| 7fafb8e0ff | |||
| fbcbe09255 | |||
| e336a4446a | |||
| 6eed8408fc | |||
| ffb35b2cdd | |||
| 2d0af6281c | |||
| 472a7f6c8a | |||
| cd06559c66 | |||
| d833933aaa | |||
| dc3495d59b | |||
| e3a2e40043 | |||
| 31e1fb743e | |||
| bc59f11096 | |||
| 6d97775aa9 | |||
| 4b9ee92434 | |||
| 3fff34e08a | |||
| 15e856dccb | |||
| 07d04171c3 | |||
| 42bd93cb3a | |||
| 756ceaff1a | |||
| 7d289f1134 | |||
| 214461e123 | |||
| b0144a3256 | |||
| 3171256c6e | |||
| 5c69f6716c | |||
| 6264781539 | |||
| d3be670974 | |||
| 0225f534d0 | |||
| 284a31737e | |||
| b37a2dea57 | |||
| 5c865e7f36 | |||
| 04d9aea8c8 | |||
| b6f52db9c3 | |||
| 99d2e40683 | |||
| 02c4ed2724 | |||
| 5f4aab8f94 | |||
| 7b41b1158e | |||
| ec65901003 | |||
| 915acee88d | |||
| 300d444f71 | |||
| f37ab40c2f | |||
| 16e285d926 | |||
| f2d122a275 | |||
| bfa5a6349e | |||
| a56689aea3 | |||
| 3a5ba820cb | |||
| ec69fef36f | |||
| ff38074f55 | |||
| 85d0ce0c40 | |||
| 777129ca90 | |||
| 06156c4d68 | |||
| 1c725b9dac | |||
| f761ee52db | |||
| 05e8cfcc0a | |||
| 912925ef2c | |||
| 4deb881ec8 | |||
| f55d6308ce | |||
| 44a3a141c0 | |||
| 37b5ba004c | |||
| d0da5d3702 | |||
| b774b47213 | |||
| 42668b5699 | |||
| 74dc00445e | |||
| 3b4563d5ea | |||
| 38669817b4 | |||
| 9444430830 | |||
| 8f1d3ae04e | |||
| 74ed95871b | |||
| cc5d67616c | |||
| eed7f09e6f | |||
| e3c1d23744 | |||
| c4b1200b43 | |||
| e30088e53b | |||
| dff09f33a3 | |||
| 1f6b1dc2fe | |||
| 3f118dae93 | |||
| a989bdde80 | |||
| 91a30ff6fe | |||
| 5102669b06 | |||
| 2e2f09be00 | |||
| e5d9da447c | |||
| c402103fe3 | |||
| 5e5df8abc8 | |||
| d125eb7b16 | |||
| a116858493 | |||
| 5f3b333e98 | |||
| 80a37855c7 | |||
| 5db1ec8ec0 | |||
| ba5c026bff | |||
| 8911c58e50 | |||
| 780a64694d | |||
| 66536aa755 | |||
| 3c5ea9c8c3 | |||
| 40d1436ce3 | |||
| a53444148e | |||
| dbb02acd37 | |||
| d237ceb1df | |||
| 20c4eac4ef | |||
| b9f1142af7 | |||
| 38a6a2b74a | |||
| fd2421618a | |||
| 79a9ea7179 | |||
| 1f504288cb | |||
| 0543420624 | |||
| 34a11c9262 | |||
| 84a9845c1d | |||
| 5b05220008 | |||
| b2bd7257e1 | |||
| 6580eb8ea4 | |||
| 46a998c113 | |||
| 8584bf1910 | |||
| 60cd951774 | |||
| e7f92d3667 | |||
| d4f8d8f689 | |||
| 608a0ebb5b | |||
| f01a15d182 | |||
| 0268b4f958 | |||
| d71cf0eb98 | |||
| 10ab40f823 | |||
| 427edfa021 | |||
| 6f18831d5c | |||
| fe71b1be20 | |||
| a3db09f422 | |||
| d185a05b1d | |||
| 78e575504a | |||
| 0a0be19b69 | |||
| 0792157e9d | |||
| cdd0ecf0b7 | |||
| 1ac721a35b | |||
| e49b1c92a2 | |||
| 0ba0d37a37 | |||
| db4295bf83 | |||
| 824c37f9d5 | |||
| acf4426952 | |||
| 673a115b52 | |||
| e8c50342ab | |||
| 598995de3b | |||
| 01cf0cc649 | |||
| fa560f462f | |||
| f6e40118a9 | |||
| fe7148dbd4 | |||
| 60de065836 | |||
| 6f82f82abb | |||
| 42f18cb4ac | |||
| 1bbd0054c2 | |||
| bedf472e9e | |||
| acdde37a3a | |||
| f4abc66eeb | |||
| ad40e12167 | |||
| 164e6f83d8 | |||
| 1b3322b5ee | |||
| 6340ec6c6d | |||
| babd92c8c0 | |||
| 0074848a4e | |||
| 7f1fe5f520 | |||
| 01458770d2 | |||
| 8d05f044f5 | |||
| f5680b59a5 | |||
| 8c09154183 | |||
| 0ecf86c7ed | |||
| 6789e8d497 | |||
| 7d0160d556 | |||
| f6a0990432 | |||
| 5d6094df97 | |||
| d98d4aeda2 | |||
| 852dcf5a2d | |||
| c8339d5eae | |||
| 82e5aa122d | |||
| 07e4991c56 | |||
| 4eb62e6c5f | |||
| fe237d1da3 | |||
| e199fcd2d9 | |||
| d7c6f63592 | |||
| 4b9c6b149a | |||
| bf82556783 | |||
| f282e84174 | |||
| d1e41e41a1 | |||
| ed1a56dc68 | |||
| 96dfe25a14 | |||
| f76d78607a | |||
| a96f688bb2 | |||
| 90a1e68c68 | |||
| b287606106 | |||
| a413185085 | |||
| aa1b283570 | |||
| f07c886711 | |||
| e66b1ebec9 | |||
| d06912e81a | |||
| 08bcd97594 | |||
| af04831fb0 | |||
| 49e51f5f6f | |||
| 8f3eecd980 | |||
| 4223a7fd30 | |||
| 6edd0ab540 | |||
| ce7146175a | |||
| 3176a85f35 | |||
| ad1c11bba5 | |||
| ac640a6842 | |||
| 8962497a8c | |||
| 636c20d67b | |||
| 5d90950591 | |||
| 7651ce5e84 | |||
| 83141d3f91 | |||
| 55f2e89bbe | |||
| b3a1938c1d | |||
| 0ce1e1be27 | |||
| 3558655b72 | |||
| 470680d463 | |||
| 44cbf8adc1 | |||
| 686490ba70 | |||
| d0ecde1e16 | |||
| 63e2fd447d | |||
| 9417908848 | |||
| 93b0073467 | |||
| 1434be262c | |||
| 75aabd6471 | |||
| bafa2addd3 | |||
| 32b787e77c | |||
| 43b58b7a5e | |||
| 9ae1fe2696 | |||
| 6d0f34e2c4 | |||
| f7d91b5107 | |||
| a3c95480d8 | |||
| 864bcadcb2 | |||
| ae852eb911 | |||
| 732311c2a4 | |||
| 203fcd82e7 | |||
| 043c9a978e | |||
| 36b248be4b | |||
| 67469103d4 | |||
| 9e413000a5 | |||
| 8e247ad9a6 | |||
| bedcbfbb7e | |||
| a2de98c91a | |||
| 73e4970a43 | |||
| ac8b7ae960 | |||
| c62986e7b1 | |||
| 81d1e615bf | |||
| a3d5e955aa | |||
| 244fae07ab | |||
| 04e77e557b | |||
| a748b54041 | |||
| e7d96f0ac2 | |||
| 4555b3c74c | |||
| f77673a5c8 | |||
| 23ebc4d927 | |||
| 8c13186757 | |||
| feafc4ef3c | |||
| 92b86674b9 | |||
| f94d224395 | |||
| 822f498646 | |||
| 5c03295478 | |||
| dc3a923041 | |||
| eccba807bc | |||
| 35fa8f5bcc | |||
| 0380910588 | |||
| e85bb4372e | |||
| 2c345cd6c2 | |||
| b5c96cbb53 | |||
| b21ee5ca31 | |||
| 9c7bff6c75 | |||
| 3d470d9aed | |||
| b2a36c2c73 | |||
| 7978687bfc | |||
| f612ec80e2 | |||
| 7df1a39fcb | |||
| 4566c1855b | |||
| a539ac62ec | |||
| fa52e5edc2 | |||
| 3ca681f050 | |||
| 8086c3d46b | |||
| 928fca843f | |||
| 25d74785d0 | |||
| e12a8e7946 | |||
| 195bf8ed55 | |||
| b14f2ce219 | |||
| d31be3d64e | |||
| 0704f2aaf6 | |||
| 832e52ac74 | |||
| f5444551b2 | |||
| 3143797b55 | |||
| cc9b1aeaab | |||
| 3d177e97e4 | |||
| 6c3aaedc83 | |||
| bf84ebef6d | |||
| 8991b29329 | |||
| fa84575be5 | |||
| 0432d5725b | |||
| 8e7b144339 | |||
| fc629db653 | |||
| d5863e1bef | |||
| c2c3a6b37c | |||
| e701699dea | |||
| 17179bd027 | |||
| b2f210700d | |||
| 52678cfe35 | |||
| f0e85c4c53 | |||
| 92f8425ace | |||
| 6ad001e8be | |||
| b6e4d5528b | |||
| 0f0b83badc | |||
| 453fbb5f58 | |||
| e7dc3a4c11 | |||
| d68b2382cf | |||
| 1b5330323c | |||
| 8aba4888e1 | |||
| 1c3b9a3859 | |||
| e8f04f5a3b | |||
| 56b814e877 | |||
| 628ccd39d6 | |||
| 59db3f9b62 | |||
| 416f922b56 | |||
| b52a86e6cc | |||
| e523b5069e | |||
| e8ae8d9807 | |||
| 64e56a861d | |||
| c1bcd09c9b | |||
| 574258804f | |||
| 21ea3d8a2b | |||
| 6de4288a85 | |||
| 61f89a2d4c | |||
| a107b5e652 | |||
| ba398e4073 | |||
| b02db2c182 | |||
| f8a04cda7a | |||
| 226e8edefc | |||
| a14a4a8d60 | |||
| 9b90b30a1f | |||
| 2ed245b25a | |||
| 4b769839d0 | |||
| 9caf3b37ac | |||
| e6965605c9 | |||
| 5b355a3e73 | |||
| 239dd1d5a1 | |||
| 37509cce9b | |||
| 227c71549b | |||
| 92d534a9c3 | |||
| 2d08f568e9 | |||
| a10d8334f3 | |||
| f88c9904fb | |||
| 0fc61e52dd | |||
| 5e44c33bb6 | |||
| df3655e958 | |||
| fe43e13145 | |||
| acd3aad8d9 | |||
| 811b4b4f22 | |||
| 7db3b9f401 | |||
| a5a099cf43 | |||
| ba605643bb | |||
| a9f7b1aeab | |||
| 4f53cfcb20 | |||
| 96e98c1abb | |||
| 5de6f6ae2b | |||
| 0685722773 | |||
| 29df3b2859 | |||
| fc6f859ced | |||
| 6b21a5416f | |||
| 74e7c8bbf1 | |||
| 757075b16a | |||
| e8903c4d48 | |||
| 98262d16ee | |||
| 339506cf10 | |||
| 63bd5df09b | |||
| 32af2d0831 | |||
| 242b03d4b5 | |||
| 87a62666ac | |||
| 2dcf5486da | |||
| 60d3d8b3ae | |||
| e9141d65fe | |||
| aae591daec | |||
| a5ca8a7472 | |||
| 36f8cc02cb | |||
| 55b9645651 | |||
| d30ecbed5b | |||
| 0bbd27f04c | |||
| ffd70986b3 | |||
| 3c53fb7403 | |||
| 7a81950819 | |||
| 74f212c702 | |||
| 36be943854 | |||
| 26a4fc75a5 | |||
| a5ec5fc265 | |||
| 541ac44be4 | |||
| 117b555fcd | |||
| a10cd04441 | |||
| b5e89e47b6 | |||
| 53dccbde2a | |||
| 439afd15fa | |||
| 8d2bfecb10 | |||
| 7d15c34ed2 | |||
| 56625dfe4b | |||
| 2eab5a3b7b | |||
| f9e811862a | |||
| 25ccc3d0e1 | |||
| 8853bf0670 | |||
| 71fa3a824b | |||
| 56fd50834c | |||
| 48ba812cf6 | |||
| 019dc6d45f | |||
| 4ef1b8dc4c | |||
| cbcb784a79 | |||
| c0d64058fd | |||
| 3c11ff63a7 | |||
| 39fa0258ad | |||
| d0dd5bfa8c | |||
| b948c47618 | |||
| 18cae8ac53 | |||
| 537a50bee5 | |||
| 1a58b228a0 | |||
| 0576231dfc | |||
| 6ad35a83dd | |||
| 33d4dfc745 | |||
| 248a135f86 | |||
| f3bf63a668 | |||
| 2dbb7395a4 | |||
| 7c1eb80d62 | |||
| f2bf093691 | |||
| 2f002bfa4a | |||
| 4a19038d54 | |||
| 15fb3dd92c | |||
| e0982d3961 | |||
| 246fb69050 | |||
| a907dd0084 | |||
| fe58361724 | |||
| 7fb2f15f54 | |||
| f93dbb4116 | |||
| a0f93a2dc3 | |||
| bc34fe3a9f | |||
| bbe49491c1 | |||
| d5ccb80f26 | |||
| 82390ec9b9 | |||
| 34ef95926e | |||
| faa0cba39d | |||
| 8b395bb29f | |||
| 50fd15379a | |||
| ed479f1155 | |||
| 04db9ba714 | |||
| 31d7f237eb | |||
| 6a973e3248 | |||
| 96d9e4977b | |||
| ef8c49f135 | |||
| 5a3897f22a | |||
| ceef00b79a | |||
| a8c460e715 | |||
| 94ee24ea11 | |||
| 1a201f2e94 | |||
| e0c6d41d4b | |||
| d75e42e23d | |||
| 44a0ae86d2 | |||
| b191425112 | |||
| c06a4ab76d | |||
| b3042312f6 | |||
| 5fd105c9a9 | |||
| 06822ad385 | |||
| 3be52280ba | |||
| 306c2d143b | |||
| 5142f978cf | |||
| 667ffd4dfd | |||
| 17626f1853 | |||
| a71a3e22e6 | |||
| da4fab2f3e | |||
| 1db84efb68 | |||
| c1c3af3c66 | |||
| 5c79567a2c | |||
| 0f5fd9af62 | |||
| 99643537d1 | |||
| 4622b9f202 | |||
| 47650386e0 | |||
| 47ea158c4c | |||
| aa3fc34646 | |||
| 92f5f1ac71 | |||
| d9cb5eacf8 | |||
| 5718c47be7 | |||
| 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 | |||
| d7d90e8e49 | |||
| cc71ce6180 | |||
| f409a3583c | |||
| ac7504e017 | |||
| 345bd3f7c1 | |||
| 2e2960ec69 | |||
| e93b103d1e | |||
| 22977a4c5b | |||
| 7da5d5322b | |||
| 49e2c40ba4 | |||
| 8041a004c2 | |||
| db62d78e04 | |||
| bd79318b1e | |||
| 2736bd9165 | |||
| b6bd48584f | |||
| 7b4f3c975a | |||
| b165fadc55 | |||
| 274e0d0557 | |||
| 7594e51a18 | |||
| bf22819b53 | |||
| 3dea4b15e0 | |||
| 5836b3292b | |||
| a8fd49a234 | |||
| 006ad72eb2 | |||
| dc254e1ee5 | |||
| 4d420b29cb | |||
| 0b9e7aa05b | |||
| b0d31a4d35 | |||
| 24155feea4 | |||
| 8c663cc84a | |||
| db0a4e35c6 | |||
| 68ef98d346 | |||
| 8d1417c636 | |||
| f23bb9fac1 | |||
| d20dde0b6d | |||
| f76b30d109 | |||
| ee1a047cde | |||
| b91ab53219 | |||
| 6eeb7ae5b2 | |||
| 0436179020 | |||
| c92cae51c8 | |||
| 57da68c0e2 | |||
| 4c533fedfd | |||
| f8ab7b8e13 | |||
| 46c5eaf0e1 | |||
| e7e73aa1d2 | |||
| 394221f3df | |||
| 7d2647f830 | |||
| 563c90a8c4 | |||
| 0e0352fdd6 | |||
| d6a8db97d8 | |||
| 05f8fa21de | |||
| ab8779086b | |||
| ed8305b199 | |||
| 1def825c7f | |||
| 3c9b5d3b17 | |||
| 3c2dd88fd3 | |||
| 0e21d5e166 | |||
| 8c221bd786 | |||
| 3b1fcdfb10 | |||
| 9bb91e1085 | |||
| 98bee122fe | |||
| 67dc2ef9ab | |||
| 6aaa9a83a6 | |||
| 2eaea8ce7c | |||
| f5ded03b9b | |||
| f509a4b3ab | |||
| 6459579f15 | |||
| 5112179fca | |||
| 3f46485382 | |||
| b640564689 | |||
| 763f8938b3 | |||
| 4c67631ea5 | |||
| 920154b4b6 | |||
| d8c7d10ed6 | |||
| adcffa62d8 | |||
| 2c5e6c0788 | |||
| a7d4a04ac2 | |||
| d199c1b6c4 | |||
| 92f24d20db | |||
| 0dd43df7aa | |||
| 1675586a29 | |||
| a1e511b19a | |||
| 260d716eb8 | |||
| 5606ed1308 | |||
| a301575dd7 | |||
| e89e803d54 | |||
| 4486a126ad | |||
| d740e7a521 | |||
| cb8eab443c | |||
| fe3b649fe9 | |||
| 51c85a1b10 | |||
| 7223efb9f8 | |||
| c3296cce3d | |||
| 74ea87676e | |||
| fc1c26b5d7 | |||
| df59c99f80 | |||
| 5ef20aba21 | |||
| 54eaee3f79 | |||
| 4c82da1f5c | |||
| 080dc01c21 | |||
| f09fbf4ba6 | |||
| 8a465edad9 | |||
| 9d1510f14d | |||
| 62ea92335d | |||
| 9b9020b512 | |||
| d1e2822b92 | |||
| 533cb747df | |||
| 33a0f9c97f | |||
| ef9a442718 | |||
| b9ac48532f | |||
| ad10413714 | |||
| 886ef425e6 | |||
| c9014da186 | |||
| fbd61d2a21 | |||
| c025f9f02b | |||
| b737acedb0 | |||
| f15bfe3038 | |||
| 8938f51292 | |||
| 4b92b8f714 | |||
| 5f13b9bca4 | |||
| 2f219aac6f | |||
| 1d9efb30e2 | |||
| ed7be00881 | |||
| a6003f6bff | |||
| 4619045375 | |||
| 469dcab5c7 | |||
| e61b8f1b34 | |||
| 79b6ec839a | |||
| fd12e30c53 | |||
| 87a9ca4318 | |||
| 3f64411174 | |||
| 57b0da1a3a | |||
| fd1cb52f5f | |||
| 7d3e74a67f | |||
| d704e322df | |||
| f598153818 | |||
| f395a0c170 | |||
| 654b33d27f | |||
| 6c12da96c9 | |||
| 1a6f639b81 | |||
| 59a00a38c9 | |||
| 2beee168e3 | |||
| a92bbc7e5a | |||
| fbc921dd07 | |||
| 59c6c3d777 | |||
| e7ab61c8d0 | |||
| 5175e68b99 | |||
| 64aa01b2cf | |||
| 7023760782 | |||
| 63964ba6a7 | |||
| 932ce435b5 | |||
| af384d88f7 | |||
| 792846ddad | |||
| 1187d9c78c | |||
| e82683b0f4 | |||
| 4b2299ed02 | |||
| 37db9ab072 | |||
| 4a4f89a992 | |||
| d3a7bba666 | |||
| 8bd6582d07 | |||
| 875089305b | |||
| c19ff71c9a | |||
| 36c320a584 | |||
| fb40060560 | |||
| a4f4887647 | |||
| 316b8c56f1 | |||
| f1d7f556fd | |||
| 1e70e1d329 | |||
| 2cf3855d35 | |||
| e02a009635 | |||
| bd6ff35603 | |||
| 1cb7727dc7 | |||
| 0c73e3d0ae | |||
| 13ec16c606 | |||
| 7d150e7e89 | |||
| 7d61948d91 | |||
| 6371d364e1 | |||
| ded9c9140d | |||
| 7c8a108e28 | |||
| 2a18ffcdba | |||
| 381d0d5e81 | |||
| be126acfd1 | |||
| fc2f5cfe4d | |||
| 9878902a89 | |||
| f1230d46f3 | |||
| d8822392f1 | |||
| 1d9cf71517 | |||
| 73de3cc91d | |||
| 2160e87fef | |||
| 88249ba8aa | |||
| 2856617fb3 | |||
| d822980d5a | |||
| b5ba59b413 | |||
| d71cbe344a | |||
| 1148e0163c | |||
| cf36c7adb1 | |||
| eac6998e17 | |||
| 17afbffdb5 | |||
| 072a439c2d | |||
| dbe01a17d2 | |||
| b3503cdede | |||
| c533600983 | |||
| 5847fbb6b6 | |||
| 1876b30c1b | |||
| 5c71f55993 | |||
| 9c0960d03d | |||
| 29a395f3f4 | |||
| a676b8d8e6 | |||
| 7ab0be3b62 | |||
| 115853fed2 | |||
| 60beb81ae4 | |||
| 5310375d42 | |||
| 7ce29e3a09 | |||
| 42c65f4f16 | |||
| bf2cc2a4d5 | |||
| 6d6d7121f6 | |||
| 2ab67380d6 | |||
| 1ac6439690 | |||
| c481841ddf | |||
| 678269c561 | |||
| 83e874cdb6 | |||
| 899387caa1 | |||
| 56c8a9e6fe | |||
| 896288a40b | |||
| fc8ce5e4b9 | |||
| 4affc3c4ce | |||
| 067be54715 | |||
| 0dad2dc64b | |||
| 867b703644 | |||
| 3d398ef6dd | |||
| 90fc5797d5 | |||
| fcfcce88dd | |||
| 85d95b2d8e | |||
| d091d9db6b | |||
| fb8fc54bb1 | |||
| e081ab5239 | |||
| 7b12fd1ad2 | |||
| 80d8c5953e | |||
| a4ec619e5a | |||
| 194037ff41 | |||
| 094d642739 | |||
| 010e03252e | |||
| 98638186b5 | |||
| c5293ef21f | |||
| 366a1c91b8 | |||
| 157450e674 | |||
| 1b8d2bc81c | |||
| f1787c43e5 | |||
| 8fb2622b66 | |||
| 95ea88e932 | |||
| f2b8461bb9 | |||
| 7838a787df | |||
| cc3f2ecb07 | |||
| 833b7c3916 | |||
| 90209f2ca2 | |||
| 2681036c32 | |||
| ee58071ff1 | |||
| e6527de786 | |||
| 3f7e4d817f | |||
| 6eaabc84aa | |||
| 814557435d | |||
| e222f2f6c3 | |||
| 0b7becb161 | |||
| bf795ab7a5 | |||
| 59df38ae8a | |||
| 2d5328fc24 | |||
| 95c82c5cc5 | |||
| fe907b0271 | |||
| e04ff048b8 | |||
| 7d25d07c6d | |||
| 7b0ddfae42 | |||
| 43fa1a7245 | |||
| 7ae74a6a18 | |||
| 9772049295 | |||
| 057530eed0 | |||
| aee34f6365 | |||
| 5519d92243 | |||
| 3d95ac1f93 | |||
| 5c938535be | |||
| 2fdecc551a | |||
| 10204afdb4 | |||
| 55c800c2a5 | |||
| 265b3f9963 | |||
| a8bf2cd1cf | |||
| 4fcd8b3dfe | |||
| e1a1a6344d | |||
| a095c10a25 | |||
| b1ea487e22 | |||
| 47265d0d10 | |||
| 6a41b41a38 | |||
| 2247e43a48 | |||
| d3986080a3 | |||
| 07277e1a5b | |||
| 1d6b5a35bd | |||
| 1ff6ee14ac | |||
| 39d7fc748f | |||
| 4d3a69cf6a | |||
| ec39f22ad8 | |||
| b806522751 | |||
| 256fa897a7 | |||
| 5c812eed6c | |||
| f0b22f9119 | |||
| 7e1884acb5 | |||
| 9512521783 | |||
| da7904a767 | |||
| 51343a171d | |||
| 3a0c1db168 | |||
| bd21692323 | |||
| 5ae245bdca | |||
| d3052cd97d | |||
| 336f85a31c | |||
| b01f7c796e | |||
| 56f438fe47 | |||
| baa5199b83 | |||
| 23ca62b304 | |||
| 2c3511195c | |||
| d31ef481f3 | |||
| a490da5e5c | |||
| 72d3576257 | |||
| ebd93a55a0 | |||
| 4d01e1afe6 | |||
| 9988330613 | |||
| d2e51e97c0 | |||
| 9f94465979 | |||
| 9ff1386751 | |||
| 5fca35f0b1 | |||
| d23c763441 | |||
| fa058c4783 | |||
| e0ddd5f045 | |||
| d83526ff5c | |||
| b7443451a4 | |||
| e90e4a22c4 | |||
| 3a53172145 | |||
| 1dfcb7bc29 | |||
| 897bb76858 | |||
| bcc67269ab | |||
| 4d24c654b9 | |||
| cba44b091b | |||
| 8235717502 | |||
| 3d29d76cd4 | |||
| f2d8f8a41b | |||
| 4b1c7e7e3c | |||
| 1cba278876 | |||
| 766898fdf9 | |||
| 13e91d594b | |||
| ca0a0da19f | |||
| cde5367f38 | |||
| 466463ebc3 | |||
| aee255a6ee | |||
| daf7598774 | |||
| 6de5cf8925 | |||
| 82cabce86e | |||
| 1d6b7e1b2e | |||
| 1c9458d056 | |||
| 4e29f2ae8b | |||
| 69321636b5 | |||
| d190b254bd | |||
| 51a630995a | |||
| 3a74f0726c | |||
| efa6c7bba0 | |||
| 52e5817327 | |||
| 79ddfc65d5 | |||
| 6acb1fd92a | |||
| 005a96f3d3 | |||
| e39e0910a1 | |||
| 56a6cee8f2 | |||
| b8141542f8 | |||
| 8fc9a90207 | |||
| 13d707d98d | |||
| aae0ff6e7a | |||
| 69c7b5a0d5 | |||
| d1ad3115fa | |||
| 770af402a4 | |||
| bd2a1b8886 | |||
| 3236c0b93a | |||
| 51aacc3f38 | |||
| 397c6f46f9 | |||
| d00f78f859 | |||
| 29fec2e0de | |||
| 88d28665ef | |||
| de1f4da258 | |||
| 7985be57ab | |||
| a835e7aaa2 | |||
| 22958cfbb1 | |||
| c4dc5eb9e1 | |||
| db758f386e | |||
| 3fb3eefa94 | |||
| 9340dff45d | |||
| 2d6c756e70 | |||
| 021cfd1737 | |||
| 03e965d449 | |||
| 34f72544d8 | |||
| d839ea9781 | |||
| 2b7f13fdbb | |||
| 7557a3a4ae | |||
| fcecba484f | |||
| fa85a0a0bd | |||
| dc64bfeba2 | |||
| 871b73c48d | |||
| 5dcff91d27 | |||
| 0041fc1dab | |||
| 314242ab08 | |||
| 09e8ddfd74 | |||
| 4cea483a87 | |||
| 99aa616188 | |||
| 444c4602c1 | |||
| 5fc438a0be | |||
| 5b6eac7140 | |||
| 7cdd184197 | |||
| be153b84cb | |||
| 06c53e2251 | |||
| 695519bdf5 | |||
| bf7d033ab2 | |||
| df67795c4a | |||
| 72c1696f43 | |||
| 8eca3683c9 | |||
| 80c17b4913 | |||
| e5050f10bb | |||
| 3a3ac83ab5 | |||
| e912e4de57 | |||
| 8dee1f0d80 | |||
| 53594ada66 | |||
| 848ed1ad72 | |||
| 307e807c8f | |||
| 6a27780d56 | |||
| 57f98dbb4a | |||
| 5af7d83ec1 | |||
| 4a6f77f43a | |||
| c96f9fb635 | |||
| e3a477a243 | |||
| 9fcd641143 | |||
| 6d1cbc5a64 | |||
| ec71060d98 | |||
| 03f706fb85 | |||
| 7ad87bd3ee | |||
| ff4570abac | |||
| c819f2f0e3 | |||
| 4e088f6183 | |||
| 1b16ea6f53 | |||
| c2cdb1264d | |||
| f262503bc8 | |||
| b2ba216cd1 | |||
| 94ba7f8e45 | |||
| a267cf59c7 | |||
| 79e8bef289 | |||
| 99e3b5f33b | |||
| afbe64f3ff | |||
| 43b1a73ae0 | |||
| d08eeb8a2d | |||
| 7c39e5c974 | |||
| cd49334199 | |||
| dd59f0bc6d | |||
| cf2d83a1ea | |||
| d5b6130936 | |||
| c0e95ea18b | |||
| 4ae29b0075 | |||
| f21a81d7ac | |||
| 67c726a141 | |||
| 34e35cd493 | |||
| a17af070c5 | |||
| fbe0a26800 | |||
| 25ad99df94 | |||
| 6338e7b8eb | |||
| 1b9846d519 | |||
| a4ece13a1d | |||
| ff5f50e3ec | |||
| 066b8430a0 | |||
| 022a08f5a1 | |||
| 2b54a91f3d | |||
| 2d01633372 | |||
| 5dc01069fc | |||
| d450008833 | |||
| a37fff6eb5 | |||
| 6604675bf9 | |||
| 1965cc2347 | |||
| 312ca27906 | |||
| 0bceadbd9a | |||
| dfc3daabcd | |||
| b9ba9adc1f | |||
| f112d45e1a | |||
| 88f139873c | |||
| 52f81274f0 | |||
| 35a50209be | |||
| 9728c136f5 | |||
| bacd1d81fd | |||
| d317c5bf03 | |||
| 9d72314b9c | |||
| f36227b506 | |||
| debd840db4 | |||
| 1531629fcd | |||
| 2cc7243573 | |||
| 269d9a6bc6 | |||
| 7bc325fa08 | |||
| 244130fc1b | |||
| a67791b8aa | |||
| 21e46a5c3b | |||
| 2df2f850d5 | |||
| 406d26ec1c | |||
| 68c1aaf433 | |||
| 9ef577dbdd | |||
| 982ecbc015 | |||
| 7e44b5abd5 | |||
| 6dbb1a0c1f | |||
| 94b1c04fa6 | |||
| 9758276f1c | |||
| 971263c52d | |||
| 2cc2d05c2f | |||
| 1df03e137b | |||
| 9b58e7bb4d | |||
| 69ecf0251d | |||
| 350a4d8825 | |||
| 44f447df7b | |||
| b0169b0edf | |||
| ae79f03e61 | |||
| 77a587abe8 | |||
| 7f587dc389 | |||
| 8dbb03114d | |||
| 415e96dec6 | |||
| abc7f135f3 | |||
| 4b93207def | |||
| f004ae6a41 | |||
| ac6120adc4 | |||
| d9c2d58519 | |||
| 22865b8af4 | |||
| 431ba06742 | |||
| ed8857552b | |||
| 5f42ca66fe | |||
| c9d003ca6d | |||
| 330f40cc18 | |||
| 44366db4d5 | |||
| 93bf28b87d | |||
| bd9c6834b7 | |||
| 96ad2bcdef | |||
| 14c03f0b37 | |||
| 5bd30fe3dc | |||
| 5be499887d | |||
| 7124963c56 | |||
| 4377808896 | |||
| 50f8f78b8d | |||
| 1bb9a13c17 | |||
| e7f1b822f7 | |||
| c9eee2e075 | |||
| 6556b3eb9b | |||
| cc92f3829e | |||
| 33ffbe151f | |||
| 60db0ff775 | |||
| 7ecac185ac | |||
| b76495fa8f | |||
| 463d4ad3fd | |||
| 0f0e41d5a4 | |||
| 2d55562dd3 | |||
| cbe40fde92 | |||
| 97e62fdd34 | |||
| aa799342e5 | |||
| ae8cb18f63 | |||
| 8f53b6f233 | |||
| 9e385215ce | |||
| 378fbedfa4 | |||
| c84e063114 | |||
| 8d316ce9f0 | |||
| 0019b93e8d | |||
| 2d66c6fb53 | |||
| b629f674ca | |||
| 670622dfd7 | |||
| f35c2ead0f | |||
| 6b7b797089 | |||
| 15d4f6354d | |||
| 77cea99b35 | |||
| 70b50bd096 | |||
| c2401e7a75 | |||
| b63c6223b0 | |||
| bcf10cc0b2 | |||
| aaa73eb196 | |||
| 707c7a1a53 | |||
| dcb3e1c0e4 | |||
| 894110ba08 | |||
| a4dceb0b74 | |||
| 1c82fdf048 | |||
| ec303e485f | |||
| 1cdcc6d190 | |||
| ef1f44f873 | |||
| 00e81e87de | |||
| 2f082b9f85 | |||
| c9e5230e37 | |||
| 86e2657613 | |||
| c23470af40 | |||
| 6c5ec3d2e9 | |||
| 38423a9f37 | |||
| 7b10e52808 | |||
| 890ee846f7 | |||
| 6a92ea74fc | |||
| 07cf96c5ce | |||
| 636c5f17f5 | |||
| 4a2ee91700 | |||
| 8e9d605248 | |||
| a4e6738353 | |||
| 0f815a0085 | |||
| 0c8c108bd1 | |||
| 04941212cd | |||
| 9ad45a3ca3 | |||
| 176c8e9b93 | |||
| f537588228 | |||
| e593c04001 | |||
| 1095d7808c | |||
| d29dccba69 | |||
| a4098919b9 | |||
| 09e7ff0582 | |||
| 09177be8f7 | |||
| eaa08bada4 | |||
| 5f93d55dab | |||
| dfc0d518f8 | |||
| eace3a0bf0 | |||
| 92adbe0983 | |||
| aadbc3dd01 | |||
| d01a28c57f | |||
| 4ee99a78b2 | |||
| bd9b37a5a0 | |||
| 7947d8b75d | |||
| 3408e467d5 | |||
| a0237a19d9 | |||
| eb15599c01 | |||
| 8c9d0d171c | |||
| a39f4c5eab | |||
| 239dffcbdf | |||
| 3af3df0544 | |||
| 7e30d043eb | |||
| 4cbaee6806 | |||
| 8297ca7e85 | |||
| da1c350067 | |||
| c8c7512600 | |||
| 9b6e12497e | |||
| 50e7deeb32 | |||
| 7e3acd0213 | |||
| e60420cb2c | |||
| c733be5611 | |||
| 4fbe93e62d | |||
| bc2ca0b386 | |||
| ccc3eeebe8 | |||
| 63ee6ef79a | |||
| cf3ac50d22 | |||
| cdf634dc41 | |||
| 21116f90a7 | |||
| 29dd0e172c | |||
| 2d5083179c | |||
| a96de39b28 | |||
| c93812179f | |||
| 26809c4b6b | |||
| 0cce5b021e | |||
| d54c1b07ce | |||
| 1a38cc2c0c | |||
| e188e1dd04 | |||
| ae18e00b13 | |||
| c18b6ec00b | |||
| 53c7bb0338 | |||
| dfa3be78e4 | |||
| 932bb1145b | |||
| 46f4f5ccbe | |||
| 7f851c46f4 | |||
| 82ae042f1c | |||
| 8ca3aab363 | |||
| f95cd60cfd | |||
| ab1e47edb4 | |||
| 6a695d2c72 | |||
| 421d73b28a | |||
| 042f67506c | |||
| 96e5513cdb | |||
| cc30752eb7 | |||
| ade061bf3c | |||
| 616945a963 | |||
| 3201fac36c | |||
| 84551df36a | |||
| b32899f101 | |||
| b4a0f81eda | |||
| 95d035f00b | |||
| 666fbecc01 | |||
| 6f8306cc18 | |||
| b0d5b9c767 | |||
| 3648c0f26a | |||
| f1b4fdd8b0 | |||
| 12658f4fb0 | |||
| ed92f9d28e | |||
| a1440621f9 | |||
| 48f8a05bae | |||
| 4701c22b67 | |||
| 7f841c1fca | |||
| 4f56dce9f7 | |||
| 4e9fb1bbce | |||
| da47b43ad3 | |||
| 87152e6403 | |||
| 895c123b13 | |||
| 35476e2c28 | |||
| 29909e07e8 | |||
| 9ac103187f | |||
| ce1494895e | |||
| 41d2f6b0e2 |
@@ -0,0 +1,4 @@
|
|||||||
|
issuesOpened: >
|
||||||
|
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||||
|
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||||
|
Thank you, and happy streaming!
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
+10
-2
@@ -1,6 +1,9 @@
|
|||||||
#built application files
|
# built application files
|
||||||
*.apk
|
*.apk
|
||||||
*.ap_
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
output.json
|
||||||
|
out/
|
||||||
|
|
||||||
# files for the dex VM
|
# files for the dex VM
|
||||||
*.dex
|
*.dex
|
||||||
@@ -30,6 +33,11 @@ Thumbs.db
|
|||||||
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
|
*.iml
|
||||||
|
|
||||||
# Compiled JNI libraries folder
|
# Compiled JNI libraries folder
|
||||||
**/jniLibs
|
**/jniLibs
|
||||||
|
app/.externalNativeBuild/
|
||||||
|
|
||||||
|
# NDK stuff
|
||||||
|
.cxx/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "app/src/main/jni/moonlight-core/moonlight-common-c"]
|
||||||
|
path = app/src/main/jni/moonlight-core/moonlight-common-c
|
||||||
|
url = https://github.com/moonlight-stream/moonlight-common-c.git
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
language: android
|
||||||
|
dist: trusty
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: 1
|
||||||
|
|
||||||
|
android:
|
||||||
|
components:
|
||||||
|
- tools
|
||||||
|
- platform-tools
|
||||||
|
- build-tools-29.0.3
|
||||||
|
- android-29
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- sdkmanager --list
|
||||||
|
|
||||||
|
install:
|
||||||
|
- yes | sdkmanager "ndk;20.0.5594570"
|
||||||
@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
|
|||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
local data_start = -1
|
local data_start = -1
|
||||||
while i < buf:len do
|
while i < buf:len() do
|
||||||
-- Make sure we have a potential start sequence and type
|
-- Make sure we have a potential start sequence and type
|
||||||
if buf:len() - i < 5 then
|
if buf:len() - i < 5 then
|
||||||
-- We need more data
|
-- We need more data
|
||||||
|
|||||||
@@ -1,42 +1,44 @@
|
|||||||
#Limelight
|
# Moonlight Android
|
||||||
|
|
||||||
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[](https://travis-ci.org/moonlight-stream/moonlight-android)
|
||||||
|
|
||||||
|
[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.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Limelight will allow you to stream your full collection of games from your Windows PC to your 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.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
## Features
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
##Installation
|
## Installation
|
||||||
|
|
||||||
* Download and install Limelight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/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
|
* 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
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
|
|
||||||
##Usage
|
## Usage
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Limelight and tap on your PC in the list
|
* 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
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
##Contribute
|
## Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -44,14 +46,18 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
Check out our [website](http://limelight-stream.com) for project links and information.
|
## 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
|
## Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||||
* [Aaron Neyer](https://github.com/Aaronneyer)
|
* [Aaron Neyer](https://github.com/Aaronneyer)
|
||||||
* [Andrew Hennessy](https://github.com/yetanothername)
|
* [Andrew Hennessy](https://github.com/yetanothername)
|
||||||
|
|
||||||
Limelight is the work of students at [Case Western](http://case.edu) and was
|
Moonlight is the work of students at [Case Western](http://case.edu) and was
|
||||||
started as a project at [MHacks](http://mhacks.org).
|
started as a project at [MHacks](http://mhacks.org).
|
||||||
|
|||||||
-112
@@ -1,112 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-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="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
|
||||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
|
||||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
|
||||||
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
|
||||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
|
||||||
<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" inherit-compiler-output="false">
|
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/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$/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/test/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/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 21 Platform" jdkType="Android SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
+93
-41
@@ -1,71 +1,123 @@
|
|||||||
import com.android.builder.model.ProductFlavor
|
|
||||||
import org.apache.tools.ant.taskdefs.condition.Os
|
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 21
|
compileSdkVersion 29
|
||||||
buildToolsVersion "21.0.2"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 21
|
targetSdkVersion 29
|
||||||
|
|
||||||
versionName "2.9"
|
versionName "9.2"
|
||||||
versionCode = 38
|
versionCode = 222
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flavorDimensions "root"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
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"
|
applicationId "com.limelight.root"
|
||||||
|
dimension "root"
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
arguments "PRODUCT_FLAVOR=nonRoot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
|
dimension "root"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
lintConfig file("lint.xml")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
runProguard false
|
// To whomever is releasing/using an APK in release mode with
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
// 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 = []
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
path "src/main/jni/Android.mk"
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
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 {
|
dependencies {
|
||||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
|
implementation 'org.bouncycastle:bcprov-jdk15on:1.64'
|
||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
implementation 'org.bouncycastle:bcpkix-jdk15on:1.64'
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
implementation 'org.jcodec:jcodec:0.2.3'
|
||||||
compile files('libs/jmdns-fixed.jar')
|
implementation 'com.squareup.okhttp3:okhttp:3.12.10'
|
||||||
compile files('libs/limelight-common.jar')
|
implementation 'com.squareup.okio:okio:1.17.5'
|
||||||
compile files('libs/tinyrtsp.jar')
|
implementation 'org.jmdns:jmdns:3.5.5'
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<lint>
|
||||||
|
<issue id="InvalidPackage">
|
||||||
|
<ignore path="**/bcpkix-jdk15on-*.jar"/>
|
||||||
|
</issue>
|
||||||
|
</lint>
|
||||||
Vendored
+28
@@ -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.**
|
||||||
@@ -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,77 +4,146 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<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.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
|
||||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
|
||||||
<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.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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:theme="@style/AppTheme" >
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:isGame="true"
|
||||||
<!-- Launcher for traditional devices -->
|
android:banner="@drawable/atv_banner"
|
||||||
|
android:appCategory="game"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
|
android:installLocation="auto"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<provider
|
||||||
|
android:name=".PosterContentProvider"
|
||||||
|
android:authorities="poster.${applicationId}"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true">
|
||||||
|
</provider>
|
||||||
|
<!-- 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" />
|
||||||
|
|
||||||
|
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
|
||||||
|
in each activity even though it is implied by targeting API 24+ -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<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" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||||
<!-- Launcher for Android TV devices -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcViewTv"
|
android:name=".ShortcutTrampoline"
|
||||||
android:logo="@drawable/atv_banner"
|
android:noHistory="true"
|
||||||
android:icon="@drawable/atv_banner"
|
android:exported="true"
|
||||||
|
android:resizeableActivity="true"
|
||||||
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">
|
||||||
<intent-filter>
|
<meta-data
|
||||||
<action android:name="android.intent.action.MAIN" />
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
android:value="com.limelight.PcView" />
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppView"
|
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
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.StreamSettings"
|
android:name=".preferences.StreamSettings"
|
||||||
android:label="Streaming Settings" >
|
android:resizeableActivity="true"
|
||||||
|
android:label="Streaming Settings">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.AddComputerManually"
|
android:name=".preferences.AddComputerManually"
|
||||||
android:label="Add Computer Manually" >
|
android:resizeableActivity="true"
|
||||||
|
android:windowSoftInputMode="stateVisible"
|
||||||
|
android:label="Add Computer Manually">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".Game"
|
android:name=".Game"
|
||||||
android:screenOrientation="sensorLandscape"
|
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
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.Connection" />
|
android:value="com.limelight.AppView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".discovery.DiscoveryService"
|
android:name=".discovery.DiscoveryService"
|
||||||
android:label="mDNS PC Auto-Discovery Service" />
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
<service
|
<service
|
||||||
android:name=".computers.ComputerManagerService"
|
android:name=".computers.ComputerManagerService"
|
||||||
android:label="Computer Management Service" />
|
android:label="Computer Management Service" />
|
||||||
|
<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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -1,295 +1,611 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
import java.io.StringReader;
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.binding.PlatformBinding;
|
import com.limelight.grid.AppGridAdapter;
|
||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.R;
|
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.Dialog;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
import com.limelight.utils.ShortcutHelper;
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
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.Bundle;
|
||||||
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ImageView;
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
|
|
||||||
public class AppView extends Activity {
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
private ListView appList;
|
|
||||||
private ArrayAdapter<AppObject> appListAdapter;
|
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private InetAddress ipAddress;
|
private AppGridAdapter appGridAdapter;
|
||||||
private String uniqueId;
|
private String uuidString;
|
||||||
private boolean remote;
|
private ShortcutHelper shortcutHelper;
|
||||||
|
|
||||||
private final static int RESUME_ID = 1;
|
private ComputerDetails computer;
|
||||||
private final static int QUIT_ID = 2;
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
private final static int CANCEL_ID = 3;
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
private String lastRawApplist;
|
||||||
public final static String ADDRESS_EXTRA = "Address";
|
private int lastRunningAppId;
|
||||||
public final static String UNIQUEID_EXTRA = "UniqueId";
|
private boolean suspendGridUpdates;
|
||||||
public final static String NAME_EXTRA = "Name";
|
private boolean inForeground;
|
||||||
public final static String REMOTE_EXTRA = "Remote";
|
|
||||||
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
@Override
|
private final static int QUIT_ID = 2;
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
private final static int CANCEL_ID = 3;
|
||||||
super.onCreate(savedInstanceState);
|
private final static int START_WITH_QUIT = 4;
|
||||||
setContentView(R.layout.activity_app_view);
|
private final static int VIEW_DETAILS_ID = 5;
|
||||||
|
private final static int CREATE_SHORTCUT_ID = 6;
|
||||||
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
|
|
||||||
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
|
public final static String NAME_EXTRA = "Name";
|
||||||
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
|
public final static String UUID_EXTRA = "UUID";
|
||||||
if (address == null || uniqueId == null) {
|
public final static String NEW_PAIR_EXTRA = "NewPair";
|
||||||
return;
|
|
||||||
}
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
TextView label = (TextView) findViewById(R.id.appListText);
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
setTitle(labelText);
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
label.setText(labelText);
|
|
||||||
|
// Wait in a separate thread to avoid stalling the UI
|
||||||
try {
|
new Thread() {
|
||||||
ipAddress = InetAddress.getByAddress(address);
|
@Override
|
||||||
} catch (UnknownHostException e) {
|
public void run() {
|
||||||
return;
|
// Wait for the binder to be ready
|
||||||
}
|
localBinder.waitForReady();
|
||||||
|
|
||||||
// Setup the list view
|
// Get the computer object
|
||||||
appList = (ListView)findViewById(R.id.pcListView);
|
computer = localBinder.getComputer(uuidString);
|
||||||
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
|
if (computer == null) {
|
||||||
appListAdapter.setNotifyOnChange(false);
|
finish();
|
||||||
appList.setAdapter(appListAdapter);
|
return;
|
||||||
appList.setItemsCanFocus(true);
|
}
|
||||||
appList.setOnItemClickListener(new OnItemClickListener() {
|
|
||||||
@Override
|
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
shortcutHelper.createAppViewShortcut(computer, true, getIntent().getBooleanExtra(NEW_PAIR_EXTRA, false));
|
||||||
long id) {
|
shortcutHelper.reportComputerShortcutUsed(computer);
|
||||||
AppObject app = appListAdapter.getItem(pos);
|
|
||||||
if (app == null || app.app == null) {
|
try {
|
||||||
return;
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
}
|
PreferenceConfiguration.readPreferences(AppView.this),
|
||||||
|
computer, localBinder.getUniqueId());
|
||||||
// Only open the context menu if something is running, otherwise start it
|
} catch (Exception e) {
|
||||||
if (getRunningAppId() != -1) {
|
e.printStackTrace();
|
||||||
openContextMenu(arg1);
|
finish();
|
||||||
}
|
return;
|
||||||
else {
|
}
|
||||||
doStart(app.app);
|
|
||||||
}
|
// 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.
|
||||||
registerForContextMenu(appList);
|
managerBinder = localBinder;
|
||||||
}
|
|
||||||
|
// Load the app grid with cached data (if possible).
|
||||||
@Override
|
// This must be done _before_ startComputerUpdates()
|
||||||
protected void onDestroy() {
|
// so the initial serverinfo response can update the running
|
||||||
super.onDestroy();
|
// icon.
|
||||||
|
populateAppGridWithCache();
|
||||||
SpinnerDialog.closeDialogs(this);
|
|
||||||
Dialog.closeDialogs();
|
// Start updates
|
||||||
}
|
startComputerUpdates();
|
||||||
|
|
||||||
@Override
|
runOnUiThread(new Runnable() {
|
||||||
protected void onResume() {
|
@Override
|
||||||
super.onResume();
|
public void run() {
|
||||||
|
if (isFinishing() || isChangingConfigurations()) {
|
||||||
updateAppList();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getRunningAppId() {
|
// Despite my best efforts to catch all conditions that could
|
||||||
int runningAppId = -1;
|
// cause the activity to be destroyed when we try to commit
|
||||||
for (int i = 0; i < appListAdapter.getCount(); i++) {
|
// I haven't been able to, so we have this try-catch block.
|
||||||
AppObject app = appListAdapter.getItem(i);
|
try {
|
||||||
if (app.app == null) {
|
getFragmentManager().beginTransaction()
|
||||||
continue;
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
}
|
.commitAllowingStateLoss();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
if (app.app.getIsRunning()) {
|
e.printStackTrace();
|
||||||
runningAppId = app.app.getAppId();
|
}
|
||||||
break;
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
}
|
}
|
||||||
return runningAppId;
|
|
||||||
}
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
|
managerBinder = null;
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
|
||||||
AppObject selectedApp = appListAdapter.getItem(info.position);
|
|
||||||
if (selectedApp == null || selectedApp.app == null) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
int runningAppId = getRunningAppId();
|
|
||||||
if (runningAppId != -1) {
|
@Override
|
||||||
if (runningAppId == selectedApp.app.getAppId()) {
|
public void onConfigurationChanged(Configuration newConfig) {
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
|
super.onConfigurationChanged(newConfig);
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
|
|
||||||
}
|
// If appGridAdapter is initialized, let it know about the configuration change.
|
||||||
else {
|
// If not, it will pick it up when it initializes.
|
||||||
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
|
if (appGridAdapter != null) {
|
||||||
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
|
// Update the app grid adapter to create grid items with the correct layout
|
||||||
}
|
appGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reinflate the app grid itself to pick up the layout change
|
||||||
|
getFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
|
.commitAllowingStateLoss();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void startComputerUpdates() {
|
||||||
public void onContextMenuClosed(Menu menu) {
|
// 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(final ComputerDetails details) {
|
||||||
|
// Do nothing if updates are suspended
|
||||||
|
if (suspendGridUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't care about other computers
|
||||||
|
if (!details.uuid.equalsIgnoreCase(uuidString)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state == ComputerDetails.State.OFFLINE) {
|
||||||
|
// The PC is unreachable now
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Display a toast to the user and quit the activity
|
||||||
|
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.disableComputerShortcut(details,
|
||||||
|
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)) {
|
||||||
|
|
||||||
|
// Let's check if the running app ID changed
|
||||||
|
if (details.runningGameId != lastRunningAppId) {
|
||||||
|
// Update the currently running game using the app ID
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
lastRawApplist = details.rawAppList;
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (poller == null) {
|
||||||
|
poller = managerBinder.createAppListPoller(computer);
|
||||||
|
}
|
||||||
|
poller.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopComputerUpdates() {
|
||||||
|
if (poller != null) {
|
||||||
|
poller.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appGridAdapter != null) {
|
||||||
|
appGridAdapter.cancelQueuedOperations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
|
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
|
||||||
|
TextView label = findViewById(R.id.appListText);
|
||||||
|
setTitle(computerName);
|
||||||
|
label.setText(computerName);
|
||||||
|
|
||||||
|
// Bind to the computer manager service
|
||||||
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
|
Service.BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateAppGridWithCache() {
|
||||||
|
try {
|
||||||
|
// Try to load from cache
|
||||||
|
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
|
||||||
|
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||||
|
updateUiWithAppList(applist);
|
||||||
|
LimeLog.info("Loaded applist from cache");
|
||||||
|
} catch (IOException | XmlPullParserException e) {
|
||||||
|
if (lastRawApplist != null) {
|
||||||
|
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
LimeLog.info("Loading applist from the network");
|
||||||
|
// We'll need to load from the network
|
||||||
|
loadAppsBlocking();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAppsBlocking() {
|
||||||
|
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
||||||
|
getResources().getString(R.string.applist_refresh_msg), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
|
||||||
|
SpinnerDialog.closeDialogs(this);
|
||||||
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
unbindService(serviceConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
// Display a decoder crash notification if we've returned after a crash
|
||||||
|
UiHelper.showDecoderCrashDialog(this);
|
||||||
|
|
||||||
|
inForeground = true;
|
||||||
|
startComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
|
||||||
|
inForeground = false;
|
||||||
|
stopComputerUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
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_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
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
AppObject app = appListAdapter.getItem(info.position);
|
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId()) {
|
||||||
{
|
case START_WITH_QUIT:
|
||||||
case RESUME_ID:
|
// Display a confirmation dialog first
|
||||||
// Resume is the same as start for us
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
doStart(app.app);
|
@Override
|
||||||
return true;
|
public void run() {
|
||||||
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
case QUIT_ID:
|
}
|
||||||
doQuit(app.app);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CANCEL_ID:
|
case START_OR_RESUME_ID:
|
||||||
return true;
|
// Resume is the same as start for us
|
||||||
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
default:
|
return true;
|
||||||
return super.onContextItemSelected(item);
|
|
||||||
|
case QUIT_ID:
|
||||||
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
suspendGridUpdates = true;
|
||||||
|
ServerHelper.doQuit(AppView.this, computer,
|
||||||
|
app.app, managerBinder, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Trigger a poll immediately
|
||||||
|
suspendGridUpdates = false;
|
||||||
|
if (poller != null) {
|
||||||
|
poller.pollNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
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(computer, app.app, appBits)) {
|
||||||
|
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateString(NvApp app) {
|
private void updateUiWithServerinfo(final ComputerDetails details) {
|
||||||
StringBuilder str = new StringBuilder();
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
str.append(app.getAppName());
|
@Override
|
||||||
if (app.getIsRunning()) {
|
public void run() {
|
||||||
str.append(" - Running");
|
boolean updated = false;
|
||||||
}
|
|
||||||
return str.toString();
|
// Look through our current app list to tag the running app
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// There can only be one or zero apps running.
|
||||||
|
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.isRunning = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
else if (existingApp.isRunning) {
|
||||||
|
// This app was running but now isn't
|
||||||
|
existingApp.isRunning = false;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This app wasn't running and still isn't
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
appGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addListPlaceholder() {
|
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||||
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean updated = false;
|
||||||
|
|
||||||
|
// First handle app updates and additions
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
|
||||||
|
// Try to update an existing app in the list first
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
// Found the app; update its properties
|
||||||
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
|
existingApp.app.setAppName(app.getAppName());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
// This app must be new
|
||||||
|
appGridAdapter.addApp(new AppObject(app));
|
||||||
|
|
||||||
|
// We could have a leftover shortcut from last time this PC was paired
|
||||||
|
// or if this app was removed then added again. Enable those shortcuts
|
||||||
|
// again if present.
|
||||||
|
shortcutHelper.enableAppShortcut(computer, app);
|
||||||
|
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next handle app removals
|
||||||
|
int i = 0;
|
||||||
|
while (i < appGridAdapter.getCount()) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// Check if this app is in the latest list
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This app was removed in the latest app list
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
shortcutHelper.disableAppShortcut(computer, existingApp.app, "App removed from PC");
|
||||||
|
appGridAdapter.removeApp(existingApp);
|
||||||
|
updated = true;
|
||||||
|
|
||||||
|
// Check this same index again because the item at i+1 is now at i after
|
||||||
|
// the removal
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on to the next item
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
appGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAppList() {
|
@Override
|
||||||
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
|
public int getAdapterFragmentLayoutId() {
|
||||||
new Thread() {
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
@Override
|
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||||
public void run() {
|
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
||||||
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
|
||||||
|
|
||||||
try {
|
|
||||||
final List<NvApp> appList = httpConn.getAppList();
|
|
||||||
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
appListAdapter.clear();
|
|
||||||
if (appList.isEmpty()) {
|
|
||||||
addListPlaceholder();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (NvApp app : appList) {
|
|
||||||
appListAdapter.add(new AppObject(generateString(app), app));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
appListAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Success case
|
|
||||||
return;
|
|
||||||
} catch (GfeHttpResponseException ignored) {
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
} catch (XmlPullParserException ignored) {
|
|
||||||
} finally {
|
|
||||||
spinner.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doStart(NvApp app) {
|
@Override
|
||||||
Intent intent = new Intent(this, Game.class);
|
public void receiveAbsListView(AbsListView listView) {
|
||||||
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
listView.setAdapter(appGridAdapter);
|
||||||
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
@Override
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
startActivity(intent);
|
long id) {
|
||||||
}
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
|
||||||
private void doQuit(final NvApp app) {
|
// Only open the context menu if something is running, otherwise start it
|
||||||
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
if (lastRunningAppId != 0) {
|
||||||
new Thread(new Runnable() {
|
openContextMenu(arg1);
|
||||||
@Override
|
} else {
|
||||||
public void run() {
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
NvHTTP httpConn;
|
}
|
||||||
String message;
|
}
|
||||||
try {
|
});
|
||||||
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
UiHelper.applyStatusBarPadding(listView);
|
||||||
if (httpConn.quitApp()) {
|
registerForContextMenu(listView);
|
||||||
message = "Successfully quit "+app.getAppName();
|
listView.requestFocus();
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
message = "Failed to quit "+app.getAppName();
|
public class AppObject {
|
||||||
}
|
public final NvApp app;
|
||||||
updateAppList();
|
public boolean isRunning;
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = "Failed to resolve host";
|
public AppObject(NvApp app) {
|
||||||
} catch (FileNotFoundException e) {
|
if (app == null) {
|
||||||
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
|
throw new IllegalArgumentException("app must not be null");
|
||||||
+ "Try rebooting your machine or reinstalling GFE.";
|
}
|
||||||
} catch (Exception e) {
|
this.app = app;
|
||||||
message = e.getMessage();
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
final String toastMessage = message;
|
public String toString() {
|
||||||
runOnUiThread(new Runnable() {
|
return app.getAppName();
|
||||||
@Override
|
}
|
||||||
public void run() {
|
}
|
||||||
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AppObject {
|
|
||||||
public String text;
|
|
||||||
public NvApp app;
|
|
||||||
|
|
||||||
public AppObject(String text, NvApp app) {
|
|
||||||
this.text = text;
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.FileHandler;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
public class LimeLog {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
|
||||||
|
|
||||||
|
public static void info(String msg) {
|
||||||
|
LOGGER.info(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void warning(String msg) {
|
||||||
|
LOGGER.warning(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void severe(String msg) {
|
||||||
|
LOGGER.severe(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setFileHandler(String fileName) throws IOException {
|
||||||
|
LOGGER.addHandler(new FileHandler(fileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 {}
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.UriMatcher;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PosterContentProvider extends ContentProvider {
|
||||||
|
|
||||||
|
|
||||||
|
public static final String AUTHORITY = "poster." + BuildConfig.APPLICATION_ID;
|
||||||
|
public static final String PNG_MIME_TYPE = "image/png";
|
||||||
|
public static final int APP_ID_PATH_INDEX = 2;
|
||||||
|
public static final int COMPUTER_UUID_PATH_INDEX = 1;
|
||||||
|
private DiskAssetLoader mDiskAssetLoader;
|
||||||
|
|
||||||
|
private static final UriMatcher sUriMatcher;
|
||||||
|
private static final String BOXART_PATH = "boxart";
|
||||||
|
private static final int BOXART_URI_ID = 1;
|
||||||
|
|
||||||
|
static {
|
||||||
|
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||||
|
sUriMatcher.addURI(AUTHORITY, BOXART_PATH, BOXART_URI_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||||
|
int match = sUriMatcher.match(uri);
|
||||||
|
if (match == BOXART_URI_ID) {
|
||||||
|
return openBoxArtFile(uri, mode);
|
||||||
|
}
|
||||||
|
return openBoxArtFile(uri, mode);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParcelFileDescriptor openBoxArtFile(Uri uri, String mode) throws FileNotFoundException {
|
||||||
|
if (!"r".equals(mode)) {
|
||||||
|
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> segments = uri.getPathSegments();
|
||||||
|
if (segments.size() != 3) {
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
String appId = segments.get(APP_ID_PATH_INDEX);
|
||||||
|
String uuid = segments.get(COMPUTER_UUID_PATH_INDEX);
|
||||||
|
File file = mDiskAssetLoader.getFile(uuid, Integer.parseInt(appId));
|
||||||
|
if (file.exists()) {
|
||||||
|
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||||
|
}
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||||
|
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType(Uri uri) {
|
||||||
|
return PNG_MIME_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Uri insert(Uri uri, ContentValues values) {
|
||||||
|
throw new UnsupportedOperationException("This provider is only for read mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
mDiskAssetLoader = new DiskAssetLoader(getContext());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(Uri uri, String[] projection, String selection,
|
||||||
|
String[] selectionArgs, String sortOrder) {
|
||||||
|
throw new UnsupportedOperationException("This provider doesn't support query");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(Uri uri, ContentValues values, String selection,
|
||||||
|
String[] selectionArgs) {
|
||||||
|
throw new UnsupportedOperationException("This provider is support read only");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Uri createBoxArtUri(String uuid, String appId) {
|
||||||
|
return new Uri.Builder()
|
||||||
|
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||||
|
.authority(AUTHORITY)
|
||||||
|
.appendPath(BOXART_PATH)
|
||||||
|
.appendPath(uuid)
|
||||||
|
.appendPath(appId)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
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 NvApp app;
|
||||||
|
private ArrayList<Intent> intentStack = new ArrayList<>();
|
||||||
|
|
||||||
|
private ComputerDetails computer;
|
||||||
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
|
|
||||||
|
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 (app != null) {
|
||||||
|
if (details.runningGameId == 0 || details.runningGameId == app.getAppId()) {
|
||||||
|
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, 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, app, 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(null, 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(String uuidString, String appIdString) {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
|
||||||
|
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
||||||
|
|
||||||
|
if (validateInput(uuidString, appIdString)) {
|
||||||
|
if (appIdString != null && !appIdString.isEmpty()) {
|
||||||
|
app = new NvApp(getIntent().getStringExtra(Game.EXTRA_APP_NAME),
|
||||||
|
Integer.parseInt(appIdString),
|
||||||
|
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
|||||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
public class PlatformBinding {
|
public class PlatformBinding {
|
||||||
public static String getDeviceName() {
|
public static String getDeviceName() {
|
||||||
String deviceName = android.os.Build.MODEL;
|
String deviceName = android.os.Build.MODEL;
|
||||||
deviceName = deviceName.replace(" ", "");
|
deviceName = deviceName.replace(" ", "");
|
||||||
return deviceName;
|
return deviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AudioRenderer getAudioRenderer() {
|
public static AudioRenderer getAudioRenderer() {
|
||||||
return new AndroidAudioRenderer();
|
return new AndroidAudioRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||||
return new AndroidCryptoProvider(c);
|
return new AndroidCryptoProvider(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,219 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
|
import android.media.AudioAttributes;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
public static final int FRAME_SIZE = 960;
|
private AudioTrack track;
|
||||||
|
|
||||||
private AudioTrack track;
|
|
||||||
|
|
||||||
@Override
|
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
|
||||||
public boolean streamInitialized(int channelCount, int sampleRate) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
int channelConfig;
|
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
int bufferSize;
|
sampleRate,
|
||||||
|
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(sampleRate)
|
||||||
|
.setChannelMask(channelConfig)
|
||||||
|
.build();
|
||||||
|
|
||||||
switch (channelCount)
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
{
|
// Use FLAG_LOW_LATENCY on L through N
|
||||||
case 1:
|
if (lowLatency) {
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
||||||
break;
|
}
|
||||||
case 2:
|
}
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
channelConfig,
|
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
.setAudioFormat(format)
|
||||||
FRAME_SIZE * 2);
|
.setAudioAttributes(attributesBuilder.build())
|
||||||
|
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||||
// Round to next frame
|
.setBufferSizeInBytes(bufferSize);
|
||||||
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
|
||||||
|
|
||||||
LimeLog.info("Audio track buffer size: "+bufferSize);
|
|
||||||
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
|
||||||
sampleRate,
|
|
||||||
channelConfig,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
|
|
||||||
track.play();
|
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
||||||
return true;
|
if (lowLatency) {
|
||||||
}
|
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
return trackBuilder.build();
|
||||||
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
}
|
||||||
track.write(audioData, offset, length);
|
else {
|
||||||
}
|
return new AudioTrack(attributesBuilder.build(),
|
||||||
|
format,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM,
|
||||||
|
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void streamClosing() {
|
public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||||
if (track != null) {
|
int channelConfig;
|
||||||
track.release();
|
int bytesPerFrame;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
switch (audioConfiguration.channelCount)
|
||||||
public int getCapabilities() {
|
{
|
||||||
return 0;
|
case 2:
|
||||||
}
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||||
|
break;
|
||||||
|
case 8:
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// AudioFormat.CHANNEL_OUT_7POINT1_SURROUND isn't available until Android 6.0,
|
||||||
|
// yet the CHANNEL_OUT_SIDE_LEFT and CHANNEL_OUT_SIDE_RIGHT constants were added
|
||||||
|
// in 5.0, so just hardcode the constant so we can work on Lollipop.
|
||||||
|
channelConfig = 0x000018fc; // AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// On KitKat and lower, creation of the AudioTrack will fail if we specify
|
||||||
|
// CHANNEL_OUT_SIDE_LEFT or CHANNEL_OUT_SIDE_RIGHT. That leaves us with
|
||||||
|
// the old CHANNEL_OUT_7POINT1 which uses left-of-center and right-of-center
|
||||||
|
// speakers instead of side-left and side-right. This non-standard layout
|
||||||
|
// is probably not what the user wants, but we don't really have a choice.
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Audio channel config: "+String.format("0x%X", channelConfig));
|
||||||
|
|
||||||
|
bytesPerFrame = audioConfiguration.channelCount * samplesPerFrame * 2;
|
||||||
|
|
||||||
|
// We're not supposed to request less than the minimum
|
||||||
|
// buffer size for our buffer, but it appears that we can
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
boolean lowLatency;
|
||||||
|
int bufferSize;
|
||||||
|
|
||||||
|
// 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(sampleRate,
|
||||||
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
|
bytesPerFrame * 2);
|
||||||
|
|
||||||
|
// Round to next frame
|
||||||
|
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unreachable
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip low latency options if hardware sample rate doesn't match the content
|
||||||
|
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
track = createAudioTrack(channelConfig, sampleRate, 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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track == null) {
|
||||||
|
// Couldn't create any audio track for playback
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.getPendingAudioDuration() < 40) {
|
||||||
|
// 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 much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.NoSuchProviderException;
|
import java.security.Provider;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -45,239 +44,228 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
|
|||||||
|
|
||||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||||
|
|
||||||
private File certFile;
|
private final File certFile;
|
||||||
private File keyFile;
|
private final File keyFile;
|
||||||
|
|
||||||
private X509Certificate cert;
|
private X509Certificate cert;
|
||||||
private RSAPrivateKey key;
|
private RSAPrivateKey key;
|
||||||
private byte[] pemCertBytes;
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
private static final Object globalCryptoLock = new Object();
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
static {
|
private static final Provider bcProvider = new BouncyCastleProvider();
|
||||||
// Install the Bouncy Castle provider
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
public AndroidCryptoProvider(Context c) {
|
||||||
}
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
|
|
||||||
public AndroidCryptoProvider(Context c) {
|
certFile = new File(dataPath + File.separator + "client.crt");
|
||||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
keyFile = new File(dataPath + File.separator + "client.key");
|
||||||
|
}
|
||||||
certFile = new File(dataPath + File.separator + "client.crt");
|
|
||||||
keyFile = new File(dataPath + File.separator + "client.key");
|
private byte[] loadFileToBytes(File f) {
|
||||||
}
|
if (!f.exists()) {
|
||||||
|
return null;
|
||||||
private byte[] loadFileToBytes(File f) {
|
}
|
||||||
if (!f.exists()) {
|
|
||||||
return null;
|
try {
|
||||||
}
|
FileInputStream fin = new FileInputStream(f);
|
||||||
|
byte[] fileData = new byte[(int) f.length()];
|
||||||
try {
|
if (fin.read(fileData) != f.length()) {
|
||||||
FileInputStream fin = new FileInputStream(f);
|
|
||||||
byte[] fileData = new byte[(int) f.length()];
|
|
||||||
if (fin.read(fileData) != f.length()) {
|
|
||||||
// Failed to read
|
// Failed to read
|
||||||
fileData = null;
|
fileData = null;
|
||||||
}
|
}
|
||||||
fin.close();
|
fin.close();
|
||||||
return fileData;
|
return fileData;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean loadCertKeyPair() {
|
|
||||||
byte[] certBytes = loadFileToBytes(certFile);
|
|
||||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
|
||||||
|
|
||||||
// If either file was missing, we definitely can't succeed
|
|
||||||
if (certBytes == null || keyBytes == null) {
|
|
||||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
|
||||||
pemCertBytes = certBytes;
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
// May happen if the cert is corrupt
|
|
||||||
LimeLog.warning("Corrupted certificate");
|
|
||||||
return false;
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("TrulyRandom")
|
|
||||||
private boolean generateCertKeyPair() {
|
|
||||||
byte[] snBytes = new byte[8];
|
|
||||||
new SecureRandom().nextBytes(snBytes);
|
|
||||||
|
|
||||||
KeyPair keyPair;
|
|
||||||
try {
|
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Expires in 20 years
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTime(now);
|
|
||||||
calendar.add(Calendar.YEAR, 20);
|
|
||||||
Date expirationDate = calendar.getTime();
|
|
||||||
|
|
||||||
BigInteger serial = new BigInteger(snBytes).abs();
|
|
||||||
|
|
||||||
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
|
||||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
|
||||||
X500Name name = nameBuilder.build();
|
|
||||||
|
|
||||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
|
||||||
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));
|
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Nothing should go wrong here
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Generated a new key pair");
|
|
||||||
|
|
||||||
// Save the resulting pair
|
|
||||||
saveCertKeyPair();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCertKeyPair() {
|
|
||||||
try {
|
|
||||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
|
||||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
|
||||||
|
|
||||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
|
||||||
StringWriter strWriter = new StringWriter();
|
|
||||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
|
||||||
pemWriter.writeObject(cert);
|
|
||||||
pemWriter.close();
|
|
||||||
|
|
||||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
|
||||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
|
||||||
String pemStr = strWriter.getBuffer().toString();
|
|
||||||
for (int i = 0; i < pemStr.length(); i++) {
|
|
||||||
char c = pemStr.charAt(i);
|
|
||||||
if (c != '\r')
|
|
||||||
certWriter.append(c);
|
|
||||||
}
|
|
||||||
certWriter.close();
|
|
||||||
|
|
||||||
// Write the private out in PKCS8 format
|
|
||||||
keyOut.write(key.getEncoded());
|
|
||||||
|
|
||||||
certOut.close();
|
|
||||||
keyOut.close();
|
|
||||||
|
|
||||||
LimeLog.info("Saved generated key pair to disk");
|
|
||||||
} catch (IOException e) {
|
|
||||||
// This isn't good because it means we'll have
|
|
||||||
// to re-pair next time
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate getClientCertificate() {
|
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
|
||||||
// the certificate and key at a time
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Return a loaded cert if we have one
|
|
||||||
if (cert != null) {
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No loaded cert yet, let's see if we have one on disk
|
|
||||||
if (loadCertKeyPair()) {
|
|
||||||
// Got one
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate a new key pair
|
|
||||||
if (!generateCertKeyPair()) {
|
|
||||||
// Failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the generated pair
|
|
||||||
loadCertKeyPair();
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public RSAPrivateKey getClientPrivateKey() {
|
private boolean loadCertKeyPair() {
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
byte[] certBytes = loadFileToBytes(certFile);
|
||||||
// the certificate and key at a time
|
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Return a loaded key if we have one
|
|
||||||
if (key != null) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No loaded key yet, let's see if we have one on disk
|
|
||||||
if (loadCertKeyPair()) {
|
|
||||||
// Got one
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate a new key pair
|
|
||||||
if (!generateCertKeyPair()) {
|
|
||||||
// Failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the generated pair
|
|
||||||
loadCertKeyPair();
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getPemEncodedClientCertificate() {
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Call our helper function to do the cert loading/generation for us
|
|
||||||
getClientCertificate();
|
|
||||||
|
|
||||||
// Return a cached value if we have it
|
|
||||||
return pemCertBytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// If either file was missing, we definitely can't succeed
|
||||||
public String encodeBase64String(byte[] data) {
|
if (certBytes == null || keyBytes == null) {
|
||||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
||||||
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
pemCertBytes = certBytes;
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
||||||
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// May happen if the cert is corrupt
|
||||||
|
LimeLog.warning("Corrupted certificate");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
// May happen if the key is corrupt
|
||||||
|
LimeLog.warning("Corrupted key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("TrulyRandom")
|
||||||
|
private boolean generateCertKeyPair() {
|
||||||
|
byte[] snBytes = new byte[8];
|
||||||
|
new SecureRandom().nextBytes(snBytes);
|
||||||
|
|
||||||
|
KeyPair keyPair;
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
|
// Should never happen
|
||||||
|
e1.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// Expires in 20 years
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(now);
|
||||||
|
calendar.add(Calendar.YEAR, 20);
|
||||||
|
Date expirationDate = calendar.getTime();
|
||||||
|
|
||||||
|
BigInteger serial = new BigInteger(snBytes).abs();
|
||||||
|
|
||||||
|
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||||
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||||
|
X500Name name = nameBuilder.build();
|
||||||
|
|
||||||
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||||
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").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();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|
||||||
|
// Save the resulting pair
|
||||||
|
saveCertKeyPair();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCertKeyPair() {
|
||||||
|
try {
|
||||||
|
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||||
|
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||||
|
|
||||||
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
|
StringWriter strWriter = new StringWriter();
|
||||||
|
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||||
|
pemWriter.writeObject(cert);
|
||||||
|
pemWriter.close();
|
||||||
|
|
||||||
|
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||||
|
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||||
|
String pemStr = strWriter.getBuffer().toString();
|
||||||
|
for (int i = 0; i < pemStr.length(); i++) {
|
||||||
|
char c = pemStr.charAt(i);
|
||||||
|
if (c != '\r')
|
||||||
|
certWriter.append(c);
|
||||||
|
}
|
||||||
|
certWriter.close();
|
||||||
|
|
||||||
|
// Write the private out in PKCS8 format
|
||||||
|
keyOut.write(key.getEncoded());
|
||||||
|
|
||||||
|
certOut.close();
|
||||||
|
keyOut.close();
|
||||||
|
|
||||||
|
LimeLog.info("Saved generated key pair to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This isn't good because it means we'll have
|
||||||
|
// to re-pair next time
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getClientCertificate() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Return a loaded cert if we have one
|
||||||
|
if (cert != null) {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded cert yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Return a loaded key if we have one
|
||||||
|
if (key != null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded key yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Call our helper function to do the cert loading/generation for us
|
||||||
|
getClientCertificate();
|
||||||
|
|
||||||
|
// Return a cached value if we have it
|
||||||
|
return pemCertBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeBase64String(byte[] data) {
|
||||||
|
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,259 +2,296 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.view.KeyEvent;
|
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
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
* @author Diego Waxemberg
|
* @author Diego Waxemberg
|
||||||
* @author Cameron Gutman
|
* @author Cameron Gutman
|
||||||
*/
|
*/
|
||||||
public class KeyboardTranslator extends KeycodeTranslator {
|
public class KeyboardTranslator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GFE's prefix for every key code
|
* GFE's prefix for every key code
|
||||||
*/
|
*/
|
||||||
public static final short KEY_PREFIX = (short) 0x80;
|
private static final short KEY_PREFIX = (short) 0x80;
|
||||||
|
|
||||||
public static final int VK_0 = 48;
|
public static final int VK_0 = 48;
|
||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
public static final int VK_A = 65;
|
public static final int VK_A = 65;
|
||||||
public static final int VK_Z = 90;
|
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_NUMPAD0 = 96;
|
public static final int VK_BACK_SLASH = 92;
|
||||||
public static final int VK_BACK_SLASH = 92;
|
public static final int VK_CAPS_LOCK = 20;
|
||||||
public static final int VK_CAPS_LOCK = 20;
|
public static final int VK_CLEAR = 12;
|
||||||
public static final int VK_CLEAR = 12;
|
public static final int VK_COMMA = 44;
|
||||||
public static final int VK_COMMA = 44;
|
public static final int VK_BACK_SPACE = 8;
|
||||||
public static final int VK_CONTROL = 17;
|
public static final int VK_EQUALS = 61;
|
||||||
public static final int VK_BACK_SPACE = 8;
|
public static final int VK_ESCAPE = 27;
|
||||||
public static final int VK_EQUALS = 61;
|
public static final int VK_F1 = 112;
|
||||||
public static final int VK_ESCAPE = 27;
|
public static final int VK_END = 35;
|
||||||
public static final int VK_F1 = 112;
|
public static final int VK_HOME = 36;
|
||||||
public static final int VK_PERIOD = 46;
|
public static final int VK_NUM_LOCK = 144;
|
||||||
public static final int VK_INSERT = 155;
|
public static final int VK_PAGE_UP = 33;
|
||||||
public static final int VK_OPEN_BRACKET = 91;
|
public static final int VK_PAGE_DOWN = 34;
|
||||||
public static final int VK_WINDOWS = 524;
|
public static final int VK_PLUS = 521;
|
||||||
public static final int VK_MINUS = 45;
|
public static final int VK_CLOSE_BRACKET = 93;
|
||||||
public static final int VK_END = 35;
|
public static final int VK_SCROLL_LOCK = 145;
|
||||||
public static final int VK_HOME = 36;
|
public static final int VK_SEMICOLON = 59;
|
||||||
public static final int VK_NUM_LOCK = 144;
|
public static final int VK_SLASH = 47;
|
||||||
public static final int VK_PAGE_UP = 33;
|
public static final int VK_SPACE = 32;
|
||||||
public static final int VK_PAGE_DOWN = 34;
|
public static final int VK_PRINTSCREEN = 154;
|
||||||
public static final int VK_PLUS = 521;
|
public static final int VK_TAB = 9;
|
||||||
public static final int VK_CLOSE_BRACKET = 93;
|
public static final int VK_LEFT = 37;
|
||||||
public static final int VK_SCROLL_LOCK = 145;
|
public static final int VK_RIGHT = 39;
|
||||||
public static final int VK_SEMICOLON = 59;
|
public static final int VK_UP = 38;
|
||||||
public static final int VK_SHIFT = 16;
|
public static final int VK_DOWN = 40;
|
||||||
public static final int VK_SLASH = 47;
|
public static final int VK_BACK_QUOTE = 192;
|
||||||
public static final int VK_SPACE = 32;
|
public static final int VK_QUOTE = 222;
|
||||||
public static final int VK_PRINTSCREEN = 154;
|
public static final int VK_PAUSE = 19;
|
||||||
public static final int VK_TAB = 9;
|
|
||||||
public static final int VK_LEFT = 37;
|
public static boolean needsShift(int keycode) {
|
||||||
public static final int VK_RIGHT = 39;
|
switch (keycode)
|
||||||
public static final int VK_UP = 38;
|
{
|
||||||
public static final int VK_DOWN = 40;
|
case KeyEvent.KEYCODE_AT:
|
||||||
public static final int VK_BACK_QUOTE = 192;
|
case KeyEvent.KEYCODE_POUND:
|
||||||
public static final int VK_QUOTE = 222;
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
public static final int VK_PAUSE = 19;
|
case KeyEvent.KEYCODE_STAR:
|
||||||
|
return true;
|
||||||
/**
|
|
||||||
* Constructs a new translator for the specified connection
|
default:
|
||||||
* @param conn the connection to which the translated codes are sent
|
return false;
|
||||||
*/
|
}
|
||||||
public KeyboardTranslator(NvConnection conn) {
|
}
|
||||||
super(conn);
|
|
||||||
}
|
/**
|
||||||
|
* Translates the given keycode and returns the GFE keycode
|
||||||
/**
|
* @param keycode the code to be translated
|
||||||
* Translates the given keycode and returns the GFE keycode
|
* @return a GFE keycode for the given keycode
|
||||||
* @param keycode the code to be translated
|
*/
|
||||||
* @return a GFE keycode for the given keycode
|
public static short translate(int keycode) {
|
||||||
*/
|
int translated;
|
||||||
@Override
|
|
||||||
public short translate(int keycode) {
|
// This is a poor man's mapping between Android key codes
|
||||||
int translated;
|
// and Windows VK_* codes. For all defined VK_ codes, see:
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||||
/* There seems to be no clean mapping between Android key codes
|
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||||
* and what Nvidia sends over the wire. If someone finds one,
|
keycode <= KeyEvent.KEYCODE_9) {
|
||||||
* I'll happily delete this code :)
|
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||||
*/
|
}
|
||||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
else if (keycode >= KeyEvent.KEYCODE_A &&
|
||||||
keycode <= KeyEvent.KEYCODE_9) {
|
keycode <= KeyEvent.KEYCODE_Z) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
||||||
}
|
}
|
||||||
else if (keycode >= KeyEvent.KEYCODE_A &&
|
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
||||||
keycode <= KeyEvent.KEYCODE_Z) {
|
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
|
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
||||||
}
|
}
|
||||||
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
|
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
||||||
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
|
keycode <= KeyEvent.KEYCODE_F12) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
|
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
||||||
}
|
}
|
||||||
else if (keycode >= KeyEvent.KEYCODE_F1 &&
|
else {
|
||||||
keycode <= KeyEvent.KEYCODE_F12) {
|
switch (keycode) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
}
|
translated = 0xA4;
|
||||||
else {
|
break;
|
||||||
switch (keycode) {
|
|
||||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
translated = 0xA5;
|
||||||
translated = VK_ALT;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
case KeyEvent.KEYCODE_BACKSLASH:
|
translated = 0xdc;
|
||||||
translated = 0xdc;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_CAPS_LOCK:
|
||||||
case KeyEvent.KEYCODE_CAPS_LOCK:
|
translated = VK_CAPS_LOCK;
|
||||||
translated = VK_CAPS_LOCK;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_CLEAR:
|
||||||
case KeyEvent.KEYCODE_CLEAR:
|
translated = VK_CLEAR;
|
||||||
translated = VK_CLEAR;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_COMMA:
|
||||||
case KeyEvent.KEYCODE_COMMA:
|
translated = 0xbc;
|
||||||
translated = 0xbc;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
translated = 0xA2;
|
||||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
break;
|
||||||
translated = VK_CONTROL;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
|
translated = 0xA3;
|
||||||
case KeyEvent.KEYCODE_DEL:
|
break;
|
||||||
translated = VK_BACK_SPACE;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_DEL:
|
||||||
|
translated = VK_BACK_SPACE;
|
||||||
case KeyEvent.KEYCODE_ENTER:
|
break;
|
||||||
translated = 0x0d;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_ENTER:
|
||||||
|
translated = 0x0d;
|
||||||
case KeyEvent.KEYCODE_EQUALS:
|
break;
|
||||||
translated = 0xbb;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_PLUS:
|
||||||
|
case KeyEvent.KEYCODE_EQUALS:
|
||||||
case KeyEvent.KEYCODE_ESCAPE:
|
translated = 0xbb;
|
||||||
translated = VK_ESCAPE;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_ESCAPE:
|
||||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
translated = VK_ESCAPE;
|
||||||
// Nvidia maps period to delete
|
break;
|
||||||
translated = VK_PERIOD;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
|
translated = 0x2e;
|
||||||
case KeyEvent.KEYCODE_INSERT:
|
break;
|
||||||
translated = -1;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
|
translated = 0x2d;
|
||||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
break;
|
||||||
translated = 0xdb;
|
|
||||||
break;
|
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||||
|
translated = 0xdb;
|
||||||
case KeyEvent.KEYCODE_META_LEFT:
|
break;
|
||||||
case KeyEvent.KEYCODE_META_RIGHT:
|
|
||||||
translated = VK_WINDOWS;
|
case KeyEvent.KEYCODE_META_LEFT:
|
||||||
break;
|
translated = 0x5b;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_MINUS:
|
|
||||||
translated = 0xbd;
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
break;
|
translated = 0x5c;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_MOVE_END:
|
|
||||||
translated = VK_END;
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
break;
|
translated = 0xbd;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_MOVE_HOME:
|
|
||||||
translated = VK_HOME;
|
case KeyEvent.KEYCODE_MOVE_END:
|
||||||
break;
|
translated = VK_END;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_NUM_LOCK:
|
|
||||||
translated = VK_NUM_LOCK;
|
case KeyEvent.KEYCODE_MOVE_HOME:
|
||||||
break;
|
translated = VK_HOME;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_PAGE_DOWN:
|
|
||||||
translated = VK_PAGE_DOWN;
|
case KeyEvent.KEYCODE_NUM_LOCK:
|
||||||
break;
|
translated = VK_NUM_LOCK;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_PAGE_UP:
|
|
||||||
translated = VK_PAGE_UP;
|
case KeyEvent.KEYCODE_PAGE_DOWN:
|
||||||
break;
|
translated = VK_PAGE_DOWN;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_PERIOD:
|
|
||||||
translated = 0xbe;
|
case KeyEvent.KEYCODE_PAGE_UP:
|
||||||
break;
|
translated = VK_PAGE_UP;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
|
||||||
translated = 0xdd;
|
case KeyEvent.KEYCODE_PERIOD:
|
||||||
break;
|
translated = 0xbe;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
|
||||||
translated = VK_SCROLL_LOCK;
|
case KeyEvent.KEYCODE_RIGHT_BRACKET:
|
||||||
break;
|
translated = 0xdd;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_SEMICOLON:
|
|
||||||
translated = 0xba;
|
case KeyEvent.KEYCODE_SCROLL_LOCK:
|
||||||
break;
|
translated = VK_SCROLL_LOCK;
|
||||||
|
break;
|
||||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
case KeyEvent.KEYCODE_SEMICOLON:
|
||||||
translated = VK_SHIFT;
|
translated = 0xba;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SLASH:
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
translated = 0xbf;
|
translated = 0xA0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SPACE:
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
translated = VK_SPACE;
|
translated = 0xA1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SYSRQ:
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
// Android defines this as SysRq/PrntScrn
|
translated = 0xbf;
|
||||||
translated = VK_PRINTSCREEN;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_SPACE:
|
||||||
case KeyEvent.KEYCODE_TAB:
|
translated = VK_SPACE;
|
||||||
translated = VK_TAB;
|
break;
|
||||||
break;
|
|
||||||
|
case KeyEvent.KEYCODE_SYSRQ:
|
||||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
// Android defines this as SysRq/PrntScrn
|
||||||
translated = VK_LEFT;
|
translated = VK_PRINTSCREEN;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
case KeyEvent.KEYCODE_TAB:
|
||||||
translated = VK_RIGHT;
|
translated = VK_TAB;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_UP:
|
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||||
translated = VK_UP;
|
translated = VK_LEFT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DPAD_DOWN:
|
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||||
translated = VK_DOWN;
|
translated = VK_RIGHT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_GRAVE:
|
case KeyEvent.KEYCODE_DPAD_UP:
|
||||||
translated = VK_BACK_QUOTE;
|
translated = VK_UP;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_APOSTROPHE:
|
case KeyEvent.KEYCODE_DPAD_DOWN:
|
||||||
translated = 0xde;
|
translated = VK_DOWN;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BREAK:
|
case KeyEvent.KEYCODE_GRAVE:
|
||||||
translated = VK_PAUSE;
|
translated = VK_BACK_QUOTE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
case KeyEvent.KEYCODE_APOSTROPHE:
|
||||||
System.out.println("No key for "+keycode);
|
translated = 0xde;
|
||||||
return 0;
|
break;
|
||||||
}
|
|
||||||
}
|
case KeyEvent.KEYCODE_BREAK:
|
||||||
|
translated = VK_PAUSE;
|
||||||
return (short) ((KEY_PREFIX << 8) | translated);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (short) ((KEY_PREFIX << 8) | translated);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,241 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
public class TouchContext {
|
public class TouchContext {
|
||||||
private int lastTouchX = 0;
|
private int lastTouchX = 0;
|
||||||
private int lastTouchY = 0;
|
private int lastTouchY = 0;
|
||||||
private int originalTouchX = 0;
|
private int originalTouchX = 0;
|
||||||
private int originalTouchY = 0;
|
private int originalTouchY = 0;
|
||||||
private long originalTouchTime = 0;
|
private long originalTouchTime = 0;
|
||||||
|
private boolean cancelled;
|
||||||
private NvConnection conn;
|
private boolean confirmedMove;
|
||||||
private int actionIndex;
|
private boolean confirmedDrag;
|
||||||
|
private Timer dragTimer;
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
private double distanceMoved;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
private double xFactor, yFactor;
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex)
|
private final NvConnection conn;
|
||||||
{
|
private final int actionIndex;
|
||||||
this.conn = conn;
|
private final int referenceWidth;
|
||||||
this.actionIndex = actionIndex;
|
private final int referenceHeight;
|
||||||
}
|
private final View targetView;
|
||||||
|
|
||||||
private boolean isTap()
|
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||||
{
|
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||||
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
|
||||||
|
public TouchContext(NvConnection conn, int actionIndex,
|
||||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
int referenceWidth, int referenceHeight, View view)
|
||||||
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
{
|
||||||
timeDelta <= TAP_TIME_THRESHOLD;
|
this.conn = conn;
|
||||||
}
|
this.actionIndex = actionIndex;
|
||||||
|
this.referenceWidth = referenceWidth;
|
||||||
private byte getMouseButtonIndex()
|
this.referenceHeight = referenceHeight;
|
||||||
{
|
this.targetView = view;
|
||||||
if (actionIndex == 1) {
|
}
|
||||||
return MouseButtonPacket.BUTTON_RIGHT;
|
|
||||||
}
|
public int getActionIndex()
|
||||||
else {
|
{
|
||||||
return MouseButtonPacket.BUTTON_LEFT;
|
return actionIndex;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private boolean isWithinTapBounds(int touchX, int touchY)
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
{
|
||||||
{
|
int xDelta = Math.abs(touchX - originalTouchX);
|
||||||
originalTouchX = lastTouchX = eventX;
|
int yDelta = Math.abs(touchY - originalTouchY);
|
||||||
originalTouchY = lastTouchY = eventY;
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
originalTouchTime = System.currentTimeMillis();
|
yDelta <= TAP_MOVEMENT_THRESHOLD;
|
||||||
|
}
|
||||||
return true;
|
|
||||||
}
|
private boolean isTap()
|
||||||
|
{
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||||
{
|
|
||||||
if (isTap())
|
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
{
|
}
|
||||||
byte buttonIndex = getMouseButtonIndex();
|
|
||||||
|
private byte getMouseButtonIndex()
|
||||||
// Lower the mouse button
|
{
|
||||||
conn.sendMouseButtonDown(buttonIndex);
|
if (actionIndex == 1) {
|
||||||
|
return MouseButtonPacket.BUTTON_RIGHT;
|
||||||
// We need to sleep a bit here because some games
|
}
|
||||||
// do input detection by polling
|
else {
|
||||||
try {
|
return MouseButtonPacket.BUTTON_LEFT;
|
||||||
Thread.sleep(100);
|
}
|
||||||
} catch (InterruptedException ignored) {}
|
}
|
||||||
|
|
||||||
// Raise the mouse button
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
{
|
||||||
}
|
// Get the view dimensions to scale inputs on this touch
|
||||||
}
|
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||||
|
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
|
||||||
{
|
originalTouchX = lastTouchX = eventX;
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
originalTouchY = lastTouchY = eventY;
|
||||||
{
|
originalTouchTime = System.currentTimeMillis();
|
||||||
// We only send moves for the primary touch point
|
cancelled = confirmedDrag = confirmedMove = false;
|
||||||
if (actionIndex == 0) {
|
distanceMoved = 0;
|
||||||
conn.sendMouseMove((short)(eventX - lastTouchX),
|
|
||||||
(short)(eventY - lastTouchY));
|
if (actionIndex == 0) {
|
||||||
}
|
// Start the timer for engaging a drag
|
||||||
|
startDragTimer();
|
||||||
lastTouchX = eventX;
|
}
|
||||||
lastTouchY = eventY;
|
|
||||||
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
|
||||||
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
return false;
|
{
|
||||||
}
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the drag timer
|
||||||
|
cancelDragTimer();
|
||||||
|
|
||||||
|
byte buttonIndex = getMouseButtonIndex();
|
||||||
|
|
||||||
|
if (confirmedDrag) {
|
||||||
|
// Raise the button after a drag
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
else if (isTap())
|
||||||
|
{
|
||||||
|
// Lower the mouse button
|
||||||
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
|
||||||
|
// We need to sleep a bit here because some games
|
||||||
|
// do input detection by polling
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
|
// Raise the mouse button
|
||||||
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startDragTimer() {
|
||||||
|
dragTimer = new Timer(true);
|
||||||
|
dragTimer.schedule(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (TouchContext.this) {
|
||||||
|
// Check if someone already set move
|
||||||
|
if (confirmedMove) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if someone cancelled us
|
||||||
|
if (dragTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncancellable now
|
||||||
|
dragTimer = null;
|
||||||
|
|
||||||
|
// We haven't been cancelled before the timer expired so begin dragging
|
||||||
|
confirmedDrag = true;
|
||||||
|
conn.sendMouseButtonDown(getMouseButtonIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, DRAG_TIME_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void cancelDragTimer() {
|
||||||
|
if (dragTimer != null) {
|
||||||
|
dragTimer.cancel();
|
||||||
|
dragTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
|
||||||
|
// If we've already confirmed something, get out now
|
||||||
|
if (confirmedMove || confirmedDrag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it leaves the tap bounds before the drag time expires, it's a move.
|
||||||
|
if (!isWithinTapBounds(eventX, eventY)) {
|
||||||
|
confirmedMove = true;
|
||||||
|
cancelDragTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've exceeded the maximum distance moved
|
||||||
|
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
|
||||||
|
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
|
||||||
|
confirmedMove = true;
|
||||||
|
cancelDragTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
|
{
|
||||||
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
|
{
|
||||||
|
// We only send moves and drags for the primary touch point
|
||||||
|
if (actionIndex == 0) {
|
||||||
|
checkForConfirmedMove(eventX, eventY);
|
||||||
|
|
||||||
|
int deltaX = eventX - lastTouchX;
|
||||||
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
|
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
||||||
|
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
||||||
|
|
||||||
|
// Fix up the signs
|
||||||
|
if (eventX < lastTouchX) {
|
||||||
|
deltaX = -deltaX;
|
||||||
|
}
|
||||||
|
if (eventY < lastTouchY) {
|
||||||
|
deltaY = -deltaY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
||||||
|
// non-zero to update lastTouch that way devices that report small touch events often
|
||||||
|
// will work correctly
|
||||||
|
if (deltaX != 0) {
|
||||||
|
lastTouchX = eventX;
|
||||||
|
}
|
||||||
|
if (deltaY != 0) {
|
||||||
|
lastTouchY = eventY;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lastTouchX = eventX;
|
||||||
|
lastTouchY = eventY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelTouch() {
|
||||||
|
cancelled = true;
|
||||||
|
|
||||||
|
// Cancel the drag timer
|
||||||
|
cancelDragTimer();
|
||||||
|
|
||||||
|
// If it was a confirmed drag, we'll need to raise the button now
|
||||||
|
if (confirmedDrag) {
|
||||||
|
conn.sendMouseButtonUp(getMouseButtonIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package com.limelight.binding.input.capture;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.InputDevice;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
|
||||||
|
// We extend AndroidPointerIconCaptureProvider because we want to also get the
|
||||||
|
// pointer icon hiding behavior over our stream view just in case pointer capture
|
||||||
|
// is unavailable on this system (ex: DeX, ChromeOS)
|
||||||
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
|
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider {
|
||||||
|
private View targetView;
|
||||||
|
|
||||||
|
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
|
||||||
|
super(activity, 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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
|
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
||||||
|
private View targetView;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
|
||||||
|
this.context = activity;
|
||||||
|
this.targetView = targetView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCaptureProviderSupported() {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enableCapture() {
|
||||||
|
super.enableCapture();
|
||||||
|
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableCapture() {
|
||||||
|
super.disableCapture();
|
||||||
|
targetView.setPointerIcon(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, 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, activity.findViewById(R.id.surfaceView));
|
||||||
|
}
|
||||||
|
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,61 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public abstract class AbstractController {
|
||||||
|
|
||||||
|
private final int deviceId;
|
||||||
|
private final int vendorId;
|
||||||
|
private final int productId;
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
|
||||||
|
protected short buttonFlags;
|
||||||
|
protected float leftTrigger, rightTrigger;
|
||||||
|
protected float rightStickX, rightStickY;
|
||||||
|
protected float leftStickX, leftStickY;
|
||||||
|
|
||||||
|
public int getControllerId() {
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVendorId() {
|
||||||
|
return vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProductId() {
|
||||||
|
return productId;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, int vendorId, int productId) {
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.listener = listener;
|
||||||
|
this.vendorId = vendorId;
|
||||||
|
this.productId = productId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
||||||
|
|
||||||
|
protected void notifyDeviceRemoved() {
|
||||||
|
listener.deviceRemoved(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void notifyDeviceAdded() {
|
||||||
|
listener.deviceAdded(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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, device.getVendorId(), device.getProductId());
|
||||||
|
this.device = device;
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Thread createInputThread() {
|
||||||
|
return new Thread() {
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// Delay for a moment before reporting the new gamepad and
|
||||||
|
// accepting new input. This allows time for the old InputDevice
|
||||||
|
// to go away before we reclaim its spot. If the old device is still
|
||||||
|
// around when we call notifyDeviceAdded(), we won't be able to claim
|
||||||
|
// the controller number used by the original InputDevice.
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
|
||||||
|
// Report that we're added _before_ reporting input
|
||||||
|
notifyDeviceAdded();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
public interface UsbDriverListener {
|
||||||
|
void reportControllerState(int controllerId, short buttonFlags,
|
||||||
|
float leftStickX, float leftStickY,
|
||||||
|
float rightStickX, float rightStickY,
|
||||||
|
float leftTrigger, float rightTrigger);
|
||||||
|
|
||||||
|
void deviceRemoved(AbstractController controller);
|
||||||
|
void deviceAdded(AbstractController controller);
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
package com.limelight.binding.input.driver;
|
||||||
|
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class UsbDriverService extends Service implements UsbDriverListener {
|
||||||
|
|
||||||
|
private static final String ACTION_USB_PERMISSION =
|
||||||
|
"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<AbstractController> controllers = new ArrayList<>();
|
||||||
|
|
||||||
|
private UsbDriverListener listener;
|
||||||
|
private int nextDeviceId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
||||||
|
// Call through to the client's listener
|
||||||
|
if (listener != null) {
|
||||||
|
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deviceRemoved(AbstractController controller) {
|
||||||
|
// Remove the the controller from our list (if not removed already)
|
||||||
|
controllers.remove(controller);
|
||||||
|
|
||||||
|
// Call through to the client's listener
|
||||||
|
if (listener != null) {
|
||||||
|
listener.deviceRemoved(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deviceAdded(AbstractController controller) {
|
||||||
|
// Call through to the client's listener
|
||||||
|
if (listener != null) {
|
||||||
|
listener.deviceAdded(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsbEventReceiver extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String action = intent.getAction();
|
||||||
|
|
||||||
|
// Initial attachment broadcast
|
||||||
|
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||||
|
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_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 = 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)) {
|
||||||
|
handleUsbDeviceState(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UsbDriverBinder extends Binder {
|
||||||
|
public void setListener(UsbDriverListener listener) {
|
||||||
|
UsbDriverService.this.listener = listener;
|
||||||
|
|
||||||
|
// Report all controllerMap that already exist
|
||||||
|
if (listener != null) {
|
||||||
|
for (AbstractController controller : controllers) {
|
||||||
|
listener.deviceAdded(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleUsbDeviceState(UsbDevice device) {
|
||||||
|
// Are we able to operate it?
|
||||||
|
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
||||||
|
// Do we have permission yet?
|
||||||
|
if (!usbManager.hasPermission(device)) {
|
||||||
|
// Let's ask for permission
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this controller to the list
|
||||||
|
controllers.add(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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 kernelSupportsXboxOne() {
|
||||||
|
String kernelVersion = System.getProperty("os.version");
|
||||||
|
LimeLog.info("Kernel Version: "+kernelVersion);
|
||||||
|
|
||||||
|
if (kernelVersion == null) {
|
||||||
|
// We'll assume this is some newer version of Android
|
||||||
|
// that doesn't let you read the kernel version this way.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.")) {
|
||||||
|
// These are old kernels that definitely don't support Xbox One controllers properly
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (kernelVersion.startsWith("4.4.") || kernelVersion.startsWith("4.9.")) {
|
||||||
|
// These aren't guaranteed to have backported kernel patches for proper Xbox One
|
||||||
|
// support (though some devices will).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The next AOSP common kernel is 4.14 which has working Xbox One controller support
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
||||||
|
return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && 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();
|
||||||
|
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||||
|
filter.addAction(ACTION_USB_PERMISSION);
|
||||||
|
registerReceiver(receiver, filter);
|
||||||
|
|
||||||
|
// Enumerate existing devices
|
||||||
|
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||||
|
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
|
||||||
|
// Start the process of claiming this device
|
||||||
|
handleUsbDeviceState(dev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
// Stop the attachment receiver
|
||||||
|
unregisterReceiver(receiver);
|
||||||
|
|
||||||
|
// Remove listeners
|
||||||
|
listener = null;
|
||||||
|
|
||||||
|
// Stop all controllers
|
||||||
|
while (controllers.size() > 0) {
|
||||||
|
// Stop and remove the controller
|
||||||
|
controllers.remove(0).stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
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;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class XboxOneController extends AbstractXboxController {
|
||||||
|
|
||||||
|
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||||
|
private static final int XB1_IFACE_PROTOCOL = 208;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
super(device, connection, deviceId, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processButtons(ByteBuffer buffer) {
|
||||||
|
byte b = buffer.get();
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
|
||||||
|
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||||
|
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
|
||||||
|
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
|
||||||
|
|
||||||
|
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||||
|
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||||
|
|
||||||
|
leftTrigger = buffer.getShort() / 1023.0f;
|
||||||
|
rightTrigger = buffer.getShort() / 1023.0f;
|
||||||
|
|
||||||
|
leftStickX = buffer.getShort() / 32767.0f;
|
||||||
|
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
|
||||||
|
rightStickX = buffer.getShort() / 32767.0f;
|
||||||
|
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 0x07:
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.position(buffer.position() + 2);
|
||||||
|
}
|
||||||
|
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,41 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
public class EvdevEvent {
|
|
||||||
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
|
||||||
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
|
||||||
|
|
||||||
/* Event types */
|
|
||||||
public static final short EV_SYN = 0x00;
|
|
||||||
public static final short EV_KEY = 0x01;
|
|
||||||
public static final short EV_REL = 0x02;
|
|
||||||
public static final short EV_MSC = 0x04;
|
|
||||||
|
|
||||||
/* Relative axes */
|
|
||||||
public static final short REL_X = 0x00;
|
|
||||||
public static final short REL_Y = 0x01;
|
|
||||||
public static final short REL_WHEEL = 0x08;
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
public static final short BTN_LEFT = 0x110;
|
|
||||||
public static final short BTN_RIGHT = 0x111;
|
|
||||||
public static final short BTN_MIDDLE = 0x112;
|
|
||||||
public static final short BTN_SIDE = 0x113;
|
|
||||||
public static final short BTN_EXTRA = 0x114;
|
|
||||||
public static final short BTN_FORWARD = 0x115;
|
|
||||||
public static final short BTN_BACK = 0x116;
|
|
||||||
public static final short BTN_TASK = 0x117;
|
|
||||||
public static final short BTN_GAMEPAD = 0x130;
|
|
||||||
|
|
||||||
/* Keys */
|
|
||||||
public static final short KEY_Q = 16;
|
|
||||||
|
|
||||||
public short type;
|
|
||||||
public short code;
|
|
||||||
public int value;
|
|
||||||
|
|
||||||
public EvdevEvent(short type, short code, int value) {
|
|
||||||
this.type = type;
|
|
||||||
this.code = code;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 String absolutePath;
|
|
||||||
private EvdevListener listener;
|
|
||||||
private boolean shutdown = false;
|
|
||||||
private int fd = -1;
|
|
||||||
|
|
||||||
private 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;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public interface EvdevListener {
|
public interface EvdevListener {
|
||||||
public static final int BUTTON_LEFT = 1;
|
int BUTTON_LEFT = 1;
|
||||||
public static final int BUTTON_MIDDLE = 2;
|
int BUTTON_MIDDLE = 2;
|
||||||
public static final int BUTTON_RIGHT = 3;
|
int BUTTON_RIGHT = 3;
|
||||||
|
int BUTTON_X1 = 4;
|
||||||
public void mouseMove(int deltaX, int deltaY);
|
int BUTTON_X2 = 5;
|
||||||
public void mouseButtonEvent(int buttonId, boolean down);
|
|
||||||
public void mouseScroll(byte amount);
|
void mouseMove(int deltaX, int deltaY);
|
||||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
void mouseButtonEvent(int buttonId, boolean down);
|
||||||
|
void mouseScroll(byte amount);
|
||||||
|
void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
public class EvdevReader {
|
|
||||||
static {
|
|
||||||
System.loadLibrary("evdev_reader");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Requires root to chmod /dev/input/eventX
|
|
||||||
public static boolean setPermissions(String[] files, int octalPermissions) {
|
|
||||||
ProcessBuilder builder = new ProcessBuilder("su");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Process p = builder.start();
|
|
||||||
|
|
||||||
OutputStream stdin = p.getOutputStream();
|
|
||||||
for (String file : files) {
|
|
||||||
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
|
|
||||||
}
|
|
||||||
stdin.write("exit\n".getBytes("UTF-8"));
|
|
||||||
stdin.flush();
|
|
||||||
|
|
||||||
p.waitFor();
|
|
||||||
p.destroy();
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,139 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
|
|
||||||
public class EvdevTranslator {
|
|
||||||
|
|
||||||
public static final short EVDEV_KEY_CODES[] = {
|
|
||||||
0, //KeyEvent.VK_RESERVED
|
|
||||||
KeyEvent.KEYCODE_ESCAPE,
|
|
||||||
KeyEvent.KEYCODE_1,
|
|
||||||
KeyEvent.KEYCODE_2,
|
|
||||||
KeyEvent.KEYCODE_3,
|
|
||||||
KeyEvent.KEYCODE_4,
|
|
||||||
KeyEvent.KEYCODE_5,
|
|
||||||
KeyEvent.KEYCODE_6,
|
|
||||||
KeyEvent.KEYCODE_7,
|
|
||||||
KeyEvent.KEYCODE_8,
|
|
||||||
KeyEvent.KEYCODE_9,
|
|
||||||
KeyEvent.KEYCODE_0,
|
|
||||||
KeyEvent.KEYCODE_MINUS,
|
|
||||||
KeyEvent.KEYCODE_EQUALS,
|
|
||||||
KeyEvent.KEYCODE_DEL,
|
|
||||||
KeyEvent.KEYCODE_TAB,
|
|
||||||
KeyEvent.KEYCODE_Q,
|
|
||||||
KeyEvent.KEYCODE_W,
|
|
||||||
KeyEvent.KEYCODE_E,
|
|
||||||
KeyEvent.KEYCODE_R,
|
|
||||||
KeyEvent.KEYCODE_T,
|
|
||||||
KeyEvent.KEYCODE_Y,
|
|
||||||
KeyEvent.KEYCODE_U,
|
|
||||||
KeyEvent.KEYCODE_I,
|
|
||||||
KeyEvent.KEYCODE_O,
|
|
||||||
KeyEvent.KEYCODE_P,
|
|
||||||
KeyEvent.KEYCODE_LEFT_BRACKET,
|
|
||||||
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
|
||||||
KeyEvent.KEYCODE_ENTER,
|
|
||||||
KeyEvent.KEYCODE_CTRL_LEFT,
|
|
||||||
KeyEvent.KEYCODE_A,
|
|
||||||
KeyEvent.KEYCODE_S,
|
|
||||||
KeyEvent.KEYCODE_D,
|
|
||||||
KeyEvent.KEYCODE_F,
|
|
||||||
KeyEvent.KEYCODE_G,
|
|
||||||
KeyEvent.KEYCODE_H,
|
|
||||||
KeyEvent.KEYCODE_J,
|
|
||||||
KeyEvent.KEYCODE_K,
|
|
||||||
KeyEvent.KEYCODE_L,
|
|
||||||
KeyEvent.KEYCODE_SEMICOLON,
|
|
||||||
KeyEvent.KEYCODE_APOSTROPHE,
|
|
||||||
KeyEvent.KEYCODE_GRAVE,
|
|
||||||
KeyEvent.KEYCODE_SHIFT_LEFT,
|
|
||||||
KeyEvent.KEYCODE_BACKSLASH,
|
|
||||||
KeyEvent.KEYCODE_Z,
|
|
||||||
KeyEvent.KEYCODE_X,
|
|
||||||
KeyEvent.KEYCODE_C,
|
|
||||||
KeyEvent.KEYCODE_V,
|
|
||||||
KeyEvent.KEYCODE_B,
|
|
||||||
KeyEvent.KEYCODE_N,
|
|
||||||
KeyEvent.KEYCODE_M,
|
|
||||||
KeyEvent.KEYCODE_COMMA,
|
|
||||||
KeyEvent.KEYCODE_PERIOD,
|
|
||||||
KeyEvent.KEYCODE_SLASH,
|
|
||||||
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
|
||||||
KeyEvent.KEYCODE_ALT_LEFT,
|
|
||||||
KeyEvent.KEYCODE_SPACE,
|
|
||||||
KeyEvent.KEYCODE_CAPS_LOCK,
|
|
||||||
KeyEvent.KEYCODE_F1,
|
|
||||||
KeyEvent.KEYCODE_F2,
|
|
||||||
KeyEvent.KEYCODE_F3,
|
|
||||||
KeyEvent.KEYCODE_F4,
|
|
||||||
KeyEvent.KEYCODE_F5,
|
|
||||||
KeyEvent.KEYCODE_F6,
|
|
||||||
KeyEvent.KEYCODE_F7,
|
|
||||||
KeyEvent.KEYCODE_F8,
|
|
||||||
KeyEvent.KEYCODE_F9,
|
|
||||||
KeyEvent.KEYCODE_F10,
|
|
||||||
KeyEvent.KEYCODE_NUM_LOCK,
|
|
||||||
KeyEvent.KEYCODE_SCROLL_LOCK,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_7,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_8,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_9,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_4,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_5,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_6,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_ADD,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_1,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_2,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_3,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_0,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_DOT,
|
|
||||||
0,
|
|
||||||
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
|
||||||
0, //KeyEvent.VK_102ND,
|
|
||||||
KeyEvent.KEYCODE_F11,
|
|
||||||
KeyEvent.KEYCODE_F12,
|
|
||||||
0, //KeyEvent.VK_RO,
|
|
||||||
0, //KeyEvent.VK_KATAKANA,
|
|
||||||
0, //KeyEvent.VK_HIRAGANA,
|
|
||||||
0, //KeyEvent.VK_HENKAN,
|
|
||||||
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
|
||||||
0, //KeyEvent.VK_MUHENKAN,
|
|
||||||
0, //KeyEvent.VK_KPJPCOMMA,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
|
||||||
KeyEvent.KEYCODE_CTRL_RIGHT,
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
|
||||||
KeyEvent.KEYCODE_SYSRQ,
|
|
||||||
KeyEvent.KEYCODE_ALT_RIGHT,
|
|
||||||
0, //KeyEvent.VK_LINEFEED,
|
|
||||||
KeyEvent.KEYCODE_HOME,
|
|
||||||
KeyEvent.KEYCODE_DPAD_UP,
|
|
||||||
KeyEvent.KEYCODE_PAGE_UP,
|
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
|
||||||
KeyEvent.KEYCODE_MOVE_END,
|
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
|
||||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
|
||||||
KeyEvent.KEYCODE_INSERT,
|
|
||||||
KeyEvent.KEYCODE_FORWARD_DEL,
|
|
||||||
0, //KeyEvent.VK_MACRO,
|
|
||||||
0, //KeyEvent.VK_MUTE,
|
|
||||||
0, //KeyEvent.VK_VOLUMEDOWN,
|
|
||||||
0, //KeyEvent.VK_VOLUMEUP,
|
|
||||||
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
|
||||||
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
|
||||||
0, //KeyEvent.VK_KPPLUSMINUS,
|
|
||||||
KeyEvent.KEYCODE_BREAK,
|
|
||||||
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
|
||||||
};
|
|
||||||
|
|
||||||
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
|
||||||
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
|
||||||
return EVDEV_KEY_CODES[evdevKeyCode];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,172 +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 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() {
|
|
||||||
// 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(066);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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);
|
||||||
|
|
||||||
|
// pass touch event to parent if out of outer circle
|
||||||
|
if (movement_radius > radius_complete && !isPressed())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// chop radius if out of outer circle or near the edge
|
||||||
|
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||||
|
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,249 @@
|
|||||||
|
/**
|
||||||
|
* 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.RectF;
|
||||||
|
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 final RectF rect = new RectF();
|
||||||
|
|
||||||
|
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(), 25));
|
||||||
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
|
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||||
|
|
||||||
|
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
|
||||||
|
rect.left = rect.top = paint.getStrokeWidth();
|
||||||
|
rect.right = getWidth() - rect.left;
|
||||||
|
rect.bottom = getHeight() - rect.top;
|
||||||
|
|
||||||
|
canvas.drawOval(rect, 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
MoveButtons,
|
||||||
|
ResizeButtons
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Active){
|
||||||
|
currentMode = ControllerMode.MoveButtons;
|
||||||
|
message = "Entering configuration mode (Move buttons)";
|
||||||
|
} else if (currentMode == ControllerMode.MoveButtons) {
|
||||||
|
currentMode = ControllerMode.ResizeButtons;
|
||||||
|
message = "Entering configuration mode (Resize buttons)";
|
||||||
|
} else {
|
||||||
|
currentMode = ControllerMode.Active;
|
||||||
|
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
||||||
|
message = "Exiting 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 setOpacity(int opacity) {
|
||||||
|
for (VirtualControllerElement element : elements) {
|
||||||
|
element.setOpacity(opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+366
@@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The default controls are specified using a grid of 128*72 cells at 16:9
|
||||||
|
private static int screenScale(int units, int height) {
|
||||||
|
return (int) (((float) height / (float) 72) * (float) units);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TRIGGER_L_BASE_X = 1;
|
||||||
|
private static final int TRIGGER_R_BASE_X = 92;
|
||||||
|
private static final int TRIGGER_DISTANCE = 23;
|
||||||
|
private static final int TRIGGER_BASE_Y = 31;
|
||||||
|
private static final int TRIGGER_WIDTH = 12;
|
||||||
|
private static final int TRIGGER_HEIGHT = 9;
|
||||||
|
|
||||||
|
// Face buttons are defined based on the Y button (button number 9)
|
||||||
|
private static final int BUTTON_BASE_X = 106;
|
||||||
|
private static final int BUTTON_BASE_Y = 1;
|
||||||
|
private static final int BUTTON_SIZE = 10;
|
||||||
|
|
||||||
|
private static final int DPAD_BASE_X = 4;
|
||||||
|
private static final int DPAD_BASE_Y = 41;
|
||||||
|
private static final int DPAD_SIZE = 30;
|
||||||
|
|
||||||
|
private static final int ANALOG_L_BASE_X = 4;
|
||||||
|
private static final int ANALOG_L_BASE_Y = 1;
|
||||||
|
private static final int ANALOG_R_BASE_X = 96;
|
||||||
|
private static final int ANALOG_R_BASE_Y = 42;
|
||||||
|
private static final int ANALOG_SIZE = 28;
|
||||||
|
|
||||||
|
private static final int L3_R3_BASE_Y = 60;
|
||||||
|
|
||||||
|
private static final int START_X = 83;
|
||||||
|
private static final int BACK_X = 34;
|
||||||
|
private static final int START_BACK_Y = 64;
|
||||||
|
private static final int START_BACK_WIDTH = 12;
|
||||||
|
private static final int START_BACK_HEIGHT = 7;
|
||||||
|
|
||||||
|
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||||
|
|
||||||
|
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||||
|
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
||||||
|
|
||||||
|
// Displace controls on the right by this amount of pixels to account for different aspect ratios
|
||||||
|
int rightDisplacement = screen.widthPixels - screen.heightPixels * 16 / 9;
|
||||||
|
|
||||||
|
int height = screen.heightPixels;
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
screenScale(DPAD_BASE_X, height),
|
||||||
|
screenScale(DPAD_BASE_Y, height),
|
||||||
|
screenScale(DPAD_SIZE, height),
|
||||||
|
screenScale(DPAD_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_A,
|
||||||
|
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||||
|
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||||
|
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_B,
|
||||||
|
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||||
|
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
|
||||||
|
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_X,
|
||||||
|
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||||
|
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
|
||||||
|
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_Y,
|
||||||
|
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||||
|
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
|
||||||
|
screenScale(BUTTON_BASE_Y, height),
|
||||||
|
screenScale(BUTTON_SIZE, height),
|
||||||
|
screenScale(BUTTON_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftTrigger(
|
||||||
|
1, "LT", -1, controller, context),
|
||||||
|
screenScale(TRIGGER_L_BASE_X, height),
|
||||||
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightTrigger(
|
||||||
|
1, "RT", -1, controller, context),
|
||||||
|
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||||
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_LB,
|
||||||
|
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||||
|
screenScale(TRIGGER_L_BASE_X + TRIGGER_DISTANCE, height),
|
||||||
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_RB,
|
||||||
|
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||||
|
screenScale(TRIGGER_R_BASE_X, height) + rightDisplacement,
|
||||||
|
screenScale(TRIGGER_BASE_Y, height),
|
||||||
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createLeftStick(controller, context),
|
||||||
|
screenScale(ANALOG_L_BASE_X, height),
|
||||||
|
screenScale(ANALOG_L_BASE_Y, height),
|
||||||
|
screenScale(ANALOG_SIZE, height),
|
||||||
|
screenScale(ANALOG_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createRightStick(controller, context),
|
||||||
|
screenScale(ANALOG_R_BASE_X, height) + rightDisplacement,
|
||||||
|
screenScale(ANALOG_R_BASE_Y, height),
|
||||||
|
screenScale(ANALOG_SIZE, height),
|
||||||
|
screenScale(ANALOG_SIZE, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_BACK,
|
||||||
|
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||||
|
screenScale(BACK_X, height),
|
||||||
|
screenScale(START_BACK_Y, height),
|
||||||
|
screenScale(START_BACK_WIDTH, height),
|
||||||
|
screenScale(START_BACK_HEIGHT, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_START,
|
||||||
|
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||||
|
screenScale(START_X, height) + rightDisplacement,
|
||||||
|
screenScale(START_BACK_Y, height),
|
||||||
|
screenScale(START_BACK_WIDTH, height),
|
||||||
|
screenScale(START_BACK_HEIGHT, height)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_LSB,
|
||||||
|
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
||||||
|
screenScale(TRIGGER_L_BASE_X, height),
|
||||||
|
screenScale(L3_R3_BASE_Y, height),
|
||||||
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
|
);
|
||||||
|
|
||||||
|
controller.addElement(createDigitalButton(
|
||||||
|
VirtualControllerElement.EID_RSB,
|
||||||
|
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
||||||
|
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
|
||||||
|
screenScale(L3_R3_BASE_Y, height),
|
||||||
|
screenScale(TRIGGER_WIDTH, height),
|
||||||
|
screenScale(TRIGGER_HEIGHT, height)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.setOpacity(config.oscOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+339
@@ -0,0 +1,339 @@
|
|||||||
|
/**
|
||||||
|
* 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 configMoveColor = 0xF0FF0000;
|
||||||
|
private int configResizeColor = 0xF0FF00FF;
|
||||||
|
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() {
|
||||||
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||||
|
return configMoveColor;
|
||||||
|
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||||
|
return configResizeColor;
|
||||||
|
else
|
||||||
|
return 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();
|
||||||
|
|
||||||
|
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
|
||||||
|
actionEnableMove();
|
||||||
|
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
|
||||||
|
actionEnableResize();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void setOpacity(int opacity) {
|
||||||
|
int hexOpacity = opacity * 255 / 100;
|
||||||
|
this.normalColor = (hexOpacity << 24) | (normalColor & 0x00FFFFFF);
|
||||||
|
this.pressedColor = (hexOpacity << 24) | (pressedColor & 0x00FFFFFF);
|
||||||
|
|
||||||
|
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,263 +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 java.util.concurrent.locks.LockSupport;
|
|
||||||
|
|
||||||
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 implements VideoDecoderRenderer {
|
|
||||||
|
|
||||||
private Thread rendererThread;
|
|
||||||
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 = 8;
|
|
||||||
|
|
||||||
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 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.DISABLE_LOOP_FILTER |
|
|
||||||
AvcDecoder.FAST_BILINEAR_FILTERING |
|
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// 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 |
|
|
||||||
AvcDecoder.FAST_DECODE;
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvcDecoder.setRenderTarget(sh.getSurface());
|
|
||||||
|
|
||||||
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) {
|
|
||||||
rendererThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
|
||||||
DecodeUnit du;
|
|
||||||
while (!isInterrupted())
|
|
||||||
{
|
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
|
||||||
if (du != null) {
|
|
||||||
submitDecodeUnit(du);
|
|
||||||
depacketizer.freeDecodeUnit(du);
|
|
||||||
}
|
|
||||||
|
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
|
||||||
|
|
||||||
if (diff > WAIT_CEILING_MS) {
|
|
||||||
LockSupport.parkNanos(1);
|
|
||||||
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 System.currentTimeMillis() + (1000 / frameRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
rendererThread.interrupt();
|
|
||||||
|
|
||||||
try {
|
|
||||||
rendererThread.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.getBufferList()) {
|
|
||||||
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.getBufferList()) {
|
|
||||||
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 = System.currentTimeMillis();
|
|
||||||
|
|
||||||
// Add delta time to the totals (excluding probable outliers)
|
|
||||||
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
|
||||||
if (delta >= 0 && delta < 300) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
|
||||||
|
|
||||||
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
|
||||||
|
|
||||||
private VideoDecoderRenderer 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 int getAverageDecoderLatency() {
|
|
||||||
if (decoderRenderer != null) {
|
|
||||||
return decoderRenderer.getAverageDecoderLatency();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getAverageEndToEndLatency() {
|
|
||||||
if (decoderRenderer != null) {
|
|
||||||
return decoderRenderer.getAverageEndToEndLatency();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
public interface CrashListener {
|
||||||
|
void notifyCrash(Exception e);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
public interface PerfOverlayListener {
|
||||||
|
void onPerfUpdate(final String text);
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
class VideoStats {
|
||||||
|
|
||||||
|
long decoderTimeMs;
|
||||||
|
long totalTimeMs;
|
||||||
|
int totalFrames;
|
||||||
|
int totalFramesReceived;
|
||||||
|
int totalFramesRendered;
|
||||||
|
int frameLossEvents;
|
||||||
|
int framesLost;
|
||||||
|
long measurementStartTimestamp;
|
||||||
|
|
||||||
|
void add(VideoStats other) {
|
||||||
|
this.decoderTimeMs += other.decoderTimeMs;
|
||||||
|
this.totalTimeMs += other.totalTimeMs;
|
||||||
|
this.totalFrames += other.totalFrames;
|
||||||
|
this.totalFramesReceived += other.totalFramesReceived;
|
||||||
|
this.totalFramesRendered += other.totalFramesRendered;
|
||||||
|
this.frameLossEvents += other.frameLossEvents;
|
||||||
|
this.framesLost += other.framesLost;
|
||||||
|
|
||||||
|
if (this.measurementStartTimestamp == 0) {
|
||||||
|
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void copy(VideoStats other) {
|
||||||
|
this.decoderTimeMs = other.decoderTimeMs;
|
||||||
|
this.totalTimeMs = other.totalTimeMs;
|
||||||
|
this.totalFrames = other.totalFrames;
|
||||||
|
this.totalFramesReceived = other.totalFramesReceived;
|
||||||
|
this.totalFramesRendered = other.totalFramesRendered;
|
||||||
|
this.frameLossEvents = other.frameLossEvents;
|
||||||
|
this.framesLost = other.framesLost;
|
||||||
|
this.measurementStartTimestamp = other.measurementStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
this.decoderTimeMs = 0;
|
||||||
|
this.totalTimeMs = 0;
|
||||||
|
this.totalFrames = 0;
|
||||||
|
this.totalFramesReceived = 0;
|
||||||
|
this.totalFramesRendered = 0;
|
||||||
|
this.frameLossEvents = 0;
|
||||||
|
this.framesLost = 0;
|
||||||
|
this.measurementStartTimestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoStatsFps getFps() {
|
||||||
|
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
|
||||||
|
|
||||||
|
VideoStatsFps fps = new VideoStatsFps();
|
||||||
|
if (elapsed > 0) {
|
||||||
|
fps.totalFps = this.totalFrames / elapsed;
|
||||||
|
fps.receivedFps = this.totalFramesReceived / elapsed;
|
||||||
|
fps.renderedFps = this.totalFramesRendered / elapsed;
|
||||||
|
}
|
||||||
|
return fps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoStatsFps {
|
||||||
|
|
||||||
|
float totalFps;
|
||||||
|
float receivedFps;
|
||||||
|
float renderedFps;
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.net.UnknownHostException;
|
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.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@@ -17,148 +18,146 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
public class ComputerDatabaseManager {
|
public class ComputerDatabaseManager {
|
||||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
private static final String COMPUTER_DB_NAME = "computers3.db";
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
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 COMPUTER_UUID_COLUMN_NAME = "UUID";
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
|
||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
|
||||||
try {
|
|
||||||
// Create or open an existing DB
|
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
|
||||||
} catch (SQLiteException e) {
|
|
||||||
// Delete the DB and try again
|
|
||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
|
||||||
}
|
|
||||||
initializeDb();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
computerDb.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeDb() {
|
|
||||||
// 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)",
|
|
||||||
COMPUTER_TABLE_NAME,
|
|
||||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
|
||||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
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);
|
|
||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
computerList.add(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
c.close();
|
|
||||||
|
|
||||||
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();
|
|
||||||
if (!c.moveToFirst()) {
|
|
||||||
// No matching computer
|
|
||||||
c.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.name = c.getString(0);
|
private static final char ADDRESS_DELIMITER = ';';
|
||||||
|
|
||||||
String uuidStr = c.getString(1);
|
private SQLiteDatabase computerDb;
|
||||||
try {
|
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
public ComputerDatabaseManager(Context c) {
|
||||||
} catch (IllegalArgumentException e) {
|
try {
|
||||||
// We'll delete this entry
|
// Create or open an existing DB
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
}
|
} catch (SQLiteException e) {
|
||||||
|
// Delete the DB and try again
|
||||||
try {
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
} catch (UnknownHostException e) {
|
}
|
||||||
// We'll delete this entry
|
initializeDb(c);
|
||||||
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
}
|
||||||
}
|
|
||||||
|
public void close() {
|
||||||
try {
|
computerDb.close();
|
||||||
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
}
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// We'll delete this entry
|
private void initializeDb(Context c) {
|
||||||
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
// 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, %s TEXT)",
|
||||||
details.macAddress = c.getString(4);
|
COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
||||||
|
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
||||||
c.close();
|
|
||||||
|
// Move all computers from the old DB (if any) to the new one
|
||||||
// If a field is corrupt or missing, delete the database entry
|
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
||||||
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
for (ComputerDetails computer : oldComputers) {
|
||||||
details.macAddress == null) {
|
updateComputer(computer);
|
||||||
deleteComputer(details.name);
|
}
|
||||||
return null;
|
oldComputers = LegacyDatabaseReader2.migrateAllComputers(c);
|
||||||
}
|
for (ComputerDetails computer : oldComputers) {
|
||||||
|
updateComputer(computer);
|
||||||
return details;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void deleteComputer(ComputerDetails details) {
|
||||||
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
StringBuilder addresses = new StringBuilder();
|
||||||
|
addresses.append(details.localAddress != null ? details.localAddress : "");
|
||||||
|
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : "");
|
||||||
|
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : "");
|
||||||
|
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : "");
|
||||||
|
|
||||||
|
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
|
||||||
|
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 static String readNonEmptyString(String input) {
|
||||||
|
if (input.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComputerDetails getComputerFromCursor(Cursor c) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
|
||||||
|
details.uuid = c.getString(0);
|
||||||
|
details.name = c.getString(1);
|
||||||
|
|
||||||
|
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
|
||||||
|
|
||||||
|
details.localAddress = readNonEmptyString(addresses[0]);
|
||||||
|
details.remoteAddress = readNonEmptyString(addresses[1]);
|
||||||
|
details.manualAddress = readNonEmptyString(addresses[2]);
|
||||||
|
details.ipv6Address = readNonEmptyString(addresses[3]);
|
||||||
|
|
||||||
|
details.macAddress = c.getString(3);
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] derCertData = c.getBlob(4);
|
||||||
|
|
||||||
|
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<>();
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
computerList.add(getComputerFromCursor(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
return computerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComputerDetails details = getComputerFromCursor(c);
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
public interface ComputerManagerListener {
|
||||||
public void notifyComputerUpdated(ComputerDetails details);
|
void notifyComputerUpdated(ComputerDetails details);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
public class IdentityManager {
|
public class IdentityManager {
|
||||||
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
||||||
private static final int UID_SIZE_IN_BYTES = 8;
|
private static final int UID_SIZE_IN_BYTES = 8;
|
||||||
|
|
||||||
private String uniqueId;
|
private String uniqueId;
|
||||||
|
|
||||||
public IdentityManager(Context c) {
|
public IdentityManager(Context c) {
|
||||||
uniqueId = loadUniqueId(c);
|
uniqueId = loadUniqueId(c);
|
||||||
if (uniqueId == null) {
|
if (uniqueId == null) {
|
||||||
uniqueId = generateNewUniqueId(c);
|
uniqueId = generateNewUniqueId(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("UID is now: "+uniqueId);
|
LimeLog.info("UID is now: "+uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUniqueId() {
|
public String getUniqueId() {
|
||||||
return uniqueId;
|
return uniqueId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String loadUniqueId(Context c) {
|
private static String loadUniqueId(Context c) {
|
||||||
// 2 Hex digits per byte
|
// 2 Hex digits per byte
|
||||||
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||||
InputStreamReader reader = null;
|
InputStreamReader reader = null;
|
||||||
LimeLog.info("Reading UID from disk");
|
LimeLog.info("Reading UID from disk");
|
||||||
try {
|
try {
|
||||||
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||||
{
|
{
|
||||||
LimeLog.severe("UID file data is truncated");
|
LimeLog.severe("UID file data is truncated");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new String(uid);
|
return new String(uid);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
LimeLog.info("No UID file found");
|
LimeLog.info("No UID file found");
|
||||||
return null;
|
return null;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LimeLog.severe("Error while reading UID file");
|
LimeLog.severe("Error while reading UID file");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
try {
|
try {
|
||||||
reader.close();
|
reader.close();
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateNewUniqueId(Context c) {
|
private static String generateNewUniqueId(Context c) {
|
||||||
// Generate a new UID hex string
|
// Generate a new UID hex string
|
||||||
LimeLog.info("Generating new UID");
|
LimeLog.info("Generating new UID");
|
||||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||||
|
|
||||||
OutputStreamWriter writer = null;
|
OutputStreamWriter writer = null;
|
||||||
try {
|
try {
|
||||||
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||||
writer.write(uidStr);
|
writer.write(uidStr);
|
||||||
LimeLog.info("UID written to disk");
|
LimeLog.info("UID written to disk");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LimeLog.severe("Error while writing UID file");
|
LimeLog.severe("Error while writing UID file");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
if (writer != null) {
|
if (writer != null) {
|
||||||
try {
|
try {
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can return a UID even if I/O fails
|
// We can return a UID even if I/O fails
|
||||||
return uidStr;
|
return uidStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LegacyDatabaseReader2 {
|
||||||
|
private static final String COMPUTER_DB_NAME = "computers2.db";
|
||||||
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
|
|
||||||
|
private static 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);
|
||||||
|
|
||||||
|
// This column wasn't always present in the old schema
|
||||||
|
if (c.getColumnCount() >= 7) {
|
||||||
|
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 static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
|
||||||
|
Cursor c = computerDb.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,76 +15,76 @@ import android.os.Binder;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
public class DiscoveryService extends Service {
|
public class DiscoveryService extends Service {
|
||||||
|
|
||||||
private MdnsDiscoveryAgent discoveryAgent;
|
|
||||||
private MdnsDiscoveryListener boundListener;
|
|
||||||
private MulticastLock multicastLock;
|
|
||||||
|
|
||||||
public class DiscoveryBinder extends Binder {
|
|
||||||
public void setListener(MdnsDiscoveryListener listener) {
|
|
||||||
boundListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startDiscovery(int queryIntervalMs) {
|
|
||||||
multicastLock.acquire();
|
|
||||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopDiscovery() {
|
|
||||||
discoveryAgent.stopDiscovery();
|
|
||||||
multicastLock.release();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MdnsComputer> getComputerSet() {
|
|
||||||
return discoveryAgent.getComputerSet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
|
||||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
|
||||||
multicastLock.setReferenceCounted(false);
|
|
||||||
|
|
||||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyComputerAdded(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
private MdnsDiscoveryAgent discoveryAgent;
|
||||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
private MdnsDiscoveryListener boundListener;
|
||||||
if (boundListener != null) {
|
private MulticastLock multicastLock;
|
||||||
boundListener.notifyComputerRemoved(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public class DiscoveryBinder extends Binder {
|
||||||
public void notifyDiscoveryFailure(Exception e) {
|
public void setListener(MdnsDiscoveryListener listener) {
|
||||||
if (boundListener != null) {
|
boundListener = listener;
|
||||||
boundListener.notifyDiscoveryFailure(e);
|
}
|
||||||
}
|
|
||||||
}
|
public void startDiscovery(int queryIntervalMs) {
|
||||||
});
|
multicastLock.acquire();
|
||||||
}
|
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||||
|
}
|
||||||
private DiscoveryBinder binder = new DiscoveryBinder();
|
|
||||||
|
public void stopDiscovery() {
|
||||||
@Override
|
discoveryAgent.stopDiscovery();
|
||||||
public IBinder onBind(Intent intent) {
|
multicastLock.release();
|
||||||
return binder;
|
}
|
||||||
}
|
|
||||||
|
public List<MdnsComputer> getComputerSet() {
|
||||||
@Override
|
return discoveryAgent.getComputerSet();
|
||||||
public boolean onUnbind(Intent intent) {
|
}
|
||||||
// Stop any discovery session
|
}
|
||||||
discoveryAgent.stopDiscovery();
|
|
||||||
multicastLock.release();
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
// Unbind the listener
|
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||||
boundListener = null;
|
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||||
return false;
|
multicastLock.setReferenceCounted(false);
|
||||||
}
|
|
||||||
|
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerAdded(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerRemoved(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyDiscoveryFailure(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private final DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onUnbind(Intent intent) {
|
||||||
|
// Stop any discovery session
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
|
multicastLock.release();
|
||||||
|
|
||||||
|
// Unbind the listener
|
||||||
|
boundListener = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.AppView;
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.grid.assets.CachedAppAssetLoader;
|
||||||
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
|
import com.limelight.grid.assets.MemoryAssetLoader;
|
||||||
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
private static final int ART_WIDTH_PX = 300;
|
||||||
|
private static final int SMALL_WIDTH_DP = 100;
|
||||||
|
private static final int LARGE_WIDTH_DP = 150;
|
||||||
|
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final String uniqueId;
|
||||||
|
|
||||||
|
private CachedAppAssetLoader loader;
|
||||||
|
|
||||||
|
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId) {
|
||||||
|
super(context, getLayoutIdForPreferences(prefs));
|
||||||
|
|
||||||
|
this.computer = computer;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
|
updateLayoutWithPreferences(context, prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||||
|
if (prefs.listMode) {
|
||||||
|
return R.layout.simple_row;
|
||||||
|
}
|
||||||
|
else if (prefs.smallIconMode) {
|
||||||
|
return R.layout.app_grid_item_small;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return R.layout.app_grid_item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||||
|
int dpi = context.getResources().getDisplayMetrics().densityDpi;
|
||||||
|
int dp;
|
||||||
|
|
||||||
|
if (prefs.smallIconMode) {
|
||||||
|
dp = SMALL_WIDTH_DP;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dp = LARGE_WIDTH_DP;
|
||||||
|
}
|
||||||
|
|
||||||
|
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
||||||
|
if (scalingDivisor < 1.0) {
|
||||||
|
// We don't want to make them bigger before draw-time
|
||||||
|
scalingDivisor = 1.0;
|
||||||
|
}
|
||||||
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
|
|
||||||
|
if (loader != null) {
|
||||||
|
// Cancel operations on the old loader
|
||||||
|
cancelQueuedOperations();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
|
new MemoryAssetLoader(),
|
||||||
|
new DiskAssetLoader(context),
|
||||||
|
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
|
||||||
|
|
||||||
|
// This will trigger the view to reload with the new layout
|
||||||
|
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelQueuedOperations() {
|
||||||
|
loader.cancelForegroundLoads();
|
||||||
|
loader.cancelBackgroundLoads();
|
||||||
|
loader.freeCacheMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortList() {
|
||||||
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
|
@Override
|
||||||
|
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||||
|
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addApp(AppView.AppObject app) {
|
||||||
|
// Queue a request to fetch this bitmap into cache
|
||||||
|
loader.queueCacheLoad(app.app);
|
||||||
|
|
||||||
|
// Add the app to our sorted list
|
||||||
|
itemList.add(app);
|
||||||
|
sortList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeApp(AppView.AppObject app) {
|
||||||
|
itemList.remove(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
||||||
|
// Let the cached asset loader handle it
|
||||||
|
loader.populateImageView(obj.app, imgView, prgView);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
|
||||||
|
// Select the text view so it starts marquee mode
|
||||||
|
txtView.setSelected(true);
|
||||||
|
|
||||||
|
// Return false to use the app's toString method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||||
|
if (obj.isRunning) {
|
||||||
|
// Show the play button overlay
|
||||||
|
overlayView.setImageResource(R.drawable.ic_play);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.limelight.grid;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
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;
|
||||||
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
|
protected final Context context;
|
||||||
|
private int layoutId;
|
||||||
|
final ArrayList<T> itemList = new ArrayList<>();
|
||||||
|
private final LayoutInflater inflater;
|
||||||
|
|
||||||
|
GenericGridAdapter(Context context, int layoutId) {
|
||||||
|
this.context = context;
|
||||||
|
this.layoutId = layoutId;
|
||||||
|
|
||||||
|
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLayoutId(int layoutId) {
|
||||||
|
if (layoutId != this.layoutId) {
|
||||||
|
this.layoutId = layoutId;
|
||||||
|
|
||||||
|
// Force the view to be redrawn with the new layout
|
||||||
|
notifyDataSetInvalidated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
itemList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return itemList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getItem(int i) {
|
||||||
|
return itemList.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int i) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int i, View convertView, ViewGroup viewGroup) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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, prgView, itemList.get(i))) {
|
||||||
|
imgView.setImageBitmap(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!populateTextView(txtView, itemList.get(i))) {
|
||||||
|
txtView.setText(itemList.get(i).toString());
|
||||||
|
}
|
||||||
|
if (overlayView != null) {
|
||||||
|
if (!populateOverlayView(overlayView, itemList.get(i))) {
|
||||||
|
overlayView.setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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 com.limelight.preferences.PreferenceConfiguration;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
|
public PcGridAdapter(Context context, PreferenceConfiguration prefs) {
|
||||||
|
super(context, getLayoutIdForPreferences(prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
|
||||||
|
if (prefs.listMode) {
|
||||||
|
return R.layout.simple_row;
|
||||||
|
}
|
||||||
|
else if (prefs.smallIconMode) {
|
||||||
|
return R.layout.pc_grid_item_small;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return R.layout.pc_grid_item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
|
||||||
|
// This will trigger the view to reload with the new layout
|
||||||
|
setLayoutId(getLayoutIdForPreferences(prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
|
itemList.add(computer);
|
||||||
|
sortList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortList() {
|
||||||
|
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
||||||
|
@Override
|
||||||
|
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
||||||
|
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeComputer(PcView.ComputerObject computer) {
|
||||||
|
return itemList.remove(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.state == ComputerDetails.State.ONLINE) {
|
||||||
|
txtView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
txtView.setAlpha(0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false to use the computer's toString method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CachedAppAssetLoader {
|
||||||
|
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
||||||
|
|
||||||
|
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
||||||
|
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
||||||
|
private static final int MAX_PENDING_DISK_LOADS = 40;
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final double scalingDivider;
|
||||||
|
private final NetworkAssetLoader networkLoader;
|
||||||
|
private final MemoryAssetLoader memoryLoader;
|
||||||
|
private final DiskAssetLoader diskLoader;
|
||||||
|
private final Bitmap placeholderBitmap;
|
||||||
|
private final Bitmap noAppImageBitmap;
|
||||||
|
|
||||||
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
|
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.scalingDivider = scalingDivider;
|
||||||
|
this.networkLoader = networkLoader;
|
||||||
|
this.memoryLoader = memoryLoader;
|
||||||
|
this.diskLoader = diskLoader;
|
||||||
|
this.noAppImageBitmap = noAppImageBitmap;
|
||||||
|
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelBackgroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
||||||
|
cacheExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelForegroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
|
||||||
|
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
||||||
|
foregroundExecutor.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((r = networkExecutor.getQueue().poll()) != null) {
|
||||||
|
networkExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void freeCacheMemory() {
|
||||||
|
memoryLoader.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||||
|
// Try 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
// Check again whether we've been cancelled or the image view is gone
|
||||||
|
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||||
|
if (in != null) {
|
||||||
|
// Write the stream straight to disk
|
||||||
|
diskLoader.populateCacheWithStream(tuple, in);
|
||||||
|
|
||||||
|
// Close the network input stream
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
|
if (task != null) {
|
||||||
|
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp != null) {
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise it's a background load and we return nothing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second with a bit of fuzz
|
||||||
|
try {
|
||||||
|
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, ProgressBar prgView, boolean diskOnly) {
|
||||||
|
this.imageViewRef = new WeakReference<>(imageView);
|
||||||
|
this.progressViewRef = new WeakReference<>(prgView);
|
||||||
|
this.diskOnly = diskOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||||
|
tuple = params[0];
|
||||||
|
|
||||||
|
// Check whether it has been cancelled or the views are gone
|
||||||
|
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp == null) {
|
||||||
|
if (!diskOnly) {
|
||||||
|
// Try to load the asset from the network
|
||||||
|
bmp = doNetworkAssetLoad(tuple, this);
|
||||||
|
} else {
|
||||||
|
// Report progress to display the placeholder and spin
|
||||||
|
// off the network-capable task
|
||||||
|
publishProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the bitmap
|
||||||
|
if (bmp != null) {
|
||||||
|
memoryLoader.populateCache(tuple, bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Void... nothing) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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. This time our AsyncDrawable
|
||||||
|
// will use the app image placeholder bitmap, rather than an empty bitmap.
|
||||||
|
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||||
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
|
||||||
|
imageView.setVisibility(View.VISIBLE);
|
||||||
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class AsyncDrawable extends BitmapDrawable {
|
||||||
|
private final WeakReference<LoaderTask> loaderTaskReference;
|
||||||
|
|
||||||
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
|
LoaderTask loaderTask) {
|
||||||
|
super(res, bitmap);
|
||||||
|
loaderTaskReference = new WeakReference<>(loaderTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderTask getLoaderTask() {
|
||||||
|
return loaderTaskReference.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoaderTask getLoaderTask(ImageView imageView) {
|
||||||
|
if (imageView == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Drawable drawable = imageView.getDrawable();
|
||||||
|
|
||||||
|
// If our drawable is in play, get the loader task
|
||||||
|
if (drawable instanceof AsyncDrawable) {
|
||||||
|
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||||
|
return asyncDrawable.getLoaderTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
||||||
|
final LoaderTask loaderTask = getLoaderTask(imageView);
|
||||||
|
|
||||||
|
// Check if any task was pending for this image view
|
||||||
|
if (loaderTask != null && !loaderTask.isCancelled()) {
|
||||||
|
final LoaderTuple taskTuple = loaderTask.tuple;
|
||||||
|
|
||||||
|
// Cancel the task if it's not already loading the same data
|
||||||
|
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
||||||
|
loaderTask.cancel(true);
|
||||||
|
} else {
|
||||||
|
// It's already loading what we want
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the load to proceed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queueCacheLoad(NvApp app) {
|
||||||
|
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
||||||
|
// It's in memory which means it must also be on disk
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue a fetch in the cache executor
|
||||||
|
cacheExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Check if the image is cached on disk
|
||||||
|
if (diskLoader.checkCacheExists(tuple)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the asset from the network and cache result on disk
|
||||||
|
doNetworkAssetLoad(tuple, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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
|
||||||
|
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(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 {
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
public final NvApp app;
|
||||||
|
|
||||||
|
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof LoaderTuple)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoaderTuple other = (LoaderTuple) o;
|
||||||
|
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
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.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class DiskAssetLoader {
|
||||||
|
// 5 MB
|
||||||
|
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(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, tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inSampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
|
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getFile(String computerUuid, int appId) {
|
||||||
|
return CacheHelper.openPath(false, cacheDir, "boxart", computerUuid, appId + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAssetsForComputer(String computerUuid) {
|
||||||
|
File dir = CacheHelper.openPath(false, cacheDir, "boxart", computerUuid);
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files != null) {
|
||||||
|
for (File f : files) {
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
|
OutputStream out = null;
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||||
|
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
||||||
|
success = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (out != null) {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
||||||
|
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.LruCache;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class MemoryAssetLoader {
|
||||||
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
|
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
||||||
|
@Override
|
||||||
|
protected int sizeOf(String key, Bitmap bitmap) {
|
||||||
|
// Sizeof returns kilobytes
|
||||||
|
return bitmap.getByteCount() / 1024;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return tuple.computer.uuid+"-"+tuple.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||||
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCache() {
|
||||||
|
memoryCache.evictAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.utils.ServerHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class NetworkAssetLoader {
|
||||||
|
private final Context context;
|
||||||
|
private final String uniqueId;
|
||||||
|
|
||||||
|
public NetworkAssetLoader(Context context, String uniqueId) {
|
||||||
|
this.context = context;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
if (in != null) {
|
||||||
|
LimeLog.info("Network asset load complete: " + tuple);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Network asset load failed: " + tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
public class ConnectionContext {
|
||||||
|
public String serverAddress;
|
||||||
|
public X509Certificate serverCert;
|
||||||
|
public StreamConfiguration streamConfig;
|
||||||
|
public NvConnectionListener connListener;
|
||||||
|
public SecretKey riKey;
|
||||||
|
public int riKeyId;
|
||||||
|
|
||||||
|
// This is the version quad from the appversion tag of /serverinfo
|
||||||
|
public String serverAppVersion;
|
||||||
|
public String serverGfeVersion;
|
||||||
|
|
||||||
|
public int negotiatedWidth, negotiatedHeight;
|
||||||
|
public boolean negotiatedHdr;
|
||||||
|
|
||||||
|
public int videoCapabilities;
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
import com.limelight.nvstream.http.PairingManager;
|
||||||
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
|
public class NvConnection {
|
||||||
|
// Context parameters
|
||||||
|
private String host;
|
||||||
|
private LimelightCryptoProvider cryptoProvider;
|
||||||
|
private String uniqueId;
|
||||||
|
private ConnectionContext context;
|
||||||
|
private static Semaphore connectionAllowed = new Semaphore(1);
|
||||||
|
private final boolean isMonkey;
|
||||||
|
|
||||||
|
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
|
||||||
|
{
|
||||||
|
this.host = host;
|
||||||
|
this.cryptoProvider = cryptoProvider;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
|
this.context = new ConnectionContext();
|
||||||
|
this.context.streamConfig = config;
|
||||||
|
this.context.serverCert = serverCert;
|
||||||
|
try {
|
||||||
|
// This is unique per connection
|
||||||
|
this.context.riKey = generateRiAesKey();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.riKeyId = generateRiKeyId();
|
||||||
|
this.isMonkey = ActivityManager.isUserAMonkey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
|
||||||
|
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
|
||||||
|
|
||||||
|
// RI keys are 128 bits
|
||||||
|
keyGen.init(128);
|
||||||
|
|
||||||
|
return keyGen.generateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int generateRiKeyId() {
|
||||||
|
return new SecureRandom().nextInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
// Interrupt any pending connection. This is thread-safe.
|
||||||
|
MoonBridge.interruptConnection();
|
||||||
|
|
||||||
|
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||||
|
// we must not invoke that functionality in parallel.
|
||||||
|
synchronized (MoonBridge.class) {
|
||||||
|
MoonBridge.stopConnection();
|
||||||
|
MoonBridge.cleanupBridge();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now a pending connection can be processed
|
||||||
|
connectionAllowed.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean startApp() throws XmlPullParserException, IOException
|
||||||
|
{
|
||||||
|
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
|
||||||
|
|
||||||
|
String serverInfo = h.getServerInfo();
|
||||||
|
|
||||||
|
context.serverAppVersion = h.getServerVersion(serverInfo);
|
||||||
|
if (context.serverAppVersion == null) {
|
||||||
|
context.connListener.displayMessage("Server version malformed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// May be missing for older servers
|
||||||
|
context.serverGfeVersion = h.getGfeVersion(serverInfo);
|
||||||
|
|
||||||
|
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
|
||||||
|
context.connListener.displayMessage("Device not paired with computer");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.negotiatedHdr = context.streamConfig.getEnableHdr();
|
||||||
|
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
|
||||||
|
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
|
||||||
|
context.negotiatedHdr = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Decide on negotiated stream parameters now
|
||||||
|
//
|
||||||
|
|
||||||
|
// Check for a supported stream resolution
|
||||||
|
if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
|
||||||
|
// Client wants 4K but the server can't do it
|
||||||
|
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
|
||||||
|
|
||||||
|
// Lower resolution to 1080p
|
||||||
|
context.negotiatedWidth = 1920;
|
||||||
|
context.negotiatedHeight = 1080;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Take what the client wanted
|
||||||
|
context.negotiatedWidth = context.streamConfig.getWidth();
|
||||||
|
context.negotiatedHeight = context.streamConfig.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Video stream format will be decided during the RTSP handshake
|
||||||
|
//
|
||||||
|
|
||||||
|
NvApp app = context.streamConfig.getApp();
|
||||||
|
|
||||||
|
// If the client did not provide an exact app ID, do a lookup with the applist
|
||||||
|
if (!context.streamConfig.getApp().isInitialized()) {
|
||||||
|
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
|
||||||
|
app = h.getAppByName(context.streamConfig.getApp().getAppName());
|
||||||
|
if (app == null) {
|
||||||
|
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a game running, resume it
|
||||||
|
if (h.getCurrentGame(serverInfo) != 0) {
|
||||||
|
try {
|
||||||
|
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
|
||||||
|
if (!h.resumeApp(context)) {
|
||||||
|
context.connListener.displayMessage("Failed to resume existing session");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return quitAndLaunch(h, context);
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 470) {
|
||||||
|
// This is the error you get when you try to resume a session that's not yours.
|
||||||
|
// Because this is fairly common, we'll display a more detailed message.
|
||||||
|
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||||
|
" so it cannot be resumed. End streaming on the original " +
|
||||||
|
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (e.getErrorCode() == 525) {
|
||||||
|
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
|
||||||
|
"quit the session and start streaming again.");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Resumed existing game session");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return launchNotRunningApp(h, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
|
||||||
|
XmlPullParserException {
|
||||||
|
try {
|
||||||
|
if (!h.quitApp()) {
|
||||||
|
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 599) {
|
||||||
|
context.connListener.displayMessage("This session wasn't started by this device," +
|
||||||
|
" so it cannot be quit. End streaming on the original " +
|
||||||
|
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return launchNotRunningApp(h, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
|
||||||
|
throws IOException, XmlPullParserException {
|
||||||
|
// Launch the app since it's not running
|
||||||
|
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
|
||||||
|
context.connListener.displayMessage("Failed to launch application");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Launched new game session");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
|
||||||
|
{
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
context.connListener = connectionListener;
|
||||||
|
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
|
||||||
|
|
||||||
|
String appName = context.streamConfig.getApp().getAppName();
|
||||||
|
|
||||||
|
context.serverAddress = host;
|
||||||
|
context.connListener.stageStarting(appName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!startApp()) {
|
||||||
|
context.connListener.stageFailed(appName, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context.connListener.stageComplete(appName);
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
context.connListener.displayMessage(e.getMessage());
|
||||||
|
context.connListener.stageFailed(appName, e.getErrorCode());
|
||||||
|
} catch (XmlPullParserException | IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
context.connListener.displayMessage(e.getMessage());
|
||||||
|
context.connListener.stageFailed(appName, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer ib = ByteBuffer.allocate(16);
|
||||||
|
ib.putInt(context.riKeyId);
|
||||||
|
|
||||||
|
// Acquire the connection semaphore to ensure we only have one
|
||||||
|
// connection going at once.
|
||||||
|
try {
|
||||||
|
connectionAllowed.acquire();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
context.connListener.displayMessage(e.getMessage());
|
||||||
|
context.connListener.stageFailed(appName, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moonlight-core is not thread-safe with respect to connection start and stop, so
|
||||||
|
// we must not invoke that functionality in parallel.
|
||||||
|
synchronized (MoonBridge.class) {
|
||||||
|
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
|
||||||
|
int ret = MoonBridge.startConnection(context.serverAddress,
|
||||||
|
context.serverAppVersion, context.serverGfeVersion,
|
||||||
|
context.negotiatedWidth, context.negotiatedHeight,
|
||||||
|
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
|
||||||
|
context.streamConfig.getMaxPacketSize(),
|
||||||
|
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
|
||||||
|
context.streamConfig.getHevcSupported(),
|
||||||
|
context.negotiatedHdr,
|
||||||
|
context.streamConfig.getHevcBitratePercentageMultiplier(),
|
||||||
|
context.streamConfig.getClientRefreshRateX100(),
|
||||||
|
context.riKey.getEncoded(), ib.array(),
|
||||||
|
context.videoCapabilities);
|
||||||
|
if (ret != 0) {
|
||||||
|
// LiStartConnection() failed, so the caller is not expected
|
||||||
|
// to stop the connection themselves. We need to release their
|
||||||
|
// semaphore count for them.
|
||||||
|
connectionAllowed.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseMove(final short deltaX, final short deltaY)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseMove(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonDown(final byte mouseButton)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseButtonUp(final byte mouseButton)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInput(final short controllerNumber,
|
||||||
|
final short activeGamepadMask, final short buttonFlags,
|
||||||
|
final byte leftTrigger, final byte rightTrigger,
|
||||||
|
final short leftStickX, final short leftStickY,
|
||||||
|
final short rightStickX, final short rightStickY)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
|
||||||
|
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendControllerInput(final short buttonFlags,
|
||||||
|
final byte leftTrigger, final byte rightTrigger,
|
||||||
|
final short leftStickX, final short leftStickY,
|
||||||
|
final short rightStickX, final short rightStickY)
|
||||||
|
{
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
|
||||||
|
leftStickY, rightStickX, rightStickY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMouseScroll(final byte scrollClicks) {
|
||||||
|
if (!isMonkey) {
|
||||||
|
MoonBridge.sendMouseScroll(scrollClicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
|
||||||
|
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
public interface NvConnectionListener {
|
||||||
|
void stageStarting(String stage);
|
||||||
|
void stageComplete(String stage);
|
||||||
|
void stageFailed(String stage, int errorCode);
|
||||||
|
|
||||||
|
void connectionStarted();
|
||||||
|
void connectionTerminated(int errorCode);
|
||||||
|
void connectionStatusUpdate(int connectionStatus);
|
||||||
|
|
||||||
|
void displayMessage(String message);
|
||||||
|
void displayTransientMessage(String message);
|
||||||
|
|
||||||
|
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package com.limelight.nvstream;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
|
public class StreamConfiguration {
|
||||||
|
public static final int INVALID_APP_ID = 0;
|
||||||
|
|
||||||
|
public static final int STREAM_CFG_LOCAL = 0;
|
||||||
|
public static final int STREAM_CFG_REMOTE = 1;
|
||||||
|
public static final int STREAM_CFG_AUTO = 2;
|
||||||
|
|
||||||
|
private NvApp app;
|
||||||
|
private int width, height;
|
||||||
|
private int refreshRate;
|
||||||
|
private int launchRefreshRate;
|
||||||
|
private int clientRefreshRateX100;
|
||||||
|
private int bitrate;
|
||||||
|
private boolean sops;
|
||||||
|
private boolean enableAdaptiveResolution;
|
||||||
|
private boolean playLocalAudio;
|
||||||
|
private int maxPacketSize;
|
||||||
|
private int remote;
|
||||||
|
private MoonBridge.AudioConfiguration audioConfiguration;
|
||||||
|
private boolean supportsHevc;
|
||||||
|
private int hevcBitratePercentageMultiplier;
|
||||||
|
private boolean enableHdr;
|
||||||
|
private int attachedGamepadMask;
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private StreamConfiguration config = new StreamConfiguration();
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setApp(NvApp app) {
|
||||||
|
config.app = app;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
|
||||||
|
config.remote = remote;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setResolution(int width, int height) {
|
||||||
|
config.width = width;
|
||||||
|
config.height = height;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
|
||||||
|
config.refreshRate = refreshRate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
|
||||||
|
config.launchRefreshRate = refreshRate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setBitrate(int bitrate) {
|
||||||
|
config.bitrate = bitrate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setEnableSops(boolean enable) {
|
||||||
|
config.sops = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
|
||||||
|
config.enableAdaptiveResolution = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
|
||||||
|
config.playLocalAudio = enable;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
|
||||||
|
config.maxPacketSize = maxPacketSize;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
|
||||||
|
config.hevcBitratePercentageMultiplier = multiplier;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
|
||||||
|
config.enableHdr = enableHdr;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
|
||||||
|
config.attachedGamepadMask = attachedGamepadMask;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
|
||||||
|
config.attachedGamepadMask = 0;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (gamepadCount > i) {
|
||||||
|
config.attachedGamepadMask |= 1 << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
|
||||||
|
config.clientRefreshRateX100 = refreshRateX100;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
|
||||||
|
config.audioConfiguration = audioConfig;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
|
||||||
|
config.supportsHevc = supportsHevc;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamConfiguration build() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StreamConfiguration() {
|
||||||
|
// Set default attributes
|
||||||
|
this.app = new NvApp("Steam");
|
||||||
|
this.width = 1280;
|
||||||
|
this.height = 720;
|
||||||
|
this.refreshRate = 60;
|
||||||
|
this.launchRefreshRate = 60;
|
||||||
|
this.bitrate = 10000;
|
||||||
|
this.maxPacketSize = 1024;
|
||||||
|
this.remote = STREAM_CFG_AUTO;
|
||||||
|
this.sops = true;
|
||||||
|
this.enableAdaptiveResolution = false;
|
||||||
|
this.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
|
||||||
|
this.supportsHevc = false;
|
||||||
|
this.enableHdr = false;
|
||||||
|
this.attachedGamepadMask = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRefreshRate() {
|
||||||
|
return refreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLaunchRefreshRate() {
|
||||||
|
return launchRefreshRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBitrate() {
|
||||||
|
return bitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxPacketSize() {
|
||||||
|
return maxPacketSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvApp getApp() {
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getSops() {
|
||||||
|
return sops;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getAdaptiveResolutionEnabled() {
|
||||||
|
return enableAdaptiveResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getPlayLocalAudio() {
|
||||||
|
return playLocalAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemote() {
|
||||||
|
return remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MoonBridge.AudioConfiguration getAudioConfiguration() {
|
||||||
|
return audioConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getHevcSupported() {
|
||||||
|
return supportsHevc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHevcBitratePercentageMultiplier() {
|
||||||
|
return hevcBitratePercentageMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getEnableHdr() {
|
||||||
|
return enableHdr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAttachedGamepadMask() {
|
||||||
|
return attachedGamepadMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getClientRefreshRateX100() {
|
||||||
|
return clientRefreshRateX100;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.limelight.nvstream.av;
|
||||||
|
|
||||||
|
public class ByteBufferDescriptor {
|
||||||
|
public byte[] data;
|
||||||
|
public int offset;
|
||||||
|
public int length;
|
||||||
|
|
||||||
|
public ByteBufferDescriptor nextDescriptor;
|
||||||
|
|
||||||
|
public ByteBufferDescriptor(byte[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBufferDescriptor(ByteBufferDescriptor desc)
|
||||||
|
{
|
||||||
|
this.data = desc.data;
|
||||||
|
this.offset = desc.offset;
|
||||||
|
this.length = desc.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reinitialize(byte[] data, int offset, int length)
|
||||||
|
{
|
||||||
|
this.data = data;
|
||||||
|
this.offset = offset;
|
||||||
|
this.length = length;
|
||||||
|
this.nextDescriptor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print()
|
||||||
|
{
|
||||||
|
print(offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print(int length)
|
||||||
|
{
|
||||||
|
print(this.offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void print(int offset, int length)
|
||||||
|
{
|
||||||
|
for (int i = offset; i < offset+length;) {
|
||||||
|
if (i + 8 <= offset+length) {
|
||||||
|
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
|
||||||
|
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
|
||||||
|
i += 8;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
System.out.printf("%x: %02x \n", i, data[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.limelight.nvstream.av.audio;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.jni.MoonBridge;
|
||||||
|
|
||||||
|
public interface AudioRenderer {
|
||||||
|
int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
void playDecodedAudio(short[] audioData);
|
||||||
|
|
||||||
|
void cleanup();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.limelight.nvstream.av.video;
|
||||||
|
|
||||||
|
public abstract class VideoDecoderRenderer {
|
||||||
|
public abstract int setup(int format, int width, int height, int redrawRate);
|
||||||
|
|
||||||
|
public abstract void start();
|
||||||
|
|
||||||
|
public abstract void stop();
|
||||||
|
|
||||||
|
// This is called once for each frame-start NALU. This means it will be called several times
|
||||||
|
// for an IDR frame which contains several parameter sets and the I-frame data.
|
||||||
|
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
|
||||||
|
int frameNumber, long receiveTimeMs);
|
||||||
|
|
||||||
|
public abstract void cleanup();
|
||||||
|
|
||||||
|
public abstract int getCapabilities();
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
|
||||||
|
public class ComputerDetails {
|
||||||
|
public enum State {
|
||||||
|
ONLINE, OFFLINE, UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persistent attributes
|
||||||
|
public String uuid;
|
||||||
|
public String name;
|
||||||
|
public String localAddress;
|
||||||
|
public String remoteAddress;
|
||||||
|
public String manualAddress;
|
||||||
|
public String ipv6Address;
|
||||||
|
public String macAddress;
|
||||||
|
public X509Certificate serverCert;
|
||||||
|
|
||||||
|
// Transient attributes
|
||||||
|
public State state;
|
||||||
|
public String activeAddress;
|
||||||
|
public PairingManager.PairState pairState;
|
||||||
|
public int runningGameId;
|
||||||
|
public String rawAppList;
|
||||||
|
|
||||||
|
public ComputerDetails() {
|
||||||
|
// Use defaults
|
||||||
|
state = State.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerDetails(ComputerDetails details) {
|
||||||
|
// Copy details from the other computer
|
||||||
|
update(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(ComputerDetails details) {
|
||||||
|
this.state = details.state;
|
||||||
|
this.name = details.name;
|
||||||
|
this.uuid = details.uuid;
|
||||||
|
if (details.activeAddress != null) {
|
||||||
|
this.activeAddress = details.activeAddress;
|
||||||
|
}
|
||||||
|
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
|
||||||
|
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
|
||||||
|
this.localAddress = details.localAddress;
|
||||||
|
}
|
||||||
|
if (details.remoteAddress != null) {
|
||||||
|
this.remoteAddress = details.remoteAddress;
|
||||||
|
}
|
||||||
|
if (details.manualAddress != null) {
|
||||||
|
this.manualAddress = details.manualAddress;
|
||||||
|
}
|
||||||
|
if (details.ipv6Address != null) {
|
||||||
|
this.ipv6Address = details.ipv6Address;
|
||||||
|
}
|
||||||
|
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
|
||||||
|
this.macAddress = details.macAddress;
|
||||||
|
}
|
||||||
|
if (details.serverCert != null) {
|
||||||
|
this.serverCert = details.serverCert;
|
||||||
|
}
|
||||||
|
this.pairState = details.pairState;
|
||||||
|
this.runningGameId = details.runningGameId;
|
||||||
|
this.rawAppList = details.rawAppList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
str.append("State: ").append(state).append("\n");
|
||||||
|
str.append("Active Address: ").append(activeAddress).append("\n");
|
||||||
|
str.append("Name: ").append(name).append("\n");
|
||||||
|
str.append("UUID: ").append(uuid).append("\n");
|
||||||
|
str.append("Local Address: ").append(localAddress).append("\n");
|
||||||
|
str.append("Remote Address: ").append(remoteAddress).append("\n");
|
||||||
|
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
|
||||||
|
str.append("Manual Address: ").append(manualAddress).append("\n");
|
||||||
|
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||||
|
str.append("Pair State: ").append(pairState).append("\n");
|
||||||
|
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class GfeHttpResponseException extends IOException {
|
||||||
|
private static final long serialVersionUID = 1543508830807804222L;
|
||||||
|
|
||||||
|
private int errorCode;
|
||||||
|
private String errorMsg;
|
||||||
|
|
||||||
|
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.errorMsg = errorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrorCode() {
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
|
||||||
|
public interface LimelightCryptoProvider {
|
||||||
|
X509Certificate getClientCertificate();
|
||||||
|
RSAPrivateKey getClientPrivateKey();
|
||||||
|
byte[] getPemEncodedClientCertificate();
|
||||||
|
String encodeBase64String(byte[] data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class NvApp {
|
||||||
|
private String appName = "";
|
||||||
|
private int appId;
|
||||||
|
private boolean initialized;
|
||||||
|
private boolean hdrSupported;
|
||||||
|
|
||||||
|
public NvApp() {}
|
||||||
|
|
||||||
|
public NvApp(String appName) {
|
||||||
|
this.appName = appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvApp(String appName, int appId, boolean hdrSupported) {
|
||||||
|
this.appName = appName;
|
||||||
|
this.appId = appId;
|
||||||
|
this.hdrSupported = hdrSupported;
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppName(String appName) {
|
||||||
|
this.appName = appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppId(String appId) {
|
||||||
|
try {
|
||||||
|
this.appId = Integer.parseInt(appId);
|
||||||
|
this.initialized = true;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LimeLog.warning("Malformed app ID: "+appId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppId(int appId) {
|
||||||
|
this.appId = appId;
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHdrSupported(boolean hdrSupported) {
|
||||||
|
this.hdrSupported = hdrSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppName() {
|
||||||
|
return this.appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAppId() {
|
||||||
|
return this.appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHdrSupported() {
|
||||||
|
return this.hdrSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return this.initialized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,702 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
import java.util.Stack;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509KeyManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParser;
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory;
|
||||||
|
|
||||||
|
import com.limelight.BuildConfig;
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.ConnectionContext;
|
||||||
|
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||||
|
|
||||||
|
import okhttp3.ConnectionPool;
|
||||||
|
import okhttp3.Handshake;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
|
||||||
|
public class NvHTTP {
|
||||||
|
private String uniqueId;
|
||||||
|
private PairingManager pm;
|
||||||
|
|
||||||
|
public static final int HTTPS_PORT = 47984;
|
||||||
|
public static final int HTTP_PORT = 47989;
|
||||||
|
public static final int CONNECTION_TIMEOUT = 3000;
|
||||||
|
public static final int READ_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
// Print URL and content to logcat on debug builds
|
||||||
|
private static boolean verbose = BuildConfig.DEBUG;
|
||||||
|
|
||||||
|
public String baseUrlHttps;
|
||||||
|
public String baseUrlHttp;
|
||||||
|
|
||||||
|
private OkHttpClient httpClient;
|
||||||
|
private OkHttpClient httpClientWithReadTimeout;
|
||||||
|
|
||||||
|
private X509TrustManager trustManager;
|
||||||
|
private X509KeyManager keyManager;
|
||||||
|
private X509Certificate serverCert;
|
||||||
|
|
||||||
|
void setServerCert(X509Certificate serverCert) {
|
||||||
|
this.serverCert = serverCert;
|
||||||
|
|
||||||
|
trustManager = new X509TrustManager() {
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return new X509Certificate[0];
|
||||||
|
}
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||||
|
throw new IllegalStateException("Should never be called");
|
||||||
|
}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||||
|
// Check the server certificate if we've paired to this host
|
||||||
|
if (!certs[0].equals(NvHTTP.this.serverCert)) {
|
||||||
|
throw new CertificateException("Certificate mismatch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeHttpState(final X509Certificate serverCert, final LimelightCryptoProvider cryptoProvider) {
|
||||||
|
// Set up TrustManager
|
||||||
|
setServerCert(serverCert);
|
||||||
|
|
||||||
|
keyManager = new X509KeyManager() {
|
||||||
|
public String chooseClientAlias(String[] keyTypes,
|
||||||
|
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
|
||||||
|
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||||
|
Socket socket) { return null; }
|
||||||
|
public X509Certificate[] getCertificateChain(String alias) {
|
||||||
|
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||||
|
}
|
||||||
|
public String[] getClientAliases(String keyType, Principal[] issuers) { return null; }
|
||||||
|
public PrivateKey getPrivateKey(String alias) {
|
||||||
|
return cryptoProvider.getClientPrivateKey();
|
||||||
|
}
|
||||||
|
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore differences between given hostname and certificate hostname
|
||||||
|
HostnameVerifier hv = new HostnameVerifier() {
|
||||||
|
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
httpClient = new OkHttpClient.Builder()
|
||||||
|
.connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
|
||||||
|
.hostnameVerifier(hv)
|
||||||
|
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||||
|
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClientWithReadTimeout = httpClient.newBuilder()
|
||||||
|
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException {
|
||||||
|
// Use the same UID for all Moonlight clients so we can quit games
|
||||||
|
// started by other Moonlight clients.
|
||||||
|
this.uniqueId = "0123456789ABCDEF";
|
||||||
|
|
||||||
|
initializeHttpState(serverCert, cryptoProvider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The URI constructor takes care of escaping IPv6 literals
|
||||||
|
this.baseUrlHttps = new URI("https", null, address, HTTPS_PORT, null, null, null).toString();
|
||||||
|
this.baseUrlHttp = new URI("http", null, address, HTTP_PORT, null, null, null).toString();
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
// Encapsulate URISyntaxException into IOException for callers to handle more easily
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pm = new PairingManager(this, cryptoProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
String buildUniqueIdUuidString() {
|
||||||
|
return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException {
|
||||||
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
XmlPullParser xpp = factory.newPullParser();
|
||||||
|
|
||||||
|
xpp.setInput(r);
|
||||||
|
int eventType = xpp.getEventType();
|
||||||
|
Stack<String> currentTag = new Stack<String>();
|
||||||
|
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
switch (eventType) {
|
||||||
|
case (XmlPullParser.START_TAG):
|
||||||
|
if (xpp.getName().equals("root")) {
|
||||||
|
verifyResponseStatus(xpp);
|
||||||
|
}
|
||||||
|
currentTag.push(xpp.getName());
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.END_TAG):
|
||||||
|
currentTag.pop();
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.TEXT):
|
||||||
|
if (currentTag.peek().equals(tagname)) {
|
||||||
|
return xpp.getText().trim();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eventType = xpp.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException {
|
||||||
|
return getXmlString(new StringReader(str), tagname);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
|
||||||
|
String statusCodeText = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code");
|
||||||
|
if (statusCodeText == null) {
|
||||||
|
throw new GfeHttpResponseException(418, "Status code is missing");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int statusCode = Integer.parseInt(statusCodeText);
|
||||||
|
if (statusCode != 200) {
|
||||||
|
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e) {
|
||||||
|
// It seems like GFE 3.20.3.63 is returning garbage for status_code in rare cases.
|
||||||
|
// Surface this in a more friendly way rather than crashing.
|
||||||
|
throw new GfeHttpResponseException(418, "Status code is not a number: "+statusCodeText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerInfo() throws IOException, XmlPullParserException {
|
||||||
|
String resp;
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP.
|
||||||
|
// For some reason, we always see PairStatus is 0 over HTTP and only 1 over HTTPS. It looks
|
||||||
|
// like there are extra request headers required to make this stuff work over HTTP.
|
||||||
|
//
|
||||||
|
|
||||||
|
// When we have a pinned cert, use HTTPS to fetch serverinfo and fall back on cert mismatch
|
||||||
|
if (serverCert != null) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
|
||||||
|
} catch (SSLHandshakeException e) {
|
||||||
|
// Detect if we failed due to a server cert mismatch
|
||||||
|
if (e.getCause() instanceof CertificateException) {
|
||||||
|
// Jump to the GfeHttpResponseException exception handler to retry
|
||||||
|
// over HTTP which will allow us to pair again to update the cert
|
||||||
|
throw new GfeHttpResponseException(401, "Server certificate mismatch");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will throw an exception if the request came back with a failure status.
|
||||||
|
// We want this because it will throw us into the HTTP case if the client is unpaired.
|
||||||
|
getServerVersion(resp);
|
||||||
|
}
|
||||||
|
catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 401) {
|
||||||
|
// Cert validation error - fall back to HTTP
|
||||||
|
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a cert validation error, throw it
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No pinned cert, so use HTTP
|
||||||
|
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
String serverInfo = getServerInfo();
|
||||||
|
|
||||||
|
details.name = getXmlString(serverInfo, "hostname");
|
||||||
|
if (details.name == null || details.name.isEmpty()) {
|
||||||
|
details.name = "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
details.uuid = getXmlString(serverInfo, "uniqueid");
|
||||||
|
details.macAddress = getXmlString(serverInfo, "mac");
|
||||||
|
details.localAddress = getXmlString(serverInfo, "LocalIP");
|
||||||
|
|
||||||
|
// This may be null, but that's okay
|
||||||
|
details.remoteAddress = getXmlString(serverInfo, "ExternalIP");
|
||||||
|
|
||||||
|
// This has some extra logic to always report unpaired if the pinned cert isn't there
|
||||||
|
details.pairState = getPairState(serverInfo);
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.runningGameId = getCurrentGame(serverInfo);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
details.runningGameId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could reach it so it's online
|
||||||
|
details.state = ComputerDetails.State.ONLINE;
|
||||||
|
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This hack is Android-specific but we do it on all platforms
|
||||||
|
// because it doesn't really matter
|
||||||
|
private OkHttpClient performAndroidTlsHack(OkHttpClient client) {
|
||||||
|
// Doing this each time we create a socket is required
|
||||||
|
// to avoid the SSLv3 fallback that causes connection failures
|
||||||
|
try {
|
||||||
|
SSLContext sc = SSLContext.getInstance("TLS");
|
||||||
|
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
|
||||||
|
return client.newBuilder().sslSocketFactory(sc.getSocketFactory(), trustManager).build();
|
||||||
|
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getCertificateIfTrusted() {
|
||||||
|
try {
|
||||||
|
Response resp = httpClient.newCall(new Request.Builder().url(baseUrlHttps).get().build()).execute();
|
||||||
|
Handshake handshake = resp.handshake();
|
||||||
|
if (handshake != null) {
|
||||||
|
return (X509Certificate)handshake.peerCertificates().get(0);
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read timeout should be enabled for any HTTP query that requires no outside action
|
||||||
|
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
|
||||||
|
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
|
||||||
|
// queries do not.
|
||||||
|
private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException {
|
||||||
|
Request request = new Request.Builder().url(url).get().build();
|
||||||
|
Response response;
|
||||||
|
|
||||||
|
if (serverCert == null && !url.startsWith(baseUrlHttp)) {
|
||||||
|
throw new IllegalStateException("Attempted HTTPS fetch without pinned cert");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableReadTimeout) {
|
||||||
|
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response = performAndroidTlsHack(httpClient).newCall(request).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseBody body = response.body();
|
||||||
|
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsuccessful, so close the response body
|
||||||
|
if (body != null) {
|
||||||
|
body.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.code() == 404) {
|
||||||
|
throw new FileNotFoundException(url);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new GfeHttpResponseException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String openHttpConnectionToString(String url, boolean enableReadTimeout) throws IOException {
|
||||||
|
try {
|
||||||
|
if (verbose) {
|
||||||
|
LimeLog.info("Requesting URL: "+url);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseBody resp = openHttpConnection(url, enableReadTimeout);
|
||||||
|
String respString = resp.string();
|
||||||
|
resp.close();
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
LimeLog.info(url+" -> "+respString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return respString;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (verbose) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
return getXmlString(serverInfo, "appversion");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
|
||||||
|
return getPairState(getServerInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
|
||||||
|
// If we don't have a server cert, we can't be paired even if the host thinks we are
|
||||||
|
if (serverCert == null) {
|
||||||
|
return PairState.NOT_PAIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
|
||||||
|
return PairState.NOT_PAIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PairState.PAIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
|
||||||
|
if (str != null) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(str);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||||
|
if (str != null) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(str);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Possible meaning of bits
|
||||||
|
// Bit 0: H.264 Baseline
|
||||||
|
// Bit 1: H.264 High
|
||||||
|
// ----
|
||||||
|
// Bit 8: HEVC Main
|
||||||
|
// Bit 9: HEVC Main10
|
||||||
|
// Bit 10: HEVC Main10 4:4:4
|
||||||
|
// Bit 11: ???
|
||||||
|
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
String str = getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||||
|
if (str != null) {
|
||||||
|
try {
|
||||||
|
return Long.parseLong(str);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGpuType(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
return getXmlString(serverInfo, "gputype");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
return getXmlString(serverInfo, "GfeVersion");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
// Only allow 4K on GFE 3.x
|
||||||
|
String gfeVersionStr = getXmlString(serverInfo, "GfeVersion");
|
||||||
|
if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentGame(String serverInfo) throws IOException, XmlPullParserException {
|
||||||
|
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
||||||
|
// has the semantics that its name would indicate. To contain the effects of this change as much
|
||||||
|
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||||
|
String serverState = getXmlString(serverInfo, "state");
|
||||||
|
if (serverState != null && serverState.endsWith("_SERVER_BUSY")) {
|
||||||
|
String game = getXmlString(serverInfo, "currentgame");
|
||||||
|
return Integer.parseInt(game);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NvApp getAppById(int appId) throws IOException, XmlPullParserException {
|
||||||
|
LinkedList<NvApp> appList = getAppList();
|
||||||
|
for (NvApp appFromList : appList) {
|
||||||
|
if (appFromList.getAppId() == appId) {
|
||||||
|
return appFromList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NOTE: Only use this function if you know what you're doing.
|
||||||
|
* It's totally valid to have two apps named the same thing,
|
||||||
|
* or even nothing at all! Look apps up by ID if at all possible
|
||||||
|
* using the above function */
|
||||||
|
public NvApp getAppByName(String appName) throws IOException, XmlPullParserException {
|
||||||
|
LinkedList<NvApp> appList = getAppList();
|
||||||
|
for (NvApp appFromList : appList) {
|
||||||
|
if (appFromList.getAppName().equalsIgnoreCase(appName)) {
|
||||||
|
return appFromList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairingManager getPairingManager() {
|
||||||
|
return pm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
|
||||||
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
XmlPullParser xpp = factory.newPullParser();
|
||||||
|
|
||||||
|
xpp.setInput(r);
|
||||||
|
int eventType = xpp.getEventType();
|
||||||
|
LinkedList<NvApp> appList = new LinkedList<NvApp>();
|
||||||
|
Stack<String> currentTag = new Stack<String>();
|
||||||
|
boolean rootTerminated = false;
|
||||||
|
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
switch (eventType) {
|
||||||
|
case (XmlPullParser.START_TAG):
|
||||||
|
if (xpp.getName().equals("root")) {
|
||||||
|
verifyResponseStatus(xpp);
|
||||||
|
}
|
||||||
|
currentTag.push(xpp.getName());
|
||||||
|
if (xpp.getName().equals("App")) {
|
||||||
|
appList.addLast(new NvApp());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.END_TAG):
|
||||||
|
currentTag.pop();
|
||||||
|
if (xpp.getName().equals("root")) {
|
||||||
|
rootTerminated = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (XmlPullParser.TEXT):
|
||||||
|
NvApp app = appList.getLast();
|
||||||
|
if (currentTag.peek().equals("AppTitle")) {
|
||||||
|
app.setAppName(xpp.getText().trim());
|
||||||
|
} else if (currentTag.peek().equals("ID")) {
|
||||||
|
app.setAppId(xpp.getText().trim());
|
||||||
|
} else if (currentTag.peek().equals("IsHdrSupported")) {
|
||||||
|
app.setHdrSupported(xpp.getText().trim().equals("1"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
eventType = xpp.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw a malformed XML exception if we've not seen the root tag ended
|
||||||
|
if (!rootTerminated) {
|
||||||
|
throw new XmlPullParserException("Malformed XML: Root tag was not terminated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that all apps in the list are initialized
|
||||||
|
ListIterator<NvApp> i = appList.listIterator();
|
||||||
|
while (i.hasNext()) {
|
||||||
|
NvApp app = i.next();
|
||||||
|
|
||||||
|
// Remove uninitialized apps
|
||||||
|
if (!app.isInitialized()) {
|
||||||
|
LimeLog.warning("GFE returned incomplete app: "+app.getAppId()+" "+app.getAppName());
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppListRaw() throws MalformedURLException, IOException {
|
||||||
|
return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
|
||||||
|
if (verbose) {
|
||||||
|
// Use the raw function so the app list is printed
|
||||||
|
return getAppListByReader(new StringReader(getAppListRaw()));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true);
|
||||||
|
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
|
||||||
|
resp.close();
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unpair() throws IOException {
|
||||||
|
openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getBoxArt(NvApp app) throws IOException {
|
||||||
|
ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() +
|
||||||
|
"&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
||||||
|
return resp.byteStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
int[] appVersionQuad = getServerAppVersionQuad(serverInfo);
|
||||||
|
if (appVersionQuad != null) {
|
||||||
|
return appVersionQuad[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
|
||||||
|
try {
|
||||||
|
String serverVersion = getServerVersion(serverInfo);
|
||||||
|
if (serverVersion == null) {
|
||||||
|
LimeLog.warning("Missing server version field");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String[] serverVersionSplit = serverVersion.split("\\.");
|
||||||
|
if (serverVersionSplit.length != 4) {
|
||||||
|
LimeLog.warning("Malformed server version field");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int[] ret = new int[serverVersionSplit.length];
|
||||||
|
for (int i = 0; i < ret.length; i++) {
|
||||||
|
ret[i] = Integer.parseInt(serverVersionSplit[i]);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
|
for ( int j = 0; j < bytes.length; j++ ) {
|
||||||
|
int v = bytes[j] & 0xFF;
|
||||||
|
hexChars[j * 2] = hexArray[v >>> 4];
|
||||||
|
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||||
|
}
|
||||||
|
return new String(hexChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
|
||||||
|
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
|
||||||
|
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
|
||||||
|
// 360p or 480p, but it is not ideal for 1440p and other resolutions.
|
||||||
|
// When we detect an unsupported resolution, disable SOPS unless it's under 720p.
|
||||||
|
// FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list
|
||||||
|
boolean enableSops = context.streamConfig.getSops();
|
||||||
|
if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 &&
|
||||||
|
context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 &&
|
||||||
|
context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) {
|
||||||
|
LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight);
|
||||||
|
enableSops = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using SOPS with FPS values over 60 causes GFE to fall back
|
||||||
|
// to 720p60. On previous GFE versions, we could avoid this by
|
||||||
|
// forcing the FPS value to 60 when launching the stream, but
|
||||||
|
// now on GFE 3.20.3 that seems to trigger some sort of
|
||||||
|
// frame rate limiter that locks the game to 60 FPS.
|
||||||
|
if (context.streamConfig.getLaunchRefreshRate() > 60) {
|
||||||
|
LimeLog.info("Disabling SOPS due to high frame rate: "+context.streamConfig.getLaunchRefreshRate());
|
||||||
|
enableSops = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String xmlStr = openHttpConnectionToString(baseUrlHttps +
|
||||||
|
"/launch?" + buildUniqueIdUuidString() +
|
||||||
|
"&appid=" + appId +
|
||||||
|
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + context.streamConfig.getLaunchRefreshRate() +
|
||||||
|
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
|
||||||
|
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||||
|
"&rikeyid="+context.riKeyId +
|
||||||
|
(!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") +
|
||||||
|
"&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) +
|
||||||
|
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo() +
|
||||||
|
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
|
||||||
|
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
|
||||||
|
false);
|
||||||
|
String gameSession = getXmlString(xmlStr, "gamesession");
|
||||||
|
return gameSession != null && !gameSession.equals("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
|
||||||
|
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() +
|
||||||
|
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||||
|
"&rikeyid="+context.riKeyId +
|
||||||
|
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
|
||||||
|
false);
|
||||||
|
String resume = getXmlString(xmlStr, "resume");
|
||||||
|
return Integer.parseInt(resume) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean quitApp() throws IOException, XmlPullParserException {
|
||||||
|
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false);
|
||||||
|
String cancel = getXmlString(xmlStr, "cancel");
|
||||||
|
if (Integer.parseInt(cancel) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
|
// if we're not the original requestor.
|
||||||
|
if (getCurrentGame(getServerInfo()) != 0) {
|
||||||
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
|
// that they can't kill someone else's stream.
|
||||||
|
throw new GfeHttpResponseException(599, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
package com.limelight.nvstream.http;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.io.*;
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.cert.*;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class PairingManager {
|
||||||
|
|
||||||
|
private NvHTTP http;
|
||||||
|
|
||||||
|
private PrivateKey pk;
|
||||||
|
private X509Certificate cert;
|
||||||
|
private SecretKey aesKey;
|
||||||
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
|
private X509Certificate serverCert;
|
||||||
|
|
||||||
|
public enum PairState {
|
||||||
|
NOT_PAIRED,
|
||||||
|
PAIRED,
|
||||||
|
PIN_WRONG,
|
||||||
|
FAILED,
|
||||||
|
ALREADY_IN_PROGRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||||
|
this.http = http;
|
||||||
|
this.cert = cryptoProvider.getClientCertificate();
|
||||||
|
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||||
|
this.pk = cryptoProvider.getClientPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
|
for ( int j = 0; j < bytes.length; j++ ) {
|
||||||
|
int v = bytes[j] & 0xFF;
|
||||||
|
hexChars[j * 2] = hexArray[v >>> 4];
|
||||||
|
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||||
|
}
|
||||||
|
return new String(hexChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] hexToBytes(String s) {
|
||||||
|
int len = s.length();
|
||||||
|
byte[] data = new byte[len / 2];
|
||||||
|
for (int i = 0; i < len; i += 2) {
|
||||||
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||||
|
+ Character.digit(s.charAt(i+1), 16));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
|
||||||
|
{
|
||||||
|
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||||
|
if (certText != null) {
|
||||||
|
byte[] certBytes = hexToBytes(certText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateRandomBytes(int length)
|
||||||
|
{
|
||||||
|
byte[] rand = new byte[length];
|
||||||
|
new SecureRandom().nextBytes(rand);
|
||||||
|
return rand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||||
|
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||||
|
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||||
|
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||||
|
return saltedPin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||||
|
sig.initVerify(cert.getPublicKey());
|
||||||
|
sig.update(data);
|
||||||
|
return sig.verify(signature);
|
||||||
|
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] signData(byte[] data, PrivateKey key) {
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||||
|
sig.initSign(key);
|
||||||
|
sig.update(data);
|
||||||
|
byte[] signature = new byte[256];
|
||||||
|
sig.sign(signature, 0, signature.length);
|
||||||
|
return signature;
|
||||||
|
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
|
||||||
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||||
|
|
||||||
|
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||||
|
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||||
|
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||||
|
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||||
|
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||||
|
blockRoundedSize, fullDecrypted);
|
||||||
|
return fullDecrypted;
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
|
||||||
|
try {
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||||
|
|
||||||
|
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||||
|
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||||
|
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||||
|
return cipher.doFinal(blockRoundedData);
|
||||||
|
} catch (GeneralSecurityException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||||
|
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||||
|
return new SecretKeySpec(aesTruncated, "AES");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||||
|
byte[] c = new byte[a.length + b.length];
|
||||||
|
System.arraycopy(a, 0, c, 0, a.length);
|
||||||
|
System.arraycopy(b, 0, c, a.length, b.length);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generatePinString() {
|
||||||
|
Random r = new Random();
|
||||||
|
return String.format((Locale)null, "%d%d%d%d",
|
||||||
|
r.nextInt(10), r.nextInt(10),
|
||||||
|
r.nextInt(10), r.nextInt(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getPairedCert() {
|
||||||
|
return serverCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
|
||||||
|
PairingHashAlgorithm hashAlgo;
|
||||||
|
|
||||||
|
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||||
|
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||||
|
if (serverMajorVersion >= 7) {
|
||||||
|
// Gen 7+ uses SHA-256 hashing
|
||||||
|
hashAlgo = new Sha256PairingHash();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Prior to Gen 7, SHA-1 is used
|
||||||
|
hashAlgo = new Sha1PairingHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a salt for hashing the PIN
|
||||||
|
byte[] salt = generateRandomBytes(16);
|
||||||
|
|
||||||
|
// Combine the salt and pin, then create an AES key from them
|
||||||
|
byte[] saltAndPin = saltPin(salt, pin);
|
||||||
|
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||||
|
|
||||||
|
// Send the salt and get the server cert. This doesn't have a read timeout
|
||||||
|
// because the user must enter the PIN before the server responds
|
||||||
|
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||||
|
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||||
|
false);
|
||||||
|
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save this cert for retrieval later
|
||||||
|
serverCert = extractPlainCert(getCert);
|
||||||
|
if (serverCert == null) {
|
||||||
|
// Attempting to pair while another device is pairing will cause GFE
|
||||||
|
// to give an empty cert in the response.
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
return PairState.ALREADY_IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require this cert for TLS to this host
|
||||||
|
http.setServerCert(serverCert);
|
||||||
|
|
||||||
|
// Generate a random challenge and encrypt it with our AES key
|
||||||
|
byte[] randomChallenge = generateRandomBytes(16);
|
||||||
|
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||||
|
|
||||||
|
// Send the encrypted challenge to the server
|
||||||
|
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||||
|
true);
|
||||||
|
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the server's response and subsequent challenge
|
||||||
|
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||||
|
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||||
|
|
||||||
|
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||||
|
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||||
|
|
||||||
|
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||||
|
byte[] clientSecret = generateRandomBytes(16);
|
||||||
|
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||||
|
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||||
|
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||||
|
true);
|
||||||
|
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the server's signed secret
|
||||||
|
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||||
|
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||||
|
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||||
|
|
||||||
|
// Ensure the authenticity of the data
|
||||||
|
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||||
|
// Cancel the pairing process
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
|
||||||
|
// Looks like a MITM
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||||
|
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||||
|
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||||
|
// Cancel the pairing process
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
|
||||||
|
// Probably got the wrong PIN
|
||||||
|
return PairState.PIN_WRONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the server our signed secret
|
||||||
|
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||||
|
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||||
|
true);
|
||||||
|
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||||
|
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||||
|
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||||
|
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||||
|
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||||
|
return PairState.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PairState.PAIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface PairingHashAlgorithm {
|
||||||
|
int getHashLength();
|
||||||
|
byte[] hashData(byte[] data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||||
|
public int getHashLength() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] hashData(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
return md.digest(data);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
// Shouldn't ever happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||||
|
public int getHashLength() {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] hashData(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
return md.digest(data);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
// Shouldn't ever happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
public class ControllerPacket {
|
||||||
|
public static final short A_FLAG = 0x1000;
|
||||||
|
public static final short B_FLAG = 0x2000;
|
||||||
|
public static final short X_FLAG = 0x4000;
|
||||||
|
public static final short Y_FLAG = (short)0x8000;
|
||||||
|
public static final short UP_FLAG = 0x0001;
|
||||||
|
public static final short DOWN_FLAG = 0x0002;
|
||||||
|
public static final short LEFT_FLAG = 0x0004;
|
||||||
|
public static final short RIGHT_FLAG = 0x0008;
|
||||||
|
public static final short LB_FLAG = 0x0100;
|
||||||
|
public static final short RB_FLAG = 0x0200;
|
||||||
|
public static final short PLAY_FLAG = 0x0010;
|
||||||
|
public static final short BACK_FLAG = 0x0020;
|
||||||
|
public static final short LS_CLK_FLAG = 0x0040;
|
||||||
|
public static final short RS_CLK_FLAG = 0x0080;
|
||||||
|
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
public class KeyboardPacket {
|
||||||
|
public static final byte KEY_DOWN = 0x03;
|
||||||
|
public static final byte KEY_UP = 0x04;
|
||||||
|
|
||||||
|
public static final byte MODIFIER_SHIFT = 0x01;
|
||||||
|
public static final byte MODIFIER_CTRL = 0x02;
|
||||||
|
public static final byte MODIFIER_ALT = 0x04;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.limelight.nvstream.input;
|
||||||
|
|
||||||
|
public class MouseButtonPacket {
|
||||||
|
public static final byte PRESS_EVENT = 0x07;
|
||||||
|
public static final byte RELEASE_EVENT = 0x08;
|
||||||
|
|
||||||
|
public static final byte BUTTON_LEFT = 0x01;
|
||||||
|
public static final byte BUTTON_MIDDLE = 0x02;
|
||||||
|
public static final byte BUTTON_RIGHT = 0x03;
|
||||||
|
public static final byte BUTTON_X1 = 0x04;
|
||||||
|
public static final byte BUTTON_X2 = 0x05;
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
package com.limelight.nvstream.jni;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
|
public class MoonBridge {
|
||||||
|
/* See documentation in Limelight.h for information about these functions and constants */
|
||||||
|
|
||||||
|
public static final AudioConfiguration AUDIO_CONFIGURATION_STEREO = new AudioConfiguration(2, 0x3);
|
||||||
|
public static final AudioConfiguration AUDIO_CONFIGURATION_51_SURROUND = new AudioConfiguration(6, 0x3F);
|
||||||
|
public static final AudioConfiguration AUDIO_CONFIGURATION_71_SURROUND = new AudioConfiguration(8, 0x63F);
|
||||||
|
|
||||||
|
public static final int VIDEO_FORMAT_H264 = 0x0001;
|
||||||
|
public static final int VIDEO_FORMAT_H265 = 0x0100;
|
||||||
|
public static final int VIDEO_FORMAT_H265_MAIN10 = 0x0200;
|
||||||
|
|
||||||
|
public static final int VIDEO_FORMAT_MASK_H264 = 0x00FF;
|
||||||
|
public static final int VIDEO_FORMAT_MASK_H265 = 0xFF00;
|
||||||
|
|
||||||
|
public static final int BUFFER_TYPE_PICDATA = 0;
|
||||||
|
public static final int BUFFER_TYPE_SPS = 1;
|
||||||
|
public static final int BUFFER_TYPE_PPS = 2;
|
||||||
|
public static final int BUFFER_TYPE_VPS = 3;
|
||||||
|
|
||||||
|
public static final int CAPABILITY_DIRECT_SUBMIT = 1;
|
||||||
|
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC = 2;
|
||||||
|
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC = 4;
|
||||||
|
|
||||||
|
public static final int DR_OK = 0;
|
||||||
|
public static final int DR_NEED_IDR = -1;
|
||||||
|
|
||||||
|
public static final int CONN_STATUS_OKAY = 0;
|
||||||
|
public static final int CONN_STATUS_POOR = 1;
|
||||||
|
|
||||||
|
private static AudioRenderer audioRenderer;
|
||||||
|
private static VideoDecoderRenderer videoRenderer;
|
||||||
|
private static NvConnectionListener connectionListener;
|
||||||
|
|
||||||
|
static {
|
||||||
|
System.loadLibrary("moonlight-core");
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CAPABILITY_SLICES_PER_FRAME(byte slices) {
|
||||||
|
return slices << 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AudioConfiguration {
|
||||||
|
public final int channelCount;
|
||||||
|
public final int channelMask;
|
||||||
|
|
||||||
|
public AudioConfiguration(int channelCount, int channelMask) {
|
||||||
|
this.channelCount = channelCount;
|
||||||
|
this.channelMask = channelMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an AudioConfiguration from the integer value returned by moonlight-common-c
|
||||||
|
// See CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION() and CHANNEL_MASK_FROM_AUDIO_CONFIGURATION()
|
||||||
|
// in Limelight.h
|
||||||
|
private AudioConfiguration(int audioConfiguration) {
|
||||||
|
// Check the magic byte before decoding to make sure we got something that's actually
|
||||||
|
// a MAKE_AUDIO_CONFIGURATION()-based value and not something else like an older version
|
||||||
|
// hardcoded AUDIO_CONFIGURATION value from an earlier version of moonlight-common-c.
|
||||||
|
if ((audioConfiguration & 0xFF) != 0xCA) {
|
||||||
|
throw new IllegalArgumentException("Audio configuration has invalid magic byte!");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channelCount = (audioConfiguration >> 8) & 0xFF;
|
||||||
|
this.channelMask = (audioConfiguration >> 16) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION() in Limelight.h
|
||||||
|
public int getSurroundAudioInfo() {
|
||||||
|
return channelMask << 16 | channelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof AudioConfiguration) {
|
||||||
|
AudioConfiguration that = (AudioConfiguration)obj;
|
||||||
|
return this.toInt() == that.toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the integer value expected by moonlight-common-c
|
||||||
|
// See MAKE_AUDIO_CONFIGURATION() in Limelight.h
|
||||||
|
public int toInt() {
|
||||||
|
return ((channelMask) << 16) | (channelCount << 8) | 0xCA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bridgeDrSetup(int videoFormat, int width, int height, int redrawRate) {
|
||||||
|
if (videoRenderer != null) {
|
||||||
|
return videoRenderer.setup(videoFormat, width, height, redrawRate);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeDrStart() {
|
||||||
|
if (videoRenderer != null) {
|
||||||
|
videoRenderer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeDrStop() {
|
||||||
|
if (videoRenderer != null) {
|
||||||
|
videoRenderer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeDrCleanup() {
|
||||||
|
if (videoRenderer != null) {
|
||||||
|
videoRenderer.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bridgeDrSubmitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength,
|
||||||
|
int decodeUnitType,
|
||||||
|
int frameNumber, long receiveTimeMs) {
|
||||||
|
if (videoRenderer != null) {
|
||||||
|
return videoRenderer.submitDecodeUnit(decodeUnitData, decodeUnitLength,
|
||||||
|
decodeUnitType, frameNumber, receiveTimeMs);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return DR_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
|
||||||
|
if (audioRenderer != null) {
|
||||||
|
return audioRenderer.setup(new AudioConfiguration(audioConfiguration), sampleRate, samplesPerFrame);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeArStart() {
|
||||||
|
if (audioRenderer != null) {
|
||||||
|
audioRenderer.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeArStop() {
|
||||||
|
if (audioRenderer != null) {
|
||||||
|
audioRenderer.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeArCleanup() {
|
||||||
|
if (audioRenderer != null) {
|
||||||
|
audioRenderer.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeArPlaySample(short[] pcmData) {
|
||||||
|
if (audioRenderer != null) {
|
||||||
|
audioRenderer.playDecodedAudio(pcmData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClStageStarting(int stage) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.stageStarting(getStageName(stage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClStageComplete(int stage) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.stageComplete(getStageName(stage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClStageFailed(int stage, int errorCode) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.stageFailed(getStageName(stage), errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClConnectionStarted() {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.connectionStarted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClConnectionTerminated(int errorCode) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.connectionTerminated(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.rumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bridgeClConnectionStatusUpdate(int connectionStatus) {
|
||||||
|
if (connectionListener != null) {
|
||||||
|
connectionListener.connectionStatusUpdate(connectionStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) {
|
||||||
|
MoonBridge.videoRenderer = videoRenderer;
|
||||||
|
MoonBridge.audioRenderer = audioRenderer;
|
||||||
|
MoonBridge.connectionListener = connectionListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cleanupBridge() {
|
||||||
|
MoonBridge.videoRenderer = null;
|
||||||
|
MoonBridge.audioRenderer = null;
|
||||||
|
MoonBridge.connectionListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native int startConnection(String address, String appVersion, String gfeVersion,
|
||||||
|
int width, int height, int fps,
|
||||||
|
int bitrate, int packetSize, int streamingRemotely,
|
||||||
|
int audioConfiguration, boolean supportsHevc,
|
||||||
|
boolean enableHdr,
|
||||||
|
int hevcBitratePercentageMultiplier,
|
||||||
|
int clientRefreshRateX100,
|
||||||
|
byte[] riAesKey, byte[] riAesIv,
|
||||||
|
int videoCapabilities);
|
||||||
|
|
||||||
|
public static native void stopConnection();
|
||||||
|
|
||||||
|
public static native void interruptConnection();
|
||||||
|
|
||||||
|
public static native void sendMouseMove(short deltaX, short deltaY);
|
||||||
|
|
||||||
|
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
|
||||||
|
|
||||||
|
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
|
||||||
|
|
||||||
|
public static native void sendMultiControllerInput(short controllerNumber,
|
||||||
|
short activeGamepadMask, short buttonFlags,
|
||||||
|
byte leftTrigger, byte rightTrigger,
|
||||||
|
short leftStickX, short leftStickY,
|
||||||
|
short rightStickX, short rightStickY);
|
||||||
|
|
||||||
|
public static native void sendControllerInput(short buttonFlags,
|
||||||
|
byte leftTrigger, byte rightTrigger,
|
||||||
|
short leftStickX, short leftStickY,
|
||||||
|
short rightStickX, short rightStickY);
|
||||||
|
|
||||||
|
public static native void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier);
|
||||||
|
|
||||||
|
public static native void sendMouseScroll(byte scrollClicks);
|
||||||
|
|
||||||
|
public static native String getStageName(int stage);
|
||||||
|
|
||||||
|
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
|
||||||
|
|
||||||
|
public static native int getPendingAudioDuration();
|
||||||
|
|
||||||
|
public static native int getPendingVideoFrames();
|
||||||
|
|
||||||
|
public static native void init();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user