Spaces:
Sleeping
Sleeping
File size: 5,909 Bytes
71b4454 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | """
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,
}
|