ruby_everywhere 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +255 -0
- data/bridge/README.md +48 -0
- data/bridge/everywhere/bridge.js +388 -3
- data/bridge/everywhere/native.css +203 -0
- data/bridge/package.json +6 -3
- data/lib/everywhere/android_resources.rb +217 -0
- data/lib/everywhere/android_sdk.rb +330 -0
- data/lib/everywhere/asset_catalog.rb +185 -0
- data/lib/everywhere/auth_handoff.rb +398 -0
- data/lib/everywhere/auth_token.rb +118 -0
- data/lib/everywhere/builders/android.rb +844 -0
- data/lib/everywhere/builders/ios.rb +72 -3
- data/lib/everywhere/commands/build.rb +51 -9
- data/lib/everywhere/commands/clean.rb +10 -5
- data/lib/everywhere/commands/dev.rb +46 -8
- data/lib/everywhere/commands/doctor.rb +89 -3
- data/lib/everywhere/commands/install.rb +1 -1
- data/lib/everywhere/commands/logs.rb +117 -8
- data/lib/everywhere/config.rb +513 -14
- data/lib/everywhere/emulator.rb +405 -0
- data/lib/everywhere/engine.rb +21 -0
- data/lib/everywhere/icon.rb +196 -0
- data/lib/everywhere/log_filter.rb +74 -0
- data/lib/everywhere/mobile_config_endpoint.rb +13 -0
- data/lib/everywhere/mobile_configs_controller.rb +18 -0
- data/lib/everywhere/native_helper.rb +198 -5
- data/lib/everywhere/omniauth.rb +88 -0
- data/lib/everywhere/paths.rb +47 -0
- data/lib/everywhere/version.rb +1 -1
- data/support/mobile/android/README.md +217 -0
- data/support/mobile/android/app/build.gradle.kts +156 -0
- data/support/mobile/android/app/everywhere.properties +8 -0
- data/support/mobile/android/app/native-packages.gradle.kts +16 -0
- data/support/mobile/android/app/proguard-rules.pro +21 -0
- data/support/mobile/android/app/src/debug/res/xml/network_security_config.xml +30 -0
- data/support/mobile/android/app/src/main/AndroidManifest.xml +42 -0
- data/support/mobile/android/app/src/main/assets/everywhere.json +9 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.codepoints +2235 -0
- data/support/mobile/android/app/src/main/assets/fonts/MaterialIcons-Regular.ttf +0 -0
- data/support/mobile/android/app/src/main/assets/json/path-configuration.json +19 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/AuthFlow.kt +268 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereApplication.kt +178 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereConfig.kt +409 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereEvents.kt +68 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/EverywhereHost.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/IconFont.kt +211 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainActivity.kt +975 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/MainTabs.kt +201 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebBottomSheetFragment.kt +13 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/WebFragment.kt +36 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/BiometricsComponent.kt +535 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/HapticsComponent.kt +141 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/MenuComponent.kt +361 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/NotificationComponent.kt +151 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/PermissionsComponent.kt +271 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/StorageComponent.kt +101 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/bridge/WebControlChannel.kt +204 -0
- data/support/mobile/android/app/src/main/java/com/rubyeverywhere/shell/extensions/EverywhereExtensions.kt +51 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_active_indicator.xml +4 -0
- data/support/mobile/android/app/src/main/res/color/bottom_navigation_item.xml +11 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_launcher_foreground.xml +26 -0
- data/support/mobile/android/app/src/main/res/drawable/ic_tab_placeholder.xml +28 -0
- data/support/mobile/android/app/src/main/res/layout/activity_main.xml +116 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet.xml +83 -0
- data/support/mobile/android/app/src/main/res/layout/bridge_menu_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet.xml +48 -0
- data/support/mobile/android/app/src/main/res/layout/tab_more_sheet_item.xml +35 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +12 -0
- data/support/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- data/support/mobile/android/app/src/main/res/values/colors.xml +21 -0
- data/support/mobile/android/app/src/main/res/values/ic_launcher_background.xml +5 -0
- data/support/mobile/android/app/src/main/res/values/strings.xml +18 -0
- data/support/mobile/android/app/src/main/res/values/styles.xml +32 -0
- data/support/mobile/android/app/src/main/res/values/themes.xml +53 -0
- data/support/mobile/android/app/src/main/res/values-night/colors.xml +11 -0
- data/support/mobile/android/app/src/main/res/values-night/themes.xml +33 -0
- data/support/mobile/android/app/src/main/res/xml/network_security_config.xml +12 -0
- data/support/mobile/android/app/src/stamped/AndroidManifest.xml +34 -0
- data/support/mobile/android/build.gradle.kts +8 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- data/support/mobile/android/gradle/wrapper/gradle-wrapper.properties +10 -0
- data/support/mobile/android/gradle.properties +14 -0
- data/support/mobile/android/gradlew +251 -0
- data/support/mobile/android/gradlew.bat +94 -0
- data/support/mobile/android/settings.gradle.kts +28 -0
- data/support/mobile/ios/App/AppDelegate.swift +7 -1
- data/support/mobile/ios/App/AuthFlow.swift +101 -0
- data/support/mobile/ios/App/Bridge/MenuComponent.swift +192 -0
- data/support/mobile/ios/App/EverywhereConfig.swift +105 -0
- data/support/mobile/ios/App/SceneDelegate.swift +144 -0
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +8 -0
- data/support/mobile/ios/README.md +10 -0
- metadata +69 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# RubyEverywhere Android Shell Template
|
|
2
|
+
|
|
3
|
+
A canonical [Hotwire Native Android][1] shell that wraps a deployed Ruby web app
|
|
4
|
+
(remote URL). The RubyEverywhere CLI stamps a copy of this directory per app —
|
|
5
|
+
the template itself is **never edited per-app**.
|
|
6
|
+
|
|
7
|
+
The iOS template is the reference implementation; this one is a feature-for-feature
|
|
8
|
+
port of `support/mobile/ios/`, and where the two differ the difference is forced
|
|
9
|
+
by the platform and is explained in a comment at the site.
|
|
10
|
+
|
|
11
|
+
## Freeze rules
|
|
12
|
+
|
|
13
|
+
Every build script, layout, theme and Kotlin source here is written once and
|
|
14
|
+
frozen. When `every build --target android-*` stamps a copy, it overwrites
|
|
15
|
+
**exactly** these files and nothing else:
|
|
16
|
+
|
|
17
|
+
1. `app/everywhere.properties` — applicationId, versionName, versionCode, app
|
|
18
|
+
label. The direct analogue of iOS's `App.xcconfig`: identity is stamped
|
|
19
|
+
without ever touching a build script. `app/build.gradle.kts` reads it at
|
|
20
|
+
configuration time.
|
|
21
|
+
2. `app/src/main/assets/everywhere.json` — runtime config (remote URL, entry
|
|
22
|
+
path, colours, permissions, auth, universal-link hosts);
|
|
23
|
+
`Config#to_shell_json`.
|
|
24
|
+
3. `app/src/main/assets/json/path-configuration.json` — baked rules +
|
|
25
|
+
`settings.tabs` (`Config#path_configuration_json("android")`). The same
|
|
26
|
+
document is served live at `/everywhere/android_v1.json` by the gem's
|
|
27
|
+
`MobileConfigEndpoint`, which the shell loads as its second, auth-aware
|
|
28
|
+
source.
|
|
29
|
+
4. `app/src/main/assets/fonts/` — the selected Material icon font and its
|
|
30
|
+
`.codepoints` map. The bundled classic set
|
|
31
|
+
(`MaterialIcons-Regular.ttf`, 357 KB, 2,235 icons) is the offline fallback;
|
|
32
|
+
`icon_font: symbols` fetches the 10.6 MB variable font into
|
|
33
|
+
`~/.rubyeverywhere/android-fonts/` on first use. The two files must always
|
|
34
|
+
travel together — 282 names exist in both fonts with *different* codepoints.
|
|
35
|
+
5. `app/src/main/res/values/colors.xml` and `values-night/colors.xml` — the
|
|
36
|
+
light and dark halves of `appearance.tint_color` / `background_color`.
|
|
37
|
+
Everything in the theme derives from those two colours.
|
|
38
|
+
6. `app/src/main/res/values/ic_launcher_background.xml` and
|
|
39
|
+
`res/drawable-<dpi>/ic_launcher_foreground.png` — the adaptive launcher
|
|
40
|
+
icon's two layers. `minSdk 28` means there are no legacy
|
|
41
|
+
`mipmap-<dpi>/ic_launcher.png` fallbacks to generate.
|
|
42
|
+
7. *(optionally)* `app/src/stamped/AndroidManifest.xml` — deep-link intent
|
|
43
|
+
filters, the auth callback scheme, and runtime `<uses-permission>` entries.
|
|
44
|
+
Written **only** when `everywhere.yml` declares `deep_linking:`, `auth:` or
|
|
45
|
+
`permissions:`. It is a separate manifest, folded in by the merger, so the
|
|
46
|
+
main manifest stays frozen — the analogue of iOS's optional
|
|
47
|
+
`App.entitlements`. See the note below on how it is wired.
|
|
48
|
+
8. *(optionally)* `app/native-packages.gradle.kts` — Maven coordinates from
|
|
49
|
+
`native.android.packages`, and
|
|
50
|
+
`app/src/main/java/com/rubyeverywhere/shell/extensions/` — the app repo's
|
|
51
|
+
`native/android/*.kt` plus the generated `EverywhereExtensions.kt` registry.
|
|
52
|
+
Both ship as empty defaults so the raw template builds unchanged.
|
|
53
|
+
9. `app/src/stamped/res/drawable*/` — the app's own images, generated from
|
|
54
|
+
`native/android/assets/**` by `Everywhere::AndroidResources`, reachable from
|
|
55
|
+
extension code as `R.drawable.<name>`. Registered as a second `res` srcDir on
|
|
56
|
+
`main` (see `sourceSets` in `app/build.gradle.kts`) and **cleared on every
|
|
57
|
+
build**, which is why it is its own directory rather than more files in
|
|
58
|
+
`main/res`: stamping merges over the work dir instead of wiping it, so an
|
|
59
|
+
asset the app deleted has to be removed by the generator that wrote it.
|
|
60
|
+
Filenames are the whole API and match iOS's exactly — `logo@2x~dark.png`
|
|
61
|
+
becomes `drawable-night-xhdpi/logo.png`.
|
|
62
|
+
10. *(dev only)* `app/src/debug/assets/dev-url.txt` — the dev-server URL.
|
|
63
|
+
Android has no `SIMCTL_CHILD_*`, so `every dev --android` stamps this and
|
|
64
|
+
rebuilds, which it has to do anyway. Read only in debug builds; the debug
|
|
65
|
+
source set never ships in a release.
|
|
66
|
+
|
|
67
|
+
**Structural changes** — new source files, new dependencies, a plugin bump, a
|
|
68
|
+
new layout — mean deliberately editing this template and recommitting, never
|
|
69
|
+
editing a stamped copy.
|
|
70
|
+
|
|
71
|
+
### Why `src/stamped/` is wired the way it is
|
|
72
|
+
|
|
73
|
+
AGP merges one manifest per *source set*, and without product flavours a variant
|
|
74
|
+
has exactly two: `main` and its build type. The generated manifest is therefore
|
|
75
|
+
registered as **both** build types' manifest in `app/build.gradle.kts`:
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
sourceSets {
|
|
79
|
+
getByName("debug") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
80
|
+
getByName("release") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A product flavour would have been the textbook answer, but it renames every task
|
|
85
|
+
(`assembleStampedDebug`), and the builder, the dev loop and this README all
|
|
86
|
+
depend on plain `:app:assembleDebug`.
|
|
87
|
+
|
|
88
|
+
That leaves the build-type manifest slot occupied, so the debug-only cleartext
|
|
89
|
+
policy is expressed as a **resource overlay** instead:
|
|
90
|
+
`src/debug/res/xml/network_security_config.xml` overrides the release-safe
|
|
91
|
+
`src/main/res/xml/network_security_config.xml` of the same name. The main
|
|
92
|
+
manifest's `android:networkSecurityConfig` attribute never changes.
|
|
93
|
+
|
|
94
|
+
## Dependency pin policy
|
|
95
|
+
|
|
96
|
+
The SPM equivalent. Plugin versions are pinned in the root `build.gradle.kts`,
|
|
97
|
+
library versions in `app/build.gradle.kts`, and both are **exact** — no ranges,
|
|
98
|
+
no `+`, no version catalog. Moving a pin is a deliberate edit here.
|
|
99
|
+
|
|
100
|
+
| | pinned at |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| Gradle | 9.2.0 (`-bin`, see below) |
|
|
103
|
+
| Android Gradle Plugin | 8.13.2 |
|
|
104
|
+
| Kotlin / kotlinx.serialization plugin | 2.3.0 |
|
|
105
|
+
| `dev.hotwire:core`, `dev.hotwire:navigation-fragments` | 1.3.0 |
|
|
106
|
+
| `compileSdk` / `targetSdk` | 35 |
|
|
107
|
+
| `minSdk` | 28 — Hotwire Native Android's floor, not negotiable |
|
|
108
|
+
| Java toolchain / JVM target | 17 |
|
|
109
|
+
|
|
110
|
+
The wrapper deliberately uses `gradle-9.2.0-**bin**.zip`, not `-all`: the `-all`
|
|
111
|
+
distribution unpacks to 522 MB in `GRADLE_USER_HOME` and the difference is
|
|
112
|
+
Gradle's own sources and javadoc, which only an IDE indexing the Gradle API ever
|
|
113
|
+
reads.
|
|
114
|
+
|
|
115
|
+
`settings.gradle.kts` keeps the `foojay-resolver-convention` plugin so the Java
|
|
116
|
+
toolchain can be provisioned independently of the launcher JVM — without it the
|
|
117
|
+
build fails on a machine whose only JDK is 21.
|
|
118
|
+
|
|
119
|
+
## Building standalone
|
|
120
|
+
|
|
121
|
+
The raw template builds green with placeholder values, unsigned:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
|
|
125
|
+
export ANDROID_HOME=$HOME/Library/Android/sdk
|
|
126
|
+
./gradlew :app:assembleDebug
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Output: `app/build/outputs/apk/debug/app-debug.apk`.
|
|
130
|
+
|
|
131
|
+
The first build downloads roughly 1.4 GB (Gradle, the Android Gradle Plugin, the
|
|
132
|
+
Hotwire artifacts) and takes a couple of minutes, almost all of it network. Warm
|
|
133
|
+
no-op rebuilds are around 7 seconds. The CLI redirects `GRADLE_USER_HOME` to
|
|
134
|
+
`~/.rubyeverywhere/android-gradle` so that cache is shared across apps and never
|
|
135
|
+
lands inside the installed gem.
|
|
136
|
+
|
|
137
|
+
## Dev loop
|
|
138
|
+
|
|
139
|
+
The shell is remote-mode only (`remote_url` in `everywhere.json`). For local
|
|
140
|
+
development, `every dev --android` stamps `src/debug/assets/dev-url.txt`,
|
|
141
|
+
rebuilds, reinstalls and relaunches — the Android equivalent of pressing `i` in
|
|
142
|
+
the iOS loop. The URL resolves down a three-step ladder:
|
|
143
|
+
|
|
144
|
+
1. `adb reverse tcp:<port> tcp:<port>`, then `http://127.0.0.1:<port>` — correct
|
|
145
|
+
on emulators *and* physical devices. Preferred.
|
|
146
|
+
2. `http://10.0.2.2:<port>` — the emulator's alias for the host loopback, used
|
|
147
|
+
automatically when `adb reverse` fails.
|
|
148
|
+
3. An explicit `--dev-url` LAN address, for physical devices.
|
|
149
|
+
|
|
150
|
+
Cleartext to those hosts is allowed only in debug builds, and only for the hosts
|
|
151
|
+
themselves — never `cleartextTrafficPermitted` globally. See
|
|
152
|
+
`app/src/debug/res/xml/network_security_config.xml`.
|
|
153
|
+
|
|
154
|
+
## Shape of the shell
|
|
155
|
+
|
|
156
|
+
| file | role |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `EverywhereApplication.kt` | Hotwire config, bridge + destination + route-handler registration, first path-configuration load |
|
|
159
|
+
| `EverywhereConfig.kt` | `everywhere.json` decode; roots, dev override, instance override, auth, deep links |
|
|
160
|
+
| `EverywhereEvents.kt` | page → shell control messages, replacing iOS's NotificationCenter |
|
|
161
|
+
| `MainActivity.kt` | the SceneDelegate counterpart: tabs, splash, reset, badges, deep links, sign-in |
|
|
162
|
+
| `MainTabs.kt` | `settings.tabs` → the fixed pool of five NavigatorHosts |
|
|
163
|
+
| `IconFont.kt` | Material icons resolved by name from the bundled font |
|
|
164
|
+
| `WebFragment.kt`, `WebBottomSheetFragment.kt` | the web destinations |
|
|
165
|
+
| `bridge/` | the six `everywhere--*` bridge components and the control channel |
|
|
166
|
+
| `AuthFlow.kt` | third-party sign-in in a Custom Tab |
|
|
167
|
+
| `EverywhereHost.kt` | what app-authored `native/android/` code calls: `everywhereVisit`, `everywhereFetch` |
|
|
168
|
+
| `extensions/EverywhereExtensions.kt` | generated app-extension registry (empty by default) |
|
|
169
|
+
|
|
170
|
+
### What app-authored screens get
|
|
171
|
+
|
|
172
|
+
`native/android/*.kt` compiles into `extensions/` and must declare
|
|
173
|
+
`package com.rubyeverywhere.shell.extensions` — the generated registry names
|
|
174
|
+
those types unqualified, and Kotlin ties nothing to the directory, so the package
|
|
175
|
+
line is what makes them referenceable. `every build` refuses a file without it.
|
|
176
|
+
|
|
177
|
+
The two helpers in `EverywhereHost.kt` are named to match iOS's
|
|
178
|
+
`EverywhereHost.swift` so extension code reads the same on both platforms:
|
|
179
|
+
|
|
180
|
+
* `everywhereVisit("/path")` — route through the active navigator, so a native
|
|
181
|
+
screen's links get the full path-configuration treatment (modals, other native
|
|
182
|
+
screens) like any web-driven visit.
|
|
183
|
+
* `everywhereFetch("/builds.json")` — GET from the app's own server carrying the
|
|
184
|
+
WebView's session cookie and the native User-Agent, so `native_app?` holds
|
|
185
|
+
server-side and an authenticated endpoint answers. Suspending; throws
|
|
186
|
+
`EverywhereFetchError` on a non-2xx.
|
|
187
|
+
|
|
188
|
+
Images live in `native/android/assets/**` and arrive as `R.drawable.<name>`
|
|
189
|
+
(freeze rule 9). There is no `everywhereHost` counterpart to iOS's — a Hotwire
|
|
190
|
+
Android destination is already a `Fragment`.
|
|
191
|
+
|
|
192
|
+
### Three invariants that will break the app if they are violated
|
|
193
|
+
|
|
194
|
+
Measured on a device during the Phase 0 spike, not inferred:
|
|
195
|
+
|
|
196
|
+
1. **`navigatorConfigurations()` must return a configuration for all five hosts,
|
|
197
|
+
on every launch.** `NavigatorHost.onCreate` throws
|
|
198
|
+
`IllegalStateException("No configuration found for NavigatorHost")` for any
|
|
199
|
+
host in `activity_main.xml` it can't find one for. "Not a tab right now" is
|
|
200
|
+
`isVisible = false`, **never** a shorter list — `switchTab` only hides hosts
|
|
201
|
+
present in the new list, so a dropped host stays on screen.
|
|
202
|
+
2. **`lazyLoadTabs = true` is mandatory.** With eager loading all five pooled
|
|
203
|
+
hosts cold-boot a WebView and fetch their start location at launch,
|
|
204
|
+
including the hidden auth-gated ones.
|
|
205
|
+
3. **A second `load()` is cheap but lossy.** Fragments and WebViews survive
|
|
206
|
+
(~96 ms), but `loadMenu()` opens with `menu.clear()`, which destroys badges
|
|
207
|
+
and icons, and resets Material's `selectedItemId` to 0 — which then takes the
|
|
208
|
+
*reselect* path and clears the selected tab's backstack whenever the selected
|
|
209
|
+
tab is the first one. `MainActivity.applyTabs` is the only caller and does
|
|
210
|
+
all three fix-ups; nothing else may call `load()`.
|
|
211
|
+
|
|
212
|
+
Tab **path** changes are not covered by any of that: `NavigatorHost` caches its
|
|
213
|
+
configuration in `onCreate`, so a changed `startLocation` needs
|
|
214
|
+
`Activity.recreate()`. `MainTabs` keeps two fingerprints so the cheap path is
|
|
215
|
+
taken whenever it can be.
|
|
216
|
+
|
|
217
|
+
[1]: https://github.com/hotwired/hotwire-native-android
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
2
|
+
import java.util.Properties
|
|
3
|
+
|
|
4
|
+
plugins {
|
|
5
|
+
id("com.android.application")
|
|
6
|
+
id("org.jetbrains.kotlin.android")
|
|
7
|
+
id("org.jetbrains.kotlin.plugin.serialization")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Per-app identity, read from a properties file rather than written into this
|
|
11
|
+
// script. `every build` overwrites everywhere.properties and nothing else here
|
|
12
|
+
// — the same contract as iOS's App.xcconfig, for the same reason: a build
|
|
13
|
+
// script the CLI edits is a build script that drifts.
|
|
14
|
+
val everywhere = Properties().apply {
|
|
15
|
+
file("everywhere.properties").inputStream().use { load(it) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fun stamped(key: String, default: String): String =
|
|
19
|
+
everywhere.getProperty("everywhere.$key")?.takeIf { it.isNotBlank() } ?: default
|
|
20
|
+
|
|
21
|
+
android {
|
|
22
|
+
// The namespace is the R class / BuildConfig package and is frozen. It is
|
|
23
|
+
// deliberately NOT the applicationId: apps rename their bundle id freely,
|
|
24
|
+
// and a namespace change would move every generated class.
|
|
25
|
+
namespace = "com.rubyeverywhere.shell"
|
|
26
|
+
compileSdk = 35
|
|
27
|
+
|
|
28
|
+
defaultConfig {
|
|
29
|
+
applicationId = stamped("applicationId", "com.rubyeverywhere.app")
|
|
30
|
+
versionName = stamped("versionName", "0.1.0")
|
|
31
|
+
versionCode = stamped("versionCode", "1").toInt()
|
|
32
|
+
|
|
33
|
+
// minSdk 28 is Hotwire Native Android's floor and is not negotiable.
|
|
34
|
+
// It also clears API 26 for Typeface.Builder.setFontVariationSettings,
|
|
35
|
+
// which IconFont needs for the Material Symbols FILL axis.
|
|
36
|
+
minSdk = 28
|
|
37
|
+
targetSdk = 35
|
|
38
|
+
|
|
39
|
+
// Consumed by AndroidManifest.xml. The label is a placeholder so the
|
|
40
|
+
// manifest itself stays frozen when the app is renamed.
|
|
41
|
+
manifestPlaceholders["everywhereLabel"] = stamped("label", "RubyEverywhere")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sourceSets {
|
|
45
|
+
// Deep-link intent filters, the auth callback scheme and runtime
|
|
46
|
+
// <uses-permission> entries are generated into their own manifest so
|
|
47
|
+
// the main one never changes. AGP merges one manifest per build type,
|
|
48
|
+
// and the generated file is identical for both — hence the same path
|
|
49
|
+
// twice. See src/stamped/AndroidManifest.xml.
|
|
50
|
+
getByName("debug") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
51
|
+
getByName("release") { manifest.srcFile("src/stamped/AndroidManifest.xml") }
|
|
52
|
+
|
|
53
|
+
// The app's own images (native/android/assets/**) are generated into
|
|
54
|
+
// src/stamped/res as drawables. A second res dir on `main` rather than
|
|
55
|
+
// more files in main/res, because the generator owns the directory
|
|
56
|
+
// outright and clears it every build — a deleted asset has to stop
|
|
57
|
+
// shipping, and stamping merges over the work dir instead of wiping it.
|
|
58
|
+
//
|
|
59
|
+
// On `main`, deliberately, not on a build type: same-variant duplicates
|
|
60
|
+
// are a merge ERROR, so an asset that collides with a template drawable
|
|
61
|
+
// fails the build instead of silently overriding it. AGP ignores the
|
|
62
|
+
// directory when it doesn't exist, which is how the raw template builds.
|
|
63
|
+
getByName("main") { res.srcDir("src/stamped/res") }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
buildTypes {
|
|
67
|
+
getByName("debug") {
|
|
68
|
+
isDebuggable = true
|
|
69
|
+
}
|
|
70
|
+
release {
|
|
71
|
+
isMinifyEnabled = false
|
|
72
|
+
proguardFiles(
|
|
73
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
74
|
+
"proguard-rules.pro"
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
buildFeatures {
|
|
80
|
+
// BuildConfig.DEBUG gates web-view debugging, verbose logging and the
|
|
81
|
+
// dev-server asset. Nothing else in the shell branches on build type.
|
|
82
|
+
buildConfig = true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
compileOptions {
|
|
86
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
87
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lint {
|
|
91
|
+
// `lintVitalRelease` crashes on this exact toolchain (AGP 8.13.2 +
|
|
92
|
+
// Kotlin 2.3.0). Its build-script visitor builds light classes for
|
|
93
|
+
// every `.gradle.kts` in the project and dies inside the Kotlin
|
|
94
|
+
// compiler with "findFirCompiledSymbol only works on compiled
|
|
95
|
+
// declarations" the moment one script `apply(from:)`s another — which
|
|
96
|
+
// native-packages.gradle.kts is, by design.
|
|
97
|
+
//
|
|
98
|
+
// Verified by bisection: commenting out the `apply(from:)` below makes
|
|
99
|
+
// lintVitalAnalyzeRelease pass, and nothing else does. So this disables
|
|
100
|
+
// a check that cannot run, not one that was failing us. Revisit when
|
|
101
|
+
// either version moves.
|
|
102
|
+
checkReleaseBuilds = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
packaging {
|
|
106
|
+
// The icon font is already a compressed container; deflating it again
|
|
107
|
+
// costs build time and saves nothing.
|
|
108
|
+
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
kotlin {
|
|
113
|
+
compilerOptions {
|
|
114
|
+
jvmTarget.set(JvmTarget.JVM_17)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
java {
|
|
119
|
+
toolchain {
|
|
120
|
+
languageVersion = JavaLanguageVersion.of(17)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
dependencies {
|
|
125
|
+
// Hotwire Native. Pinned exactly, not to a range: this is the template's
|
|
126
|
+
// dependency pin policy, the analogue of iOS's Package.resolved.
|
|
127
|
+
implementation("dev.hotwire:core:1.3.0")
|
|
128
|
+
implementation("dev.hotwire:navigation-fragments:1.3.0")
|
|
129
|
+
|
|
130
|
+
// Material — BottomNavigationView, badges, bottom sheets, theming.
|
|
131
|
+
implementation("com.google.android.material:material:1.12.0")
|
|
132
|
+
|
|
133
|
+
// activity_main.xml's root.
|
|
134
|
+
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
|
135
|
+
|
|
136
|
+
// WebViewCompat: addWebMessageListener (the control channel) and
|
|
137
|
+
// addDocumentStartJavaScript (window.__EVERYWHERE_CONFIG__). Android
|
|
138
|
+
// WebView has no user-script API of its own; these are the substitutes.
|
|
139
|
+
implementation("androidx.webkit:webkit:1.12.1")
|
|
140
|
+
|
|
141
|
+
// Custom Tabs — the ASWebAuthenticationSession stand-in for third-party
|
|
142
|
+
// sign-in, and what Hotwire's own BrowserTabRouteDecisionHandler uses.
|
|
143
|
+
implementation("androidx.browser:browser:1.8.0")
|
|
144
|
+
|
|
145
|
+
// Biometrics + the encrypted store they gate.
|
|
146
|
+
implementation("androidx.biometric:biometric:1.1.0")
|
|
147
|
+
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
|
148
|
+
|
|
149
|
+
// everywhere.json, the bridge components' message payloads, and the
|
|
150
|
+
// control channel's postMessage bodies.
|
|
151
|
+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// GENERATED: Maven coordinates from `native.android.packages`. Applied last so
|
|
155
|
+
// an app's own dependency can override a version above if it must.
|
|
156
|
+
apply(from = "native-packages.gradle.kts")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# STAMPED by `every build --android`. The direct analogue of iOS's
|
|
2
|
+
# App.xcconfig: identity lives here so no build script is ever edited per app.
|
|
3
|
+
# The values below are the template's placeholders — the raw template builds
|
|
4
|
+
# green with them.
|
|
5
|
+
everywhere.applicationId=com.rubyeverywhere.app
|
|
6
|
+
everywhere.versionName=0.1.0
|
|
7
|
+
everywhere.versionCode=1
|
|
8
|
+
everywhere.label=RubyEverywhere
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// GENERATED by `every build --android` from `native.android.packages` in
|
|
2
|
+
// config/everywhere.yml. The checked-in copy is the empty default, so the raw
|
|
3
|
+
// template builds unchanged.
|
|
4
|
+
//
|
|
5
|
+
// native:
|
|
6
|
+
// android:
|
|
7
|
+
// packages:
|
|
8
|
+
// - "com.airbnb.android:lottie:6.4.0"
|
|
9
|
+
//
|
|
10
|
+
// This is a script plugin applied from app/build.gradle.kts, so it has no
|
|
11
|
+
// typed `implementation(...)` accessor — dependencies are added by
|
|
12
|
+
// configuration name. Keeping the generated surface to a single file means
|
|
13
|
+
// the frozen build script never changes shape when an app adds a package.
|
|
14
|
+
dependencies {
|
|
15
|
+
// add("implementation", "group:artifact:version")
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Bridge components are instantiated reflectively through
|
|
2
|
+
# BridgeComponentFactory and named by string from the page, so R8 has no call
|
|
3
|
+
# graph reaching them. Without this the release build strips every component
|
|
4
|
+
# and the page's `everywhere--*` messages silently go nowhere.
|
|
5
|
+
-keep class com.rubyeverywhere.shell.bridge.** { *; }
|
|
6
|
+
|
|
7
|
+
# App-provided native code (native/android/*.kt), same reasoning: it is
|
|
8
|
+
# referenced by identifier from the path configuration, not from Kotlin.
|
|
9
|
+
-keep class com.rubyeverywhere.shell.extensions.** { *; }
|
|
10
|
+
|
|
11
|
+
# Fragment destinations are resolved from the navigation graph by class name.
|
|
12
|
+
-keep class * extends androidx.fragment.app.Fragment { *; }
|
|
13
|
+
|
|
14
|
+
# kotlinx.serialization generates a companion `serializer()` per @Serializable
|
|
15
|
+
# class; the plugin's own rules cover its runtime, these cover our models.
|
|
16
|
+
-keepclassmembers class com.rubyeverywhere.shell.** {
|
|
17
|
+
*** Companion;
|
|
18
|
+
}
|
|
19
|
+
-keepclasseswithmembers class com.rubyeverywhere.shell.** {
|
|
20
|
+
kotlinx.serialization.KSerializer serializer(...);
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Debug only. The analogue of iOS's NSAllowsLocalNetworking: enough cleartext
|
|
4
|
+
to reach a Rails server on the developer's machine, and not one host more.
|
|
5
|
+
Never `cleartextTrafficPermitted="true"` on the base config — that would
|
|
6
|
+
quietly downgrade every request the app makes while debugging, including the
|
|
7
|
+
ones going to the real, deployed app.
|
|
8
|
+
|
|
9
|
+
* 127.0.0.1 / localhost — correct on emulators AND physical devices once
|
|
10
|
+
`adb reverse tcp:<port> tcp:<port>` is set up. The preferred rung.
|
|
11
|
+
* 10.0.2.2 — the Android emulator's alias for the host's loopback, used when
|
|
12
|
+
`adb reverse` is unavailable.
|
|
13
|
+
* .local — a Bonjour hostname, which is how most LAN setups are reachable.
|
|
14
|
+
|
|
15
|
+
network-security-config takes hostnames and IP literals only; there is no
|
|
16
|
+
CIDR syntax, so the private ranges cannot be expressed here. An explicit
|
|
17
|
+
`dev_url` of http://192.168.1.20:3000 therefore needs its host added, and
|
|
18
|
+
appending a domain entry to this file for that case is the one edit the
|
|
19
|
+
builder is allowed to make here.
|
|
20
|
+
-->
|
|
21
|
+
<network-security-config>
|
|
22
|
+
<base-config cleartextTrafficPermitted="false" />
|
|
23
|
+
|
|
24
|
+
<domain-config cleartextTrafficPermitted="true">
|
|
25
|
+
<domain includeSubdomains="false">127.0.0.1</domain>
|
|
26
|
+
<domain includeSubdomains="false">localhost</domain>
|
|
27
|
+
<domain includeSubdomains="false">10.0.2.2</domain>
|
|
28
|
+
<domain includeSubdomains="true">local</domain>
|
|
29
|
+
</domain-config>
|
|
30
|
+
</network-security-config>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
FROZEN. Nothing per-app belongs in this file. The app label arrives as a
|
|
4
|
+
manifest placeholder from everywhere.properties; deep-link intent filters,
|
|
5
|
+
the auth callback scheme and runtime permissions are merged in from
|
|
6
|
+
src/stamped/AndroidManifest.xml.
|
|
7
|
+
-->
|
|
8
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
9
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
10
|
+
|
|
11
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
12
|
+
|
|
13
|
+
<application
|
|
14
|
+
android:name=".EverywhereApplication"
|
|
15
|
+
android:allowBackup="false"
|
|
16
|
+
android:enableOnBackInvokedCallback="true"
|
|
17
|
+
android:icon="@mipmap/ic_launcher"
|
|
18
|
+
android:label="${everywhereLabel}"
|
|
19
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
20
|
+
android:supportsRtl="true"
|
|
21
|
+
android:theme="@style/Theme.Everywhere"
|
|
22
|
+
android:networkSecurityConfig="@xml/network_security_config"
|
|
23
|
+
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
|
24
|
+
|
|
25
|
+
<activity
|
|
26
|
+
android:name=".MainActivity"
|
|
27
|
+
android:exported="true"
|
|
28
|
+
android:configChanges="orientation|screenSize"
|
|
29
|
+
android:theme="@style/Theme.Everywhere.Splash"
|
|
30
|
+
android:windowSoftInputMode="adjustResize"
|
|
31
|
+
android:launchMode="singleTask">
|
|
32
|
+
|
|
33
|
+
<intent-filter>
|
|
34
|
+
<action android:name="android.intent.action.MAIN" />
|
|
35
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
36
|
+
</intent-filter>
|
|
37
|
+
|
|
38
|
+
</activity>
|
|
39
|
+
|
|
40
|
+
</application>
|
|
41
|
+
|
|
42
|
+
</manifest>
|