adb-toolkit/frontend/src/components/views/ViewAppInspect.tsx

278 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { Search, Package, Shield, Activity, Server, Database, Cpu, FileCode, AlertTriangle } from 'lucide-react'
import { InspectApp, CheckPinning, ListPackages } from '../../lib/wails'
import { notify } from '../../lib/notify'
import type { AppInspection, PackageInfo } from '../../lib/types'
export default function ViewAppInspect() {
const [search, setSearch] = useState('')
const [packages, setPackages] = useState<PackageInfo[]>([])
const [pkgsLoaded, setPkgsLoaded] = useState(false)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<AppInspection | null>(null)
const [pinning, setPinning] = useState('')
const [activeTab, setActiveTab] = useState('overview')
const [showManifest, setShowManifest] = useState(false)
const loadPackages = async () => {
if (pkgsLoaded) return
try {
const pkgs = await ListPackages('all')
setPackages(pkgs || [])
setPkgsLoaded(true)
} catch {}
}
const inspect = async (pkg: string) => {
if (!pkg.trim()) return
setLoading(true)
setResult(null)
setPinning('')
setActiveTab('overview')
try {
const data = await InspectApp(pkg.trim())
setResult(data)
} catch (e: any) {
notify.error(e)
} finally {
setLoading(false)
}
}
const checkPinning = async () => {
if (!result) return
try {
const out = await CheckPinning(result.packageName)
setPinning(out)
} catch (e: any) {
notify.error(e)
}
}
const filtered = packages.filter(p =>
p.packageName.toLowerCase().includes(search.toLowerCase())
).slice(0, 20)
const tabs = [
{ id: 'overview', label: 'Overview', icon: <Package size={12} /> },
{ id: 'permissions', label: `Permissions (${result?.permissions?.length || 0})`, icon: <Shield size={12} /> },
{ id: 'components', label: 'Components', icon: <Activity size={12} /> },
{ id: 'libs', label: 'Native Libs', icon: <Cpu size={12} /> },
{ id: 'cert', label: 'Signing Cert', icon: <FileCode size={12} /> },
{ id: 'manifest', label: 'Full Dump', icon: <Database size={12} /> },
]
return (
<div className="flex h-full overflow-hidden">
{/* Left: package picker */}
<div className="w-64 shrink-0 border-r border-bg-border flex flex-col overflow-hidden">
<div className="p-3 border-b border-bg-border space-y-2 shrink-0">
<p className="section-title">App Inspector</p>
<div className="relative">
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-muted" />
<input
className="input pl-7 text-xs w-full"
placeholder="Package name..."
value={search}
onChange={e => setSearch(e.target.value)}
onFocus={loadPackages}
onKeyDown={e => e.key === 'Enter' && inspect(search)}
/>
</div>
<button onClick={() => inspect(search)} disabled={!search || loading} className="btn-primary text-xs w-full justify-center">
{loading ? 'Inspecting...' : 'Inspect'}
</button>
</div>
<div className="flex-1 overflow-auto">
{filtered.map(p => (
<button
key={p.packageName}
onClick={() => { setSearch(p.packageName); inspect(p.packageName) }}
className="w-full text-left px-3 py-2 text-xs text-text-secondary hover:bg-bg-raised hover:text-text-primary transition-colors border-b border-bg-border/30"
>
<p className="truncate mono">{p.packageName}</p>
<p className={p.isEnabled ? 'text-accent-green' : 'text-danger'}>{p.isEnabled ? 'enabled' : 'disabled'}</p>
</button>
))}
{!pkgsLoaded && (
<p className="text-text-muted text-xs text-center p-4">Type to search or focus to load package list</p>
)}
</div>
</div>
{/* Right: inspection results */}
<div className="flex-1 flex flex-col overflow-hidden">
{!result && !loading && (
<div className="flex flex-col items-center justify-center h-full gap-3 text-text-muted">
<Package size={32} className="opacity-20" />
<p className="text-sm">Select a package to inspect</p>
</div>
)}
{loading && (
<div className="flex items-center justify-center h-full">
<div className="w-6 h-6 border-2 border-accent-green border-t-transparent rounded-full animate-spin" />
</div>
)}
{result && (
<>
{/* Package header */}
<div className="border-b border-bg-border px-4 py-3 flex items-start justify-between shrink-0">
<div>
<p className="mono text-sm text-text-primary">{result.packageName}</p>
<p className="text-xs text-text-muted mt-0.5">
v{result.versionName} (code {result.versionCode}) · SDK {result.minSdk}{result.targetSdk}
</p>
<div className="flex gap-2 mt-1.5 flex-wrap">
{result.isSystem && <span className="badge-gray">system</span>}
{result.isDebuggable && <span className="badge-yellow">debuggable</span>}
{result.isEnabled
? <span className="badge-green">enabled</span>
: <span className="badge-red">disabled</span>
}
<span className="badge-gray">UID {result.uid}</span>
</div>
</div>
<button onClick={checkPinning} className="btn-ghost text-xs shrink-0">
<AlertTriangle size={12} /> Check Pinning
</button>
</div>
{/* Pinning result */}
{pinning && (
<div className="border-b border-warn/20 bg-warn/5 px-4 py-2 text-xs shrink-0">
<pre className="whitespace-pre-wrap text-warn/90">{pinning}</pre>
</div>
)}
{/* Tabs */}
<div className="border-b border-bg-border flex shrink-0 overflow-x-auto">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-1.5 px-3 py-2 text-xs whitespace-nowrap border-b-2 transition-colors ${
activeTab === tab.id
? 'border-accent-green text-accent-green'
: 'border-transparent text-text-muted hover:text-text-secondary'
}`}
>
{tab.icon} {tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="flex-1 overflow-auto p-4">
{activeTab === 'overview' && (
<div className="grid grid-cols-2 gap-x-8 gap-y-2">
{[
{ label: 'Install path', value: result.installPath },
{ label: 'Data dir', value: result.dataDir },
{ label: 'Installed by', value: result.installer || 'Unknown' },
{ label: 'First installed', value: result.firstInstall },
{ label: 'Last updated', value: result.lastUpdated },
{ label: 'UID', value: result.uid },
{ label: 'Target SDK', value: result.targetSdk },
{ label: 'Min SDK', value: result.minSdk },
].map(({ label, value }) => (
<div key={label} className="flex gap-2 min-w-0">
<span className="text-text-muted text-xs w-28 shrink-0">{label}</span>
<span className="text-xs text-text-primary mono truncate">{value || 'N/A'}</span>
</div>
))}
</div>
)}
{activeTab === 'permissions' && (
<div className="space-y-1">
{result.permissions?.length === 0 && (
<p className="text-text-muted text-xs">No permissions detected</p>
)}
{result.permissions?.map(p => (
<div key={p} className="flex items-center gap-2 py-1 border-b border-bg-border/30">
<Shield size={11} className="text-warn shrink-0" />
<span className="mono text-xs text-text-secondary">{p}</span>
</div>
))}
</div>
)}
{activeTab === 'components' && (
<div className="space-y-4">
{[
{ label: 'Activities', items: result.activities, icon: <Activity size={11} /> },
{ label: 'Services', items: result.services, icon: <Server size={11} /> },
{ label: 'Receivers', items: result.receivers, icon: <Activity size={11} /> },
{ label: 'Providers', items: result.providers, icon: <Database size={11} /> },
].map(({ label, items, icon }) => (
<div key={label}>
<p className="section-title mb-2">{label} ({items?.length || 0})</p>
{!items?.length && <p className="text-text-muted text-xs">None detected</p>}
{items?.map(item => (
<div key={item} className="flex items-center gap-2 py-1 border-b border-bg-border/30">
<span className="text-text-muted shrink-0">{icon}</span>
<span className="mono text-xs text-text-secondary">{item}</span>
</div>
))}
</div>
))}
</div>
)}
{activeTab === 'libs' && (
<div className="space-y-1">
{!result.nativeLibs?.length && (
<p className="text-text-muted text-xs">No native libraries detected</p>
)}
{result.nativeLibs?.map(lib => (
<div key={lib} className="flex items-center gap-2 py-1 border-b border-bg-border/30">
<Cpu size={11} className="text-text-muted shrink-0" />
<span className="mono text-xs text-text-secondary">{lib}</span>
</div>
))}
</div>
)}
{activeTab === 'cert' && (
<div className="space-y-3">
{[
{ label: 'Subject', value: result.certSubject },
{ label: 'Issuer', value: result.certIssuer },
{ label: 'Expires', value: result.certExpiry },
{ label: 'SHA-256', value: result.certSha256 },
].map(({ label, value }) => (
<div key={label}>
<p className="text-xs text-text-muted mb-0.5">{label}</p>
<p className="mono text-xs text-text-primary bg-bg-raised rounded px-3 py-1.5 break-all">
{value || 'Not available — run aapt or apksigner manually for cert details'}
</p>
</div>
))}
</div>
)}
{activeTab === 'manifest' && (
<div>
<button
onClick={() => setShowManifest(v => !v)}
className="btn-ghost text-xs mb-3"
>
{showManifest ? 'Hide' : 'Show'} full package dump ({result.manifestDump?.split('\n').length} lines)
</button>
{showManifest && (
<pre className="mono text-xs text-text-secondary whitespace-pre-wrap break-words leading-relaxed bg-bg-raised rounded p-3 border border-bg-border max-h-[60vh] overflow-auto">
{result.manifestDump}
</pre>
)}
</div>
)}
</div>
</>
)}
</div>
</div>
)
}