Class: Everywhere::Builders::Android
- Inherits:
-
Object
- Object
- Everywhere::Builders::Android
- Defined in:
- lib/everywhere/builders/android.rb
Overview
Assembles a Hotwire Native Android app around an already-deployed (remote mode) app — the Android half of Builders::Ios, and deliberately the same five steps: copy the gem's frozen Gradle template, stamp an explicit list of per-app files (never a build script), and hand it to Gradle.
The stamped list, and nothing else:
app/everywhere.properties identity (app/build.gradle.kts reads it)
app/native-packages.gradle.kts native.android.packages
app/src/stamped/AndroidManifest.xml deep links, auth scheme, permissions
app/src/main/assets/everywhere.json Config#to_shell_json
app/src/main/assets/json/path-configuration.json baked tabs + rules
app/src/main/assets/fonts/ the selected Material icon font
app/src/main/res/values/colors.xml tint / background, light (replaced in place)
app/src/main/res/values-night/colors.xml tint / background, dark
app/src/main/res/mipmap-*/ launcher icon layers + anydpi-v26
app/src/stamped/res/drawable*/ native/android/assets/** as drawables
app/src/main/java/com/rubyeverywhere/shell/extensions/ native/android/*.kt + registry
MVP scope is a debug/unsigned-release APK. Keystore signing, AAB and Play upload hang off the same seam later — see docs/android-plan.md §9.
Constant Summary collapse
- GRADLE_MODULE =
Fixed by the template contract: one Gradle module, always :app, so build paths and task names never depend on per-app values.
":app"- SHELL_PACKAGE =
The Kotlin package the shell's sources live in. NOT the applicationId: the app id is stamped per app, the code's package is frozen, and mixing them up is the classic Android manifest mistake — so anything we generate that names a class spells the package out in full.
"com.rubyeverywhere.shell"- EXTENSIONS_PACKAGE =
"#{SHELL_PACKAGE}.extensions"- MAIN_ACTIVITY =
The activity every stamped intent-filter attaches to. Its
element already exists in the frozen main manifest; the stamped source set only adds filters to it. "#{SHELL_PACKAGE}.MainActivity"- ICON_FONT_COMMIT =
google/material-design-icons, pinned by commit for the same reason the iOS side pins Package.resolved: an icon set that silently changes under a cached build is a rendering bug nobody can reproduce. Each entry carries the SHA-256 of both files, so a corrupted or substituted download fails loudly instead of shipping.
"528cb964c01fb2b09bc3b9208f82b6d8f8c1c1e2"- ICON_FONT_BASE =
"https://raw.githubusercontent.com/google/material-design-icons"- FETCHED_ICON_FONTS =
Keyed by the asset filename Config::ANDROID_ICON_FONTS names; "path" is the repo path without an extension (upstream spells the variable fonts with their axis list in brackets — we ship them under the plain name the shell asks Typeface for).
{ "MaterialSymbolsOutlined.ttf" => { "path" => "variablefont/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght]", "ttf" => "b5126c4655e0756f334d684104156b98b82ef7d61212a81c041b9e6cfa8ba925", "codepoints" => "8567a3d0512ce8a735a739d325e83ee7010d9bf7f6f8ed6a699c07c817ea907d" }, "MaterialSymbolsRounded.ttf" => { "path" => "variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght]", "ttf" => "8817f9df195b582d45a6031c1d9e1533595dd7a6f0e76abc309bdc3cc4d72ee1", "codepoints" => "8567a3d0512ce8a735a739d325e83ee7010d9bf7f6f8ed6a699c07c817ea907d" }, "MaterialSymbolsSharp.ttf" => { "path" => "variablefont/MaterialSymbolsSharp[FILL,GRAD,opsz,wght]", "ttf" => "c56a37227b580aa30bcbf352a770a2598dc86bca43bf060ff8d0550c081827e6", "codepoints" => "8567a3d0512ce8a735a739d325e83ee7010d9bf7f6f8ed6a699c07c817ea907d" } }.freeze
- BUNDLED_FONT_DIRS =
Where the bundled (classic) font sits inside the template. Two spellings because the template ships it as the default asset, and a template that keeps its fonts out of assets/ still resolves.
[%w[app src main assets fonts], %w[fonts]].freeze
- STAGE_MARKER =
Marks which template a work dir was staged from. A gem upgrade can add, rename or delete template files, and staging merges rather than wipes (see #stage), so a stale Kotlin file from an older template would keep compiling in. Version mismatch = wipe once, then merge as usual.
".everywhere-template"- APPLICATION_ID =
What AGP accepts as an applicationId: two or more segments, each a Java identifier starting with a letter. No hyphens, unlike a bundle id.
/\A[a-zA-Z]\w*(\.[a-zA-Z]\w*)+\z/
Class Method Summary collapse
-
.fetch!(url, dest, sha256:, redirects: 3) ⇒ Object
Download to a sibling .part file, verify, then rename — the atomicity the cache-hit rule above depends on.
Instance Method Summary collapse
-
#build!(dist_dir:, dev: false, dev_url: nil) ⇒ Object
Build the APK and copy it into dist_dir.
-
#initialize(config:, root:, target:, template_dir: nil) ⇒ Android
constructor
A new instance of Android.
Constructor Details
#initialize(config:, root:, target:, template_dir: nil) ⇒ Android
Returns a new instance of Android.
99 100 101 102 103 104 |
# File 'lib/everywhere/builders/android.rb', line 99 def initialize(config:, root:, target:, template_dir: nil) @config = config @root = root @target = target @template_dir = template_dir end |
Class Method Details
.fetch!(url, dest, sha256:, redirects: 3) ⇒ Object
Download to a sibling .part file, verify, then rename — the atomicity the cache-hit rule above depends on. A class method so tests (and the offline path) have one seam to stub instead of a live socket.
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
# File 'lib/everywhere/builders/android.rb', line 463 def self.fetch!(url, dest, sha256:, redirects: 3) require "net/http" require "uri" require "digest" uri = URI.parse(url) part = "#{dest}.part" Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http| http.request(Net::HTTP::Get.new(uri)) do |response| case response when Net::HTTPRedirection raise "too many redirects for #{url}" if redirects <= 0 return fetch!(response["location"], dest, sha256: sha256, redirects: redirects - 1) when Net::HTTPSuccess File.open(part, "wb") { |f| response.read_body { |chunk| f.write(chunk) } } else raise "HTTP #{response.code} for #{url}" end end end digest = Digest::SHA256.file(part).hexdigest raise "#{File.basename(dest)} checksum mismatch (got #{digest[0, 12]}…)" unless digest == sha256 File.rename(part, dest) dest ensure FileUtils.rm_f("#{dest}.part") end |
Instance Method Details
#build!(dist_dir:, dev: false, dev_url: nil) ⇒ Object
Build the APK and copy it into dist_dir. dev builds the debug variant
and relaxes the remote-mode requirement (the dev URL is stamped into a
debug asset each run — Android has no SIMCTL_CHILD_* equivalent).
Returns the path of the collected APK.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/everywhere/builders/android.rb', line 110 def build!(dist_dir:, dev: false, dev_url: nil) # The template is resolved before preflight, not after it like iOS: # preflight has to settle the icon font, and the bundled font lives in # the template. Everything that can fail slowly (a font download) or # cheaply (a typo'd icon name) is therefore decided before Gradle — the # difference between a 5-second error and one that lands 90 seconds in. template = @template_dir ? File.(@template_dir) : Paths.android_dir! preflight!(dev: dev, template: template) work = stage(template) stamp!(work) stamp_dev_url!(work, dev_url) variant = dev ? "debug" : "release" gradle!(work, variant, dist_dir) collect(work, variant, dist_dir) end |