8000 gh-71042: Add `platform.android_ver` (#116674) · python/cpython@74c8568 · GitHub
[go: up one dir, main page]

Skip to content

Commit 74c8568

Browse files
authored
gh-71042: Add platform.android_ver (#116674)
1 parent ce00de4 commit 74c8568

File tree

9 files changed

+164
-16
lines changed

9 files changed

+164
-16
lines changed

Doc/library/platform.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,39 @@ Linux Platforms
301301
return ids
302302

303303
.. versionadded:: 3.10
304+
305+
306+
Android Platform
307+
----------------
308+
309+
.. function:: android_ver(release="", api_level=0, manufacturer="", \
310+
model="", device="", is_emulator=False)
311+
312+
Get Android device information. Returns a :func:`~collections.namedtuple`
313+
with the following attributes. Values which cannot be determined are set to
314+
the defaults given as parameters.
315+
316+
* ``release`` - Android version, as a string (e.g. ``"14"``).
317+
318+
* ``api_level`` - API level of the running device, as an integer (e.g. ``34``
319+
for Android 14). To get the API level which Python was built against, see
320+
:func:`sys.getandroidapilevel`.
321+
322+
* ``manufacturer`` - `Manufacturer name
323+
<https://developer.android.com/reference/android/os/Build#MANUFACTURER>`__.
324+
325+
* ``model`` - `Model name
326+
<https://developer.android.com/reference/android/os/Build#MODEL>`__ –
327+
typically the marketing name or model number.
328+
329+
* ``device`` - `Device name
330+
<https://developer.android.com/reference/android/os/Build#DEVICE>`__ –
331+
typically the model number or a codename.
332+
333+
* ``is_emulator`` - ``True`` if the device is an emulator; ``False`` if it's
334+
a physical device.
335+
336+
Google maintains a `list of known model and device names
337+
<https://storage.googleapis.com/play_public/supported_devices.html>`__.
338+
339+
.. versionadded:: 3.13

Doc/library/sys.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,9 @@ always available.
753753

754754
.. function:: getandroidapilevel()
755755

756-
Return the build time API version of Android as an integer.
756+
Return the build-time API level of Android as an integer. This represents the
757+
minimum version of Android this build of Python can run on. For runtime
758+
version information, see :func:`platform.android_ver`.
757759

758760
.. availability:: Android.
759761

Lib/platform.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,47 @@ def java_ver(release='', vendor='', vminfo=('', '', ''), osinfo=('', '', '')):
542542

543543
return release, vendor, vminfo, osinfo
544544

545+
546+
AndroidVer = collections.namedtuple(
547+
"AndroidVer", "release api_level manufacturer model device is_emulator")
548+
549+
def android_ver(release="", api_level=0, manufacturer="", model="", device="",
550+
is_emulator=False):
551+
if sys.platform == "android":
552+
try:
553+
from ctypes import CDLL, c_char_p, create_string_buffer
554+
except ImportError:
555+
pass
556+
else:
557+
# An NDK developer confirmed that this is an officially-supported
558+
# API (https://stackoverflow.com/a/28416743). Use `getattr` to avoid
559+
# private name mangling.
560+
system_property_get = getattr(CDLL("libc.so"), "__system_property_get")
561+
system_property_get.argtypes = (c_char_p, c_char_p)
562+
563+
def getprop(name, default):
564+
# https://android.googlesource.com/platform/bionic/+/refs/tags/android-5.0.0_r1/libc/include/sys/system_properties.h#39
565+
PROP_VALUE_MAX = 92
566+
buffer = create_string_buffer(PROP_VALUE_MAX)
567+
length = system_property_get(name.encode("UTF-8"), buffer)
568+
if length == 0:
569+
# This API doesn’t distinguish between an empty property and
570+
# a missing one.
571+
return default
572+
else:
573+
return buffer.value.decode("UTF-8", "backslashreplace")
574+
575+
release = getprop("ro.build.version.release", release)
576+
api_level = int(getprop("ro.build.version.sdk", api_level))
577+
manufacturer = getprop("ro.product.manufacturer", manufacturer)
578+
model = getprop("ro.product.model", model)
579+
device = getprop("ro.product.device", device)
580+
is_emulator = getprop("ro.kernel.qemu", "0") == "1"
581+
582+
return AndroidVer(
583+
release, api_level, manufacturer, model, device, is_emulator)
584+
585+
545586
### System name aliasing
546587

547588
def system_alias(system, release, version):
@@ -972,6 +1013,11 @@ def uname():
9721013
system = 'Windows'
9731014
release = 'Vista'
9741015

1016+
# On Android, return the name and version of the OS rather than the kernel.
1017+
if sys.platform == 'android':
1018+
system = 'Android'
1019+
release = android_ver().release
1020+
9751021
vals = system, node, release, version, machine
9761022
# Replace 'unknown' values with the more portable ''
9771023
_uname_cache = uname_result(*map(_unknown_as_blank, vals))

Lib/test/pythoninfo.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ def collect_platform(info_add):
179179
info_add(f'platform.freedesktop_os_release[{key}]',
180180
os_release[key])
181181

182+
if sys.platform == 'android':
183+
call_func(info_add, 'platform.android_ver', platform, 'android_ver')
184+
182185

183186
def collect_locale(info_add):
184187
import locale

Lib/test/support/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,18 +1801,18 @@ def missing_compiler_executable(cmd_names=[]):
18011801
return cmd[0]
18021802

18031803

1804-
_is_android_emulator = None
1804+
_old_android_emulator = None
18051805
def setswitchinterval(interval):
18061806
# Setting a very low gil interval on the Android emulator causes python
18071807
# to hang (issue #26939).
1808-
minimum_interval = 1e-5
1808+
minimum_interval = 1e-4 # 100 us
18091809
if is_android and interval < minimum_interval:
1810-
global _is_android_emulator
1811-
if _is_android_emulator is None:
1812-
im 10000 port subprocess
1813-
_is_android_emulator = (subprocess.check_output(
1814-
['getprop', 'ro.kernel.qemu']).strip() == b'1')
1815-
if _is_android_emulator:
1810+
global _old_android_emulator
1811+
if _old_android_emulator is None:
1812+
import platform
1813+
av = platform.android_ver()
1814+
_old_android_emulator = av.is_emulator and av.api_level < 24
1815+
if _old_android_emulator:
18161816
interval = minimum_interval
18171817
return sys.setswitchinterval(interval)
18181818

Lib/test/test_asyncio/test_base_events.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import concurrent.futures
44
import errno
55
import math
6+
import platform
67
import socket
78
import sys
89
import threading
@@ -1430,6 +1431,10 @@ def test_create_connection_no_inet_pton(self, m_socket):
14301431
self._test_create_connection_ip_addr(m_socket, False)
14311432

14321433
@patch_socket
1434+
@unittest.skipIf(
1435+
support.is_android and platform.android_ver().api_level < 23,
1436+
"Issue gh-71123: this fails on Android before API level 23"
1437+
)
14331438
def test_create_connection_service_name(self, m_socket):
14341439
m_socket.getaddrinfo = socket.getaddrinfo
14351440
sock = m_socket.socket.return_value

Lib/test/test_platform.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ def test_uname(self):
219219
self.assertEqual(res[-1], res.processor)
220220
self.assertEqual(len(r 7802 es), 6)
221221

222+
if os.name == "posix":
223+
uname = os.uname()
224+
self.assertEqual(res.node, uname.nodename)
225+
self.assertEqual(res.version, uname.version)
226+
self.assertEqual(res.machine, uname.machine)
227+
228+
if sys.platform == "android":
229+
self.assertEqual(res.system, "Android")
230+
self.assertEqual(res.release, platform.android_ver().release)
231+
else:
232+
self.assertEqual(res.system, uname.sysname)
233+
self.assertEqual(res.release, uname.release)
234+
222235
@unittest.skipUnless(sys.platform.startswith('win'), "windows only test")
223236
def test_uname_win32_without_wmi(self):
224237
def raises_oserror(*a):
@@ -458,6 +471,43 @@ def test_libc_ver(self):
458471
self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
459472
('glibc', '1.23.4'))
460473

474+
def test_android_ver(self):
475+
res = platform.android_ver()
476+
self.assertIsInstance(res, tuple)
477+
self.assertEqual(res, (res.release, res.api_level, res.manufacturer,
478+
res.model, res.device, res.is_emulator))
479+
480+
if sys.platform == "android":
481+
for name in ["release", "manufacturer", "model", "device"]:
482+
with self.subTest(name):
483+
value = getattr(res, name)
484+
self.assertIsInstance(value, str)
485+
self.assertNotEqual(value, "")
486+
487+
self.assertIsInstance(res.api_level, int)
488+
self.assertGreaterEqual(res.api_level, sys.getandroidapilevel())
489+
490+
self.assertIsInstance(res.is_emulator, bool)
491+
492+
# When not running on Android, it should return the default values.
493+
else:
494+
self.assertEqual(res.release, "")
495+
self.assertEqual(res.api_level, 0)
496+
self.assertEqual(res.manufacturer, "")
497+
self.assertEqual(res.model, "")
498+
self.assertEqual(res.device, "")
499+
self.assertEqual(res.is_emulator, False)
500+
501+
# Default values may also be overridden using parameters.
502+
res = platform.android_ver(
503+
"alpha", 1, "bravo", "charlie", "delta", True)
504+
self.assertEqual(res.release, "alpha")
505+
self.assertEqual(res.api_level, 1)
506+
self.assertEqual(res.manufacturer, "bravo")
507+
self.assertEqual(res.model, "charlie")
508+
self.assertEqual(res.device, "delta")
509+
self.assertEqual(res.is_emulator, True)
510+
461511
@support.cpython_only
462512
def test__comparable_version(self):
463513
from platform import _comparable_version as V

Lib/test/test_socket.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,10 @@ def socket_setdefaulttimeout(timeout):
209209

210210
HAVE_SOCKET_VSOCK = _have_socket_vsock()
211211

212-
HAVE_SOCKET_UDPLITE = hasattr(socket, "IPPROTO_UDPLITE")
212+
# Older Android versions block UDPLITE with SELinux.
213+
HAVE_SOCKET_UDPLITE = (
214+
hasattr(socket, "IPPROTO_UDPLITE")
215+
and not (support.is_android and platform.android_ver().api_level < 29))
213216

214217
HAVE_SOCKET_BLUETOOTH = _have_socket_bluetooth()
215218

@@ -1217,8 +1220,8 @@ def testGetServBy(self):
12171220
else:
12181221
raise OSError
12191222
# Try same call with optional protocol omitted
1220-
# Issue #26936: Android getservbyname() was broken before API 23.
1221-
if (not support.is_android) or sys.getandroidapilevel() >= 23:
1223+
# Issue gh-71123: this fails on Android before API level 23.
1224+
if not (support.is_android and platform.android_ver().api_level < 23):
12221225
port2 = socket.getservbyname(service)
12231226
eq(port, port2)
12241227
# Try udp, but don't barf if it doesn't exist
@@ -1229,8 +1232,9 @@ def testGetServBy(self):
12291232
else:
12301233
eq(udpport, port)
12311234
# Now make sure the lookup by port returns the same service name
1232-
# Issue #26936: Android getservbyport() is broken.
1233-
if not support.is_android:
1235+
# Issue #26936: when the protocol is omitted, this fails on Android
1236+
# before API level 28.
1237+
if not (support.is_android and platform.android_ver().api_level < 28):
12341238
eq(socket.getservbyport(port2), service)
12351239
eq(socket.getservbyport(port, 'tcp'), service)
12361240
if udpport is not None:
@@ -1575,8 +1579,8 @@ def testGetaddrinfo(self):
15751579
socket.getaddrinfo('::1', 80)
15761580
# port can be a string service name such as "http", a numeric
15771581
# port number or None
1578-
# Issue #26936: Android getaddrinfo() was broken before API level 23.
1579-
if (not support.is_android) or sys.getandroidapilevel() >= 23:
1582+
# Issue #26936: this fails on Android before API level 23.
1583+
if not (support.is_android and platform.android_ver().api_level < 23):
15801584
socket.getaddrinfo(HOST, "http")
15811585
socket.getaddrinfo(HOST, 80)
15821586
socket.getaddrinfo(HOST, None)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`platform.android_ver`, which provides device and OS information
2+
on Android.

0 commit comments

Comments
 (0)
0