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,
    }