Share a link. Pick a quality. It lands in your Downloads folder.
A fast, polished media downloader for Android — video, audio and photos from TikTok, Instagram, Facebook, YouTube and direct links, saved where every other app can see them.
- Highlights
- Supported sources
- How it works
- Getting started
- Building a release
- Configuration
- Backend (optional)
- Project structure
- Reliability & networking
- Privacy
- Testing on a device
- Roadmap
- License
- Share‑sheet first. Tap Share → Hoza Download in any app. A floating half‑sheet slides up over the app you were in — you never leave it.
- Video, audio and photos. Reels, slideshows, carousels and single photos are all first‑class: pick one photo, or Save all in one tap.
- Real download engine. Streaming to disk, byte‑range resume, pause, cancel, retry, concurrency cap, stall detection, and segmented transfers on hosts that allow it.
- Truthful progress. A live notification with percent, size, speed and time left — plus Pause / Resume / Cancel buttons — and the same figures in the app, animated, never invented.
- Self‑healing. Expired CDN links are re‑resolved before they fail, 429/5xx get sensible back‑off, and a false "offline" from Android is verified against the real network before anything is held.
- Remote‑updatable extractors. When a platform changes its page markup, the fix ships as a JSON file — no store release.
- Designed, not assembled. Dark and light themes, a stroke‑drawn launch mark that flies into the masthead, posters that fly into the tray, illustrated empty states, rings that draw their own check when a download completes.
- Does not bypass anything. No DRM, no login walls, no private posts. If a source will not share it, Hoza says so and stops.
| Source | Video | Audio | Photos | Notes |
|---|---|---|---|---|
| TikTok | ✅ | — | ✅ slideshows | Short links (vt.tiktok.com, /t/…) expanded automatically |
| ✅ reels | — | ✅ single & carousel, full size | Read via the public embed page | |
| ✅ | — | ✅ | Region‑dependent — some networks receive a login wall for all content | |
| YouTube | ✅ | ✅ | — | Separate audio/video streams are merged on device |
| Direct links | ✅ | ✅ | ✅ | .mp4 .webm .m4a .mp3 .jpg .png .webp … |
| Other pages | ✅ | ✅ | — | Open Graph / JSON‑LD media on any public page |
Only content a source publishes as downloadable is offered. Private, age‑gated or sign‑in‑only posts are reported, not worked around.
Share / paste link
│
▼
LinkCanonicalizer expand short links, strip tracking params
│
▼
SourceRegistry 10‑min cache · cancellable · kill switches from remote config
│ first provider that claims the link, then fallbacks
▼
Providers YouTube → Social (TikTok/IG/FB) → DirectMedia → PageMedia
│ each returns real, probed variants — never padded
▼
Link sheet Video / Audio / Image switch, quality tiles, Save all
│
▼
DownloadsController queue, concurrency, holds, retries, expiry refresh
│
▼
Download engines HTTP streaming · segmented · muxing (video + audio)
│
▼
MediaStore Download/Hoza Download/{Videos,Audio,Images}
Everything runs in one Flutter isolate shared by both windows (the full app and the floating share sheet), kept alive by a foreground service while transfers run.
Requirements
- Flutter 3.44+ (Dart 3.12+)
- Android SDK; a device or emulator on API 24+
- JDK 17 for the Gradle build
git clone <your-repo-url>
cd hoza_download
flutter pub get
flutter run # debug build on the connected device
flutter analyze # expected: No issues found!The debug build installs as com.hoza.download.debug, so it can live next to
a release install.
flutter build apk --release # single APK → build/app/outputs/flutter-apk/
flutter build appbundle --release # AAB for Play → build/app/outputs/bundle/release/Signing. Copy android/key.properties.example to android/key.properties
and fill in your keystore details. key.properties, *.jks and *.keystore
are git‑ignored — never commit them, and back the keystore up: losing it means
losing the ability to update the app on the store.
Versioning. Bump version: in pubspec.yaml and AppInfo.version in
lib/core/constants/app_info.dart; the remote config compares against the
latter.
All configuration is compile‑time constants in lib/core/constants/app_info.dart:
| Constant | Purpose | Default |
|---|---|---|
remoteConfigUrl |
HTTPS URL of the remote config JSON (kill switches, extractor patterns, ping/telemetry endpoints). Empty = fully offline, built‑in defaults. | '' |
version |
App version shown in About and sent with telemetry | 1.0.0 |
downloadFolder |
Shown to the user as the save location | Download/Hoza Download |
Download behaviour the user controls lives in Settings: default media type, video/audio format, quality preference, concurrent downloads, Wi‑Fi only, auto‑start, notifications, theme.
The app runs entirely on‑device. A small backend adds three things:
| Endpoint | Gives you |
|---|---|
GET /config |
Fix a broken platform in minutes — swap extractor regexes or switch a platform off with a message, no store release |
POST /events |
Anonymous outcome counts per platform (never a URL, title or device id) — know Instagram broke before users tell you |
POST /resolve |
yt‑dlp fallback resolver for links the phone cannot read (e.g. region blocks) |
The cheapest deployment is one static JSON file on any HTTPS host — see
server/README.md for the file format, the FastAPI
reference service, and a Dockerfile.
lib/
main.dart opens the database, loads settings, starts the app
app/ root widget, router, theme tokens & ThemeData
core/ constants, logging, formatters, URL/expiry/filename utilities
data/
database/ SQLite schema (v4), migrations, DAO, settings store
models/ DownloadRecord, settings, media & status enums
providers/ DownloadsController (queue), settings, share hand‑off
features/
downloader/
data/ engines, providers, link canonicaliser, source registry
domain/ provider contracts, error kinds, variant selection
presentation/ link sheet, download sheet, picker, progress view
downloads/ home/ settings/ share/ shell/ splash/ onboarding/ about/
services/
config/ RemoteConfig + ExtractorCatalog (built‑in patterns live here)
networking/ shared HttpClient and timeouts
platform/ permissions, foreground service bridge, notifications, network
share/ share channel, pending‑share store
telemetry/ FailureReporter (anonymous, batched, opt‑in)
shared/widgets/ component library: sheets, buttons, rings, flights, art…
android/app/src/main/kotlin/com/hoza/download/
HozaEngine.kt the one Flutter engine both windows attach to
ShareSheetActivity.kt the translucent share window
DownloadForegroundService.kt ongoing progress notification with actions
DownloadActionReceiver.kt Pause / Resume / Cancel → Dart
NotificationsChannel.kt completion, failure and info notifications
MediaStoreChannel.kt publish, exists, open, share, rename
NetworkChannel.kt connectivity that survives Wi‑Fi ↔ mobile hand‑over
MuxerChannel.kt merges separate audio/video tracks
server/ optional FastAPI backend + config/extractors.json
docs/HARDENING_PLAN.md the reliability checklist and what each item became
State management is Riverpod. Business logic lives in controllers and
services; widgets present. DownloadsController is the single source of truth
for records and scheduling.
- Link resolution — redirect‑first for short links, tracking stripped, 10‑minute cache, cancelled when the sheet closes, HTML‑as‑200 rejected.
- Expiry‑aware —
x-expires/oe/expireparsed from CDN URLs; a queued download with an expired address is re‑resolved before the request. - Retry policy — 429 → 30 s / 90 s; 5xx → 5 / 15 / 45 s; 401/403 → re‑read the source page; 404/410 → stop and say the file was removed.
- Network truth — an "offline" verdict from Android is verified with an
HTTP
204probe (captive portals detected) and re‑checked with back‑off. - Process death — interrupted transfers are settled to Paused on next launch with a notification; a share that never got its sheet is replayed.
- Storage — Android 10+:
MediaStore.Downloads, no permission. Android 9 and older: public Downloads withWRITE_EXTERNAL_STORAGE(capped at API 28).MANAGE_EXTERNAL_STORAGEis never requested.
- No accounts, no sign‑in, no cookies beyond the single page fetch a download needs.
- Nothing leaves the device unless you configure
remoteConfigUrland the config names atelemetryUrl— and then only{platform, event, reason, appVersion, osVersion}. Never a URL, title, file name or identifier. - Permissions:
INTERNET,ACCESS_NETWORK_STATE,FOREGROUND_SERVICE,FOREGROUND_SERVICE_DATA_SYNC,POST_NOTIFICATIONS, andWRITE_EXTERNAL_STORAGEonly up to Android 9.
flutter analyze # must be clean
flutter build apk --debug # must succeedThen on a phone: share from TikTok, Instagram and YouTube with the app closed
and open; switch Wi‑Fi → mobile mid‑download; deny then allow notifications;
lock the screen for ten minutes; force‑stop mid‑download and reopen. The full
manual checklist is in docs/HARDENING_PLAN.md.
- Photo picker grid with per‑photo selection in the link sheet
- "Use mobile data this time" override on the Wi‑Fi‑only hold
- Clipboard link detection on Home
- Grouped card for multi‑photo sets in the Downloads list
- Arabic and Kurdish localisation with RTL layout
- Client hook for the backend
/resolvefallback
Copyright © 2026 Rahoz Osman. All rights reserved.
No license is granted for redistribution or commercial use. Open an issue if you would like to discuss usage.