- New sidebar views: GSI Loader (DSU + permanent fastboot flash), APK Audit, Firmware, Intent Lab, Screen Mirror - New backend modules: backend_gsi.go, backend_apkaudit*.go, backend_firmware.go, backend_intent.go, backend_magisk.go, backend_applock.go, backend_scrcpy.go, backend_payload.go, backend_privacy.go, backend_overview.go, backend_bootinfo.go, backend_flasher.go, backend_transfer.go, backend_filehttp.go - App Lock / danger-gate infrastructure (DangerGate, LockGate, applock.ts) - Privileged uninstall via embedded Android helper dex (android-helper/) - nfpm packaging version bump to 1.2.0
123 lines
4.2 KiB
Go
123 lines
4.2 KiB
Go
package main
|
|
|
|
// Intent Lab — list an app's launchable (exported) activities and start them via
|
|
// `am start`, plus a free-form implicit-intent launcher. Lets a user reach hidden
|
|
// settings menus / internal screens that aren't on the launcher.
|
|
//
|
|
// Activity discovery uses `dumpsys package <pkg>`: components that appear in the
|
|
// Activity Resolver Table have an intent filter, so they're launchable by the
|
|
// shell user. Non-filtered/exported=false activities generally can't be started
|
|
// from adb; the launcher surfaces the real `am start` result either way.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type IntentActivity struct {
|
|
Name string `json:"name"` // activity class, relative to the package
|
|
Component string `json:"component"` // full "package/activity" target for am start
|
|
Exported bool `json:"exported"` // appears in the resolver table (has an intent filter)
|
|
}
|
|
|
|
// componentRe guards the `am start -n <component>` target against shell injection
|
|
// on the device side (adb shell reparses the command).
|
|
var componentRe = regexp.MustCompile(`^[A-Za-z0-9_.]+/[A-Za-z0-9_.$]+$`)
|
|
var actionRe = regexp.MustCompile(`^[A-Za-z0-9_.]+$`)
|
|
|
|
// ListActivities returns the launchable activities of an installed package.
|
|
func (a *App) ListActivities(packageName string) ([]IntentActivity, error) {
|
|
if err := validatePackageName(packageName); err != nil {
|
|
return nil, err
|
|
}
|
|
dump, err := a.runAdbShellTimeout(30*1e9, "dumpsys", "package", packageName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query package: %w", err)
|
|
}
|
|
|
|
prefix := packageName + "/"
|
|
seen := map[string]bool{}
|
|
var acts []IntentActivity
|
|
inActivities := false
|
|
|
|
for _, line := range strings.Split(dump, "\n") {
|
|
t := strings.TrimSpace(line)
|
|
switch {
|
|
case strings.Contains(t, "Activity Resolver Table"):
|
|
inActivities = true
|
|
continue
|
|
case strings.Contains(t, "Receiver Resolver Table"),
|
|
strings.Contains(t, "Service Resolver Table"),
|
|
strings.Contains(t, "Provider Resolver Table"),
|
|
strings.Contains(t, "Preferred Activities"),
|
|
strings.Contains(t, "Key Set Manager"):
|
|
inActivities = false
|
|
}
|
|
if !inActivities {
|
|
continue
|
|
}
|
|
for _, tok := range strings.Fields(t) {
|
|
if strings.HasPrefix(tok, prefix) && len(tok) > len(prefix) && !seen[tok] {
|
|
seen[tok] = true
|
|
acts = append(acts, IntentActivity{
|
|
Name: strings.TrimPrefix(tok, prefix),
|
|
Component: tok,
|
|
Exported: true,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(acts, func(i, j int) bool { return acts[i].Name < acts[j].Name })
|
|
return acts, nil
|
|
}
|
|
|
|
// StartActivity launches an explicit component ("package/activity").
|
|
func (a *App) StartActivity(component string) (string, error) {
|
|
component = strings.TrimSpace(component)
|
|
if !componentRe.MatchString(component) {
|
|
return "", fmt.Errorf("invalid component, expected package/activity: %s", component)
|
|
}
|
|
out, err := a.runAdbShell("am", "start", "-n", component)
|
|
return interpretAmResult(out, err)
|
|
}
|
|
|
|
// StartIntentAction launches an implicit intent by action, with an optional data URI.
|
|
func (a *App) StartIntentAction(action, data string) (string, error) {
|
|
action = strings.TrimSpace(action)
|
|
if action == "" || !actionRe.MatchString(action) {
|
|
return "", fmt.Errorf("invalid or empty action")
|
|
}
|
|
args := []string{"am", "start", "-a", action}
|
|
if data = strings.TrimSpace(data); data != "" {
|
|
// Reject shell metacharacters — adb shell reparses this on the device.
|
|
if strings.ContainsAny(data, " \t\n\r;&|`$<>()\"'\\") {
|
|
return "", fmt.Errorf("data URI contains disallowed characters")
|
|
}
|
|
args = append(args, "-d", data)
|
|
}
|
|
out, err := a.runAdbShell(args...)
|
|
return interpretAmResult(out, err)
|
|
}
|
|
|
|
// interpretAmResult normalises `am start` output — it often prints errors to
|
|
// stdout with a zero exit, so inspect the text as well as err.
|
|
func interpretAmResult(out string, err error) (string, error) {
|
|
out = strings.TrimSpace(out)
|
|
if err != nil {
|
|
if out != "" {
|
|
return "", fmt.Errorf("%s", firstLine(out))
|
|
}
|
|
return "", err
|
|
}
|
|
if strings.Contains(out, "Error:") || strings.Contains(out, "Exception") ||
|
|
strings.Contains(out, "does not exist") || strings.Contains(out, "Permission Denial") {
|
|
return "", fmt.Errorf("%s", firstLine(out))
|
|
}
|
|
if out == "" {
|
|
out = "Started."
|
|
}
|
|
return out, nil
|
|
}
|