""" Real Android Developer documentation lookup. Fetches from developer.android.com search and reference pages. No authentication required. """ from __future__ import annotations import logging import re import time from typing import Any from urllib.parse import quote_plus import httpx logger = logging.getLogger("dolor3v.tools.android_docs") _BASE = "https://developer.android.com" _SEARCH_URL = f"{_BASE}/s/results" _HEADERS = { "User-Agent": ( "Mozilla/5.0 (Linux; Android 14; Pixel 8) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Mobile Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", } # Curated fast-path: well-known Android API references _KNOWN_REFS: dict[str, str] = { "activity": "/reference/android/app/Activity", "fragment": "/reference/androidx/fragment/app/Fragment", "viewmodel": "/reference/androidx/lifecycle/ViewModel", "livedata": "/reference/androidx/lifecycle/LiveData", "room": "/reference/androidx/room/package-summary", "compose": "/jetpack/compose", "navigation": "/guide/navigation", "workmanager": "/reference/androidx/work/WorkManager", "coroutines": "/kotlin/coroutines", "hilt": "/training/dependency-injection/hilt-android", "retrofit": "https://square.github.io/retrofit/", "jetpack": "/jetpack", "manifest": "/guide/topics/manifest/manifest-intro", "permissions": "/guide/topics/permissions/overview", "intent": "/reference/android/content/Intent", "service": "/reference/android/app/Service", "broadcastreceiver": "/reference/android/content/BroadcastReceiver", "contentprovider": "/reference/android/content/ContentProvider", "recyclerview": "/reference/androidx/recyclerview/widget/RecyclerView", "constraintlayout": "/reference/androidx/constraintlayout/widget/ConstraintLayout", "gradle": "/build/releases/gradle-plugin", "apk": "/studio/build/build-variants", "aab": "/guide/app-bundle", "proguard": "/studio/build/shrink-code", "keystore": "/training/articles/keystore", } def _fast_path(query: str) -> str | None: q = query.lower().strip() for key, path in _KNOWN_REFS.items(): if key in q: return path if path.startswith("http") else f"{_BASE}{path}" return None async def lookup_android_docs( query: str, max_results: int = 5, ) -> dict[str, Any]: """ Fetch real Android Developer documentation. Tries fast-path known references first, then falls back to search. """ t0 = time.monotonic() # Fast path for known APIs fast_url = _fast_path(query) results = [] async with httpx.AsyncClient( timeout=20.0, follow_redirects=True, headers=_HEADERS, ) as client: if fast_url: try: resp = await client.get(fast_url) if resp.status_code == 200: # Extract title and description from HTML html = resp.text title_match = re.search(r"]*>([^<]+)", html, re.I) title = title_match.group(1).strip() if title_match else query # Extract meta description desc_match = re.search( r']+name=["\']description["\'][^>]+content=["\']([^"\']+)["\']', html, re.I ) description = desc_match.group(1).strip() if desc_match else "" # Extract h1/h2 headings as sections headings = re.findall(r"]*>([^<]+)", html, re.I) sections = [re.sub(r"\s+", " ", h).strip() for h in headings[:8]] results.append({ "title": title, "url": fast_url, "description": description, "sections": sections, "source": "fast_path", }) except Exception as exc: logger.warning("Fast path fetch failed for %s: %s", fast_url, exc) # Search fallback if not results: try: search_url = f"{_BASE}/s/results?q={quote_plus(query)}" resp = await client.get(search_url) html = resp.text if resp.status_code == 200 else "" # Parse search result links links = re.findall( r'href=["\'](/(?:reference|guide|training|jetpack|develop|studio)[^"\'#?]*)["\']', html, ) seen: set[str] = set() for link in links: if link not in seen: seen.add(link) full_url = f"{_BASE}{link}" results.append({ "title": link.split("/")[-1].replace("-", " ").title(), "url": full_url, "description": "", "source": "search", }) if len(results) >= max_results: break except Exception as exc: logger.warning("Android docs search failed: %s", exc) # If still nothing, return known reference index if not results: results = [ {"title": k.title(), "url": f"{_BASE}{v}" if not v.startswith("http") else v, "source": "index"} for k, v in list(_KNOWN_REFS.items())[:max_results] ] latency_ms = round((time.monotonic() - t0) * 1000) return { "query": query, "results": results[:max_results], "latency_ms": latency_ms, "source_base": _BASE, }