androidtvremote2 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {androidtvremote2-0.3.2/src/androidtvremote2.egg-info → androidtvremote2-0.3.3}/PKG-INFO +2 -2
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/README.md +1 -1
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/pyproject.toml +1 -1
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/androidtv_remote.py +13 -3
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/certificate_generator.py +1 -1
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/model.py +21 -21
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/pairing.py +9 -4
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/remote.py +21 -8
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3/src/androidtvremote2.egg-info}/PKG-INFO +2 -2
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/tests/test_androidtv_remote.py +54 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/tests/test_pairing.py +25 -4
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/tests/test_remote.py +56 -13
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/LICENSE +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/setup.cfg +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/__init__.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/base.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/const.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/exceptions.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/polo_pb2.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/polo_pb2.pyi +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/py.typed +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/remotemessage_pb2.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/remotemessage_pb2.pyi +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/voice_stream.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/SOURCES.txt +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/dependency_links.txt +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/requires.txt +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/top_level.txt +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/tests/test_base.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/tests/test_certificate_generator.py +0 -0
- {androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/tests/test_voice_stream.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidtvremote2
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A Python library for interacting with Android TV using the Android TV Remote protocol v2
|
|
5
5
|
Author-email: tronikos <tronikos@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -26,7 +26,7 @@ Dynamic: license-file
|
|
|
26
26
|
A Python library for interacting with Android TV using the Android TV Remote protocol v2. This is the same protocol the Google TV mobile app is using. It doesn't require ADB or enabling developer tools on the Android TV device. It only requires the [Android TV Remote Service](https://play.google.com/store/apps/details?id=com.google.android.tv.remote.service) that comes pre-installed on most Android TV devices.
|
|
27
27
|
|
|
28
28
|
For a list of the most common commands you can send to the Android TV see: [TvKeys](https://github.com/tronikos/androidtvremote2/blob/main/TvKeys.txt).
|
|
29
|
-
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/
|
|
29
|
+
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/main/src/androidtvremote2/remotemessage.proto).
|
|
30
30
|
In addition to commands you can send URLs to open apps registered to handle them. See [this guide](https://community.home-assistant.io/t/android-tv-remote-app-links-deep-linking-guide/567921) for how to find deep links for apps.
|
|
31
31
|
|
|
32
32
|
Voice commands can also be sent as PCM 16-bit mono 8 kHz audio data.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A Python library for interacting with Android TV using the Android TV Remote protocol v2. This is the same protocol the Google TV mobile app is using. It doesn't require ADB or enabling developer tools on the Android TV device. It only requires the [Android TV Remote Service](https://play.google.com/store/apps/details?id=com.google.android.tv.remote.service) that comes pre-installed on most Android TV devices.
|
|
4
4
|
|
|
5
5
|
For a list of the most common commands you can send to the Android TV see: [TvKeys](https://github.com/tronikos/androidtvremote2/blob/main/TvKeys.txt).
|
|
6
|
-
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/
|
|
6
|
+
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/main/src/androidtvremote2/remotemessage.proto).
|
|
7
7
|
In addition to commands you can send URLs to open apps registered to handle them. See [this guide](https://community.home-assistant.io/t/android-tv-remote-app-links-deep-linking-guide/567921) for how to find deep links for apps.
|
|
8
8
|
|
|
9
9
|
Voice commands can also be sent as PCM 16-bit mono 8 kHz audio data.
|
|
@@ -77,7 +77,7 @@ class AndroidTVRemote:
|
|
|
77
77
|
|
|
78
78
|
:param client_name: client name. Will be shown on the Android TV during pairing.
|
|
79
79
|
:param certfile: filename that contains the client certificate in PEM format.
|
|
80
|
-
:param keyfile: filename that contains the
|
|
80
|
+
:param keyfile: filename that contains the private key in PEM format.
|
|
81
81
|
:param host: IP address of the Android TV.
|
|
82
82
|
:param api_port: port for connecting and sending commands.
|
|
83
83
|
:param pair_port: port for pairing.
|
|
@@ -210,7 +210,7 @@ class AndroidTVRemote:
|
|
|
210
210
|
self._is_available_updated_callbacks.remove(callback)
|
|
211
211
|
|
|
212
212
|
async def async_generate_cert_if_missing(self) -> bool:
|
|
213
|
-
"""Generate client certificate and
|
|
213
|
+
"""Generate client certificate and private key if missing.
|
|
214
214
|
|
|
215
215
|
:returns: True if a new certificate was generated.
|
|
216
216
|
"""
|
|
@@ -244,6 +244,8 @@ class AndroidTVRemote:
|
|
|
244
244
|
:raises InvalidAuth: if pairing is needed first.
|
|
245
245
|
"""
|
|
246
246
|
ssl_context = await self._create_ssl_context()
|
|
247
|
+
if self._remote_message_protocol:
|
|
248
|
+
self._remote_message_protocol.close()
|
|
247
249
|
on_con_lost = self._loop.create_future()
|
|
248
250
|
on_remote_started = self._loop.create_future()
|
|
249
251
|
try:
|
|
@@ -271,8 +273,13 @@ class AndroidTVRemote:
|
|
|
271
273
|
raise InvalidAuth("Need to pair") from exc
|
|
272
274
|
raise CannotConnect(f"Couldn't connect to {self.host}:{self._api_port}") from exc
|
|
273
275
|
|
|
274
|
-
|
|
276
|
+
try:
|
|
277
|
+
await asyncio.wait((on_con_lost, on_remote_started), return_when=asyncio.FIRST_COMPLETED)
|
|
278
|
+
except BaseException:
|
|
279
|
+
self._remote_message_protocol.close()
|
|
280
|
+
raise
|
|
275
281
|
if on_con_lost.done():
|
|
282
|
+
self._remote_message_protocol.close()
|
|
276
283
|
con_lost_exc = on_con_lost.result()
|
|
277
284
|
LOGGER.debug(
|
|
278
285
|
"Couldn't connect to %s:%s. Error: %s",
|
|
@@ -317,12 +324,15 @@ class AndroidTVRemote:
|
|
|
317
324
|
|
|
318
325
|
def keep_reconnecting(self, invalid_auth_callback: Callable[[], None] | None = None) -> None:
|
|
319
326
|
"""Create a task to keep reconnecting whenever connection is lost."""
|
|
327
|
+
if self._reconnect_task:
|
|
328
|
+
self._reconnect_task.cancel()
|
|
320
329
|
self._reconnect_task = self._loop.create_task(self._async_reconnect(invalid_auth_callback))
|
|
321
330
|
|
|
322
331
|
def disconnect(self) -> None:
|
|
323
332
|
"""Disconnect any open connections."""
|
|
324
333
|
if self._reconnect_task:
|
|
325
334
|
self._reconnect_task.cancel()
|
|
335
|
+
self._reconnect_task = None
|
|
326
336
|
if self._remote_message_protocol:
|
|
327
337
|
self._remote_message_protocol.close()
|
|
328
338
|
self._remote_message_protocol = None
|
{androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/certificate_generator.py
RENAMED
|
@@ -18,7 +18,7 @@ from cryptography.x509.oid import NameOID
|
|
|
18
18
|
def generate_selfsigned_cert(hostname: str) -> tuple[bytes, bytes]:
|
|
19
19
|
"""Generate self signed certificate for a hostname.
|
|
20
20
|
|
|
21
|
-
:return: self signed certificate and
|
|
21
|
+
:return: self signed certificate and private key in PEM format
|
|
22
22
|
"""
|
|
23
23
|
key = rsa.generate_private_key(
|
|
24
24
|
public_exponent=65537,
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
"""Data models for AndroidTVRemote."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import TypedDict
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class DeviceInfo(TypedDict):
|
|
9
|
-
"""A TypedDict for device information."""
|
|
10
|
-
|
|
11
|
-
manufacturer: str
|
|
12
|
-
model: str
|
|
13
|
-
sw_version: str
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class VolumeInfo(TypedDict):
|
|
17
|
-
"""A TypedDict for volume information."""
|
|
18
|
-
|
|
19
|
-
level: int
|
|
20
|
-
max: int
|
|
21
|
-
muted: bool
|
|
1
|
+
"""Data models for AndroidTVRemote."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeviceInfo(TypedDict):
|
|
9
|
+
"""A TypedDict for device information."""
|
|
10
|
+
|
|
11
|
+
manufacturer: str
|
|
12
|
+
model: str
|
|
13
|
+
sw_version: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VolumeInfo(TypedDict):
|
|
17
|
+
"""A TypedDict for volume information."""
|
|
18
|
+
|
|
19
|
+
level: int
|
|
20
|
+
max: int
|
|
21
|
+
muted: bool
|
|
@@ -34,6 +34,11 @@ def _create_message() -> OuterMessage:
|
|
|
34
34
|
return msg
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _int_to_bytes(val: int) -> bytes:
|
|
38
|
+
"""Convert a positive integer to big-endian bytes."""
|
|
39
|
+
return val.to_bytes((val.bit_length() + 7) // 8, "big")
|
|
40
|
+
|
|
41
|
+
|
|
37
42
|
def _get_modulus_and_exponent(cert: x509.Certificate) -> tuple[int, int]:
|
|
38
43
|
"""Extract modulus and exponent from a certificate."""
|
|
39
44
|
public_numbers = cert.public_key().public_numbers() # type: ignore[union-attr]
|
|
@@ -113,10 +118,10 @@ class PairingProtocol(ProtobufProtocol):
|
|
|
113
118
|
server_modulus, server_exponent = _get_modulus_and_exponent(server_cert)
|
|
114
119
|
|
|
115
120
|
h = hashlib.sha256()
|
|
116
|
-
h.update(
|
|
117
|
-
h.update(
|
|
118
|
-
h.update(
|
|
119
|
-
h.update(
|
|
121
|
+
h.update(_int_to_bytes(client_modulus))
|
|
122
|
+
h.update(_int_to_bytes(client_exponent))
|
|
123
|
+
h.update(_int_to_bytes(server_modulus))
|
|
124
|
+
h.update(_int_to_bytes(server_exponent))
|
|
120
125
|
h.update(bytes.fromhex(pairing_code[2:]))
|
|
121
126
|
hash_result = h.digest()
|
|
122
127
|
|
|
@@ -112,6 +112,8 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
112
112
|
self.volume_info: VolumeInfo | None = None
|
|
113
113
|
self.ime_counter: int = 0
|
|
114
114
|
self.ime_field_counter: int = 0
|
|
115
|
+
self._has_ime_counter = False
|
|
116
|
+
self._has_ime_field_counter = False
|
|
115
117
|
self._loop = loop
|
|
116
118
|
self._idle_disconnect_task: asyncio.Task[None] | None = None
|
|
117
119
|
self._reset_idle_disconnect_task()
|
|
@@ -170,9 +172,10 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
170
172
|
def send_text(self, text: str) -> None:
|
|
171
173
|
"""Send a text string to Android TV via the input method.
|
|
172
174
|
|
|
173
|
-
The text
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
The text is inserted at the cursor, which is left after it: `start` and `end` are the text's length.
|
|
176
|
+
`ime_counter` and `ime_field_counter` come from the focused field's status (`remote_ime_key_inject`
|
|
177
|
+
and `remote_ime_show_request`), falling back to `remote_ime_batch_edit` when the key inject carries
|
|
178
|
+
no counter.
|
|
176
179
|
|
|
177
180
|
:param text: The text string to be sent.
|
|
178
181
|
"""
|
|
@@ -181,8 +184,7 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
181
184
|
|
|
182
185
|
self._reset_idle_disconnect_task()
|
|
183
186
|
msg = RemoteMessage()
|
|
184
|
-
|
|
185
|
-
ime_object = RemoteImeObject(start=param_value, end=param_value, value=text)
|
|
187
|
+
ime_object = RemoteImeObject(start=len(text), end=len(text), value=text)
|
|
186
188
|
edit_info = RemoteEditInfo(insert=1, text_field_status=ime_object)
|
|
187
189
|
batch_edit = RemoteImeBatchEdit(
|
|
188
190
|
ime_counter=self.ime_counter,
|
|
@@ -311,11 +313,22 @@ class RemoteProtocol(ProtobufProtocol):
|
|
|
311
313
|
elif msg.HasField("remote_set_active"):
|
|
312
314
|
new_msg.remote_set_active.active = self._active_features
|
|
313
315
|
elif msg.HasField("remote_ime_key_inject"):
|
|
314
|
-
|
|
316
|
+
inject = msg.remote_ime_key_inject
|
|
317
|
+
self.current_app = inject.app_info.app_package
|
|
315
318
|
self._on_current_app_updated(self.current_app)
|
|
319
|
+
self.ime_counter = inject.app_info.counter
|
|
320
|
+
self.ime_field_counter = inject.text_field_status.counter_field
|
|
321
|
+
self._has_ime_counter = bool(self.ime_counter)
|
|
322
|
+
self._has_ime_field_counter = bool(self.ime_field_counter)
|
|
323
|
+
elif msg.HasField("remote_ime_show_request"):
|
|
324
|
+
if msg.remote_ime_show_request.HasField("remote_text_field_status"):
|
|
325
|
+
self.ime_field_counter = msg.remote_ime_show_request.remote_text_field_status.counter_field
|
|
326
|
+
self._has_ime_field_counter = True
|
|
316
327
|
elif msg.HasField("remote_ime_batch_edit"):
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
if not self._has_ime_counter:
|
|
329
|
+
self.ime_counter = msg.remote_ime_batch_edit.ime_counter
|
|
330
|
+
if not self._has_ime_field_counter:
|
|
331
|
+
self.ime_field_counter = msg.remote_ime_batch_edit.field_counter
|
|
319
332
|
elif msg.HasField("remote_set_volume_level"):
|
|
320
333
|
self.volume_info = {
|
|
321
334
|
"level": msg.remote_set_volume_level.volume_level,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: androidtvremote2
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A Python library for interacting with Android TV using the Android TV Remote protocol v2
|
|
5
5
|
Author-email: tronikos <tronikos@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -26,7 +26,7 @@ Dynamic: license-file
|
|
|
26
26
|
A Python library for interacting with Android TV using the Android TV Remote protocol v2. This is the same protocol the Google TV mobile app is using. It doesn't require ADB or enabling developer tools on the Android TV device. It only requires the [Android TV Remote Service](https://play.google.com/store/apps/details?id=com.google.android.tv.remote.service) that comes pre-installed on most Android TV devices.
|
|
27
27
|
|
|
28
28
|
For a list of the most common commands you can send to the Android TV see: [TvKeys](https://github.com/tronikos/androidtvremote2/blob/main/TvKeys.txt).
|
|
29
|
-
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/
|
|
29
|
+
For a full list see [remotemessage.proto](https://github.com/tronikos/androidtvremote2/blob/main/src/androidtvremote2/remotemessage.proto).
|
|
30
30
|
In addition to commands you can send URLs to open apps registered to handle them. See [this guide](https://community.home-assistant.io/t/android-tv-remote-app-links-deep-linking-guide/567921) for how to find deep links for apps.
|
|
31
31
|
|
|
32
32
|
Voice commands can also be sent as PCM 16-bit mono 8 kHz audio data.
|
|
@@ -17,6 +17,8 @@ from androidtvremote2.exceptions import ConnectionClosed, InvalidAuth
|
|
|
17
17
|
from androidtvremote2.remote import VOICE_CHUNK_MIN_SIZE
|
|
18
18
|
from androidtvremote2.remotemessage_pb2 import RemoteDirection, RemoteKeyCode, RemoteMessage
|
|
19
19
|
|
|
20
|
+
from .conftest import FakeTransport, parse_written
|
|
21
|
+
|
|
20
22
|
if TYPE_CHECKING:
|
|
21
23
|
from collections.abc import Callable
|
|
22
24
|
from pathlib import Path
|
|
@@ -232,6 +234,58 @@ async def test_disconnect_cancels_the_reconnect_task(remote: AndroidTVRemote, co
|
|
|
232
234
|
await asyncio.sleep(0)
|
|
233
235
|
|
|
234
236
|
assert reconnect_task.cancelled() or reconnect_task.done()
|
|
237
|
+
assert remote._reconnect_task is None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def test_keep_reconnecting_cancels_previous_reconnect_task(remote: AndroidTVRemote, connected: RemoteHarness) -> None:
|
|
241
|
+
"""Calling keep_reconnecting() twice cancels the previous reconnect task."""
|
|
242
|
+
remote.keep_reconnecting()
|
|
243
|
+
first_task = remote._reconnect_task
|
|
244
|
+
assert first_task is not None
|
|
245
|
+
|
|
246
|
+
remote.keep_reconnecting()
|
|
247
|
+
second_task = remote._reconnect_task
|
|
248
|
+
await asyncio.sleep(0)
|
|
249
|
+
|
|
250
|
+
assert first_task.cancelled()
|
|
251
|
+
assert second_task is not None
|
|
252
|
+
assert not second_task.done()
|
|
253
|
+
remote.disconnect()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def test_cancelling_async_connect_closes_protocol_and_idle_task(
|
|
257
|
+
remote: AndroidTVRemote, monkeypatch: pytest.MonkeyPatch
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Cancelling async_connect while waiting for remote_start closes the transport and idle timer."""
|
|
260
|
+
await remote.async_generate_cert_if_missing()
|
|
261
|
+
fake_transport = FakeTransport()
|
|
262
|
+
connected_event = asyncio.Event()
|
|
263
|
+
|
|
264
|
+
async def fake_create_connection(
|
|
265
|
+
protocol_factory: Callable[[], object], *_args: object, **_kwargs: object
|
|
266
|
+
) -> tuple[object, object]:
|
|
267
|
+
proto = protocol_factory()
|
|
268
|
+
proto.connection_made(fake_transport) # type: ignore[attr-defined]
|
|
269
|
+
connected_event.set()
|
|
270
|
+
return fake_transport, proto
|
|
271
|
+
|
|
272
|
+
monkeypatch.setattr(remote._loop, "create_connection", fake_create_connection)
|
|
273
|
+
|
|
274
|
+
connect_task = asyncio.create_task(remote.async_connect())
|
|
275
|
+
await connected_event.wait()
|
|
276
|
+
await asyncio.sleep(0)
|
|
277
|
+
|
|
278
|
+
assert remote._remote_message_protocol is not None
|
|
279
|
+
idle_task = remote._remote_message_protocol._idle_disconnect_task
|
|
280
|
+
assert idle_task is not None
|
|
281
|
+
|
|
282
|
+
connect_task.cancel()
|
|
283
|
+
with pytest.raises(asyncio.CancelledError):
|
|
284
|
+
await connect_task
|
|
285
|
+
|
|
286
|
+
assert fake_transport.is_closing()
|
|
287
|
+
assert idle_task.cancelled()
|
|
288
|
+
assert parse_written(fake_transport.written, RemoteMessage) == []
|
|
235
289
|
|
|
236
290
|
|
|
237
291
|
async def test_disconnect_is_idempotent(remote: AndroidTVRemote, connected: RemoteHarness) -> None:
|
|
@@ -74,10 +74,8 @@ def expected_pin(client_cert_pem: bytes, server_cert_pem: bytes, nonce: str) ->
|
|
|
74
74
|
client_n, client_e = modulus_and_exponent(client_cert_pem)
|
|
75
75
|
server_n, server_e = modulus_and_exponent(server_cert_pem)
|
|
76
76
|
h = hashlib.sha256()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
h.update(bytes.fromhex(f"{server_n:X}"))
|
|
80
|
-
h.update(bytes.fromhex(f"0{server_e:X}"))
|
|
77
|
+
for val in (client_n, client_e, server_n, server_e):
|
|
78
|
+
h.update(val.to_bytes((val.bit_length() + 7) // 8, "big"))
|
|
81
79
|
h.update(bytes.fromhex(nonce))
|
|
82
80
|
return f"{h.digest()[0]:02X}{nonce}"
|
|
83
81
|
|
|
@@ -147,6 +145,29 @@ async def test_finish_pairing_sends_the_secret(
|
|
|
147
145
|
assert secret.secret.secret[0] == int(pin[0:2], 16)
|
|
148
146
|
|
|
149
147
|
|
|
148
|
+
async def test_finish_pairing_handles_odd_hex_length_modulus_and_even_hex_exponent(
|
|
149
|
+
pairing: PairingHarness, monkeypatch: pytest.MonkeyPatch
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Moduli with an odd number of hex digits or 2-nibble exponents don't fail bytes conversion."""
|
|
152
|
+
await run_start_pairing(pairing)
|
|
153
|
+
pairing.clear()
|
|
154
|
+
# 0xabc has 3 hex digits (odd); 0x11 (17) has 2 hex digits (would be 3 with a leading '0').
|
|
155
|
+
monkeypatch.setattr("androidtvremote2.pairing._get_modulus_and_exponent", lambda _cert: (0xABC, 0x11))
|
|
156
|
+
h = hashlib.sha256()
|
|
157
|
+
for val in (0xABC, 0x11, 0xABC, 0x11):
|
|
158
|
+
h.update(val.to_bytes((val.bit_length() + 7) // 8, "big"))
|
|
159
|
+
h.update(bytes.fromhex("ABCD"))
|
|
160
|
+
pin = f"{h.digest()[0]:02X}ABCD"
|
|
161
|
+
|
|
162
|
+
task = asyncio.ensure_future(pairing.protocol.async_finish_pairing(pin))
|
|
163
|
+
await wait_until_sent(pairing, 1)
|
|
164
|
+
pairing.receive(device_msg("secret_ack", secret=b"\x00" * 32))
|
|
165
|
+
await task
|
|
166
|
+
|
|
167
|
+
(secret,) = pairing.sent()
|
|
168
|
+
assert secret.secret.secret == h.digest()
|
|
169
|
+
|
|
170
|
+
|
|
150
171
|
async def test_duplicate_acks_do_not_raise(pairing: PairingHarness) -> None:
|
|
151
172
|
"""A repeated acknowledgement doesn't blow up inside data_received.
|
|
152
173
|
|
|
@@ -167,22 +167,65 @@ async def test_volume_level_updates_volume_info(remote_factory: Callable[..., Re
|
|
|
167
167
|
assert harness.volume_info_updates == [{"level": 12, "max": 100, "muted": True}]
|
|
168
168
|
|
|
169
169
|
|
|
170
|
-
async def
|
|
170
|
+
async def test_send_text_uses_the_focused_fields_counters(
|
|
171
171
|
remote_factory: Callable[..., RemoteHarness],
|
|
172
172
|
) -> None:
|
|
173
|
-
"""
|
|
173
|
+
"""ime_counter and ime_field_counter come from remote_ime_key_inject, not overwritten by batch_edit."""
|
|
174
174
|
harness = remote_factory()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
inject = RemoteMessage()
|
|
176
|
+
inject.remote_ime_key_inject.app_info.app_package = "org.jellyfin.androidtv"
|
|
177
|
+
inject.remote_ime_key_inject.app_info.counter = 5
|
|
178
|
+
inject.remote_ime_key_inject.text_field_status.counter_field = 141
|
|
179
|
+
harness.receive(inject)
|
|
180
|
+
batch = RemoteMessage()
|
|
181
|
+
batch.remote_ime_batch_edit.ime_counter = 1
|
|
182
|
+
batch.remote_ime_batch_edit.field_counter = 1
|
|
183
|
+
harness.receive(batch)
|
|
179
184
|
|
|
180
|
-
assert (harness.protocol.ime_counter, harness.protocol.ime_field_counter) == (
|
|
185
|
+
assert (harness.protocol.ime_counter, harness.protocol.ime_field_counter) == (5, 141)
|
|
181
186
|
|
|
182
187
|
harness.protocol.send_text("hi")
|
|
183
188
|
(sent,) = harness.sent()
|
|
184
|
-
assert sent.remote_ime_batch_edit.ime_counter ==
|
|
185
|
-
assert sent.remote_ime_batch_edit.field_counter ==
|
|
189
|
+
assert sent.remote_ime_batch_edit.ime_counter == 5
|
|
190
|
+
assert sent.remote_ime_batch_edit.field_counter == 141
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def test_ime_show_request_updates_the_field_counter(
|
|
194
|
+
remote_factory: Callable[..., RemoteHarness],
|
|
195
|
+
) -> None:
|
|
196
|
+
"""A focused field announces updated counter_field in remote_ime_show_request."""
|
|
197
|
+
harness = remote_factory()
|
|
198
|
+
msg = RemoteMessage()
|
|
199
|
+
msg.remote_ime_show_request.remote_text_field_status.counter_field = 7
|
|
200
|
+
harness.receive(msg)
|
|
201
|
+
|
|
202
|
+
assert harness.protocol.ime_field_counter == 7
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def test_ime_batch_edit_is_used_when_key_inject_has_no_counters(
|
|
206
|
+
remote_factory: Callable[..., RemoteHarness],
|
|
207
|
+
) -> None:
|
|
208
|
+
"""When key_inject carries zero counters, batch_edit counters are used and stale counters are cleared."""
|
|
209
|
+
harness = remote_factory()
|
|
210
|
+
inject = RemoteMessage()
|
|
211
|
+
inject.remote_ime_key_inject.app_info.app_package = "com.android.vending"
|
|
212
|
+
inject.remote_ime_key_inject.app_info.counter = 3
|
|
213
|
+
inject.remote_ime_key_inject.text_field_status.counter_field = 38
|
|
214
|
+
harness.receive(inject)
|
|
215
|
+
assert (harness.protocol.ime_counter, harness.protocol.ime_field_counter) == (3, 38)
|
|
216
|
+
|
|
217
|
+
# Switching to an app that reports 0 counters clears the previous app's counters.
|
|
218
|
+
youtube = RemoteMessage()
|
|
219
|
+
youtube.remote_ime_key_inject.app_info.app_package = "com.google.android.youtube.tv"
|
|
220
|
+
harness.receive(youtube)
|
|
221
|
+
assert (harness.protocol.ime_counter, harness.protocol.ime_field_counter) == (0, 0)
|
|
222
|
+
|
|
223
|
+
# A subsequent batch_edit populates the counters as fallback.
|
|
224
|
+
batch = RemoteMessage()
|
|
225
|
+
batch.remote_ime_batch_edit.ime_counter = 9
|
|
226
|
+
batch.remote_ime_batch_edit.field_counter = 4
|
|
227
|
+
harness.receive(batch)
|
|
228
|
+
assert (harness.protocol.ime_counter, harness.protocol.ime_field_counter) == (9, 4)
|
|
186
229
|
|
|
187
230
|
|
|
188
231
|
async def test_remote_error_is_logged_as_an_error(
|
|
@@ -206,7 +249,7 @@ async def test_unhandled_message_is_logged_and_ignored(
|
|
|
206
249
|
"""A message this library has no branch for doesn't produce a reply."""
|
|
207
250
|
harness = remote_factory()
|
|
208
251
|
msg = RemoteMessage()
|
|
209
|
-
msg.
|
|
252
|
+
msg.remote_app_link_launch_request.app_link = "https://example.com"
|
|
210
253
|
with caplog.at_level(logging.DEBUG, logger="androidtvremote2"):
|
|
211
254
|
harness.receive(msg)
|
|
212
255
|
|
|
@@ -283,7 +326,7 @@ async def test_send_key_command_dispatches_the_text_prefix(remote_factory: Calla
|
|
|
283
326
|
|
|
284
327
|
|
|
285
328
|
async def test_send_text_builds_the_batch_edit(remote_factory: Callable[..., RemoteHarness]) -> None:
|
|
286
|
-
"""send_text
|
|
329
|
+
"""send_text puts the cursor after the text: start and end are its length."""
|
|
287
330
|
harness = remote_factory()
|
|
288
331
|
harness.protocol.send_text("abcd")
|
|
289
332
|
|
|
@@ -291,8 +334,8 @@ async def test_send_text_builds_the_batch_edit(remote_factory: Callable[..., Rem
|
|
|
291
334
|
edit = sent.remote_ime_batch_edit.edit_info[0]
|
|
292
335
|
assert edit.insert == 1
|
|
293
336
|
assert edit.text_field_status.value == "abcd"
|
|
294
|
-
assert edit.text_field_status.start ==
|
|
295
|
-
assert edit.text_field_status.end ==
|
|
337
|
+
assert edit.text_field_status.start == 4
|
|
338
|
+
assert edit.text_field_status.end == 4
|
|
296
339
|
|
|
297
340
|
|
|
298
341
|
async def test_send_text_rejects_empty_text(remote_factory: Callable[..., RemoteHarness]) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2/remotemessage_pb2.pyi
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/requires.txt
RENAMED
|
File without changes
|
{androidtvremote2-0.3.2 → androidtvremote2-0.3.3}/src/androidtvremote2.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|