Skip to content

Commit e53f608

Browse files
committed
Phase 6A/6B: android_api, notification, url_replace, auto_template, cloud_config_getter modules; new RPC handlers for Android API registration, ExtraApp CRUD, cloud config manager; get_app_status calls android_api for local version; renew_all fires RenewProgress notifications
1 parent 387c3c6 commit e53f608

12 files changed

Lines changed: 1264 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ futures = "0.3"
3737
tokio-util = { version = "0.7", features = ["io"] }
3838
jsonl = "4.0"
3939
file-locker = "1.1"
40+
url = "2"
4041

4142
[dev-dependencies]
4243
mockito = "1.7.1"

src/database/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ impl Database {
106106
self.extra_apps.delete::<ExtraAppRecord>(id)
107107
}
108108

109+
/// Find an ExtraApp record by matching its `app_id` map.
110+
pub fn get_extra_app_by_app_id(
111+
&self,
112+
app_id: &std::collections::HashMap<String, Option<String>>,
113+
) -> Result<Option<ExtraAppRecord>> {
114+
let all = self.extra_apps.load_all::<ExtraAppRecord>()?;
115+
Ok(all.into_iter().find(|r| &r.app_id == app_id))
116+
}
117+
109118
// --- ExtraHub CRUD ---
110119

111120
pub fn load_extra_hubs(&self) -> Result<Vec<ExtraHubRecord>> {

src/manager/android_api.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use std::collections::HashMap;
2+
3+
use jsonrpsee::core::client::ClientT;
4+
use jsonrpsee::http_client::HttpClientBuilder;
5+
use jsonrpsee::rpc_params;
6+
use once_cell::sync::OnceCell;
7+
use serde::{Deserialize, Serialize};
8+
9+
use crate::database::models::app::AppRecord;
10+
11+
/// Global Android API client, registered via `register_android_api` RPC.
12+
static ANDROID_API: OnceCell<AndroidApi> = OnceCell::new();
13+
14+
pub fn set_android_api(url: String) {
15+
let _ = ANDROID_API.set(AndroidApi { url });
16+
}
17+
18+
pub fn get_android_api() -> Option<&'static AndroidApi> {
19+
ANDROID_API.get()
20+
}
21+
22+
/// Rust-side HTTP JSON-RPC client that calls back into the Kotlin
23+
/// KotlinHubRpcServer for Android-specific functionality.
24+
///
25+
/// Kotlin exposes `get_local_version` and `get_installed_apps` methods
26+
/// on the same Ktor HTTP server that handles hub provider calls.
27+
pub struct AndroidApi {
28+
url: String,
29+
}
30+
31+
#[derive(Serialize, Deserialize, Debug)]
32+
struct GetLocalVersionParams {
33+
app_id: HashMap<String, Option<String>>,
34+
}
35+
36+
#[derive(Serialize, Deserialize, Debug)]
37+
struct GetInstalledAppsParams {
38+
ignore_system: bool,
39+
}
40+
41+
impl AndroidApi {
42+
/// Query Kotlin for the locally-installed version of an app.
43+
///
44+
/// Calls `get_local_version({app_id})` on the Kotlin Ktor server.
45+
/// Returns `None` if the app is not installed or the call fails.
46+
pub async fn get_local_version(
47+
&self,
48+
app_id: &HashMap<String, Option<String>>,
49+
) -> Option<String> {
50+
let client = HttpClientBuilder::default().build(&self.url).ok()?;
51+
let params = rpc_params!(GetLocalVersionParams {
52+
app_id: app_id.clone(),
53+
});
54+
client
55+
.request::<Option<String>, _>("get_local_version", params)
56+
.await
57+
.unwrap_or(None)
58+
}
59+
60+
/// Query Kotlin for all installed Android apps and Magisk modules.
61+
///
62+
/// Calls `get_installed_apps({ignore_system})` on the Kotlin Ktor server.
63+
/// Returns an empty list if the call fails.
64+
pub async fn get_installed_apps(&self, ignore_system: bool) -> Vec<AppRecord> {
65+
let client = match HttpClientBuilder::default().build(&self.url) {
66+
Ok(c) => c,
67+
Err(_) => return vec![],
68+
};
69+
let params = rpc_params!(GetInstalledAppsParams { ignore_system });
70+
client
71+
.request::<Vec<AppRecord>, _>("get_installed_apps", params)
72+
.await
73+
.unwrap_or_default()
74+
}
75+
}
76+
77+
#[cfg(test)]
78+
mod tests {
79+
use super::*;
80+
81+
#[test]
82+
fn test_get_local_version_params_serialization() {
83+
let app_id: HashMap<String, Option<String>> = HashMap::from([(
84+
"android_app_package".to_string(),
85+
Some("com.example.app".to_string()),
86+
)]);
87+
let params = GetLocalVersionParams {
88+
app_id: app_id.clone(),
89+
};
90+
let json = serde_json::to_string(&params).unwrap();
91+
assert!(json.contains("android_app_package"));
92+
assert!(json.contains("com.example.app"));
93+
}
94+
95+
#[test]
96+
fn test_get_installed_apps_params_serialization() {
97+
let params = GetInstalledAppsParams {
98+
ignore_system: true,
99+
};
100+
let json = serde_json::to_string(&params).unwrap();
101+
assert!(json.contains("ignore_system"));
102+
assert!(json.contains("true"));
103+
}
104+
}

src/manager/app_manager.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use std::collections::HashMap;
22
use std::sync::Arc;
33
use tokio::sync::{RwLock, Semaphore};
44

5+
use super::android_api;
56
use super::app_status::AppStatus;
67
use super::data_getter::DataGetter;
8+
use super::notification::{notify_if_registered, ManagerEvent};
79
use super::updater::get_release_status;
810
use super::version_map::VersionMap;
911
use crate::database::get_db;
@@ -90,24 +92,40 @@ impl AppManager {
9092
}
9193

9294
/// Return the current AppStatus for an app.
95+
///
96+
/// Queries Kotlin for the locally-installed version via `AndroidApi.get_local_version()`
97+
/// if a callback URL has been registered; otherwise local_version is `None`.
9398
pub async fn get_app_status(&self, record_id: &str) -> AppStatus {
9499
let app = match self.apps.read().await.get(record_id).cloned() {
95100
Some(a) => a,
96101
None => return AppStatus::AppInactive,
97102
};
103+
104+
// Query local version from Android via registered callback
105+
let local_version: Option<String> = match android_api::get_android_api() {
106+
Some(api) => api.get_local_version(&app.app_id).await,
107+
None => None,
108+
};
109+
98110
let mut maps = self.version_maps.write().await;
99111
let vm = maps.entry(record_id.to_string()).or_insert_with(|| {
100112
VersionMap::new(
101113
app.invalid_version_number_field_regex.clone(),
102114
app.include_version_number_field_regex.clone(),
103115
)
104116
});
105-
get_release_status(vm, None, app.ignore_version_number.as_deref(), true)
117+
get_release_status(
118+
vm,
119+
local_version.as_deref(),
120+
app.ignore_version_number.as_deref(),
121+
true,
122+
)
106123
}
107124

108125
/// Refresh version data for all saved apps.
109126
///
110127
/// Uses a semaphore (max 10 concurrent hub requests) matching Kotlin's logic.
128+
/// Fires `RenewProgress` notifications to Kotlin UI as each app completes.
111129
pub async fn renew_all(
112130
&self,
113131
hubs: &[crate::database::models::hub::HubRecord],
@@ -169,6 +187,12 @@ impl AppManager {
169187
)
170188
});
171189
vm.add_single_release(&hub.uuid, release.clone());
190+
// Notify progress after batch result
191+
let done = completed.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
192+
if let Some(ref f) = cb {
193+
f(done, total);
194+
}
195+
notify_if_registered(ManagerEvent::RenewProgress { done, total }).await;
172196
} else {
173197
need_full.push(app.clone());
174198
}
@@ -199,6 +223,7 @@ impl AppManager {
199223
if let Some(ref f) = cb {
200224
f(done, total);
201225
}
226+
notify_if_registered(ManagerEvent::RenewProgress { done, total }).await;
202227
}
203228
});
204229
handles.push(handle);

0 commit comments

Comments
 (0)