""" Android HTTP API — AndroidHttpClient, HttpUrlConnection, HttpResponseCache, SSLCertificateSocketFactory, TimedText 方法签名完全对齐 Android SDK """ from __future__ import annotations import os import sys import io import json import time import socket import hashlib import tempfile import threading import urllib.request import urllib.error import urllib.parse import ssl as _ssl from datetime import datetime from typing import Any, Dict, List, Optional, Set, Tuple, Callable, BinaryIO from http.client import HTTPResponse, HTTPConnection from urllib.request import Request as _UrlRequest from .._core import Stub, AndroidOnlyError, _app_data_dir from ..java.lang import RuntimeException IllegalArgumentException = type("IllegalArgumentException", (RuntimeException,), {}) # ═══════════════════════════════════════════════════════════════ # 常量 # ═══════════════════════════════════════════════════════════════ HTTP_GET = "GET" HTTP_POST = "POST" HTTP_PUT = "PUT" HTTP_DELETE = "DELETE" HTTP_HEAD = "HEAD" HTTP_OPTIONS = "OPTIONS" HTTP_PATCH = "PATCH" HTTP_TRACE = "TRACE" HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 HTTP_201_CREATED = 201 HTTP_202_ACCEPTED = 202 HTTP_204_NO_CONTENT = 204 HTTP_301_MOVED_PERMANENTLY = 301 HTTP_302_FOUND = 302 HTTP_304_NOT_MODIFIED = 304 HTTP_307_TEMPORARY_REDIRECT = 307 HTTP_400_BAD_REQUEST = 400 HTTP_401_UNAUTHORIZED = 401 HTTP_403_FORBIDDEN = 403 HTTP_404_NOT_FOUND = 404 HTTP_405_METHOD_NOT_ALLOWED = 405 HTTP_408_REQUEST_TIMEOUT = 408 HTTP_409_CONFLICT = 409 HTTP_410_GONE = 410 HTTP_429_TOO_MANY_REQUESTS = 429 HTTP_500_INTERNAL_SERVER_ERROR = 500 HTTP_502_BAD_GATEWAY = 502 HTTP_503_SERVICE_UNAVAILABLE = 503 HTTP_504_GATEWAY_TIMEOUT = 504 CHUNK_SIZE = 8192 DEFAULT_CONNECT_TIMEOUT = 30000 DEFAULT_READ_TIMEOUT = 30000 MAX_REDIRECTS = 20 # ═══════════════════════════════════════════════════════════════ # SSLCertificateSocketFactory # ═══════════════════════════════════════════════════════════════ class SSLCertificateSocketFactory(Stub): """SSLCertificateSocketFactory — SSL 证书套接字工厂""" SSL_SECURITY_PROTOCOL_TLS = 1 SSL_SECURITY_PROTOCOL_SSLv3 = 2 SSL_SECURITY_PROTOCOL_DTLS = 3 SSL_SECURITY_PROTOCOL_TLSv1 = 4 SSL_SECURITY_PROTOCOL_TLSv1_1 = 5 SSL_SECURITY_PROTOCOL_TLSv1_2 = 6 SSL_SECURITY_PROTOCOL_TLSv1_3 = 7 def __init__(self, handshakeTimeoutMillis: int = 0): super().__init__() self._handshake_timeout = handshakeTimeoutMillis self._security_protocol = SSLCertificateSocketFactory.SSL_SECURITY_PROTOCOL_TLS self._trust_all = False self._key_manager: Optional[Any] = None self._trust_manager: Optional[Any] = None self._secure_random: Optional[Any] = None self._default_ssl_context = _ssl.create_default_context() @staticmethod def getDefault(handshakeTimeoutMillis: int = 0) -> "SSLCertificateSocketFactory": return SSLCertificateSocketFactory(handshakeTimeoutMillis) @staticmethod def getSocketFactory() -> "SSLCertificateSocketFactory": return SSLCertificateSocketFactory() def createSocket(self, s: Optional[socket.socket] = None, host: str = "", port: int = 443, autoClose: bool = True) -> socket.socket: if s is None: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: context = _ssl.create_default_context() if self._trust_all: context.check_hostname = False context.verify_mode = _ssl.CERT_NONE return context.wrap_socket(s, server_hostname=host if host else None) except Exception: return s def setTrustAll(self, trustAll: bool) -> None: self._trust_all = trustAll def setKeyManager(self, km: Any) -> None: self._key_manager = km def setTrustManager(self, tm: Any) -> None: self._trust_manager = tm def setSecureRandom(self, sr: Any) -> None: self._secure_random = sr def getSecurityProtocol(self) -> int: return self._security_protocol def setSecurityProtocol(self, protocol: int) -> None: self._security_protocol = protocol def getHandshakeTimeout(self) -> int: return self._handshake_timeout def toString(self) -> str: return f"SSLCertificateSocketFactory(protocol={self._security_protocol})" # ═══════════════════════════════════════════════════════════════ # HttpResponseCache # ═══════════════════════════════════════════════════════════════ class HttpResponseCache(Stub): """HttpResponseCache — HTTP 响应缓存""" _instance: Optional["HttpResponseCache"] = None def __init__(self, directory: str, maxSize: int): super().__init__() self._directory = directory self._max_size = maxSize self._current_size = 0 self._cache: Dict[str, Tuple[str, float, int]] = {} self._hit_count = 0 self._miss_count = 0 self._network_count = 0 self._write_success_count = 0 self._write_abort_count = 0 self._lock = threading.Lock() os.makedirs(directory, exist_ok=True) @staticmethod def getDefault() -> Optional["HttpResponseCache"]: return HttpResponseCache._instance @staticmethod def install(directory: str, maxSize: int) -> "HttpResponseCache": cache = HttpResponseCache(directory, maxSize) HttpResponseCache._instance = cache return cache def get(self, key: str) -> Optional[Tuple[str, float]]: with self._lock: entry = self._cache.get(key) if entry: data, expiry, size = entry if time.time() < expiry: self._hit_count += 1 return (data, expiry) else: del self._cache[key] self._current_size -= size self._miss_count += 1 return None self._miss_count += 1 return None def put(self, key: str, data: str, ttl_seconds: int = 300) -> None: with self._lock: old = self._cache.get(key) if old: self._current_size -= old[2] size = len(data.encode("utf-8")) expires = time.time() + ttl_seconds self._cache[key] = (data, expires, size) self._current_size += size self._write_success_count += 1 self._evict_if_needed() def _evict_if_needed(self) -> None: while self._current_size > self._max_size and self._cache: oldest_key = min(self._cache.keys(), key=lambda k: self._cache[k][1]) oldest = self._cache.pop(oldest_key) self._current_size -= oldest[2] self._write_abort_count += 1 def flush(self) -> None: pass def delete(self) -> None: with self._lock: self._cache.clear() self._current_size = 0 HttpResponseCache._instance = None def maxSize(self) -> int: return self._max_size def size(self) -> int: with self._lock: return self._current_size def hitCount(self) -> int: return self._hit_count def missCount(self) -> int: return self._miss_count def networkCount(self) -> int: return self._network_count def writeSuccessCount(self) -> int: return self._write_success_count def writeAbortCount(self) -> int: return self._write_abort_count def isCached(self, key: str) -> bool: with self._lock: return key in self._cache def clear(self) -> None: with self._lock: self._cache.clear() self._current_size = 0 def toString(self) -> str: return ( f"HttpResponseCache(size={self._current_size}/{self._max_size}, " f"hits={self._hit_count}, misses={self._miss_count})" ) # ═══════════════════════════════════════════════════════════════ # TimedText # ═══════════════════════════════════════════════════════════════ class TimedText(Stub): """TimedText — 定时文本(字幕)""" def __init__(self, text: str = "", startMs: int = 0, endMs: int = 0): super().__init__() self._text = text self._start_ms = startMs self._end_ms = endMs self._style: Optional[Dict[str, Any]] = None self._subtitle_id = 0 self._track_id = 0 self._mime_type: Optional[str] = None self._language: Optional[str] = None self._flow = False def getText(self) -> str: return self._text def setText(self, text: str) -> None: self._text = text def getStartMs(self) -> int: return self._start_ms def setStartMs(self, startMs: int) -> None: self._start_ms = startMs def getEndMs(self) -> int: return self._end_ms def setEndMs(self, endMs: int) -> None: self._end_ms = endMs def getDurationMs(self) -> int: return self._end_ms - self._start_ms def getStyle(self) -> Optional[Dict[str, Any]]: return self._style def setStyle(self, style: Dict[str, Any]) -> None: self._style = dict(style) def getLanguage(self) -> Optional[str]: return self._language def setLanguage(self, lang: str) -> None: self._language = lang def getMimeType(self) -> Optional[str]: return self._mime_type def setMimeType(self, mimeType: str) -> None: self._mime_type = mimeType def isFlow(self) -> bool: return self._flow def setFlow(self, flow: bool) -> None: self._flow = flow def toString(self) -> str: return f"TimedText(text={self._text[:50]}, start={self._start_ms}, end={self._end_ms})" # ═══════════════════════════════════════════════════════════════ # HttpUrlConnection (扩展) # ═══════════════════════════════════════════════════════════════ class HttpUrlConnection(Stub): """HttpURLConnection (扩展) — HTTP 连接管理""" def __init__(self, url: str = ""): super().__init__() self._url = url self._method = HTTP_GET self._connect_timeout = DEFAULT_CONNECT_TIMEOUT self._read_timeout = DEFAULT_READ_TIMEOUT self._request_properties: Dict[str, str] = {} self._follow_redirects = True self._instance_follow_redirects = True self._use_caches = True self._allow_user_interaction = False self._do_input = True self._do_output = False self._if_modified_since = 0 self._fixed_length = -1 self._fixed_stream_mode = False self._chunk_length = 0 self._response_code = -1 self._response_message: Optional[str] = None self._content_length = -1 self._content_type: Optional[str] = None self._content_encoding: Optional[str] = None self._expiration: Optional[int] = None self._date: Optional[int] = None self._last_modified: Optional[int] = None self._header_fields: Dict[str, List[str]] = {} self._input_stream: Optional[BinaryIO] = None self._output_stream: Optional[BinaryIO] = None self._connected = False self._sslsocket_factory: Optional[SSLCertificateSocketFactory] = None def setRequestMethod(self, method: str) -> None: self._method = method.upper() def getRequestMethod(self) -> str: return self._method def setConnectTimeout(self, timeoutMillis: int) -> None: self._connect_timeout = timeoutMillis def getConnectTimeout(self) -> int: return self._connect_timeout def setReadTimeout(self, timeoutMillis: int) -> None: self._read_timeout = timeoutMillis def getReadTimeout(self) -> int: return self._read_timeout def setRequestProperty(self, key: str, value: str) -> None: self._request_properties[key] = value def addRequestProperty(self, key: str, value: str) -> None: if key in self._request_properties: self._request_properties[key] += ", " + value else: self._request_properties[key] = value def getRequestProperty(self, key: str) -> Optional[str]: return self._request_properties.get(key) def getRequestProperties(self) -> Dict[str, str]: return dict(self._request_properties) def setInstanceFollowRedirects(self, follow: bool) -> None: self._instance_follow_redirects = follow def getInstanceFollowRedirects(self) -> bool: return self._instance_follow_redirects def setFollowRedirects(self, follow: bool) -> None: self._follow_redirects = follow def getFollowRedirects(self) -> bool: return self._follow_redirects def setUseCaches(self, use: bool) -> None: self._use_caches = use def getUseCaches(self) -> bool: return self._use_caches def setAllowUserInteraction(self, allow: bool) -> None: self._allow_user_interaction = allow def getAllowUserInteraction(self) -> bool: return self._allow_user_interaction def setDoInput(self, do: bool) -> None: self._do_input = do def getDoInput(self) -> bool: return self._do_input def setDoOutput(self, do: bool) -> None: self._do_output = do def getDoOutput(self) -> bool: return self._do_output def setIfModifiedSince(self, ifModifiedSince: int) -> None: self._if_modified_since = ifModifiedSince def getIfModifiedSince(self) -> int: return self._if_modified_since def setFixedLengthStreamingMode(self, contentLength: int) -> None: self._fixed_length = contentLength self._fixed_stream_mode = True def setChunkedStreamingMode(self, chunklen: int) -> None: self._chunk_length = chunklen def getHeaderField(self, name: str) -> Optional[str]: values = self._header_fields.get(name) return values[0] if values else None def getHeaderFields(self) -> Dict[str, List[str]]: return dict(self._header_fields) def getHeaderFieldKey(self, n: int) -> Optional[str]: keys = list(self._header_fields.keys()) if 0 <= n < len(keys): return keys[n] return None def getHeaderFieldInt(self, name: str, default: int) -> int: val = self.getHeaderField(name) if val: try: return int(val) except ValueError: pass return default def getHeaderFieldLong(self, name: str, default: int) -> int: return self.getHeaderFieldInt(name, default) def getHeaderFieldDate(self, name: str, default: int) -> int: return default def getContentLength(self) -> int: return self._content_length def getContentLengthLong(self) -> int: return self._content_length def getContentType(self) -> Optional[str]: return self._content_type def getContentEncoding(self) -> Optional[str]: return self._content_encoding def getExpiration(self) -> int: return self._expiration or 0 def getDate(self) -> int: return self._date or 0 def getLastModified(self) -> int: return self._last_modified or 0 def getResponseCode(self) -> int: if not self._connected: self.connect() return self._response_code def getResponseMessage(self) -> Optional[str]: return self._response_message def getInputStream(self) -> Optional[BinaryIO]: if not self._connected: self.connect() return self._input_stream def getOutputStream(self) -> Optional[BinaryIO]: if not self._connected: self.connect() return self._output_stream def getErrorStream(self) -> Optional[BinaryIO]: return None def connect(self) -> None: if self._connected: return self._connected = True try: req = _UrlRequest(self._url, method=self._method.encode() if hasattr(self._method, 'encode') else self._method.encode()) for k, v in self._request_properties.items(): req.add_header(k, v) self._response_code = 200 self._response_message = "OK" self._content_type = "text/plain" self._content_length = 0 except Exception as e: self._response_code = 0 self._response_message = str(e) def disconnect(self) -> None: self._connected = False self._input_stream = None self._output_stream = None def getURL(self) -> str: return self._url def getPermission(self) -> Optional[Any]: return None def toString(self) -> str: return f"HttpURLConnection(url={self._url}, method={self._method})" # ═══════════════════════════════════════════════════════════════ # AndroidHttpClient # ═══════════════════════════════════════════════════════════════ class AndroidHttpClient(Stub): """AndroidHttpClient — Android HTTP 客户端 (扩展)""" def __init__(self, userAgent: str = "AndroidHttpClient"): super().__init__() self._user_agent = userAgent self._connect_timeout = DEFAULT_CONNECT_TIMEOUT self._read_timeout = DEFAULT_READ_TIMEOUT self._follow_redirects = True self._compression_enabled = True self._cookie_store: Dict[str, Dict[str, str]] = {} self._auth_cache: Dict[str, Tuple[str, str]] = {} self._lock = threading.Lock() @staticmethod def newInstance(userAgent: str = "AndroidHttpClient") -> "AndroidHttpClient": return AndroidHttpClient(userAgent) def execute(self, request: Any) -> Any: return None def executeHttp(self, request: Any, context: Any) -> Any: return None def execute(self, host: Any, request: Any) -> Any: return None def execute(self, host: Any, request: Any, context: Any) -> Any: return None def getConnectionManager(self) -> Any: return None def getCredentialsProvider(self) -> Any: return None def getCookieStore(self) -> Any: return None def getAuthSchemeRegistry(self) -> Any: return None def enableCurlLogging(self, tag: str, level: int) -> None: pass def disableCurlLogging(self) -> None: pass def setUserAgent(self, userAgent: str) -> None: self._user_agent = userAgent def getConnectTimeout(self) -> int: return self._connect_timeout def setConnectTimeout(self, timeout: int) -> None: self._connect_timeout = timeout def getReadTimeout(self) -> int: return self._read_timeout def setReadTimeout(self, timeout: int) -> None: self._read_timeout = timeout def isCompressionEnabled(self) -> bool: return self._compression_enabled def setCompressionEnabled(self, enabled: bool) -> None: self._compression_enabled = enabled def clearCredentials(self) -> None: with self._lock: self._auth_cache.clear() def clearCookies(self) -> None: with self._lock: self._cookie_store.clear() def close(self) -> None: pass @staticmethod def modifyRequestToAcceptGzipResponse(request: Any) -> Any: return request @staticmethod def getUngzippedContent(entity: Any) -> Any: return entity def toString(self) -> str: return f"AndroidHttpClient(user_agent={self._user_agent})"