adb-toolkit/package_service.go

344 lines
10 KiB
Go

package main
import (
"context"
"fmt"
"strings"
"sync"
"time"
)
// ListPackages returns all installed packages filtered by type.
// filterType: "user", "system", or "all"
func (a *App) ListPackages(filterType string) ([]PackageInfo, error) {
var wg sync.WaitGroup
var mu sync.Mutex
var enabledPkgs, disabledPkgs []string
var errEnabled, errDisabled error
// Build base args - all discrete
buildArgs := func(stateFlag string) []string {
args := []string{"pm", "list", "packages", stateFlag}
switch filterType {
case "user":
args = append(args, "-3")
case "system":
args = append(args, "-s")
}
return args
}
wg.Add(2)
go func() {
defer wg.Done()
output, err := a.runAdbShell(buildArgs("-e")...)
mu.Lock()
defer mu.Unlock()
if err != nil {
errEnabled = err
return
}
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if pkg := strings.TrimPrefix(line, "package:"); pkg != line {
enabledPkgs = append(enabledPkgs, strings.TrimSpace(pkg))
}
}
}()
go func() {
defer wg.Done()
output, err := a.runAdbShell(buildArgs("-d")...)
mu.Lock()
defer mu.Unlock()
if err != nil {
errDisabled = err
return
}
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if pkg := strings.TrimPrefix(line, "package:"); pkg != line {
disabledPkgs = append(disabledPkgs, strings.TrimSpace(pkg))
}
}
}()
wg.Wait()
if errEnabled != nil {
return nil, fmt.Errorf("failed to list packages: %w", errEnabled)
}
// Don't fail if disabled list fails - some devices restrict it
_ = errDisabled
pkgMap := make(map[string]PackageInfo)
for _, p := range enabledPkgs {
pkgMap[p] = PackageInfo{PackageName: p, IsEnabled: true}
}
for _, p := range disabledPkgs {
pkgMap[p] = PackageInfo{PackageName: p, IsEnabled: false}
}
packages := make([]PackageInfo, 0, len(pkgMap))
for _, pkg := range pkgMap {
packages = append(packages, pkg)
}
return packages, nil
}
// InstallPackage installs an APK from a local file path.
// filePath is a discrete arg - safe.
func (a *App) InstallPackage(filePath string) (string, error) {
ctx, cancel := a.beginCancellableOp(15 * time.Minute)
defer cancel()
// adb install -r <path> - all discrete args
output, err := a.runCommandContext(ctx, "adb", "install", "-r", filePath)
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
return "", fmt.Errorf("installation cancelled")
}
return "", fmt.Errorf("install failed: %w", err)
}
return output, nil
}
// UninstallPackage uninstalls a package by name.
// packageName is a discrete arg - safe.
func (a *App) UninstallPackage(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
// pm uninstall <pkg> - all discrete args
output, err := a.runAdbShell("pm", "uninstall", packageName)
if err != nil {
return "", fmt.Errorf("uninstall failed for %s: %w", packageName, err)
}
return output, nil
}
// DisablePackage disables a package for user 0.
// packageName is a discrete arg - safe.
func (a *App) DisablePackage(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
// pm disable-user --user 0 <pkg> - all discrete args
output, err := a.runAdbShell("pm", "disable-user", "--user", "0", packageName)
if err != nil {
return "", fmt.Errorf("disable failed for %s: %w", packageName, err)
}
// Accept any "new state:" response as success
if strings.Contains(output, "new state:") {
return output, nil
}
return "", fmt.Errorf("disable failed for %s: %s", packageName, output)
}
// EnablePackage enables a previously disabled package.
// packageName is a discrete arg - safe.
func (a *App) EnablePackage(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
// pm enable --user 0 <pkg> - all discrete args
output, err := a.runAdbShell("pm", "enable", "--user", "0", packageName)
if err != nil {
return "", fmt.Errorf("enable failed for %s: %w", packageName, err)
}
// Accept "new state: enabled" or "new state: enabled-user" (Android version variants)
if strings.Contains(output, "new state: enabled") {
return output, nil
}
return "", fmt.Errorf("enable failed for %s: %s", packageName, output)
}
// ClearData clears app data for a package.
func (a *App) ClearData(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
output, err := a.runAdbShell("pm", "clear", packageName)
if err != nil {
return "", fmt.Errorf("clear data failed for %s: %w", packageName, err)
}
if strings.Contains(output, "Failed") {
return "", fmt.Errorf("clear data failed for %s: %s", packageName, output)
}
return "Data cleared successfully", nil
}
// PullApk pulls an installed APK from the device to a user-chosen local path.
func (a *App) PullApk(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
// Get remote APK path - pm path <pkg> - discrete args
pathOutput, err := a.runAdbShell("pm", "path", packageName)
if err != nil {
return "", fmt.Errorf("cannot find APK for %s: %w", packageName, err)
}
remotePath := strings.TrimPrefix(strings.TrimSpace(pathOutput), "package:")
remotePath = strings.TrimSpace(remotePath)
if remotePath == "" {
return "", fmt.Errorf("could not parse APK path from: %s", pathOutput)
}
localPath, err := a.SelectSaveFile(packageName + ".apk")
if err != nil {
return "", fmt.Errorf("save dialog failed: %w", err)
}
if localPath == "" {
return "APK pull cancelled.", nil
}
ctx, cancel := a.beginCancellableOp(10 * time.Minute)
defer cancel()
// adb pull <remote> <local> - all discrete args
_, err = a.runCommandContext(ctx, "adb", "pull", remotePath, localPath)
if err != nil {
if strings.Contains(err.Error(), "cancelled") {
return "", fmt.Errorf("pull cancelled")
}
return "", fmt.Errorf("pull failed: %w", err)
}
return fmt.Sprintf("APK saved to %s", localPath), nil
}
// UninstallMultiplePackages uninstalls a list of packages.
func (a *App) UninstallMultiplePackages(packageNames []string) (string, error) {
return a.batchPackageOp("uninstall", packageNames, a.UninstallPackage)
}
// DisableMultiplePackages disables a list of packages.
func (a *App) DisableMultiplePackages(packageNames []string) (string, error) {
return a.batchPackageOp("disable", packageNames, a.DisablePackage)
}
// EnableMultiplePackages enables a list of packages.
func (a *App) EnableMultiplePackages(packageNames []string) (string, error) {
return a.batchPackageOp("enable", packageNames, a.EnablePackage)
}
// batchPackageOp runs a package operation on multiple packages and summarises results.
func (a *App) batchPackageOp(opName string, packageNames []string, op func(string) (string, error)) (string, error) {
if len(packageNames) == 0 {
return "", fmt.Errorf("no packages selected")
}
var successCount, failCount int
var errDetails strings.Builder
for _, pkg := range packageNames {
if _, err := op(pkg); err != nil {
failCount++
errDetails.WriteString(fmt.Sprintf("• %s: %v\n", pkg, err))
} else {
successCount++
}
}
summary := fmt.Sprintf("Successfully %sd %d package(s).", opName, successCount)
if failCount > 0 {
summary += fmt.Sprintf("\nFailed: %d\n%s", failCount, errDetails.String())
}
return summary, nil
}
// ForceStopPackage force stops a running app.
func (a *App) ForceStopPackage(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
_, err := a.runAdbShell("am", "force-stop", packageName)
if err != nil {
return "", fmt.Errorf("force stop failed: %w", err)
}
return fmt.Sprintf("Force stopped %s", packageName), nil
}
// GetPackageInfo returns detailed info about a package.
func (a *App) GetPackageInfo(packageName string) (string, error) {
if err := validatePackageName(packageName); err != nil {
return "", err
}
// dumpsys package <pkg> - discrete args
output, err := a.runAdbShellTimeout(10*time.Second, "dumpsys", "package", packageName)
if err != nil {
return "", fmt.Errorf("failed to get package info: %w", err)
}
return output, nil
}
// SideloadPackage sideloads a package via adb sideload (for OTA updates in recovery).
func (a *App) SideloadPackage(filePath string) (string, error) {
ctx, cancel := a.beginCancellableOp(0) // No timeout - user cancellable
defer cancel()
output, err := a.runCommandContext(ctx, "adb", "sideload", filePath)
if err != nil {
return "", fmt.Errorf("sideload failed: %w", err)
}
return output, nil
}
// validatePackageName checks that a package name looks like a valid Android package.
// Android packages are dot-separated identifiers: com.example.app
// This prevents passing arbitrary strings as package names.
func validatePackageName(name string) error {
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("package name cannot be empty")
}
// Basic sanity: must not contain shell metacharacters
// Since we pass as discrete args this is belt-and-suspenders,
// but good to validate inputs regardless
for _, ch := range name {
if ch == ';' || ch == '&' || ch == '|' || ch == '`' || ch == '$' || ch == '\n' || ch == '\r' {
return fmt.Errorf("invalid character in package name: %q", ch)
}
}
return nil
}
// RunAdbHostCommand runs a raw adb command from the terminal view.
// args is split on spaces and each element passed as a discrete arg.
// This is intentionally permissive since it's the shell terminal feature.
func (a *App) RunAdbHostCommand(rawArgs string) (string, error) {
if rawArgs == "" {
return "", fmt.Errorf("command cannot be empty")
}
args := strings.Fields(rawArgs)
if len(args) == 0 {
return "", fmt.Errorf("no arguments provided")
}
// Restrict to known safe adb subcommands in the terminal
// The shell view still allows full adb usage, this just prevents
// passing arbitrary binaries
ctx, cancel := context.WithTimeout(context.Background(), DefaultCommandTimeout)
defer cancel()
return a.runCommandContext(ctx, "adb", args...)
}
// RunShellCommand runs an adb shell command from the terminal view.
// The command string is split on spaces - NO shell interpretation.
// This means pipes/redirects won't work, but it's much safer.
func (a *App) RunShellCommand(command string) (string, error) {
if command == "" {
return "", fmt.Errorf("command cannot be empty")
}
// Split into discrete args - no shell, no injection
args := strings.Fields(command)
return a.runAdbShell(args...)
}