Add Android device deployment flow (#15146)

* Add Android device deployment flow

  Notify the Android Flutter UI when the server requires deployment, add a deploy dialog with API token/custom ID inputs, and reuse shared deploy logic
  for CLI and FFI

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Hide Android deploy API token input

Signed-off-by: 21pages <sunboeasy@gmail.com>

* add more translations

Signed-off-by: 21pages <sunboeasy@gmail.com>

* optimize transations

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Hide deploy action for outgoing-only clients

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Fix deployment register throttle state reset

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Move Android deploy dialog out of settings page

Signed-off-by: 21pages <sunboeasy@gmail.com>

* Use async mutex for deploy register throttle

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2026-06-02 14:28:30 +08:00
committed by GitHub
parent 32c6e32e04
commit d99ddf6816
59 changed files with 604 additions and 66 deletions

View File

@@ -1020,6 +1020,102 @@ pub fn get_api_server() -> String {
)
}
pub enum DeployResult {
Ok,
NotEnabled,
InvalidInput,
IdTaken(String),
Error(String),
}
impl DeployResult {
pub fn message(&self) -> String {
match self {
Self::Ok => "".to_owned(),
Self::NotEnabled => "The server does not require explicit deployment.".to_owned(),
Self::InvalidInput => "Invalid input.".to_owned(),
Self::IdTaken(id) => {
format!(
"Id `{}` is already used by another machine on the server.",
id
)
}
Self::Error(err) => err.clone(),
}
}
}
pub fn deploy_device(token: String, new_id: Option<String>) -> DeployResult {
if Config::no_register_device() {
return DeployResult::Error("Cannot deploy an unregistrable device!".to_owned());
}
let token = token.trim();
if token.is_empty() {
return DeployResult::Error("token is required!".to_owned());
}
#[cfg(any(target_os = "android", target_os = "ios"))]
let local_id = Config::get_id();
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let local_id = ipc::get_id();
let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone());
let uuid = crate::encode64(hbb_common::get_uuid());
let pk = crate::encode64(Config::get_key_pair().1);
let body = serde_json::json!({
"id": id_to_deploy,
"uuid": uuid,
"pk": pk,
});
let header = "Authorization: Bearer ".to_owned() + token;
let url = get_api_server() + "/api/devices/deploy";
let text = match crate::post_request_sync(url, body.to_string(), &header) {
Ok(text) => text,
Err(err) => return DeployResult::Error(format!("Request failed: {}", err)),
};
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
match parsed["result"].as_str().unwrap_or("") {
"OK" => {
if let Some(new_id) = new_id {
if new_id != local_id {
#[cfg(any(target_os = "android", target_os = "ios"))]
{
Config::set_key_confirmed(false);
Config::set_id(&new_id);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Err(err) = ipc::set_config("id", new_id) {
return DeployResult::Error(format!(
"Failed to persist deployed id locally: {}",
err
));
}
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
if let Err(err) = ipc::notify_deployed() {
log::warn!("Failed to notify deployed state: {}", err);
}
#[cfg(target_os = "android")]
{
crate::rendezvous_mediator::NEEDS_DEPLOY
.store(false, std::sync::atomic::Ordering::SeqCst);
crate::rendezvous_mediator::reset_needs_deploy_notification();
crate::rendezvous_mediator::RendezvousMediator::restart();
}
DeployResult::Ok
}
"NOT_ENABLED" => DeployResult::NotEnabled,
"INVALID_INPUT" => DeployResult::InvalidInput,
"ID_TAKEN" => DeployResult::IdTaken(id_to_deploy),
_ => {
if text.is_empty() {
DeployResult::Error("Unknown response.".to_owned())
} else {
DeployResult::Error(text)
}
}
}
}
#[inline]
pub fn has_hwcodec() -> bool {
// Has real hardware codec using gpu