David Prince
production: clean source snapshot — no history bloat
71b4454
Raw
History Blame Contribute Delete
5.91 kB
"""
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"<title[^>]*>([^<]+)</title>", html, re.I)
title = title_match.group(1).strip() if title_match else query
# Extract meta description
desc_match = re.search(
r'<meta[^>]+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"<h[12][^>]*>([^<]+)</h[12]>", 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,
}