Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| } | |