94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// privilegedHelperDex is the on-device uninstall helper, compiled to dex.
|
|
// See android-helper/Main.java (rebuild with android-helper/build.sh).
|
|
//
|
|
//go:embed android-helper/atk-helper.dex
|
|
var privilegedHelperDex []byte
|
|
|
|
const privilegedHelperRemotePath = "/data/local/tmp/atk-helper.dex"
|
|
|
|
// pushPrivilegedHelper writes the embedded dex to a temp file and pushes it to
|
|
// the device. Cheap (~3 KB) so it is safe to call before each privileged op.
|
|
func (a *App) pushPrivilegedHelper() error {
|
|
tmp := filepath.Join(os.TempDir(), "atk-helper.dex")
|
|
if err := os.WriteFile(tmp, privilegedHelperDex, 0o644); err != nil {
|
|
return fmt.Errorf("write helper: %w", err)
|
|
}
|
|
defer os.Remove(tmp)
|
|
|
|
ctx, cancel := a.beginCancellableOp(30 * time.Second)
|
|
defer cancel()
|
|
if _, err := a.runCommandContext(ctx, "adb", "push", tmp, privilegedHelperRemotePath); err != nil {
|
|
return fmt.Errorf("push helper to device: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// privilegedUninstall removes a package for the given user via the on-device
|
|
// app_process helper. It runs as the shell user (uid 2000) and calls
|
|
// IPackageInstaller.uninstall() with the DELETE_SYSTEM_APP flag - the same
|
|
// capability Canta gets through Shizuku, but driven over adb with no root.
|
|
// This removes protected system apps that `pm uninstall --user` refuses.
|
|
func (a *App) privilegedUninstall(packageName string, userID int) (string, error) {
|
|
if err := validatePackageName(packageName); err != nil {
|
|
return "", err
|
|
}
|
|
if err := a.pushPrivilegedHelper(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// CLASSPATH=<dex> app_process /system/bin Main <pkg> <user>
|
|
out, err := a.runAdbShellTimeout(30*time.Second,
|
|
"CLASSPATH="+privilegedHelperRemotePath, "app_process", "/system/bin",
|
|
"Main", packageName, strconv.Itoa(userID))
|
|
if err != nil {
|
|
return "", fmt.Errorf("privileged helper failed: %s", friendlyPmError(err.Error()))
|
|
}
|
|
if idx := strings.Index(out, "ATK_ERR"); idx >= 0 {
|
|
return "", fmt.Errorf("privileged uninstall failed for %s: %s",
|
|
packageName, strings.TrimSpace(out[idx+len("ATK_ERR"):]))
|
|
}
|
|
|
|
// The helper's result callback is fire-and-forget, so confirm removal
|
|
// directly rather than trusting ATK_OK alone.
|
|
if a.isInstalledForUser(packageName, userID) {
|
|
return "", fmt.Errorf("privileged uninstall reported success but %s is still present for user %d",
|
|
packageName, userID)
|
|
}
|
|
return fmt.Sprintf("%s removed for user %d (privileged)", packageName, userID), nil
|
|
}
|
|
|
|
// isInstalledForUser reports whether packageName is currently installed for the
|
|
// given user (exact match, not substring).
|
|
func (a *App) isInstalledForUser(packageName string, userID int) bool {
|
|
out, err := a.runAdbShell("pm", "list", "packages", "--user", strconv.Itoa(userID), packageName)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, line := range strings.Split(out, "\n") {
|
|
if strings.TrimSpace(line) == "package:"+packageName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isProtectedSystemApp reports whether a pm uninstall failure is the protected
|
|
// system-app guard - i.e. removable only by passing DELETE_SYSTEM_APP, which
|
|
// the pm CLI never does but our privileged helper can.
|
|
func isProtectedSystemApp(text string) bool {
|
|
return strings.Contains(text, "only root can delete") ||
|
|
strings.Contains(text, "Only root may") ||
|
|
strings.Contains(text, "DELETE_FAILED_INTERNAL_ERROR")
|
|
}
|