- 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
211 lines
5.6 KiB
Go
211 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
// Streaming push/pull with live progress. adb prints transfer progress as
|
|
// `[ 42%] path` lines terminated by a carriage return; we split on \r as well
|
|
// as \n so each update is its own token, parse the percentage, and forward it
|
|
// to the frontend via transfer:* events. ETA is computed frontend-side from the
|
|
// percentage over elapsed time. Integrates with beginCancellableOp so the
|
|
// existing Cancel button (CancelOperation) aborts a transfer.
|
|
|
|
var transferPercentRe = regexp.MustCompile(`(\d+)%`)
|
|
|
|
// scanCRLF is a bufio.SplitFunc that breaks on either \n or \r, so adb's
|
|
// carriage-return progress updates surface as discrete tokens.
|
|
func scanCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
if atEOF && len(data) == 0 {
|
|
return 0, nil, nil
|
|
}
|
|
for i, b := range data {
|
|
if b == '\n' || b == '\r' {
|
|
return i + 1, data[:i], nil
|
|
}
|
|
}
|
|
if atEOF {
|
|
return len(data), data, nil
|
|
}
|
|
return 0, nil, nil
|
|
}
|
|
|
|
func baseName(p string) string {
|
|
p = strings.TrimRight(p, "/")
|
|
if i := strings.LastIndexAny(p, `/\`); i >= 0 {
|
|
return p[i+1:]
|
|
}
|
|
return p
|
|
}
|
|
|
|
// runTransfer runs an adb push/pull, emitting transfer:progress events as it
|
|
// goes. kind is "push" or "pull"; label is the item name shown in the UI.
|
|
func (a *App) runTransfer(ctx context.Context, kind, label string, args ...string) error {
|
|
adbPath, err := a.getBinaryPath("adb")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, adbPath, args...)
|
|
setCommandSysProcAttr(cmd)
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("stdout pipe: %w", err)
|
|
}
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start adb %s: %w", kind, err)
|
|
}
|
|
|
|
emit := func(percent int) {
|
|
runtime.EventsEmit(a.ctx, "transfer:progress", map[string]interface{}{
|
|
"kind": kind, "label": label, "percent": percent,
|
|
})
|
|
}
|
|
emit(0)
|
|
|
|
scanner := bufio.NewScanner(stdout)
|
|
scanner.Buffer(make([]byte, 64*1024), 1024*1024)
|
|
scanner.Split(scanCRLF)
|
|
last := -1
|
|
for scanner.Scan() {
|
|
tok := strings.TrimSpace(scanner.Text())
|
|
if tok == "" {
|
|
continue
|
|
}
|
|
if m := transferPercentRe.FindStringSubmatch(tok); m != nil {
|
|
if p, perr := strconv.Atoi(m[1]); perr == nil && p != last {
|
|
last = p
|
|
emit(p)
|
|
}
|
|
}
|
|
}
|
|
|
|
if werr := cmd.Wait(); werr != nil {
|
|
if ctx.Err() == context.Canceled {
|
|
return fmt.Errorf("cancelled")
|
|
}
|
|
msg := strings.TrimSpace(stderr.String())
|
|
if msg == "" {
|
|
msg = werr.Error()
|
|
}
|
|
return fmt.Errorf("%s", msg)
|
|
}
|
|
emit(100)
|
|
return nil
|
|
}
|
|
|
|
// PushWithProgress pushes a local file into remoteDir, streaming progress.
|
|
func (a *App) PushWithProgress(localPath, remoteDir string) (string, error) {
|
|
ctx, cancel := a.beginCancellableOp(60 * time.Minute)
|
|
defer cancel()
|
|
|
|
name := baseName(localPath)
|
|
err := a.runTransfer(ctx, "push", name, "push", localPath, remoteDir)
|
|
runtime.EventsEmit(a.ctx, "transfer:done", nil)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "cancelled") {
|
|
return "", fmt.Errorf("push cancelled")
|
|
}
|
|
return "", fmt.Errorf("push failed: %w", err)
|
|
}
|
|
return fmt.Sprintf("Pushed %s", name), nil
|
|
}
|
|
|
|
// PushPathsWithProgress pushes the given local files into remoteDir on the
|
|
// device, one at a time, each with its own progress bar. Used by the Computer
|
|
// browser's "Push to device" action.
|
|
func (a *App) PushPathsWithProgress(localPaths []string, remoteDir string) (string, error) {
|
|
if len(localPaths) == 0 {
|
|
return "", fmt.Errorf("no files selected")
|
|
}
|
|
if strings.TrimSpace(remoteDir) == "" {
|
|
return "", fmt.Errorf("no device destination")
|
|
}
|
|
|
|
ctx, cancel := a.beginCancellableOp(60 * time.Minute)
|
|
defer cancel()
|
|
|
|
var ok, fail int
|
|
var details strings.Builder
|
|
for _, lp := range localPaths {
|
|
name := baseName(lp)
|
|
err := a.runTransfer(ctx, "push", name, "push", lp, remoteDir)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "cancelled") {
|
|
details.WriteString(fmt.Sprintf("• %s: cancelled\n", name))
|
|
fail++
|
|
break
|
|
}
|
|
fail++
|
|
details.WriteString(fmt.Sprintf("• %s: %v\n", name, err))
|
|
} else {
|
|
ok++
|
|
}
|
|
}
|
|
runtime.EventsEmit(a.ctx, "transfer:done", nil)
|
|
|
|
summary := fmt.Sprintf("Pushed %d item(s) to %s.", ok, remoteDir)
|
|
if fail > 0 {
|
|
summary += fmt.Sprintf(" Failed: %d\n%s", fail, details.String())
|
|
}
|
|
return summary, nil
|
|
}
|
|
|
|
// PullPathsWithProgress pulls the given remote paths into a user-chosen local
|
|
// directory, one at a time, each with its own progress bar.
|
|
func (a *App) PullPathsWithProgress(remotePaths []string) (string, error) {
|
|
if len(remotePaths) == 0 {
|
|
return "", fmt.Errorf("no files selected")
|
|
}
|
|
|
|
localDir, err := a.SelectDirectoryForPull()
|
|
if err != nil {
|
|
return "", fmt.Errorf("folder dialog failed: %w", err)
|
|
}
|
|
if localDir == "" {
|
|
return "Pull cancelled.", nil
|
|
}
|
|
|
|
ctx, cancel := a.beginCancellableOp(0)
|
|
defer cancel()
|
|
|
|
var ok, fail int
|
|
var details strings.Builder
|
|
for _, rp := range remotePaths {
|
|
name := baseName(rp)
|
|
// -a preserves timestamps
|
|
err := a.runTransfer(ctx, "pull", name, "pull", "-a", rp, localDir)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "cancelled") {
|
|
details.WriteString(fmt.Sprintf("• %s: cancelled\n", name))
|
|
fail++
|
|
break
|
|
}
|
|
fail++
|
|
details.WriteString(fmt.Sprintf("• %s: %v\n", name, err))
|
|
} else {
|
|
ok++
|
|
}
|
|
}
|
|
runtime.EventsEmit(a.ctx, "transfer:done", nil)
|
|
|
|
summary := fmt.Sprintf("Pulled %d item(s) to %s.", ok, localDir)
|
|
if fail > 0 {
|
|
summary += fmt.Sprintf(" Failed: %d\n%s", fail, details.String())
|
|
}
|
|
return summary, nil
|
|
}
|