Skip to content

UI Hider

UI Hider covers selected parts of an app. The other parts of the app stay visible and usable.

For example, UI Hider can cover a recommendation feed or an Explore tab.

Turn on UI Hider

Tap Reducers. Then, tap UI Hider and turn on Enable UI Hider.

Use a built-in rule

Curbox includes rules for common distracting interface elements. Turn on each rule that you want to use.

Instagram rules

RuleResult
Hide home feed except the following tabCovers the main Home feed. The Following feed and other features stay available.
Hide explore tabCovers the Explore tab.

To open the Following feed, tap the Instagram wordmark at the top of the screen. Then, tap Following.

YouTube rules

RuleResult
Hide everything except the videoCovers recommendations, comments, and the description below the video.
Hide feed and only let me access search resultsCovers the Home feed. Search results stay available.

X rules

RuleResult
Hide “For You” and only let me access the Following tabCovers the For You feed. The Following feed stays available.

Pinterestcd rules

RuleResult
Hide “For You” and only let me access the Following tabCovers the For You feed. The Following feed stays available.

Create a custom script

Use a custom script when a built-in rule does not cover the required element. A script can find Android accessibility nodes and cover their screen areas.

You need basic programming knowledge to create a script. A script can use variables, conditions, loops, functions, and persistent values.

  1. Open UI Hider Tap Reducers. Then, tap UI Hider.

  2. Turn on UI Hider Turn on the main switch.

  3. Create a script Tap Add script.

  4. Enter the app package Enter the Android package name for the target app.

  5. Enter the script Add the script in the editor.

  6. Save the script Tap Save. Curbox reports a syntax error if the script is not valid.

How a script runs

  • Each script is connected to one Android app package.
  • Curbox runs the script only while that app is in the foreground.
  • Curbox runs the script again after screen content or navigation changes.
  • Each run has time, operation, node, recursion, and overlay limits.
  • Curbox stops a run that exceeds a limit.
  • Curbox redraws overlays after each run.
  • An overlay disappears if the next run does not draw it.

You do not have to remove an old overlay in the script.

First example

This script covers the Instagram Home feed. It keeps the stories, top bar, and bottom navigation visible.

# Only act inside Instagram
if app != "com.instagram.android" {
return
}
top = find(id="com.instagram.android:id/action_bar_container")
nav = find(id="com.instagram.android:id/tab_bar")
# Cover the area between the two anchors
if top != null and nav != null {
y = top.bottom
height = nav.top - y
if height > 0 {
draw(0, y, screen.width, height)
}
}

The script finds two reference nodes. It reads their screen positions and covers the area between them.

Language reference

Comments

Use # to start a comment. Curbox ignores all text after # on that line.

# This is a comment
x = 5 # This is also a comment

Value types

The scripting language uses dynamic types.

TypeExamples or function
Number5, 3.14, -200
String"hello", "com.instagram.android"
Booleantrue, false
Nullnull means that there is no value
List[1, 2, 3] or the result from findAll(...)
NodeAn interface element from find(), root(), or a related function

All numbers use a decimal representation internally. Strings support \n, \t, \", and \\ escape sequences.

Variables

Use = or := to assign a value. You do not declare a variable before its first assignment.

x = 10
name = "feed"
x = x + 1

Operators

TypeOperators
Arithmetic+, -, *, /, %
Comparison==, !=, <, <=, >, >=
Logicaland, or, not
Grouping( ... )

The + operator joins values if either value is a string. For example, "y=" + 5 returns "y=5".

The and and or operators use short-circuit evaluation. Only false and null are false values. All other values are true values, including 0 and "".

Lists and ranges

List indexes start at 0.

nums = [10, 20, 30]
first = nums[0]
count = len(nums)

A range includes its start value and excludes its end value. Thus, 0..3 contains 0, 1, and 2.

for i in 0..3 {
log(i)
}

Conditions

if x > 100 {
log("big")
} else if x > 10 {
log("medium")
} else {
log("small")
}

Loops

Use a range for a count-based loop:

for i in 0..10 {
log(i)
}

Use a list to process matched nodes:

buttons = findAll(class="android.widget.Button")
for button in buttons {
log(button.text)
}

Use while for a condition-based loop:

i = 0
while i < 5 {
i = i + 1
}

Use break to stop a loop. Use continue to start the next iteration.

Functions

Use fn to define a function.

fn area(node) {
return node.w * node.h
}
banner = find(id="com.app:id/banner")
if banner != null and area(banner) > 50000 {
hide(banner)
}

A function returns null if it reaches the end or uses return without a value. A top-level return stops the script.

Runtime API

Context values

Each script can use these values:

ValueContent
appPackage name of the foreground app
screen.widthScreen width in pixels
screen.heightScreen height in pixels
event.typeEvent type: window_state, content, scrolled, clicked, selected, or other
event.packageThe same package name as app
event.textEvent text, or null
event.classClass name of the event source, or null

Example:

if event.type == "scrolled" {
# Do work that is necessary after a scroll
}

Find nodes

Use these functions to find interface nodes:

  • root() returns the root node for the current screen.
  • find(...) returns the first matching node or null.
  • findAll(...) returns a list of matching nodes. The list can be empty.

Use node.find(...) or node.findAll(...) to search only inside a node.

Selectors

Selectors are named arguments. If you use multiple selectors, a node must match all of them.

SelectorMatch condition
id=View ID is equal to the value
text=Text is equal to the value
desc=Content description is equal to the value
class=Class name is equal to the value
textContains=Text contains the value, without a case check
descContains=Content description contains the value, without a case check
clickable=Clickable state is equal to the Boolean value
scrollable=Scrollable state is equal to the Boolean value
selected=Selected state is equal to the Boolean value
checked=Checked state is equal to the Boolean value
feed = find(id="com.instagram.android:id/feed")
likes = findAll(descContains="like")
firstClickable = find(clickable=true)

Match translated interface text

Displayed text can change with the app language. Use appString("resource_name") to get a translated string resource from the foreground app.

forYou = appString("guide_tab_title_for_you")
if forYou != null and find(desc=forYou, selected=true) != null {
# The translated For You tab is selected
}

appString(...) returns null if it cannot find the app or resource.

Node properties

Use node.property to read a node property.

GroupProperties
Identity and contentid, text, desc, class, path
Positionx, y, left, top, right, bottom
Sizew, h, width, height
Centercx, cy
Stateclickable, scrollable, checked, selected, focused, enabled, visible
ChildrenchildCount

The path property gives a best-effort class and index path from the root. For example, it can return FrameLayout[0]/RecyclerView[1].

Node methods

MethodResult
node.child(i)Child at index i, or null
node.children()List of direct children
node.parent()Parent node, or null
node.find(...)First matching node in the subtree, or null
node.findAll(...)List of matching nodes in the subtree
node.hide(color=, touch=)Overlay that covers the node bounds
bar = find(id="com.app:id/toolbar")
if bar != null {
log("toolbar children=" + bar.childCount)
for child in bar.children() {
log(child.class + " @ " + child.x + "," + child.y)
}
}

Draw overlays and do actions

Use these functions:

  • draw(x, y, w, h, color=, touch=, key=) draws an overlay rectangle.
  • hide(node, color=, touch=) draws an overlay on the node bounds.
  • back() does the Android Back action.
  • home() does the Android Home action.
  • log(...) writes a message with the UiHider log tag.

The draw() arguments have these functions:

ArgumentFunction
x, ySet the upper-left position in pixels
w, hSet the width and height in pixels
color=Sets a hexadecimal color; the default follows the light or dark theme
touch=Blocks touches when true; permits touches through the overlay when false
key=Gives the overlay a stable identifier for smoother updates
# Cover a fixed area and permit touches through it
draw(0, 0, screen.width, 120, color="#101010", touch=false)
# Leave a screen when its Reels tab is present
if find(id="com.app:id/reels_tab") != null {
back()
}

Math and utility functions

The standard library has these functions:

  • Math: min(...), max(...), abs(x), floor(x), ceil(x), round(x), sqrt(x), pow(base, exp), clamp(value, low, high)
  • Length: len(x) for a string or list
  • Conversion: int(x) and str(x)
  • Lists: range(n) and range(start, end)
  • App resources: appString(name)

min(...) and max(...) accept multiple numbers or one list.

height = clamp(nav.top - top.bottom, 0, screen.height)

Persistent storage

Each script has a private key-value store. Curbox keeps the values in memory and saves them to disk in the background.

FunctionResult
save(key, value)Saves a number, string, Boolean value, or list
load(key)Returns the saved value or null
has(key)Returns true if the key exists
remove(key)Removes the saved value

Do not save a node. A node is valid only during the run that returned it. Save a node property instead.

Use storage to save a value that is expensive to calculate and does not change frequently:

forYou = load("forYouLabel")
if forYou == null {
forYou = appString("guide_tab_title_for_you")
if forYou != null {
save("forYouLabel", forYou)
}
}

Use storage to keep a value between runs:

runs = load("runs")
if runs == null {
runs = 0
}
save("runs", runs + 1)

Do not save screen positions that change when the user scrolls.

Script patterns

Cover an area between two nodes

top = find(id="com.app:id/header")
bottom = find(id="com.app:id/footer")
if top != null and bottom != null {
height = bottom.top - top.bottom
if height > 0 {
draw(0, top.bottom, screen.width, height)
}
}

Cover all matching nodes

for tile in findAll(id="com.app:id/recommend_card") {
hide(tile)
}

Stop on an unrelated screen

if find(id="com.app:id/search_bar") == null {
return
}
# Add actions for the applicable screen here

Draw a transparent overlay

draw(
0,
screen.height / 2,
screen.width,
screen.height / 2,
color="#80000000",
touch=false
)

Runtime limits

Curbox applies these limits to each run:

  • Operation limit
  • Time limit of approximately 40 milliseconds
  • Node-visit limit
  • Function recursion limit
  • Overlay-count limit

Curbox stops the run if the script reaches a limit. The accessibility service continues to operate. Curbox writes a warning to the UiHider log tag.

Troubleshoot a script

  • Check a find() result for null before you read its properties.
  • Check view IDs after an update to the target app.
  • Remember that a range excludes its end value.
  • Draw each required overlay during every run.
  • Use return early when the current screen does not apply.
  • Use a specific find(id=...) call when an ID is available.
  • Keep loops small because Curbox runs a script frequently.

Use this command to read script log messages:

Terminal window
adb logcat -s UiHider

Quick reference

# Context: app screen.width screen.height event.type event.package event.text event.class
# Find: root() find(<selectors>) findAll(<selectors>) node.find(...) node.findAll(...)
# Selectors: id text desc class textContains descContains clickable scrollable selected checked
# Node: id text desc class path x y left top right bottom w h width height cx cy childCount
# State: clickable scrollable checked selected focused enabled visible
# Methods: child(i) children() parent() find(...) findAll(...) hide(color=, touch=)
# Actions: draw(x,y,w,h, color=, touch=, key=) hide(node, ...) back() home() log(...)
# Utility: min max abs floor ceil round sqrt pow clamp len int str range appString
# Storage: save(key, value) load(key) has(key) remove(key)
# Control: if else if else while for x in <list|a..b> break continue
# Functions: fn name(args) { ... return ... } return