Skip to content

Commit 224e1b4

Browse files
Initial commit
0 parents  commit 224e1b4

15 files changed

Lines changed: 604 additions & 0 deletions

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Normalize EOL for all files that Git considers text files.
2+
* text=auto eol=lf

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Godot 4+ specific ignores
2+
.godot/
3+
/android/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Anish Kumar
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# android-device-explorer
2+
A Godot Editor plugin for exploring and managing Android device file system
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
@tool
2+
extends VBoxContainer
3+
4+
const ADB_PATH := "/home/anish/Android/Sdk/platform-tools/adb"
5+
const PACKAGE_NAME := "org.godotengine.editor.v4.debug"
6+
const DATA_ROOT := "/data/data/" + PACKAGE_NAME
7+
const TMP_DIR := "/data/local/tmp"
8+
const STORAGE_ROOT := "/storage/emulated/0"
9+
const PULL_PUSH_TEMP = TMP_DIR + "/godot-device-explorer-plugin"
10+
11+
@onready var tree: Tree = $Tree
12+
@onready var devices_btn: OptionButton = $HBoxContainer/OptionButton
13+
@onready var menu_button: MenuButton = $HBoxContainer/MenuButton
14+
15+
var current_device := ""
16+
var show_all := false
17+
18+
19+
func _ready() -> void:
20+
_setup_ui()
21+
_load_devices()
22+
23+
24+
func _setup_ui() -> void:
25+
tree.hide_root = true
26+
tree.item_collapsed.connect(_on_item_collapsed)
27+
28+
devices_btn.custom_minimum_size = Vector2(32, 32)
29+
devices_btn.selected = -1
30+
devices_btn.pressed.connect(_load_devices)
31+
devices_btn.item_selected.connect(_on_device_selected)
32+
33+
menu_button.icon = EditorInterface.get_base_control().get_theme_icon("GuiTabMenuHl", "EditorIcons")
34+
var popup := menu_button.get_popup()
35+
popup.clear()
36+
popup.add_check_item("Show Full Filesystem", 0)
37+
popup.set_item_checked(0, show_all)
38+
popup.id_pressed.connect(_on_menu_item_pressed)
39+
40+
41+
func _load_devices() -> void:
42+
var device_list := _get_devices()
43+
var selected = devices_btn.get_selected_id()
44+
devices_btn.clear()
45+
46+
for d in device_list:
47+
devices_btn.add_item(d)
48+
49+
if device_list.size() > 0:
50+
if selected != -1:
51+
devices_btn.select(selected)
52+
else:
53+
devices_btn.select(0)
54+
_on_device_selected(0)
55+
else:
56+
devices_btn.text = "No Device Found"
57+
tree.clear()
58+
59+
60+
func _on_device_selected(index: int) -> void:
61+
current_device = devices_btn.get_item_text(index)
62+
_load_root()
63+
64+
65+
func _load_root() -> void:
66+
tree.clear()
67+
var root := tree.create_item()
68+
69+
if show_all:
70+
root.set_text(0, "/")
71+
root.set_metadata(0, {"path": "/", "is_dir": true})
72+
_add_dummy(root)
73+
_on_dir_expanded(root)
74+
else:
75+
root.set_text(0, "Device Scopes")
76+
_create_tree_item(root, "App Data", DATA_ROOT, true)
77+
_create_tree_item(root, "Temp Storage", TMP_DIR, true)
78+
_create_tree_item(root, "Internal Storage", STORAGE_ROOT, true)
79+
80+
_run_adb(["shell", "mkdir", PULL_PUSH_TEMP])
81+
82+
83+
func _create_tree_item(parent: TreeItem, text: String, path: String, is_dir: bool, custom_icon := "", skip_dummy := false) -> TreeItem:
84+
var item := tree.create_item(parent)
85+
item.set_text(0, text)
86+
item.set_metadata(0, {"path": path, "is_dir": is_dir})
87+
88+
var gui := EditorInterface.get_base_control()
89+
var icon_name = custom_icon if custom_icon != "" else ("Folder" if is_dir else _get_icon_for_ext(path))
90+
item.set_icon(0, gui.get_theme_icon(icon_name, "EditorIcons"))
91+
92+
if is_dir:
93+
item.set_icon_modulate(0, gui.get_theme_color("accent_color", "Editor"))
94+
if not skip_dummy: _add_dummy(item)
95+
96+
item.collapsed = true
97+
return item
98+
99+
100+
func _add_dummy(parent: TreeItem) -> void:
101+
var dummy := tree.create_item(parent)
102+
dummy.set_text(0, "Empty...")
103+
dummy.set_metadata(0, null)
104+
105+
106+
func _on_item_collapsed(item: TreeItem) -> void:
107+
if not item.collapsed:
108+
_on_dir_expanded(item)
109+
110+
111+
func _on_dir_expanded(item: TreeItem, refresh := false) -> void:
112+
var meta = item.get_metadata(0)
113+
if not meta or not meta.is_dir: return
114+
115+
# Skip if already loaded (and not refreshing)
116+
if item.get_child_count() > 0 && not refresh:
117+
var first_child := item.get_child(0)
118+
if first_child.get_text(0) != "Empty...":
119+
return
120+
121+
var path: String = meta.path
122+
123+
# Direct access to /data is restricted by system permissions.
124+
# Manually constructing a sub-directory tree to allow navigation into accessible paths (example, app userdata or tmp).
125+
if path == "/data":
126+
var first_child := item.get_child(0)
127+
first_child.free() # remove the dummy file for it.
128+
_populate_special_data_dir.call_deferred(item)
129+
return
130+
131+
var files: Array = _list_dir(path)
132+
if files.size() > 0:
133+
# Files are loaded, now remove the dummy file or old items.
134+
for child in item.get_children():
135+
child.free()
136+
137+
for f in files:
138+
var full_path = path.rstrip("/") + "/" + f.name
139+
_create_tree_item.call_deferred(item, f.name, full_path, f.is_dir)
140+
141+
142+
func _populate_special_data_dir(parent: TreeItem) -> void:
143+
var data_node = _create_tree_item(parent, "data", "/data/data", true, "", true)
144+
var local_node = _create_tree_item(parent, "local", "/data/local", true, "", true)
145+
var tmp_node = _create_tree_item(local_node, "tmp", TMP_DIR, true)
146+
var pkg_node = _create_tree_item(data_node, PACKAGE_NAME, DATA_ROOT, true)
147+
148+
149+
func _on_menu_item_pressed(id: int) -> void:
150+
if id == 0:
151+
show_all = !show_all
152+
menu_button.get_popup().set_item_checked(0, show_all)
153+
_load_root()
154+
155+
156+
func _run_adb(p_args: PackedStringArray) -> String:
157+
var args: PackedStringArray = []
158+
if current_device != "":
159+
args.append_array(["-s", current_device])
160+
args.append_array(p_args)
161+
162+
var output := []
163+
OS.execute(ADB_PATH, args, output, true)
164+
return output[0] if output.size() > 0 else ""
165+
166+
167+
func _get_devices() -> Array[String]:
168+
var raw := _run_adb(["devices"])
169+
var lines := raw.split("\n")
170+
var result: Array[String] = []
171+
for i in range(1, lines.size()):
172+
var parts := lines[i].strip_edges().split("\t")
173+
if parts.size() >= 2 and parts[1] == "device":
174+
result.append(parts[0])
175+
return result
176+
177+
178+
func _list_dir(path: String) -> Array:
179+
var args := ["shell"]
180+
if path.begins_with(DATA_ROOT):
181+
args.append_array(["run-as", PACKAGE_NAME, "ls", "-1", "-p", path])
182+
else:
183+
args.append("ls -1 -p '%s'" % path)
184+
185+
var result := _run_adb(args)
186+
var files := []
187+
for line in result.split("\n"):
188+
line = line.strip_edges()
189+
if line == "" or line.begins_with("total"): continue
190+
files.append({"name": line.rstrip("/"), "is_dir": line.ends_with("/")})
191+
return files
192+
193+
194+
func _get_icon_for_ext(path: String) -> String:
195+
var ext := path.get_extension().to_lower()
196+
match ext:
197+
"gd": return "GDScript"
198+
"tscn": return "PackedScene"
199+
"res", "tres": return "Resource"
200+
"png", "jpg", "svg": return "ImageTexture"
201+
"txt", "json": return "TextFile"
202+
_: return "File"
203+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://d55o7lyu5gfc
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[gd_scene format=3 uid="uid://c01i5jf0306r"]
2+
3+
[ext_resource type="Script" uid="uid://d55o7lyu5gfc" path="res://addons/android_device_explorer/device_explorer.gd" id="1_hjw5w"]
4+
5+
[sub_resource type="DPITexture" id="DPITexture_hjw5w"]
6+
_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#e0e0e0\" d=\"M8 0a2 2 0 0 0 0 4 2 2 0 0 0 0-4zm0 6a2 2 0 0 0 0 4 2 2 0 0 0 0-4zm0 6a2 2 0 0 0 0 4 2 2 0 0 0 0-4z\"/></svg>
7+
"
8+
saturation = 2.0
9+
color_map = {
10+
Color(1, 0.37254903, 0.37254903, 1): Color(1, 0.47, 0.42, 1),
11+
Color(0.37254903, 1, 0.5921569, 1): Color(0.45, 0.95, 0.5, 1),
12+
Color(1, 0.8666667, 0.39607844, 1): Color(0.83, 0.78, 0.62, 1)
13+
}
14+
15+
[node name="DeviceExplorer" type="VBoxContainer" unique_id=877953201]
16+
anchors_preset = 15
17+
anchor_right = 1.0
18+
anchor_bottom = 1.0
19+
grow_horizontal = 2
20+
grow_vertical = 2
21+
script = ExtResource("1_hjw5w")
22+
23+
[node name="HBoxContainer" type="HBoxContainer" parent="." unique_id=1345110892]
24+
layout_mode = 2
25+
26+
[node name="OptionButton" type="OptionButton" parent="HBoxContainer" unique_id=1384706306]
27+
custom_minimum_size = Vector2(32, 32)
28+
layout_mode = 2
29+
size_flags_horizontal = 3
30+
31+
[node name="MenuButton" type="MenuButton" parent="HBoxContainer" unique_id=640725038]
32+
layout_mode = 2
33+
icon = SubResource("DPITexture_hjw5w")
34+
35+
[node name="Tree" type="Tree" parent="." unique_id=1901016460]
36+
layout_mode = 2
37+
size_flags_vertical = 3
38+
hide_root = true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[plugin]
2+
3+
name="Android Device Explorer"
4+
description="This plugins adds a new dock in the editor to access the filesystem of the connected Android device."
5+
author="syntaxerror247"
6+
version="0.1"
7+
script="plugin.gd"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@tool
2+
extends EditorPlugin
3+
4+
var dock
5+
6+
func _enter_tree():
7+
var dock_scene = preload("res://addons/android_device_explorer/device_explorer.tscn").instantiate()
8+
9+
dock = EditorDock.new()
10+
dock.add_child(dock_scene)
11+
dock.title = "Android Device Explorer"
12+
dock.default_slot = EditorDock.DOCK_SLOT_RIGHT_BL
13+
dock.available_layouts = EditorDock.DOCK_LAYOUT_VERTICAL | EditorDock.DOCK_LAYOUT_FLOATING
14+
add_dock(dock)
15+
16+
func _exit_tree():
17+
remove_dock(dock)
18+
dock.queue_free()

0 commit comments

Comments
 (0)