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:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

360
apps/pkg/uiflow/uiflow.go Normal file
View 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]
}
}
}
}