Initial commit: Null DRM Official
Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
This commit is contained in:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
360
apps/pkg/uiflow/uiflow.go
Normal file
360
apps/pkg/uiflow/uiflow.go
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
// Package uiflow holds the phone-UI primitives app modules drive playback with:
|
||||
// wait for a node, tap a node, wait for playback, find scrollable live cards.
|
||||
// It knows nothing about any particular app — every threshold and pattern is an
|
||||
// argument.
|
||||
package uiflow
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"drmdecryption/adb"
|
||||
)
|
||||
|
||||
// Pick selects among several matching nodes.
|
||||
type Pick string
|
||||
|
||||
const (
|
||||
PickLargest Pick = "largest"
|
||||
PickSmallest Pick = "smallest"
|
||||
PickFirst Pick = "first"
|
||||
)
|
||||
|
||||
// Match describes which UI node to look for. An empty Match never matches.
|
||||
type Match struct {
|
||||
TextRE []string
|
||||
DescRE []string
|
||||
TextContains []string
|
||||
DescContains []string
|
||||
ResourceContains []string
|
||||
Pick Pick
|
||||
MinArea int
|
||||
}
|
||||
|
||||
type compiled struct {
|
||||
textRE, descRE []*regexp.Regexp
|
||||
m Match
|
||||
}
|
||||
|
||||
// CompileRes compiles case-insensitive patterns, skipping empties.
|
||||
func CompileRes(pats []string) []*regexp.Regexp {
|
||||
out := make([]*regexp.Regexp, 0, len(pats))
|
||||
for _, p := range pats {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, regexp.MustCompile("(?i)"+p))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m Match) compile() compiled {
|
||||
return compiled{textRE: CompileRes(m.TextRE), descRE: CompileRes(m.DescRE), m: m}
|
||||
}
|
||||
|
||||
func (c compiled) empty() bool {
|
||||
return len(c.textRE) == 0 && len(c.descRE) == 0 &&
|
||||
len(c.m.TextContains) == 0 && len(c.m.DescContains) == 0 && len(c.m.ResourceContains) == 0
|
||||
}
|
||||
|
||||
func (c compiled) matches(n adb.Node) bool {
|
||||
if c.empty() {
|
||||
return false
|
||||
}
|
||||
if n.Text != "" {
|
||||
for _, p := range c.textRE {
|
||||
if p.MatchString(n.Text) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range c.m.TextContains {
|
||||
if s != "" && strings.Contains(strings.ToLower(n.Text), strings.ToLower(s)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if n.Desc != "" {
|
||||
for _, p := range c.descRE {
|
||||
if p.MatchString(n.Desc) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range c.m.DescContains {
|
||||
if s != "" && strings.Contains(strings.ToLower(n.Desc), strings.ToLower(s)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, s := range c.m.ResourceContains {
|
||||
if s != "" && strings.Contains(n.ResourceID, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pickNode(hits []adb.Node, pick Pick, minArea int) *adb.Node {
|
||||
if len(hits) == 0 {
|
||||
return nil
|
||||
}
|
||||
var best *adb.Node
|
||||
switch pick {
|
||||
case PickSmallest:
|
||||
best = &hits[0]
|
||||
for i := range hits {
|
||||
if hits[i].Area() < best.Area() {
|
||||
best = &hits[i]
|
||||
}
|
||||
}
|
||||
case PickFirst:
|
||||
best = &hits[0]
|
||||
default: // largest
|
||||
best = &hits[0]
|
||||
for i := range hits {
|
||||
if hits[i].Area() > best.Area() {
|
||||
best = &hits[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
if minArea > 0 && best.Area() < minArea {
|
||||
return nil
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// Find polls the UI until a node matches, returning it.
|
||||
func Find(c *adb.Client, cacheDir string, m Match, timeout time.Duration) (*adb.Node, error) {
|
||||
cm := m.compile()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
nodes, err := c.DumpUI(cacheDir)
|
||||
if err != nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
var hits []adb.Node
|
||||
for _, n := range nodes {
|
||||
if cm.matches(n) {
|
||||
hits = append(hits, n)
|
||||
}
|
||||
}
|
||||
if n := pickNode(hits, m.Pick, m.MinArea); n != nil {
|
||||
return n, nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
return nil, fmt.Errorf("ui node not found within %s", timeout)
|
||||
}
|
||||
|
||||
// WaitUI waits for a node to exist without tapping it.
|
||||
func WaitUI(c *adb.Client, cacheDir string, m Match, timeout time.Duration) error {
|
||||
_, err := Find(c, cacheDir, m, timeout)
|
||||
return err
|
||||
}
|
||||
|
||||
// TapUI waits for a node and taps its centre.
|
||||
func TapUI(c *adb.Client, cacheDir string, m Match, timeout time.Duration) error {
|
||||
n, err := Find(c, cacheDir, m, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("[*] tap_ui %q/%q @ %d,%d\n", n.Text, n.Desc, n.CX(), n.CY())
|
||||
return c.Tap(n.CX(), n.CY())
|
||||
}
|
||||
|
||||
// PlaybackOpts tunes WaitPlayback. The UI signals are a fallback for players
|
||||
// whose audio session does not show up in dumpsys.
|
||||
type PlaybackOpts struct {
|
||||
Timeout time.Duration
|
||||
SoftFail bool
|
||||
OrDescContains []string
|
||||
OrResourceContains []string
|
||||
}
|
||||
|
||||
// WaitPlayback blocks until the package is playing audio, or a UI signal says the
|
||||
// player is up. With SoftFail a timeout is logged and tolerated.
|
||||
func WaitPlayback(c *adb.Client, cacheDir, pkg string, opt PlaybackOpts) error {
|
||||
if opt.Timeout <= 0 {
|
||||
opt.Timeout = 45 * time.Second
|
||||
}
|
||||
deadline := time.Now().Add(opt.Timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if c.PlaybackActive(pkg) {
|
||||
fmt.Printf("[+] %s is PLAYING\n", pkg)
|
||||
return nil
|
||||
}
|
||||
if len(opt.OrDescContains) > 0 || len(opt.OrResourceContains) > 0 {
|
||||
if nodes, err := c.DumpUI(cacheDir); err == nil {
|
||||
for _, n := range nodes {
|
||||
desc := strings.ToLower(n.Desc)
|
||||
for _, d := range opt.OrDescContains {
|
||||
if d != "" && strings.Contains(desc, strings.ToLower(d)) {
|
||||
fmt.Printf("[+] UI signal %q\n", d)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for _, r := range opt.OrResourceContains {
|
||||
if r != "" && strings.Contains(n.ResourceID, r) {
|
||||
fmt.Printf("[+] resource signal %q\n", r)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if opt.SoftFail {
|
||||
fmt.Printf("[!] timed out waiting for playback; continuing capture\n")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("timed out waiting for playback")
|
||||
}
|
||||
|
||||
// CardOpts describes what a content card looks like in a given app: a clickable
|
||||
// row at least MinH tall and MinW wide, below MinY, whose description/text is not
|
||||
// chrome. Callers supply the denylists; nothing is hardcoded.
|
||||
type CardOpts struct {
|
||||
MinY, MinH, MinW int
|
||||
DescDeny []string
|
||||
TextDeny []string
|
||||
// ScrollMax limits swipe attempts when the wanted index is off-screen.
|
||||
ScrollMax int
|
||||
// Swipe coordinates used to scroll the card list.
|
||||
SwipeX, SwipeFromY, SwipeToY, SwipeMS int
|
||||
}
|
||||
|
||||
// WithDefaults fills unset geometry with values that suit a typical 1080x1920
|
||||
// phone card list.
|
||||
func (o CardOpts) WithDefaults() CardOpts {
|
||||
if o.MinY <= 0 {
|
||||
o.MinY = 300
|
||||
}
|
||||
if o.MinH <= 0 {
|
||||
o.MinH = 250
|
||||
}
|
||||
if o.MinW <= 0 {
|
||||
o.MinW = 600
|
||||
}
|
||||
if o.ScrollMax <= 0 {
|
||||
o.ScrollMax = 8
|
||||
}
|
||||
if o.SwipeX == 0 {
|
||||
o.SwipeX = 540
|
||||
}
|
||||
if o.SwipeFromY == 0 {
|
||||
o.SwipeFromY = 1700
|
||||
}
|
||||
if o.SwipeToY == 0 {
|
||||
o.SwipeToY = 700
|
||||
}
|
||||
if o.SwipeMS == 0 {
|
||||
o.SwipeMS = 350
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// Cards returns the content cards visible in a UI dump, top to bottom.
|
||||
func Cards(nodes []adb.Node, opt CardOpts) []adb.Node {
|
||||
opt = opt.WithDefaults()
|
||||
var cards []adb.Node
|
||||
for _, n := range nodes {
|
||||
if !n.Clickable {
|
||||
continue
|
||||
}
|
||||
h := n.Y2 - n.Y1
|
||||
w := n.X2 - n.X1
|
||||
if n.Y1 < opt.MinY || h < opt.MinH || w < opt.MinW {
|
||||
continue
|
||||
}
|
||||
if containsAny(n.Desc, opt.DescDeny) {
|
||||
continue
|
||||
}
|
||||
if containsAnyFold(n.Text, opt.TextDeny) {
|
||||
continue
|
||||
}
|
||||
cards = append(cards, n)
|
||||
}
|
||||
sortByTop(cards)
|
||||
return cards
|
||||
}
|
||||
|
||||
// WaitCard polls for the index-th content card, scrolling when it is not yet on
|
||||
// screen (each scroll consumes one index, matching how the list advances).
|
||||
func WaitCard(c *adb.Client, cacheDir string, index int, timeout time.Duration, opt CardOpts) (*adb.Node, error) {
|
||||
opt = opt.WithDefaults()
|
||||
deadline := time.Now().Add(timeout)
|
||||
target := index
|
||||
var last int
|
||||
scrolled := 0
|
||||
for time.Now().Before(deadline) {
|
||||
nodes, err := c.DumpUI(cacheDir)
|
||||
if err == nil {
|
||||
cards := Cards(nodes, opt)
|
||||
last = len(cards)
|
||||
if target >= 0 && target < len(cards) {
|
||||
card := cards[target]
|
||||
return &card, nil
|
||||
}
|
||||
if target >= last && last > 0 && scrolled < opt.ScrollMax {
|
||||
fmt.Printf("[*] card need local #%d (%d on screen) — scrolling\n", target, last)
|
||||
_ = c.Swipe(opt.SwipeX, opt.SwipeFromY, opt.SwipeX, opt.SwipeToY, opt.SwipeMS)
|
||||
scrolled++
|
||||
if target > 0 {
|
||||
target--
|
||||
}
|
||||
time.Sleep(1200 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
}
|
||||
return nil, fmt.Errorf("only saw %d cards (need index %d)", last, index)
|
||||
}
|
||||
|
||||
// TapCard waits for and taps the index-th content card.
|
||||
func TapCard(c *adb.Client, cacheDir string, index int, timeout time.Duration, opt CardOpts) error {
|
||||
card, err := WaitCard(c, cacheDir, index, timeout, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("[*] tap card #%d @ %d,%d\n", index, card.CX(), card.CY())
|
||||
return c.Tap(card.CX(), card.CY())
|
||||
}
|
||||
|
||||
// MonkeyLaunch starts an app through its launcher intent.
|
||||
func MonkeyLaunch(c *adb.Client, pkg string) error {
|
||||
_, _, err := c.Run("shell", "monkey", "-p", pkg, "-c", "android.intent.category.LAUNCHER", "1")
|
||||
return err
|
||||
}
|
||||
|
||||
func containsAny(s string, needles []string) bool {
|
||||
for _, n := range needles {
|
||||
if n != "" && strings.Contains(s, n) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsAnyFold(s string, needles []string) bool {
|
||||
ls := strings.ToLower(s)
|
||||
for _, n := range needles {
|
||||
if n != "" && strings.Contains(ls, strings.ToLower(n)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sortByTop(cards []adb.Node) {
|
||||
for i := 0; i < len(cards); i++ {
|
||||
for j := i + 1; j < len(cards); j++ {
|
||||
if cards[j].Y1 < cards[i].Y1 {
|
||||
cards[i], cards[j] = cards[j], cards[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue