Skip to content

About

Chat with Work for Android: AI bots in Europe. They work. You just decide. Built with Hotwire Native.

Topics

Resources

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

Chat with Work for Android

The Android app for Chat with Work: AI bots in Europe. They work. You just decide. Your team's bots do their chores across the tools you connect, and every change or question that needs you waits on the Decisions page for you to work through from your phone. It runs on our servers in Germany with every model in the EU, or on your own servers.

The app is built with Hotwire Native. Its screens are the web app's own pages, so every feature the web app gains reaches the app without a release, and it goes native wherever that makes it feel like an Android app: it opens on a chat, with Material 3's navigation drawer as a native sidebar (your chats, projects, what waits for you, bots), Material top app bars with the page's actions and overflow menu, a native composer (with the photo picker, camera, ML Kit's document scanner, and dictation recorded on the phone and transcribed on the server), full-screen modals, a welcome screen, native dialogs, snackbars, haptics, search, the share sheet (both ways: text and files shared into a new chat too), Live Updates that follow the steps of answers you leave running, a home screen widget, a Quick Settings tile, shortcuts on the icon (New chat, New bot, Decisions), Android's default digital assistant (a new chat with the composer focused), sign-in through Chrome's Auth Tab, push notifications that show the answer's start or what waits for you (fetched from the server as they arrive, never through Google), App Links, predictive back, edge to edge, and Live Wire's colors in light and dark.

What the server has to do for all of that is written down in docs/server-contract.md, next to the iOS app's contract it shares.

Requirements

  • Android 9 (API 28) or later; built for Android 17 (API 37)
  • JDK 21 or later to build (CI uses 21; Android Studio's bundled JDK works), Android SDK 37
  • Pinned dependencies, in gradle/libs.versions.toml: Hotwire Native Android 1.3.1, Joe Masilotti's bridge-components v0.14.0 (from JitPack), Material Components 1.14, AndroidX, Firebase Cloud Messaging (BoM 34.19)

Building

./gradlew assembleDebug                 # debug build, for the emulator and a local server
./gradlew installDebug -PbaseUrl=http://localhost:3000
./gradlew testDebugUnitTest lintDebug spotlessCheck
./gradlew assembleStaging assembleRelease

Or open the folder in Android Studio and run the app configuration. ./gradlew spotlessApply formats Kotlin (ktlint) and the bundled JSON.

Configuration

Each build type points at one server and installs as its own app, so all three can sit on one phone:

Build type Server Application id Name on the home screen
debug http://10.0.2.2:3000, or -PbaseUrl=... com.chatwithwork.app.debug CWW Dev
staging https://staging.chatwithwork.com com.chatwithwork.app.staging CWW Staging
release https://chatwithwork.com com.chatwithwork.app Chat with Work

10.0.2.2 is your computer as the emulator sees it, so the default debug build talks to bin/dev; Android 17 asks for the local network permission before it can. For a phone, or to skip that, forward the port and point at localhost:

adb reverse tcp:3000 tcp:3000
./gradlew installDebug -PbaseUrl=http://localhost:3000

Debug builds allow plain http to 10.0.2.2, localhost and 127.0.0.1 only, enable web view debugging (chrome://inspect), and show the server's host on the welcome screen, as staging builds do.

Push notifications need app/google-services.json from the Firebase project (git-ignored; see docs/push-notifications.md). Without it the app builds and runs, with push off.

Permissions

Permission Why
INTERNET the server
POST_NOTIFICATIONS push notifications, offered once, the first time a change waits for approval or an answer takes a while, with a short explanation first, or from Settings › Notifications (docs/push-notifications.md)
ACCESS_NETWORK_STATE whether there's a connection at launch: without one the app opens the chat last shown, whose copy the server's service worker may keep for reading offline (granted at install; Firebase already asked for it)
RECORD_AUDIO, MODIFY_AUDIO_SETTINGS dictation: the native composer records (OGG/Opus, sent to the server to transcribe; Android 10 and later), and the page's microphone in pages that record themselves (getUserMedia)

The web view gets the microphone only for pages on the build's own server, and nothing else: AppWebChromeClient (fragments/AppWebChromeClient.kt) denies the camera and any other site, such as an MCP App's frame, and hands the rest to Hotwire Native, which asks Android for RECORD_AUDIO the first time someone dictates. The microphone is optional hardware, so devices without one can still install the app.

Signing

This repository is public: keystores, google-services.json and keys never go in it. Release builds read their signing key from Gradle properties or environment variables:

Variable What
RELEASE_KEYSTORE_PATH the upload keystore file
RELEASE_KEYSTORE_PASSWORD its password
RELEASE_KEY_ALIAS the key's alias
RELEASE_KEY_PASSWORD the key's password

Without them, assembleRelease makes an unsigned APK and staging builds are signed with the debug key. Google Play signs what it distributes with the app signing key it holds (Play App Signing); the key here is the upload key, and the app signing key's fingerprint is what /.well-known/assetlinks.json names.

How it's put together

app/src/main/kotlin/com/chatwithwork/app/
  ChatWithWorkApplication.kt  Hotwire configuration, bridge components, path configuration
  main/        MainActivity (the sidebar shell, welcome screen, sign-in, links), view model
  sidebar/     the native sidebar: the server's JSON, its rows, the drawer's views
  composer/    the native composer: its state, view, "+" sheet, files, and dictation
  shortcuts/   the home screen widget, the Quick Settings tile, and where they and the icon's shortcuts lead
  fragments/   the web screen (top app bar over the shared web view), page titles, error states
  bridge/      the bridge components (native halves) and the action sheet they share
  routing/     the app's URLs, Custom Tabs, route decisions for other sites and apps
  push/        Firebase Cloud Messaging, the notification channels, the payload, Live Updates
app/src/main/assets/json/path-configuration.json   the bundled path configuration
app/src/main/res/        Live Wire's colors, Geist, Material Symbols icons, layouts
app/src/debug/           plain-http config and the push simulator for development
app/src/test/            unit tests (JUnit, Truth)
docs/                    the server contract and design notes
  • MainActivity owns one navigator, whose root is always a place (a chat, Decisions, Bots, Settings: the path configuration's replace_root, and whatever the sidebar opens, Places), with only drill-downs pushed over it, the sidebar (a DrawerLayout whose content SidebarDrawer draws from the server's JSON, fetched with the web view's cookies), and the welcome screen. It watches the navigator's traffic for what the server says: a 401 or the sign-in page means no session, a recede while signed out means signed in, a link into another organization starts over there.
  • Web screens are HotwireWebFragments with the app's own layout: a Material top app bar (the page's <title>, its bridge buttons and overflow menu) over the web view. Modals are full-screen fragments, not Hotwire's bottom sheets, where bridge components that need a top app bar crash.
  • A page before it paints shows a skeleton of its kind, not a blank screen: the server's path configuration names the kind (skeleton: a chat, New chat, a list, Settings, a form, or any other page; docs/server-contract.md, section 5), fragments/ChatSkeleton.kt and PageSkeleton.kt give each its shape, and SkeletonView.kt draws them: bones in Live Wire's colors, with one shimmer sweeping across them, still under Remove animations, read as "Loading" ("Loading chat" on a chat) by screen readers. The body crossfades to the page at its first paint: a cold boot's first contentful paint (FirstPaint), or a Turbo visit's render. The title's bone stays until the page names itself, and a chat's composer bone until the page has rendered and the real one can show under it. An error removes it all, and a timeout of 10 seconds does too. It shows only the first time a screen appears, never coming back to one.
  • Bridge components: the app's own button, menu, form, share, alert, toast, haptic, theme, context-menu, auth-session, notification-token, sidebar and composer, with Joe Masilotti's messages where his component exists, plus his search and review-prompt as they are. All 15 are the same names and messages as the iOS app's.
  • Path configuration: bundled, and replaced at launch by the server's /configurations/android_v2.json.

The playground

The iOS repository's Playground/server.py stands in for the Rails app as it will be once it implements the server contract: fixture pages in the web app's own markup, styled by its real stylesheet, with the bridge controllers and the contract's authentication. With crmne/chatwithwork-ios checked out next to this repository:

python3 ../chatwithwork-ios/Playground/server.py --port 8765
adb reverse tcp:8765 tcp:8765
./gradlew installDebug -PbaseUrl=http://localhost:8765

Sign in with the prefilled form; everything on its pages is made up.

Releasing

Versions come from tags: for v1.2.3, build with -PversionName=1.2.3 -PversionCode=10203 (MAJOR·10000 + MINOR·100 + PATCH), signing variables set, and upload app/build/outputs/bundle/release/app-release.aab to Google Play:

./gradlew bundleRelease -PversionName=1.2.3 -PversionCode=10203

script/bundle-release v1.2.3 does both, with the upload key from 1Password (script/upload-key makes it once). docs/google-play.md has the rest: the listing and screenshots in fastlane/, the Data safety answers, the content rating, and the demo login for review.

Third-party assets

  • Geist by Vercel, under the SIL Open Font License 1.1 (app/src/main/assets/licenses/geist-OFL.txt), subset to Latin.
  • Material Symbols by Google, under the Apache License 2.0: the icon drawables and, through bridge-components, the symbol font the bridge buttons draw from.

License

Licensed under either of

at your option. Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in this project, as defined in the Apache-2.0 license, is dual licensed as above, without any additional terms or conditions.

"Chat with Work" and its logo are trademarks of Plenty UG. The license covers the code, not the name or the logo in app/src/main/res.

See SECURITY.md for how to report a vulnerability.

About

Chat with Work for Android: AI bots in Europe. They work. You just decide. Built with Hotwire Native.

Topics

Resources

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages