waterui_cli/android/
backend.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    backend::Backend,
7    templates::{self, TemplateContext},
8};
9
10/// Configuration for the Android backend in a `WaterUI` project.
11///
12/// `[backend.android]` in `Water.toml`
13#[derive(Debug, Serialize, Deserialize, Clone)]
14pub struct AndroidBackend {
15    #[serde(
16        default = "default_android_project_path",
17        skip_serializing_if = "is_default_android_project_path"
18    )]
19    project_path: PathBuf,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    version: Option<String>,
22}
23
24impl AndroidBackend {
25    /// Create a new Android backend configuration with default settings.
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            project_path: default_android_project_path(),
30            version: None,
31        }
32    }
33
34    /// Set a custom project path (defaults to "android").
35    #[must_use]
36    pub fn with_project_path(mut self, path: impl Into<PathBuf>) -> Self {
37        self.project_path = path.into();
38        self
39    }
40
41    /// Get the path to the Android project within the `WaterUI` project.
42    #[must_use]
43    pub const fn project_path(&self) -> &PathBuf {
44        &self.project_path
45    }
46
47    /// Get the path to the Gradle wrapper script within the Android project.
48    #[must_use]
49    pub fn gradlew_path(&self) -> PathBuf {
50        let base = &self.project_path;
51        if cfg!(windows) {
52            base.join("gradlew.bat")
53        } else {
54            base.join("gradlew")
55        }
56    }
57}
58
59impl Default for AndroidBackend {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl Backend for AndroidBackend {
66    const DEFAULT_PATH: &'static str = "android";
67
68    fn path(&self) -> &Path {
69        &self.project_path
70    }
71
72    async fn init(
73        project: &crate::project::Project,
74    ) -> Result<Self, crate::backend::FailToInitBackend> {
75        let manifest = project.manifest();
76
77        // Derive app name from the display name (remove spaces for filesystem)
78        let app_name = manifest
79            .package
80            .name
81            .chars()
82            .filter(|c| c.is_alphanumeric())
83            .collect::<String>();
84
85        // Get the relative path to the backend from project root (e.g., "android" or ".water/android")
86        let backend_relative_path = project.backend_relative_path::<Self>();
87
88        // Determine the Android backend path - this is unused for local dev,
89        // kept for potential future remote backend support
90        let android_backend_path = manifest
91            .waterui_path
92            .as_ref()
93            .map(|p| PathBuf::from(p).join("backends/android"));
94
95        let project_path = default_android_project_path();
96
97        // Extract enabled permissions from the manifest
98        let android_permissions: Vec<String> = manifest
99            .permissions
100            .iter()
101            .filter(|(_, entry)| entry.is_enabled())
102            .map(|(name, _)| name.clone())
103            .collect();
104
105        let ctx = TemplateContext {
106            app_display_name: manifest.package.name.clone(),
107            app_name,
108            crate_name: project.crate_name().to_string(),
109            bundle_identifier: manifest.package.bundle_identifier.clone(),
110            author: String::new(),
111            android_backend_path,
112            use_remote_dev_backend: manifest.waterui_path.is_none(),
113            waterui_path: manifest.waterui_path.as_ref().map(PathBuf::from),
114            backend_project_path: Some(backend_relative_path),
115            android_permissions,
116        };
117
118        templates::android::scaffold(&project.backend_path::<Self>(), &ctx)
119            .await
120            .map_err(crate::backend::FailToInitBackend::Io)?;
121
122        Ok(Self {
123            project_path,
124            version: None,
125        })
126    }
127}
128
129fn default_android_project_path() -> PathBuf {
130    PathBuf::from("android")
131}
132
133fn is_default_android_project_path(s: &Path) -> bool {
134    s == Path::new("android")
135}