https://github.com/python/cpython/commit/74c8568d07719529b874897598d8b3bc25ff0434
commit: 74c8568d07719529b874897598d8b3bc25ff0434
branch: main
author: Malcolm Smith <[email protected]>
committer: vstinner <[email protected]>
date: 2024-03-27T17:53:27+01:00
summary:

gh-71042: Add `platform.android_ver` (#116674)

files:
A Misc/NEWS.d/next/Library/2024-03-12-19-32-17.gh-issue-71042.oI0Ron.rst
M Doc/library/platform.rst
M Doc/library/sys.rst
M Lib/platform.py
M Lib/test/pythoninfo.py
M Lib/test/support/__init__.py
M Lib/test/test_asyncio/test_base_events.py
M Lib/test/test_platform.py
M Lib/test/test_socket.py

diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst
index 4bc3956449b930..6af9168d15749f 100644
--- a/Doc/library/platform.rst
+++ b/Doc/library/platform.rst
@@ -301,3 +301,39 @@ Linux Platforms
           return ids
 
    .. versionadded:: 3.10
+
+
+Android Platform
+----------------
+
+.. function:: android_ver(release="", api_level=0, manufacturer="", \
+                          model="", device="", is_emulator=False)
+
+   Get Android device information. Returns a :func:`~collections.namedtuple`
+   with the following attributes. Values which cannot be determined are set to
+   the defaults given as parameters.
+
+   * ``release`` - Android version, as a string (e.g. ``"14"``).
+
+   * ``api_level`` - API level of the running device, as an integer (e.g. 
``34``
+     for Android 14). To get the API level which Python was built against, see
+     :func:`sys.getandroidapilevel`.
+
+   * ``manufacturer`` - `Manufacturer name
+     
<https://developer.android.com/reference/android/os/Build#MANUFACTURER>`__.
+
+   * ``model`` - `Model name
+     <https://developer.android.com/reference/android/os/Build#MODEL>`__ –
+     typically the marketing name or model number.
+
+   * ``device`` - `Device name
+     <https://developer.android.com/reference/android/os/Build#DEVICE>`__ –
+     typically the model number or a codename.
+
+   * ``is_emulator`` - ``True`` if the device is an emulator; ``False`` if it's
+     a physical device.
+
+   Google maintains a `list of known model and device names
+   <https://storage.googleapis.com/play_public/supported_devices.html>`__.
+
+   .. versionadded:: 3.13
diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst
index 087a3454c33272..19d6856efe5d09 100644
--- a/Doc/library/sys.rst
+++ b/Doc/library/sys.rst
@@ -753,7 +753,9 @@ always available.
 
 .. function:: getandroidapilevel()
 
-   Return the build time API version of Android as an integer.
+   Return the build-time API level of Android as an integer. This represents 
the
+   minimum version of Android this build of Python can run on. For runtime
+   version information, see :func:`platform.android_ver`.
 
    .. availability:: Android.
 
diff --git a/Lib/platform.py b/Lib/platform.py
index 2756f298f9676f..df1d987036455f 100755
--- a/Lib/platform.py
+++ b/Lib/platform.py
@@ -542,6 +542,47 @@ def java_ver(release='', vendor='', vminfo=('', '', ''), 
osinfo=('', '', '')):
 
     return release, vendor, vminfo, osinfo
 
+
+AndroidVer = collections.namedtuple(
+    "AndroidVer", "release api_level manufacturer model device is_emulator")
+
+def android_ver(release="", api_level=0, manufacturer="", model="", device="",
+                is_emulator=False):
+    if sys.platform == "android":
+        try:
+            from ctypes import CDLL, c_char_p, create_string_buffer
+        except ImportError:
+            pass
+        else:
+            # An NDK developer confirmed that this is an officially-supported
+            # API (https://stackoverflow.com/a/28416743). Use `getattr` to 
avoid
+            # private name mangling.
+            system_property_get = getattr(CDLL("libc.so"), 
"__system_property_get")
+            system_property_get.argtypes = (c_char_p, c_char_p)
+
+            def getprop(name, default):
+                # 
https://android.googlesource.com/platform/bionic/+/refs/tags/android-5.0.0_r1/libc/include/sys/system_properties.h#39
+                PROP_VALUE_MAX = 92
+                buffer = create_string_buffer(PROP_VALUE_MAX)
+                length = system_property_get(name.encode("UTF-8"), buffer)
+                if length == 0:
+                    # This API doesn’t distinguish between an empty property 
and
+                    # a missing one.
+                    return default
+                else:
+                    return buffer.value.decode("UTF-8", "backslashreplace")
+
+            release = getprop("ro.build.version.release", release)
+            api_level = int(getprop("ro.build.version.sdk", api_level))
+            manufacturer = getprop("ro.product.manufacturer", manufacturer)
+            model = getprop("ro.product.model", model)
+            device = getprop("ro.product.device", device)
+            is_emulator = getprop("ro.kernel.qemu", "0") == "1"
+
+    return AndroidVer(
+        release, api_level, manufacturer, model, device, is_emulator)
+
+
 ### System name aliasing
 
 def system_alias(system, release, version):
@@ -972,6 +1013,11 @@ def uname():
         system = 'Windows'
         release = 'Vista'
 
+    # On Android, return the name and version of the OS rather than the kernel.
+    if sys.platform == 'android':
+        system = 'Android'
+        release = android_ver().release
+
     vals = system, node, release, version, machine
     # Replace 'unknown' values with the more portable ''
     _uname_cache = uname_result(*map(_unknown_as_blank, vals))
diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py
index 814358746d6d8a..5612c55746a516 100644
--- a/Lib/test/pythoninfo.py
+++ b/Lib/test/pythoninfo.py
@@ -179,6 +179,9 @@ def collect_platform(info_add):
             info_add(f'platform.freedesktop_os_release[{key}]',
                      os_release[key])
 
+    if sys.platform == 'android':
+        call_func(info_add, 'platform.android_ver', platform, 'android_ver')
+
 
 def collect_locale(info_add):
     import locale
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index a1c7987fa0db47..3d7868768231f5 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -1801,18 +1801,18 @@ def missing_compiler_executable(cmd_names=[]):
             return cmd[0]
 
 
-_is_android_emulator = None
+_old_android_emulator = None
 def setswitchinterval(interval):
     # Setting a very low gil interval on the Android emulator causes python
     # to hang (issue #26939).
-    minimum_interval = 1e-5
+    minimum_interval = 1e-4   # 100 us
     if is_android and interval < minimum_interval:
-        global _is_android_emulator
-        if _is_android_emulator is None:
-            import subprocess
-            _is_android_emulator = (subprocess.check_output(
-                               ['getprop', 'ro.kernel.qemu']).strip() == b'1')
-        if _is_android_emulator:
+        global _old_android_emulator
+        if _old_android_emulator is None:
+            import platform
+            av = platform.android_ver()
+            _old_android_emulator = av.is_emulator and av.api_level < 24
+        if _old_android_emulator:
             interval = minimum_interval
     return sys.setswitchinterval(interval)
 
diff --git a/Lib/test/test_asyncio/test_base_events.py 
b/Lib/test/test_asyncio/test_base_events.py
index 4cd872d3a5b2d8..c14a0bb180d79b 100644
--- a/Lib/test/test_asyncio/test_base_events.py
+++ b/Lib/test/test_asyncio/test_base_events.py
@@ -3,6 +3,7 @@
 import concurrent.futures
 import errno
 import math
+import platform
 import socket
 import sys
 import threading
@@ -1430,6 +1431,10 @@ def test_create_connection_no_inet_pton(self, m_socket):
         self._test_create_connection_ip_addr(m_socket, False)
 
     @patch_socket
+    @unittest.skipIf(
+        support.is_android and platform.android_ver().api_level < 23,
+        "Issue gh-71123: this fails on Android before API level 23"
+    )
     def test_create_connection_service_name(self, m_socket):
         m_socket.getaddrinfo = socket.getaddrinfo
         sock = m_socket.socket.return_value
diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py
index 9f8aeeea257311..57f27b247d9d15 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -219,6 +219,19 @@ def test_uname(self):
         self.assertEqual(res[-1], res.processor)
         self.assertEqual(len(res), 6)
 
+        if os.name == "posix":
+            uname = os.uname()
+            self.assertEqual(res.node, uname.nodename)
+            self.assertEqual(res.version, uname.version)
+            self.assertEqual(res.machine, uname.machine)
+
+            if sys.platform == "android":
+                self.assertEqual(res.system, "Android")
+                self.assertEqual(res.release, platform.android_ver().release)
+            else:
+                self.assertEqual(res.system, uname.sysname)
+                self.assertEqual(res.release, uname.release)
+
     @unittest.skipUnless(sys.platform.startswith('win'), "windows only test")
     def test_uname_win32_without_wmi(self):
         def raises_oserror(*a):
@@ -458,6 +471,43 @@ def test_libc_ver(self):
         self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
                          ('glibc', '1.23.4'))
 
+    def test_android_ver(self):
+        res = platform.android_ver()
+        self.assertIsInstance(res, tuple)
+        self.assertEqual(res, (res.release, res.api_level, res.manufacturer,
+                               res.model, res.device, res.is_emulator))
+
+        if sys.platform == "android":
+            for name in ["release", "manufacturer", "model", "device"]:
+                with self.subTest(name):
+                    value = getattr(res, name)
+                    self.assertIsInstance(value, str)
+                    self.assertNotEqual(value, "")
+
+            self.assertIsInstance(res.api_level, int)
+            self.assertGreaterEqual(res.api_level, sys.getandroidapilevel())
+
+            self.assertIsInstance(res.is_emulator, bool)
+
+        # When not running on Android, it should return the default values.
+        else:
+            self.assertEqual(res.release, "")
+            self.assertEqual(res.api_level, 0)
+            self.assertEqual(res.manufacturer, "")
+            self.assertEqual(res.model, "")
+            self.assertEqual(res.device, "")
+            self.assertEqual(res.is_emulator, False)
+
+            # Default values may also be overridden using parameters.
+            res = platform.android_ver(
+                "alpha", 1, "bravo", "charlie", "delta", True)
+            self.assertEqual(res.release, "alpha")
+            self.assertEqual(res.api_level, 1)
+            self.assertEqual(res.manufacturer, "bravo")
+            self.assertEqual(res.model, "charlie")
+            self.assertEqual(res.device, "delta")
+            self.assertEqual(res.is_emulator, True)
+
     @support.cpython_only
     def test__comparable_version(self):
         from platform import _comparable_version as V
diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py
index a7e657f5718524..661a859b0d0601 100644
--- a/Lib/test/test_socket.py
+++ b/Lib/test/test_socket.py
@@ -209,7 +209,10 @@ def socket_setdefaulttimeout(timeout):
 
 HAVE_SOCKET_VSOCK = _have_socket_vsock()
 
-HAVE_SOCKET_UDPLITE = hasattr(socket, "IPPROTO_UDPLITE")
+# Older Android versions block UDPLITE with SELinux.
+HAVE_SOCKET_UDPLITE = (
+    hasattr(socket, "IPPROTO_UDPLITE")
+    and not (support.is_android and platform.android_ver().api_level < 29))
 
 HAVE_SOCKET_BLUETOOTH = _have_socket_bluetooth()
 
@@ -1217,8 +1220,8 @@ def testGetServBy(self):
         else:
             raise OSError
         # Try same call with optional protocol omitted
-        # Issue #26936: Android getservbyname() was broken before API 23.
-        if (not support.is_android) or sys.getandroidapilevel() >= 23:
+        # Issue gh-71123: this fails on Android before API level 23.
+        if not (support.is_android and platform.android_ver().api_level < 23):
             port2 = socket.getservbyname(service)
             eq(port, port2)
         # Try udp, but don't barf if it doesn't exist
@@ -1229,8 +1232,9 @@ def testGetServBy(self):
         else:
             eq(udpport, port)
         # Now make sure the lookup by port returns the same service name
-        # Issue #26936: Android getservbyport() is broken.
-        if not support.is_android:
+        # Issue #26936: when the protocol is omitted, this fails on Android
+        # before API level 28.
+        if not (support.is_android and platform.android_ver().api_level < 28):
             eq(socket.getservbyport(port2), service)
         eq(socket.getservbyport(port, 'tcp'), service)
         if udpport is not None:
@@ -1575,8 +1579,8 @@ def testGetaddrinfo(self):
             socket.getaddrinfo('::1', 80)
         # port can be a string service name such as "http", a numeric
         # port number or None
-        # Issue #26936: Android getaddrinfo() was broken before API level 23.
-        if (not support.is_android) or sys.getandroidapilevel() >= 23:
+        # Issue #26936: this fails on Android before API level 23.
+        if not (support.is_android and platform.android_ver().api_level < 23):
             socket.getaddrinfo(HOST, "http")
         socket.getaddrinfo(HOST, 80)
         socket.getaddrinfo(HOST, None)
diff --git 
a/Misc/NEWS.d/next/Library/2024-03-12-19-32-17.gh-issue-71042.oI0Ron.rst 
b/Misc/NEWS.d/next/Library/2024-03-12-19-32-17.gh-issue-71042.oI0Ron.rst
new file mode 100644
index 00000000000000..3641cbb9b2fc1a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-03-12-19-32-17.gh-issue-71042.oI0Ron.rst
@@ -0,0 +1,2 @@
+Add :func:`platform.android_ver`, which provides device and OS information
+on Android.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to