Skip to main content

waterui_cli/android/
backend.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    android::platform::{AndroidAbi, AndroidPlatform, clean_android, is_android_platform},
7    backend::Backend,
8    build::BuildOptions,
9    device::Artifact,
10    platform::{PackageOptions, TargetBackend, TargetPlatform},
11    project::Project,
12    templates::{self, TemplateContext},
13};
14
15/// Configuration for the Android backend in a `WaterUI` project.
16///
17/// `[backend.android]` in `Water.toml`
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct AndroidBackend {
20    #[serde(
21        default = "default_android_project_path",
22        skip_serializing_if = "is_default_android_project_path"
23    )]
24    project_path: PathBuf,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    version: Option<String>,
27}
28
29impl AndroidBackend {
30    /// Create a new Android backend configuration with default settings.
31    #[must_use]
32    pub fn new() -> Self {
33        Self {
34            project_path: default_android_project_path(),
35            version: None,
36        }
37    }
38
39    /// Set a custom project path (defaults to "android").
40    #[must_use]
41    pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
42        self.project_path = path.into();
43        self
44    }
45
46    /// Get the path to the Android project within the `WaterUI` project.
47    #[must_use]
48    pub const fn project_path(&self) -> &PathBuf {
49        &self.project_path
50    }
51
52    /// Get the path to the Gradle wrapper script within the Android project.
53    #[must_use]
54    pub fn gradlew_path(&self) -> PathBuf {
55        let base = &self.project_path;
56        if cfg!(windows) {
57            base.join("gradlew.bat")
58        } else {
59            base.join("gradlew")
60        }
61    }
62}
63
64impl Default for AndroidBackend {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl Backend for AndroidBackend {
71    const DEFAULT_PATH: &'static str = "android";
72
73    // Preserve Gradle build caches during re-scaffolding
74    const CACHE_PATHS: &'static [&'static str] = &[".gradle", "build", "app"];
75
76    fn path(&self) -> &Path {
77        &self.project_path
78    }
79
80    async fn init(project: &Project) -> Result<Self, crate::backend::FailToInitBackend> {
81        let manifest = project.manifest();
82
83        // Derive app name from the display name (remove spaces for filesystem)
84        let app_name = manifest
85            .package
86            .name
87            .chars()
88            .filter(|c| c.is_alphanumeric())
89            .collect::<String>();
90
91        let project_path = default_android_project_path();
92
93        // Android is where a missing declaration actually breaks things, so
94        // surface anything a dependency needs that the app has not enabled.
95        match crate::assets::scan_required_permissions(project).await {
96            Ok(required) => crate::assets::warn_missing_permissions(project, &required, |key| {
97                key.android_permission_name().is_some()
98            }),
99            Err(error) => tracing::debug!("skipped permission audit: {error}"),
100        }
101
102        // Extract enabled permissions from the manifest
103        let android_permissions = manifest
104            .permissions
105            .iter()
106            .filter(|(_, entry)| entry.is_enabled())
107            .filter_map(|(key, _)| {
108                key.android_permission_name()
109                    .map(|name| templates::AndroidPermissionTemplateEntry { name })
110            })
111            .collect();
112
113        let ctx = TemplateContext::for_project_manifest(
114            manifest,
115            project.crate_name().clone(),
116            app_name,
117            &project
118                .resolved_framework()
119                .await
120                .map_err(crate::backend::FailToInitBackend::Config)?,
121        )
122        .with_backend_project_path(project.backend_path::<Self>())
123        .with_project_root_path(project.root().to_path_buf())
124        .with_android_permissions(android_permissions);
125
126        templates::android::scaffold(&project.backend_path::<Self>(), &ctx)
127            .await
128            .map_err(crate::backend::FailToInitBackend::Io)?;
129
130        Ok(Self {
131            project_path,
132            version: None,
133        })
134    }
135
136    fn supports(&self, platform: TargetPlatform) -> bool {
137        is_android_platform(platform)
138    }
139
140    async fn build(
141        &self,
142        project: &Project,
143        platform: TargetPlatform,
144        options: BuildOptions,
145    ) -> eyre::Result<PathBuf> {
146        debug_assert_eq!(platform, TargetPlatform::Android);
147        project
148            .browser_runtime_plan(platform, TargetBackend::Android)
149            .await?;
150        AndroidPlatform::arm64().build(project, options).await
151    }
152
153    async fn package(
154        &self,
155        project: &Project,
156        platform: TargetPlatform,
157        options: PackageOptions,
158    ) -> eyre::Result<Artifact> {
159        debug_assert_eq!(platform, TargetPlatform::Android);
160        AndroidPlatform::package_with_abis(project, options, &[AndroidAbi::Arm64V8a]).await
161    }
162
163    async fn clean(&self, project: &Project, _platform: TargetPlatform) -> eyre::Result<()> {
164        clean_android(project).await
165    }
166}
167
168fn default_android_project_path() -> PathBuf {
169    PathBuf::from("android")
170}
171
172fn is_default_android_project_path(s: &Path) -> bool {
173    s == Path::new("android")
174}