21 Commits
Author SHA1 Message Date
shihaam cd807ff304 add few more OSes and add sub categories 2026-03-12 15:20:11 +05:00
shihaam 0d5a73251c Sanitize user input to prevent shell escape 2026-03-12 13:33:51 +05:00
shihaam 9f09ac1ad0 update fastlane change logs 2026-03-11 12:53:11 +05:00
shihaam 6cdd812144 version 1.4 2026-03-11 12:45:51 +05:00
shihaam cbee1786ca findos icon is now public 2026-03-11 12:45:29 +05:00
shihaam 5ea665c02f feature to indicate app created folders 2026-03-11 12:44:53 +05:00
shihaam 7c269d5e59 make OS icon a public fun 2026-03-11 12:44:24 +05:00
shihaam fbda9fedfb haha more unix way 2026-03-11 12:43:48 +05:00
shihaam 79ccd6753e added a file manager for directory selection 2026-03-11 12:42:49 +05:00
shihaam 3decb7307e update empty isodroid dir hits in home page 2026-03-11 11:35:45 +05:00
shihaam d297ab1059 new version 1.3 2026-03-11 11:13:22 +05:00
shihaam 371e38dc2f show dynamic path 2026-03-11 11:10:26 +05:00
shihaam 1e850a2d1a fix path typos 2026-03-11 11:08:28 +05:00
shihaam 4cd9202609 fix app name, update version no, update images 2026-03-11 01:04:19 +05:00
shihaam 6becf2907e fix app name, update version no, update images 2026-03-11 01:01:15 +05:00
shihaam c34900a2b1 Add SPDX license headers to source files 2026-03-10 23:46:43 +05:00
shihaam f654ba52a1 Add SPDX license headers to source files 2026-03-10 23:42:40 +05:00
shihaam e7735a0446 script to build isodrive 2026-03-10 21:15:17 +05:00
shihaam 1e0d5dd640 script to pull precompiled binaries 2026-03-10 20:07:25 +05:00
shihaam 2672f8dfa5 copy actual images back to where it belong 2026-03-10 18:20:17 +05:00
shihaam 157f5103f1 prep for fdroid release 2026-03-10 18:15:39 +05:00
63 changed files with 986 additions and 83 deletions
+48
View File
@@ -0,0 +1,48 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4] - 2026-03-11
### Added
- Directory browser for changing ISO directory in settings
- Create new directories from the directory browser
- Delete directories created by the app (long press)
- Shows ISO/IMG files with OS icons in directory browser
### Changed
- Empty state on home screen now shows current path and helpful hints
- Version number is now read dynamically from app config
- Renamed "folder" to "directory" throughout the UI
## [1.3] - 2025-03-11
### Changed
- Fix default image dir to be /sdcard/isodroid instead of /sdcard/isodrive
## [1.2] - 2025-03-10
### Changed
- Fix app name displaying as "ISO Drive" instead of "ISO Droid" in some places
## [1.1] - 2025-03-10
### Changed
- New app icon
## [1.0] - 2025-03-10
### Added
- Mount ISO/IMG files as USB mass storage or CD-ROM
- Create blank IMG files for writable USB drives
- Read-only or read-write mount options
- Persistent notification with quick unmount button
- OS-specific icons for popular distributions (Linux, BSD, etc.)
- Download links to popular operating systems (Linux, BSD, Windows, Recovery tools)
- Rename and delete files
- Material 3 dynamic theming with dark mode support
- Support for Magisk, KernelSU, and APatch root solutions
- Bundled isodrive binary for all architectures (arm64-v8a, armeabi-v7a, x86_64, x86)
+1 -1
View File
@@ -35,7 +35,7 @@ Android app for mounting ISO/IMG files as USB mass storage or CD-ROM devices on
1. Download the APK from the links above
2. Install the APK on your rooted Android device
3. Grant root access when prompted
4. Place your ISO/IMG files in `/sdcard/isodrive/` (or configure a different directory in settings)
4. Place your ISO/IMG files in `/sdcard/isodroid/` (or configure a different directory in settings)
> **Note**: The app includes a bundled `isodrive` binary. No additional setup required!
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId = "sh.sar.isodroid"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.1"
versionCode = 4
versionName = "1.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
+116
View File
@@ -2,13 +2,23 @@
{
"name": "Arch Linux",
"category": "Linux",
"subcategory": "Enthusiast",
"description": "A simple, lightweight distribution",
"icon": "archlinux.svg",
"url": "https://archlinux.org/download/"
},
{
"name": "Gentoo Linux",
"category": "Linux",
"subcategory": "Enthusiast",
"description": "A highly flexible, source-based Linux distribution.",
"icon": "gentoo.svg",
"url": "https://www.gentoo.org/downloads/"
},
{
"name": "Debian",
"category": "Linux",
"subcategory": "Server",
"description": "Debian is a complete Free Operating System!",
"icon": "debian.svg",
"url": "https://www.debian.org/download"
@@ -16,6 +26,7 @@
{
"name": "Fedora",
"category": "Linux",
"subcategory": "Desktop",
"description": "The Fedora Project is a community of people working together to build a free and open source software platform and to collaborate on and share user-focused solutions built on that platform.",
"icon": "fedora.svg",
"url": "https://www.fedoraproject.org/"
@@ -23,6 +34,7 @@
{
"name": "Linux Mint",
"category": "Linux",
"subcategory": "Desktop",
"description": "Linux Mint is an operating system for desktop and laptop computers. It is designed to work 'out of the box' and comes fully equipped with the apps most people need.",
"icon": "linuxmint.svg",
"url": "https://linuxmint.com/download.php"
@@ -30,6 +42,7 @@
{
"name": "NixOS",
"category": "Linux",
"subcategory": "Enthusiast",
"description": "Declarative builds and deployments.",
"icon": "nixos.svg",
"url": "https://nixos.org/download/#nixos-iso"
@@ -37,6 +50,7 @@
{
"name": "Pop!_OS",
"category": "Linux",
"subcategory": "Desktop",
"description": "Unleash your potential on a Linux operating system made to be productive and personal.",
"icon": "popos.svg",
"url": "https://system76.com/pop/download/"
@@ -44,6 +58,7 @@
{
"name": "Tails",
"category": "Linux",
"subcategory": "Security",
"description": "Tails is a portable operating system that protects against surveillance and censorship.",
"icon": "tails.svg",
"url": "https://tails.net/install/download/index.en.html"
@@ -51,6 +66,7 @@
{
"name": "Ubuntu",
"category": "Linux",
"subcategory": "Desktop",
"description": "",
"icon": "ubuntu.svg",
"url": "https://ubuntu.com/download"
@@ -58,6 +74,7 @@
{
"name": "FreeBSD",
"category": "BSD",
"subcategory": null,
"description": "FreeBSD is an operating system for a variety of platforms which focuses on features, speed, and stability.",
"icon": "freebsd.svg",
"url": "https://www.freebsd.org/where/"
@@ -65,22 +82,121 @@
{
"name": "OPNsense",
"category": "BSD",
"subcategory": "Networking",
"description": "OPNsense® is an open source, feature rich firewall and routing platform, offering cutting-edge network protection.",
"icon": "opnsense.svg",
"url": "https://opnsense.org/download/"
},
{
"name": "pfSense",
"category": "BSD",
"subcategory": "Networking",
"description": "Secure networks start here.™",
"icon": "pfsense.svg",
"url": "https://www.pfsense.org/download/"
},
{
"name": "Windows 11",
"category": "Windows",
"subcategory": null,
"description": "",
"icon": "windows11.svg",
"url": "https://www.microsoft.com/en-us/software-download/windows11"
},
{
"name": "Windows 10",
"category": "Windows",
"subcategory": null,
"description": "",
"icon": "windows10.svg",
"url": "https://www.microsoft.com/en-us/software-download/windows10ISO"
},
{
"name": "Hiren's BootCD PE",
"category": "Recovery",
"subcategory": null,
"description": "Hiren's BootCD PE (Preinstallation Environment) is a restored edition of Hiren's BootCD based on Windows PE",
"icon": null,
"url": "https://www.hirensbootcd.org/download/"
},
{
"name": "Clonezilla",
"category": "Recovery",
"subcategory": null,
"description": "Clonezilla is a partition and disk imaging/cloning program.",
"icon": null,
"url": "https://clonezilla.org/downloads.php"
},
{
"name": "Bazzite",
"category": "Linux",
"subcategory": "Desktop",
"description": "Bazzite makes gaming and everyday use smoother and simpler across desktop PCs, handhelds, tablets, and home theater PCs.",
"icon": null,
"url": "https://bazzite.gg/#image-picker"
},
{
"name": "Manjaro",
"category": "Linux",
"subcategory": "Desktop",
"description": "Taking the raw power and flexibility of Arch Linux and making it more accessible for a greater audience.",
"icon": "manjaro.svg",
"url": "https://manjaro.org/products"
},
{
"name": "Super Grub2",
"category": "Recovery",
"subcategory": null,
"description": "Get back to your GNU/Linux & Windows computers !",
"icon": null,
"url": "https://www.supergrubdisk.org/category/download/"
},
{
"name": "Kali Linux",
"category": "Linux",
"subcategory": "Security",
"description": "The most advanced penetration testing platform ever made.",
"icon": "kalilinux.svg",
"url": "https://www.kali.org/get-kali/#kali-installer-images"
},
{
"name": "ParrotOS",
"category": "Linux",
"subcategory": "Security",
"description": "The ultimate framework for your Cyber Security operations",
"icon": "parrotsecurity.svg",
"url": "https://www.parrotsec.org/download/"
},
{
"name": "ProxmoxVE",
"category": "Linux",
"subcategory": "Server",
"description": "Proxmox Virtual Environment is a complete open-source platform for enterprise virtualization.",
"icon": "proxmox.svg",
"url": "https://www.proxmox.com/en/downloads/proxmox-virtual-environment/iso"
},
{
"name": "Android-x86",
"category": "Android",
"subcategory": null,
"description": "This is a project to port Android Open Source Project to x86 platform",
"icon": "android.svg",
"url": "https://www.android-x86.org/download"
},
{
"name": "RemixOS",
"category": "Android",
"subcategory": null,
"description": "RemixOS brings Android to laptops and PCs with a powerful desktop-like experience.",
"icon": null,
"url": "https://www.fosshub.com/Remix-OS.html"
},
{
"name": "TempleOS",
"category": "Other",
"subcategory": null,
"description": "",
"icon": null,
"url": "https://templeos.org/Downloads/"
}
]
+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Android</title><path d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>elementary</title><path d="M12 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24zm0 1a11 11 0 0 1 10.59 8.01 19.09 19.09 0 0 1-4.66 6.08c-.94.81-1.96 1.53-3.08 2.04-1.13.5-2.37.8-3.6.72a6.23 6.23 0 0 1-2.66-.76 20.02 20.02 0 0 0 5.68-4.58 9.97 9.97 0 0 0 2.31-4.17c.18-.79.2-1.6.04-2.4a4.42 4.42 0 0 0-1.08-2.11 4.33 4.33 0 0 0-2-1.19 5.25 5.25 0 0 0-2.33-.08A7.8 7.8 0 0 0 7.2 4.85a9.77 9.77 0 0 0-2.94 7.49 7.88 7.88 0 0 0 1.95 4.59 18 18 0 0 1-3.56.85A11 11 0 0 1 12 1zm.07 2.22c.77 0 1.55.24 2.17.7.55.42.97 1.02 1.2 1.68.23.65.3 1.37.21 2.06a7.85 7.85 0 0 1-1.7 3.76 16.22 16.22 0 0 1-6.37 4.96c-.48-.42-.9-.92-1.2-1.48a6.61 6.61 0 0 1-.75-3.87c.12-1.32.58-2.6 1.2-3.79a7.92 7.92 0 0 1 3.02-3.42c.68-.37 1.45-.6 2.22-.6zm10.83 7.3A11 11 0 0 1 3.52 19a19.8 19.8 0 0 0 3.63-1.2c.51.4 1.08.71 1.67.94a8 8 0 0 0 5.44-.04 13.3 13.3 0 0 0 4.64-2.95 20 20 0 0 0 4-5.22z"/></svg>

After

Width:  |  Height:  |  Size: 940 B

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Gentoo</title><path d="M9.94 0a7.31 7.31 0 00-1.26.116c-4.344.795-7.4 4.555-7.661 7.031-.126 1.215.53 2.125.89 2.526.977 1.085 2.924 1.914 4.175 2.601-1.81 1.543-2.64 2.296-3.457 3.154C1.403 16.712.543 18.125.54 19.138c0 .325-.053 1.365.371 2.187.16.309.613 1.338 1.98 2.109.874.494 2.119.675 3.337.501 3.772-.538 8.823-3.737 12.427-6.716 2.297-1.9 3.977-3.739 4.462-4.644.39-.731.434-2.043.207-2.866-.645-2.337-5.887-7.125-10.172-9.051A7.824 7.824 0 009.94 0zm-.008.068a7.4 7.4 0 013.344.755c3.46 1.7 9.308 6.482 9.739 8.886.534 2.972-9.931 11.017-16.297 12.272-2.47.485-4.576.618-5.537-1.99-.832-2.262.783-3.916 3.16-6.09a92.546 92.546 0 012.96-2.576c.065-.069-5.706-2.059-5.89-4.343C1.221 4.634 4.938.3 9.697.076c.08-.004.157-.007.235-.008zm-.112.52a5.647 5.647 0 00-.506.032c-2.337.245-2.785.547-4.903 2.149-.71.537-2.016 1.844-2.35 3.393-.128.59.024 1.1.448 1.458 1.36 1.144 3.639 2.072 5.509 2.97.547.263.185.74-.698 1.505-2.227 1.928-5.24 4.276-5.45 6.066-.099.842.19 1.988 1.213 2.574 1.195.685 3.676.238 5.333-.379 2.422-.902 5.602-2.892 8.127-4.848 2.625-2.034 5.067-4.617 5.188-5.038.148-.517.133-.996-.154-1.546-.448-.862-1.049-1.503-1.694-2.22-1.732-1.825-3.563-3.43-5.754-4.658C12.694 1.242 11.417.564 9.82.588zm1.075 3.623c.546 0 1.176.173 1.853.5 1.688.817 3.422 2.961-.015 4.195-.935.336-3.9-.824-3.81-2.407.09-1.57.854-2.289 1.972-2.288zm.285 1.367c-.317-.002-.575.079-.694.263-.557.861-.303 1.472.212 1.862.192-.457 2.156.043 2.148.472a.32.32 0 00.055-.032c1.704-1.282-.472-2.557-1.72-2.565z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Manjaro</title><path d="M2.182 0A2.177 2.177 0 0 0 0 2.182v19.636C0 23.027.973 24 2.182 24h4.363V6.545h8.728V0Zm15.273 0v24h4.363A2.177 2.177 0 0 0 24 21.818V2.182A2.177 2.177 0 0 0 21.818 0ZM8.727 8.727V24h6.546V8.727Z"/></svg>

After

Width:  |  Height:  |  Size: 306 B

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>openSUSE</title><path d="M10.724 0a12 12 0 0 0-9.448 4.623c1.464.391 2.5.727 2.81.832.005-.19.037-1.893.037-1.893s.004-.04.025-.06c.026-.026.065-.018.065-.018.385.056 8.602 1.274 12.066 3.292.427.25.638.517.902.786.958.99 2.223 5.108 2.359 5.957.005.033-.036.07-.054.083a5.177 5.177 0 0 1-.313.228c-.82.55-2.708 1.872-5.13 1.656-2.176-.193-5.018-1.44-8.445-3.699.336.79.668 1.58 1 2.371.497.258 5.287 2.7 7.651 2.651 1.904-.04 3.941-.968 4.756-1.458 0 0 .179-.108.257-.048.085.066.061.167.041.27-.05.234-.164.66-.242.863l-.065.165c-.093.25-.183.482-.356.625-.48.436-1.246.784-2.446 1.305-1.855.812-4.865 1.328-7.66 1.31-1.001-.022-1.968-.133-2.817-.232-1.743-.197-3.161-.357-4.026.269A12 12 0 0 0 10.724 24a12 12 0 0 0 12-12 12 12 0 0 0-12-12zM13.4 6.963a3.503 3.503 0 0 0-2.521.942 3.498 3.498 0 0 0-1.114 2.449 3.528 3.528 0 0 0 3.39 3.64 3.48 3.48 0 0 0 2.524-.946 3.504 3.504 0 0 0 1.114-2.446 3.527 3.527 0 0 0-3.393-3.64zm-.03 1.035a2.458 2.458 0 0 1 2.368 2.539 2.43 2.43 0 0 1-.774 1.706 2.456 2.456 0 0 1-1.762.659 2.461 2.461 0 0 1-2.364-2.542c.02-.655.3-1.26.777-1.707a2.419 2.419 0 0 1 1.756-.655zm.402 1.23c-.602 0-1.087.325-1.087.727 0 .4.485.725 1.087.725.6 0 1.088-.326 1.088-.725 0-.402-.487-.726-1.088-.726Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
./parrotsecurity.svg
+1
View File
@@ -0,0 +1 @@
./parrotsecurity.svg
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Parrot Security</title><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0Zm6.267 2.784L13.03 5.54l8.05-.179-8.05 3.333-2.154 2.688 5.007 9.038-1.536-1.605 1.645 3.456-4.937-5.527-6.268-6.28L2.77 12.11l.7-3.442 4.018-.261.823-4.06Z"/></svg>

After

Width:  |  Height:  |  Size: 351 B

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>pfSense</title><path d="M2.013 0C.898 0 0 .929 0 2.044v17.775L3.252 8.27h3.282L6.1 9.785h.063c.186-.217.433-.403.742-.62.31-.216.62-.402.96-.588.342-.186.713-.31 1.116-.433.402-.124.805-.155 1.208-.155.867 0 1.579.154 2.198.433.62.279 1.084.712 1.455 1.239.31.464.5 1.019.593 1.669.006.06.027.135.027.189.062.712-.031 1.518-.28 2.385a8.571 8.571 0 0 1-1.02 2.322 9.885 9.885 0 0 1-1.58 1.95 8.125 8.125 0 0 1-2.044 1.364 5.536 5.536 0 0 1-2.354.495 5.655 5.655 0 0 1-1.982-.34c-.588-.217-.99-.62-1.238-1.177h-.062L2.353 24h19.603A2.042 2.042 0 0 0 24 21.956V4.706c-.093-.03-.186-.06-.248-.092a2.771 2.771 0 0 0-.557-.062c-.557 0-1.022.124-1.394.372-.34.248-.65.743-.867 1.518l-.526 1.826h2.013l.495 1.58-1.3 1.27h-2.014l-2.446 8.67h-3.53l2.446-8.67h-1.455l.805-2.85h1.425l.557-2.044c.185-.619.403-1.238.681-1.795a4.996 4.996 0 0 1 1.053-1.487c.433-.434.99-.775 1.641-1.022.65-.248 1.487-.372 2.447-.372.248 0 .464 0 .712.031A2.082 2.082 0 0 0 21.988 0zm6.565 11.118c-.898 0-1.672.278-2.323.805-.65.526-1.083 1.239-1.331 2.106-.248.867-.217 1.579.155 2.105.31.557.929.805 1.858.805.898 0 1.672-.278 2.322-.805.65-.526 1.115-1.238 1.363-2.105.247-.867.185-1.58-.155-2.106-.34-.527-.991-.805-1.89-.805Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
proxmox.svg
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Slackware</title><path d="M12.924 1.228c-.584-.01-1.251 0-1.485.027-2.46.282-4.138 1.3-4.753 2.891-.218.552-.274 1.002-.243 1.772.048 1.21.419 2.004 1.262 2.742 1.225 1.06 2.98 1.508 5.998 1.508 2.737 0 3.71.413 3.916 1.675.313 1.867-1.57 3.07-4.414 2.827-1.878-.16-3.496-.912-4.223-1.967a7.772 7.772 0 01-.355-.62c-.382-.76-.64-.978-1.176-.978-.43.005-.732.165-.918.494l-.133.24v4.03l.137.296c.165.344.4.546.744.63.35.09.794-.036 1.42-.402l.5-.29.826.185c1.82.403 2.75.523 4.065.523 1.103.005 1.548-.046 2.455-.285 1.124-.297 1.974-.785 2.717-1.57.8-.844 1.15-1.853 1.097-3.147-.069-1.628-.695-2.698-2-3.414-.96-.525-2.292-.79-4.377-.88-2.042-.086-2.794-.155-3.515-.32-.51-.12-.785-.25-1.076-.515-.653-.589-.59-1.755.136-2.482.642-.637 1.511-.928 2.774-.928 1.432.005 2.393.27 3.412.955.185.127.721.62 1.193 1.092.886.902 1.135 1.082 1.506 1.082.244 0 .59-.163.732-.344.26-.329.303-.63.303-2.2 0-1.66-.043-1.91-.377-2.282-.387-.425-.848-.42-1.75.031l-.59.297-.63-.17c-1.496-.392-2.038-.477-3.178-.504zM0 13.775v9h24v-1H1v-8z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Solus</title><path d="M7.453 0c-.18.587-.369 1.167-.565 1.75A11.638 11.638 0 0 0 0 12.364a11.638 11.638 0 0 0 .516 3.403l-.339.598L0 16.73l.279.143a3.448 3.448 0 0 0 .741.222A11.638 11.638 0 0 0 2 18.868c4.034.343 8.55.512 12.446-.056 3.192-.463 5.94-1.423 7.735-3.117.252-.233.474-.474.674-.722.019-.038.037-.053.06-.076.011 0 .026-.037.038-.052.015 0 .03-.038.041-.057.008 0 .015-.038.023-.038.33-.444.587-.892.801-1.31l.181-.365-.365-.365a5.936 5.936 0 0 0-.361-.35A11.638 11.638 0 0 0 11.635.722a11.638 11.638 0 0 0-3.211.463C7.96.508 7.596.041 7.453 0zm.365 1.637C9.06 3.82 10.13 5.06 11.454 7.457c.132 1.524.67 9.45.727 10.181-.392-.037-2.485-.24-5.104-.515-1.43-.147-2.899-.316-4.092-.49l-1.9-.447c2.149-3.787 5.551-9.727 6.737-14.548zm4.543 6.18s4.991 3.927 7.092 8.73c-2.56 1.26-4.916 1.098-6.361 1.09 1.023-2.634 1.023-6.21-.73-9.82zm3.456 2.184a45.14 45.14 0 0 1 2.91.907c1.768.629 3.417 1.49 4.365 2.364a6.956 6.956 0 0 1-2.91 2.91c.151-1.495-.39-2.933-1.456-4.002-.787-.787-1.822-1.453-2.91-2.183zm6.707 6.478c-2.352 1.667-5.126 2.68-7.965 3.112a41.026 41.026 0 0 1-3.715.34h-.323a53.48 53.48 0 0 1-3.727 0 85.763 85.763 0 0 1-4.178-.23h-.003c2.555 3.255 6.993 4.893 11.092 4.102a11.367 11.367 0 0 0 4.498-1.852 11.638 11.638 0 0 0 .007 0c.312-.214.614-.444.903-.685a11.638 11.638 0 0 0 .038-.037 11.555 11.555 0 0 0 3.376-4.762zM2.511 19.584a11.638 11.638 0 0 0 .023.038c-.008 0-.015-.038-.023-.038z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Zorin</title><path d="M4 18.944L5.995 22.4h12.01L20 18.944H4zM24 12l-2.013 3.488H9.216l12.771-6.976L24 12zM0 12l2.013-3.488h12.771L2.013 15.488 0 12zm4-6.944L5.995 1.6h12.01L20 5.056H4z"/></svg>

After

Width:  |  Height:  |  Size: 272 B

@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid
import android.app.Application
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid
import android.content.Context
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.data
import java.io.File
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.data
data class MountOptions(
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.data
enum class MountType {
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.isodrive
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.isodrive
import android.content.Context
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.isodrive
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.notification
import android.content.BroadcastReceiver
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.notification
import android.app.NotificationChannel
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.notification
import android.content.BroadcastReceiver
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.root
import com.topjohnwu.superuser.Shell
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.layout.Column
@@ -71,9 +76,10 @@ fun CreateImgDialog(
val fullFileName = if (fileName.isNotBlank()) "$fileName.img" else ""
val fileExists = fullFileName.isNotEmpty() && existingFiles.contains(fullFileName)
val hasInvalidChars = fileName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
val isValidInput = fileName.isNotBlank() &&
!fileName.contains("/") &&
!hasInvalidChars &&
sizeValue.toLongOrNull()?.let { it > 0 } == true &&
!fileExists
@@ -130,11 +136,11 @@ fun CreateImgDialog(
// File name input
OutlinedTextField(
value = fileName,
onValueChange = { fileName = it.replace("/", "") },
onValueChange = { fileName = it },
label = { Text("File Name") },
suffix = { Text(".img") },
singleLine = true,
isError = fileExists,
isError = fileExists || hasInvalidChars,
modifier = Modifier.fillMaxWidth()
)
@@ -148,6 +154,16 @@ fun CreateImgDialog(
)
}
// Show error if invalid characters
if (hasInvalidChars) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Invalid file name",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.height(16.dp))
// Size input
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -50,7 +55,7 @@ import sh.sar.isodroid.data.IsoFile
* 1. Longest match (most specific)
* 2. Earliest position in filename (if same length)
*/
private fun findOsIcon(context: android.content.Context, filename: String): String? {
fun findOsIcon(context: android.content.Context, filename: String): String? {
return try {
// Dynamically load available icon files from assets
val availableIcons = context.assets.list("osicons")
@@ -106,10 +111,19 @@ fun FileBrowser(
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Place ISO or IMG files in this directory",
text = "Place ISO or IMG files in:\n$currentPath",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Tap + to create an empty IMG file\nChange directory in Settings",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}
} else {
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.clickable
@@ -165,7 +170,8 @@ private fun RenameDialog(
val nameWithoutExtension = currentName.removeSuffix(extension)
var newName by remember { mutableStateOf(nameWithoutExtension) }
val isValid = newName.isNotBlank() && !newName.contains("/")
val hasInvalidChars = newName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
val isValid = newName.isNotBlank() && !hasInvalidChars
AlertDialog(
onDismissRequest = onDismiss,
@@ -174,12 +180,22 @@ private fun RenameDialog(
Column {
OutlinedTextField(
value = newName,
onValueChange = { newName = it.replace("/", "") },
onValueChange = { newName = it },
label = { Text("File Name") },
suffix = { Text(extension) },
singleLine = true,
isError = hasInvalidChars,
modifier = Modifier.fillMaxWidth()
)
if (hasInvalidChars) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Invalid file name",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
},
confirmButton = {
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.layout.Arrangement
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.components
import androidx.compose.foundation.background
@@ -42,30 +47,6 @@ import sh.sar.isodroid.ui.theme.MountedGreen
import sh.sar.isodroid.ui.theme.UnmountedGray
import java.io.File
/**
* Finds a matching OS icon filename for a given file by dynamically checking available icons.
*/
private fun findOsIcon(context: android.content.Context, filename: String): String? {
return try {
val availableIcons = context.assets.list("osicons")
?.filter { it.endsWith(".svg", ignoreCase = true) }
?.map { it.removeSuffix(".svg").lowercase() }
?: emptyList()
val lowerFilename = filename.lowercase()
availableIcons
.filter { lowerFilename.contains(it) }
.maxWithOrNull(compareBy(
{ it.length },
{ -lowerFilename.indexOf(it) }
))
?.let { "$it.svg" }
} catch (e: Exception) {
null
}
}
@Composable
fun StatusCard(
mountStatus: MountStatus,
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.screens
import android.content.Context
@@ -45,6 +50,7 @@ import org.json.JSONArray
data class OsDownload(
val name: String,
val category: String,
val subcategory: String?,
val description: String,
val icon: String?,
val url: String
@@ -62,6 +68,7 @@ private fun loadOsDownloads(context: Context): List<OsDownload> {
OsDownload(
name = obj.getString("name"),
category = obj.getString("category"),
subcategory = if (obj.isNull("subcategory")) null else obj.optString("subcategory", null),
description = obj.optString("description", ""),
icon = if (obj.isNull("icon")) null else obj.optString("icon", null),
url = obj.getString("url")
@@ -83,11 +90,9 @@ fun DownloadsScreen(
val context = LocalContext.current
val downloads = remember { loadOsDownloads(context) }
// Group by category and maintain order: Linux, BSD, Windows, Recovery
val categoryOrder = listOf("Linux", "BSD", "Windows", "Recovery")
// Group by category alphabetically
val groupedDownloads = remember(downloads) {
downloads.groupBy { it.category }
.toSortedMap(compareBy { categoryOrder.indexOf(it).takeIf { i -> i >= 0 } ?: Int.MAX_VALUE })
downloads.groupBy { it.category }.toSortedMap()
}
fun openUrl(url: String) {
@@ -161,22 +166,86 @@ private fun DownloadCategory(
)
) {
Column {
downloads.forEachIndexed { index, os ->
DownloadItem(
os = os,
onClick = { onItemClick(os) }
)
if (index < downloads.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
// Check if any items have subcategories
val hasSubcategories = downloads.any { it.subcategory != null }
if (hasSubcategories) {
// Group by subcategory alphabetically
val groupedBySubcategory = downloads
.groupBy { it.subcategory ?: "" }
.toSortedMap()
var isFirstGroup = true
groupedBySubcategory.forEach { (subcategory, osList) ->
if (subcategory.isNotEmpty()) {
// Subcategory header
if (!isFirstGroup) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
)
}
SubcategoryHeader(subcategory)
}
isFirstGroup = false
osList.sortedBy { it.name.lowercase() }.forEachIndexed { index, os ->
DownloadItem(
os = os,
onClick = { onItemClick(os) }
)
if (index < osList.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f)
)
}
}
}
} else {
// No subcategories, show flat list
downloads.forEachIndexed { index, os ->
DownloadItem(
os = os,
onClick = { onItemClick(os) }
)
if (index < downloads.lastIndex) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
)
}
}
}
}
}
}
@Composable
private fun SubcategoryHeader(title: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
)
Text(
text = title,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 12.dp)
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
)
}
}
@Composable
private fun DownloadItem(
os: OsDownload,
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.screens
import androidx.compose.foundation.layout.Box
@@ -84,7 +89,7 @@ fun MainScreen(
Scaffold(
topBar = {
TopAppBar(
title = { Text("ISO Drive") },
title = { Text("ISO Droid") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.screens
import android.Manifest
@@ -5,23 +10,38 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.Album
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.CreateNewFolder
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Info
@@ -30,6 +50,7 @@ import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -42,20 +63,33 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import coil.compose.AsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import kotlinx.coroutines.launch
import sh.sar.isodroid.root.RootManager
import sh.sar.isodroid.ui.components.findOsIcon
import sh.sar.isodroid.viewmodel.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@@ -68,7 +102,6 @@ fun SettingsScreen(
val context = LocalContext.current
val activity = context as? androidx.activity.ComponentActivity
var showPathDialog by remember { mutableStateOf(false) }
var tempPath by remember(uiState.currentPath) { mutableStateOf(uiState.isoDirectory) }
// Track notification permission with lifecycle-aware refresh
var hasNotificationPermission by remember {
@@ -226,11 +259,18 @@ fun SettingsScreen(
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = "ISO Drive",
text = "ISO Droid",
style = MaterialTheme.typography.titleMedium
)
val versionName = remember {
try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
} catch (e: Exception) {
"Unknown"
}
}
Text(
text = "Version 1.0",
text = "Version $versionName",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -254,13 +294,13 @@ fun SettingsScreen(
color = MaterialTheme.colorScheme.primary
)
Text(
text = "git.shihaam.dev/shihaam/ISODrive",
text = "git.shihaam.dev/shihaam/ISODroid",
style = MaterialTheme.typography.bodySmall.copy(
textDecoration = TextDecoration.Underline
),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.clickable {
openUrl("https://git.shihaam.dev/shihaam/ISODrive")
openUrl("https://git.shihaam.dev/shihaam/ISODroid")
}
)
}
@@ -301,7 +341,7 @@ fun SettingsScreen(
}
}
Text(
text = "The CLI tool that powers ISO Drive. Mounts ISO/IMG files as bootable USB devices using configfs.",
text = "The CLI tool that powers ISO Droid. Mounts ISO/IMG files as bootable USB devices using configfs.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
@@ -364,40 +404,394 @@ fun SettingsScreen(
}
}
// Path edit dialog
// Directory browser dialog
if (showPathDialog) {
DirectoryBrowserDialog(
initialPath = uiState.isoDirectory,
onDismiss = { showPathDialog = false },
onSelect = { selectedPath ->
viewModel.setIsoDirectory(selectedPath)
showPathDialog = false
}
)
}
}
private data class BrowserItem(
val name: String,
val isDirectory: Boolean,
val fullPath: String,
val isDeletable: Boolean = false
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun DirectoryBrowserDialog(
initialPath: String,
onDismiss: () -> Unit,
onSelect: (String) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val hapticFeedback = LocalHapticFeedback.current
var currentPath by remember { mutableStateOf(initialPath) }
var items by remember { mutableStateOf<List<BrowserItem>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
var showCreateFolderDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf<String?>(null) }
var newFolderName by remember { mutableStateOf("") }
val storageRoot = Environment.getExternalStorageDirectory().absolutePath
fun loadContents(path: String) {
scope.launch {
isLoading = true
// Load directories
val dirResult = RootManager.executeCommand(
"find \"$path\" -maxdepth 1 -mindepth 1 -type d 2>/dev/null"
)
val directories = if (dirResult.success && dirResult.output.isNotBlank()) {
dirResult.output.lines()
.filter { it.isNotBlank() }
.map { it.trim() }
.filter { !it.substringAfterLast("/").startsWith(".") }
.map { dirPath ->
// Check if this directory was created by the app (has .isodroiddir marker)
val markerCheck = RootManager.executeCommand(
"test -f \"$dirPath/.isodroiddir\" && echo 'yes' || echo 'no'"
)
val isDeletable = markerCheck.output.trim() == "yes"
BrowserItem(dirPath.substringAfterLast("/"), true, dirPath, isDeletable)
}
} else {
emptyList()
}
// Load ISO/IMG files
val fileResult = RootManager.executeCommand(
"find \"$path\" -maxdepth 1 -type f \\( -iname '*.iso' -o -iname '*.img' \\) 2>/dev/null"
)
val files = if (fileResult.success && fileResult.output.isNotBlank()) {
fileResult.output.lines()
.filter { it.isNotBlank() }
.map { it.trim() }
.map { BrowserItem(it.substringAfterLast("/"), false, it) }
} else {
emptyList()
}
items = (directories.sortedBy { it.name.lowercase() } + files.sortedBy { it.name.lowercase() })
isLoading = false
}
}
fun createFolder(name: String) {
scope.launch {
val trimmedName = name.trim()
if (trimmedName.isEmpty()) return@launch
val newPath = "$currentPath/$trimmedName"
RootManager.executeCommand("mkdir -p \"$newPath\"")
// Create marker file to indicate this folder was created by the app
RootManager.executeCommand("touch \"$newPath/.isodroiddir\"")
// Auto-navigate into the new folder
currentPath = newPath
}
}
fun deleteFolder(path: String) {
scope.launch {
RootManager.executeCommand("rm -rf \"$path\"")
loadContents(currentPath)
}
}
LaunchedEffect(currentPath) {
loadContents(currentPath)
}
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Select Directory")
IconButton(onClick = { showCreateFolderDialog = true }) {
Icon(
imageVector = Icons.Default.CreateNewFolder,
contentDescription = "Create directory",
tint = MaterialTheme.colorScheme.primary
)
}
}
},
text = {
Column {
// Current path display
Text(
text = currentPath,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()
// Content list
if (isLoading) {
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 200.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(modifier = Modifier.size(32.dp))
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 200.dp, max = 300.dp)
) {
// Parent directory (..)
if (currentPath != "/" && currentPath != storageRoot) {
item {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
val parent = currentPath.substringBeforeLast("/")
currentPath = parent.ifEmpty { "/" }
}
.padding(vertical = 12.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Folder,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "..",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
// Items (directories and files)
items(items) { item ->
val isIso = item.name.lowercase().endsWith(".iso")
val isImg = item.name.lowercase().endsWith(".img")
val osIcon = if (!item.isDirectory) findOsIcon(context, item.name) else null
Row(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
if (item.isDirectory) {
currentPath = item.fullPath
}
},
onLongClick = {
if (item.isDirectory) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
when {
item.fullPath == initialPath -> {
Toast.makeText(
context,
"Can't delete your current ISO directory. Change it first, then try again.",
Toast.LENGTH_SHORT
).show()
}
item.isDeletable -> {
showDeleteDialog = item.fullPath
}
else -> {
Toast.makeText(
context,
"Can't delete this directory — it wasn't created by ISO Droid",
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
.padding(vertical = 8.dp, horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon with badge
Box(modifier = Modifier.size(32.dp)) {
if (item.isDirectory) {
Icon(
imageVector = Icons.Default.Folder,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxSize()
)
} else if (osIcon != null) {
// OS icon with file type badge
AsyncImage(
model = ImageRequest.Builder(context)
.data("file:///android_asset/osicons/$osIcon")
.decoderFactory(SvgDecoder.Factory())
.build(),
contentDescription = item.name,
modifier = Modifier.fillMaxSize(),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary)
)
// File type badge
Icon(
imageVector = if (isIso) Icons.Default.Album else Icons.AutoMirrored.Filled.InsertDriveFile,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.size(14.dp)
.align(Alignment.BottomEnd)
.offset(x = 2.dp, y = 2.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.padding(2.dp)
)
} else {
// Fallback icon
Icon(
imageVector = if (isIso) Icons.Default.Album else Icons.AutoMirrored.Filled.InsertDriveFile,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.fillMaxSize()
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = item.name,
style = MaterialTheme.typography.bodyMedium,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f),
color = if (item.isDirectory)
MaterialTheme.colorScheme.onSurface
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// Empty state
if (items.isEmpty()) {
item {
Text(
text = "Empty directory",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 16.dp, horizontal = 4.dp)
)
}
}
}
}
}
},
confirmButton = {
TextButton(onClick = { onSelect(currentPath) }) {
Text("Select")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
// Create folder dialog
if (showCreateFolderDialog) {
val hasInvalidChars = newFolderName.any { !it.isLetterOrDigit() && it !in "-_. ()[]+," }
val isValidInput = newFolderName.isNotBlank() && !hasInvalidChars
AlertDialog(
onDismissRequest = { showPathDialog = false },
title = { Text("ISO Directory") },
onDismissRequest = {
showCreateFolderDialog = false
newFolderName = ""
},
title = { Text("Create Directory") },
text = {
Column {
Text(
text = "Enter the path to the directory containing your ISO/IMG files.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = tempPath,
onValueChange = { tempPath = it },
label = { Text("Path") },
value = newFolderName,
onValueChange = { newFolderName = it },
label = { Text("Directory name") },
singleLine = true,
isError = hasInvalidChars,
modifier = Modifier.fillMaxWidth()
)
if (hasInvalidChars) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Invalid directory name",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
},
confirmButton = {
TextButton(
onClick = {
viewModel.setIsoDirectory(tempPath)
showPathDialog = false
}
if (isValidInput) {
createFolder(newFolderName)
showCreateFolderDialog = false
newFolderName = ""
}
},
enabled = isValidInput
) {
Text("Save")
Text("Create")
}
},
dismissButton = {
TextButton(onClick = { showPathDialog = false }) {
TextButton(onClick = {
showCreateFolderDialog = false
newFolderName = ""
}) {
Text("Cancel")
}
}
)
}
// Delete confirmation dialog
showDeleteDialog?.let { pathToDelete ->
AlertDialog(
onDismissRequest = { showDeleteDialog = null },
title = { Text("Delete Directory") },
text = {
Text("Are you sure you want to delete \"${pathToDelete.substringAfterLast("/")}\" and all its contents?")
},
confirmButton = {
TextButton(
onClick = {
deleteFolder(pathToDelete)
showDeleteDialog = null
}
) {
Text("Delete", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = null }) {
Text("Cancel")
}
}
@@ -1,8 +1,14 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.screens
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -92,7 +98,7 @@ private fun WelcomeStep(
Spacer(modifier = Modifier.height(24.dp))
Text(
text = "Welcome to ISO Drive",
text = "Welcome to ISO Droid",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
@@ -147,7 +153,7 @@ private fun RootAccessStep(
title = "Root Access",
description = when (hasRoot) {
false -> "Root access was denied. The app requires root to mount ISO files. You can try again or skip and grant access later from your root manager."
else -> "ISO Drive needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
else -> "ISO Droid needs superuser (root) access to mount ISO files as USB devices. This is required because mounting USB gadgets is a system-level operation."
},
granted = hasRoot
)
@@ -253,7 +259,7 @@ private fun NotificationStep(
description = when {
permanentlyDenied -> "Notification permission was denied. You can enable it later in system settings if you change your mind."
wasDenied -> "Notification permission was denied. You can try again or continue without notifications."
else -> "ISO Drive shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
else -> "ISO Droid shows a notification when an ISO is mounted, with a quick unmount button. This helps you keep track of the mount status."
},
granted = if (wasDenied) false else if (hasPermission) true else null
)
@@ -318,7 +324,7 @@ private fun CompleteStep(
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "ISO Drive is ready to use. Place your ISO or IMG files in the isodrive folder and start mounting.",
text = "ISO Droid is ready to use. Place your ISO or IMG files in the isodroid directory and start mounting.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant
@@ -327,7 +333,7 @@ private fun CompleteStep(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Default directory: /sdcard/isodrive/",
text = "Default directory: ${Environment.getExternalStorageDirectory().absolutePath}/isodroid/",
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
@@ -339,7 +345,7 @@ private fun CompleteStep(
onClick = onFinish,
modifier = Modifier.fillMaxWidth()
) {
Text("Start Using ISO Drive")
Text("Start Using ISO Droid")
}
}
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.theme
import androidx.compose.ui.graphics.Color
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.theme
import android.app.Activity
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.ui.theme
import androidx.compose.material3.Typography
@@ -1,3 +1,8 @@
/*
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package sh.sar.isodroid.viewmodel
import android.app.Application
@@ -47,7 +52,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
companion object {
private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
private val DEFAULT_ISO_DIRECTORY = "${Environment.getExternalStorageDirectory().absolutePath}/isodrive"
private val DEFAULT_ISO_DIRECTORY = "${Environment.getExternalStorageDirectory().absolutePath}/isodroid"
}
private var initialized = false
@@ -201,6 +206,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Create directory if it doesn't exist
if (!directory.exists()) {
RootManager.executeCommand("mkdir -p \"$currentPath\"")
// Create marker file to indicate this folder was created by the app
RootManager.executeCommand("touch \"$currentPath/.isodroiddir\"")
}
// Try multiple methods to list files
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
# Build isodrive from source for all Android architectures
# Requires: Android NDK (or runs via nix-shell on NixOS)
SCRIPT_DIR=$(dirname "$(realpath "$0")")
ISODRIVE_DIR="/tmp/isodrive"
OUTPUT_DIR="$SCRIPT_DIR/app/src/main/assets/bin"
# Clone isodrive source
if [[ -d "$ISODRIVE_DIR" ]]; then
echo "Updating isodrive source..."
git -C "$ISODRIVE_DIR" pull
else
echo "Cloning isodrive..."
git clone --depth 1 https://github.com/nitanmarcel/isodrive "$ISODRIVE_DIR"
fi
SRCS="$ISODRIVE_DIR/src/util.cpp $ISODRIVE_DIR/src/configfsisomanager.cpp $ISODRIVE_DIR/src/androidusbisomanager.cpp $ISODRIVE_DIR/src/main.cpp"
CFLAGS="-I$ISODRIVE_DIR/src/include -static-libstdc++ -Os -s"
build_all() {
local NDK="$1"
local TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
echo "Building arm64-v8a..."
"$TOOLCHAIN/aarch64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/arm64-v8a/isodrive"
echo "Building armeabi-v7a..."
"$TOOLCHAIN/armv7a-linux-androideabi26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/armeabi-v7a/isodrive"
echo "Building x86_64..."
"$TOOLCHAIN/x86_64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86_64/isodrive"
echo "Building x86..."
"$TOOLCHAIN/i686-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86/isodrive"
echo "Done! Built isodrive for all architectures."
ls -la "$OUTPUT_DIR"/*/isodrive
}
# On NixOS, prefer nix-shell (local Android SDK has /bin/bash issues)
if command -v nix-shell &>/dev/null; then
echo "Using nix-shell to get Android NDK..."
export SRCS CFLAGS OUTPUT_DIR
NIXPKGS_ALLOW_UNFREE=1 nix-shell -p androidenv.androidPkgs.ndk-bundle --run '
SDK_ROOT=$(find /nix/store -maxdepth 1 -name "*android-sdk-ndk*" -type d 2>/dev/null | head -1)
NDK=$(ls -d "$SDK_ROOT/libexec/android-sdk/ndk/"* | head -1)
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
echo "Using NDK: $NDK"
echo "Building arm64-v8a..."
"$TOOLCHAIN/aarch64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/arm64-v8a/isodrive"
echo "Building armeabi-v7a..."
"$TOOLCHAIN/armv7a-linux-androideabi26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/armeabi-v7a/isodrive"
echo "Building x86_64..."
"$TOOLCHAIN/x86_64-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86_64/isodrive"
echo "Building x86..."
"$TOOLCHAIN/i686-linux-android26-clang++" $CFLAGS $SRCS -o "$OUTPUT_DIR/x86/isodrive"
echo "Done!"
ls -la "$OUTPUT_DIR"/*/isodrive
'
elif [[ -n "${ANDROID_NDK_HOME:-}" ]]; then
build_all "$ANDROID_NDK_HOME"
elif [[ -n "${ANDROID_NDK:-}" ]]; then
build_all "$ANDROID_NDK"
else
echo "Error: Android NDK not found."
echo "Set ANDROID_NDK_HOME or ANDROID_NDK, or install nix-shell."
exit 1
fi
+4 -4
View File
@@ -229,13 +229,13 @@ fun toCommandArgs(): List<String> {
**Example commands:**
```bash
# Mount as read-only mass storage
isodrive "/sdcard/isodrive/ubuntu.iso" -configfs
isodrive "/sdcard/isodroid/ubuntu.iso" -configfs
# Mount as writable drive
isodrive "/sdcard/isodrive/drive.img" -rw -configfs
isodrive "/sdcard/isodroid/drive.img" -rw -configfs
# Mount as CD-ROM
isodrive "/sdcard/isodrive/windows.iso" -cdrom -configfs
isodrive "/sdcard/isodroid/windows.iso" -cdrom -configfs
```
## Event System
@@ -338,7 +338,7 @@ private val KEY_ISO_DIRECTORY = stringPreferencesKey("iso_directory")
```
Stores:
- Custom ISO directory path (default: `/sdcard/isodrive/`)
- Custom ISO directory path (default: `/sdcard/isodroid/`)
### SharedPreferences
Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 109 KiB

@@ -0,0 +1,9 @@
* New app icon
* Mount ISO/IMG files as USB mass storage or CD-ROM
* Create blank IMG files for writable USB drives
* Read-only or read-write mount options
* Persistent notification with quick unmount button
* OS-specific icons for popular distributions
* Download links to popular operating systems
* Rename and delete files
* Material 3 dynamic theming with dark mode support
@@ -0,0 +1 @@
* Fix app name displaying as "ISO Drive" instead of "ISO Droid"
@@ -0,0 +1 @@
* Fix default ISO directory path to /sdcard/isodroid
@@ -0,0 +1,6 @@
* Directory browser for changing ISO directory in settings
* Create new directories from the directory browser
* Delete directories created by the app (long press)
* Shows ISO/IMG files with OS icons in directory browser
* Empty state on home screen now shows current path and helpful hints
* Version number now read dynamically from app config
@@ -0,0 +1,24 @@
ISO Droid lets you mount ISO and IMG files as USB mass storage or CD-ROM devices on rooted Android devices. Connect your phone to any PC and it will appear as a bootable USB drive or CD-ROM.
Features:
* Mount ISO/IMG files as USB mass storage or CD-ROM
* Create blank IMG files for writable USB drives
* Read-only or read-write mount options
* Persistent notification with quick unmount button
* OS-specific icons for popular distributions (Linux, BSD, etc.)
* Download links to popular operating systems
* Rename and delete files
* Material 3 dynamic theming with dark mode support
Requirements:
* Rooted Android device (Magisk, KernelSU, APatch, etc.)
* USB gadget support (configfs or sysfs)
* Android 8.0+ (API 26)
Usage:
1. Place your ISO/IMG files in /sdcard/isodroid/
2. Select an ISO/IMG file from the list
3. Choose mount options (Mass Storage or CD-ROM)
4. Tap Mount
5. Connect your phone to a PC via USB
6. Unmount via the app or notification when done
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

@@ -0,0 +1 @@
Mount ISO/IMG files as USB drives on rooted Android
@@ -0,0 +1 @@
ISO Droid
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Get the directory where this script lives
SCRIPT_DIR=$(dirname "$(realpath "$0")")
ISODRIVE_VERSION=$(curl -sI https://github.com/nitanmarcel/isodrive-magisk/releases/latest | grep -i ^location | grep -oP 'v\K[\d.]+')
curl -sL https://github.com/nitanmarcel/isodrive-magisk/releases/download/v$ISODRIVE_VERSION/isodrive-magisk-v$ISODRIVE_VERSION.zip -o /tmp/isodrive-magisk.zip
unzip -q /tmp/isodrive-magisk.zip -d /tmp/isodrive-magisk
# Move the isodrive binary for each architecture
mv /tmp/isodrive-magisk/libs/arm64-v8a/isodrive $SCRIPT_DIR/app/src/main/assets/bin/arm64-v8a/
mv /tmp/isodrive-magisk/libs/armeabi-v7a/isodrive $SCRIPT_DIR/app/src/main/assets/bin/armeabi-v7a/
mv /tmp/isodrive-magisk/libs/x86/isodrive $SCRIPT_DIR/app/src/main/assets/bin/x86/
mv /tmp/isodrive-magisk/libs/x86_64/isodrive $SCRIPT_DIR/app/src/main/assets/bin/x86_64/
# Clean up temp files
rm -rf /tmp/isodrive-magisk /tmp/isodrive-magisk.zip