| 1 |
# 📱 OxygenHub Android |
| 2 |
|
| 3 |
[](.flow/run) |
| 4 |
|
| 5 |
 |
| 6 |
 |
| 7 |
 |
| 8 |
|
| 9 |
<img src="docs/screenshots/launcher-icon.png" alt="The launcher icon: as a circle, a squircle and a themed icon" width="400"> |
| 10 |
|
| 11 |
The official native Android client for OxygenHub, designed for modern devices and fully optimized for foldables like the Pixel 10 Pro Fold. |
| 12 |
|
| 13 |
Built with **Kotlin**, **Jetpack Compose**, and deep **Rust interop** (via `uniffi-rs`) to execute native `oxy` commands directly on your device. |
| 14 |
|
| 15 |
--- |
| 16 |
|
| 17 |
<p> |
| 18 |
<img src="docs/screenshots/dashboard-phone.png" alt="The dashboard on a phone" width="200"> |
| 19 |
<img src="docs/screenshots/sign-in-code.png" alt="Signing in: the code to approve, dark" width="200"> |
| 20 |
<img src="docs/screenshots/inbox-phone-dark.png" alt="The inbox, dark" width="200"> |
| 21 |
</p> |
| 22 |
<p> |
| 23 |
<img src="docs/screenshots/repositories-foldable.png" alt="Repositories and a repository's issues side by side on an open foldable" width="400"> |
| 24 |
<img src="docs/screenshots/repositories-tablet-dark.png" alt="Repositories on a tablet, dark" width="400"> |
| 25 |
</p> |
| 26 |
|
| 27 |
## ✅ What works today |
| 28 |
|
| 29 |
* 🔑 **Sign in with your browser** to oxygenhub.io or any hub (#2). The app |
| 30 |
shows a code; you approve it on the hub's `/device` page, signed in there |
| 31 |
with your password and second factor, so the app never sees either. The |
| 32 |
device token is sealed with a key in the Android Keystore (StrongBox where |
| 33 |
the phone has one), never written in the clear, logged or backed up. |
| 34 |
Signing out (Account) revokes it on the hub. |
| 35 |
* 🏠 **Dashboard:** a greeting with your avatar, your newest repositories, |
| 36 |
the unread count. Your avatar is the picture you uploaded to the hub, or |
| 37 |
the hub's identicon for you (#13). |
| 38 |
* 📚 **Repositories:** yours or all you can see, filtered as you type; a |
| 39 |
repository's **issues** (open, closed, all; pinned first). An issue opens on |
| 40 |
the hub's web page for now (#4 brings it into the app). |
| 41 |
* 🔀 **Proposals:** a repository's proposals beside its issues; a proposal's |
| 42 |
checks, approvals, changes and what stands in the way, and **Land** (or |
| 43 |
land when its checks pass) from the phone (#17). |
| 44 |
* 📥 **Inbox:** notifications, unread first, with a badge on the navigation; |
| 45 |
opening one marks it read, and the whole inbox can be marked read. |
| 46 |
* 📱 **Phone, foldable and tablet:** a navigation bar on a phone, a rail when |
| 47 |
wider; repositories and their issues side by side from a medium width up, |
| 48 |
so a foldable opened (Pixel 10 Pro Fold) shows both, kept off the hinge. |
| 49 |
* 🌗 **Light and dark**, with the wallpaper's colours (dynamic colour), or the |
| 50 |
hub's own palette. |
| 51 |
* Pull to refresh everywhere, with loading, empty and error states. |
| 52 |
|
| 53 |
The screenshots are rendered from Compose previews with made-up data, no |
| 54 |
emulator: `scripts/screenshots.sh`. |
| 55 |
|
| 56 |
## 🛣️ Planned |
| 57 |
|
| 58 |
The plan is the repository's issues: browse code, history and drops (#3), |
| 59 |
issues and proposals (#4), Flow runs and live logs (#5), trust and sealed |
| 60 |
approvals (#6), releases (#7), and the phone as an OxygenFlow runner (#8–#11). |
| 61 |
|
| 62 |
* 🧬 **Commutative History:** Native visualization of the change graph. |
| 63 |
* 🔀 **Offline-First Conflicts:** Resolve conflicts on the go; they're just data. |
| 64 |
* 🌊 **Job Flow Control:** Orchestrate OxygenFlow CI/CD pipelines right from your phone. |
| 65 |
|
| 66 |
## 🔑 Signing in |
| 67 |
|
| 68 |
1. Open the app. The hub is `https://oxygenhub.io` unless you change it (any |
| 69 |
HTTPS address of a hub works; the last one is remembered). |
| 70 |
2. Tap **Sign in**. The app shows a code such as `BCDF-GHJK`. |
| 71 |
3. Tap **Open the hub to approve** (or open `/device` on the hub from a |
| 72 |
computer and type the code), sign in there if you need to, check what the |
| 73 |
app asks for, and approve it. You may untick scopes. |
| 74 |
4. The app signs in on its own within a few seconds. |
| 75 |
|
| 76 |
The app asks for `repo:read` and `user:read`. The token lasts while it is |
| 77 |
used and ends after 90 days without use; it is listed on the hub under |
| 78 |
Settings, Tokens as "device: OxygenHub for Android on" and the phone's model. |
| 79 |
|
| 80 |
## 🛠️ Setup |
| 81 |
|
| 82 |
1. Clone the repository with `oxy clone`, and fetch the pinned Oxygen |
| 83 |
sources: `oxy submodule update --init` (rust-core builds over `oxygen/`). |
| 84 |
2. Install the Android SDK with an NDK (Android Studio's SDK manager), and |
| 85 |
point `local.properties` at it (`sdk.dir=…`) or set `ANDROID_HOME`. |
| 86 |
3. Install the Rust side: `cargo install cargo-ndk` and |
| 87 |
`rustup target add aarch64-linux-android x86_64-linux-android`. |
| 88 |
4. Build, test and lint; the build compiles rust-core for arm64-v8a and |
| 89 |
x86_64 and generates its Kotlin bindings itself: |
| 90 |
```bash |
| 91 |
./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintDebug |
| 92 |
``` |
| 93 |
|
| 94 |
## 🧭 Layout of the code |
| 95 |
|
| 96 |
Under `app/src/main/java/io/oxygenhub/android`: |
| 97 |
|
| 98 |
* `api/`: the hub's JSON types (by hand, from oxygenhub's `hub-api`), the |
| 99 |
`HubApi` client over OkHttp, and the device sign-in (`DeviceAuth`). |
| 100 |
* `auth/`: the session, `KeystoreSessionStore` (the sealed token) and |
| 101 |
`SessionManager` (sign in, sign out, a refused token). |
| 102 |
* `ui/`: one package per screen, each a view model and its composables; |
| 103 |
`ui/nav` holds the routes and the adaptive navigation; `ui/common` the |
| 104 |
loading, empty and error states. |
| 105 |
|
| 106 |
The unit tests (`app/src/test`) run the client against MockWebServer with |
| 107 |
JSON fixtures written from hub-api's types, and the view models against a |
| 108 |
fake hub. |
| 109 |
|
| 110 |
## 🏗️ CI/CD |
| 111 |
|
| 112 |
We dogfood **OxygenFlow** for our builds. |
| 113 |
The pipeline is defined in `.flow/pipeline.kdl`. |