linux-rust: handle disconnections and fix rename

This commit is contained in:
Kavish Devar
2026-04-20 14:26:27 +05:30
parent a78a8bb6be
commit 4a9a2e7b64
5 changed files with 111 additions and 80 deletions

View File

@@ -28,7 +28,7 @@ pub mod opcodes {
pub const EAR_DETECTION: u8 = 0x06; pub const EAR_DETECTION: u8 = 0x06;
pub const CONVERSATION_AWARENESS: u8 = 0x4B; pub const CONVERSATION_AWARENESS: u8 = 0x4B;
pub const INFORMATION: u8 = 0x1D; pub const INFORMATION: u8 = 0x1D;
pub const RENAME: u8 = 0x1E; pub const RENAME: u8 = 0x1A;
pub const PROXIMITY_KEYS_REQ: u8 = 0x30; pub const PROXIMITY_KEYS_REQ: u8 = 0x30;
pub const PROXIMITY_KEYS_RSP: u8 = 0x31; pub const PROXIMITY_KEYS_RSP: u8 = 0x31;
pub const STEM_PRESS: u8 = 0x19; pub const STEM_PRESS: u8 = 0x19;
@@ -959,9 +959,10 @@ impl AACPManager {
pub async fn send_rename_packet(&self, name: &str) -> Result<()> { pub async fn send_rename_packet(&self, name: &str) -> Result<()> {
let name_bytes = name.as_bytes(); let name_bytes = name.as_bytes();
let size = name_bytes.len(); let size = name_bytes.len();
let mut packet = Vec::with_capacity(5 + size); let mut packet = Vec::with_capacity(6 + size);
packet.push(opcodes::RENAME); packet.push(opcodes::RENAME);
packet.push(0x00); packet.push(0x00);
packet.push(0x01);
packet.push(size as u8); packet.push(size as u8);
packet.push(0x00); packet.push(0x00);
packet.extend_from_slice(name_bytes); packet.extend_from_slice(name_bytes);
@@ -1215,8 +1216,8 @@ async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
manager.receive_packet(data).await; manager.receive_packet(data).await;
} }
Err(e) => { Err(e) => {
error!("Read error: {}", e); debug!("Read error: {}", e);
debug!( info!(
"We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)." "We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)."
); );
let mut state = manager.state.lock().await; let mut state = manager.state.lock().await;

View File

@@ -8,9 +8,9 @@ use ksni::Handle;
use log::{debug, error, info}; use log::{debug, error, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::time::{Duration, sleep}; use tokio::time::{Duration, sleep};
use crate::utils::get_app_settings_path;
pub struct AirPodsDevice { pub struct AirPodsDevice {
pub mac_address: Address, pub mac_address: Address,
@@ -25,7 +25,6 @@ impl AirPodsDevice {
mac_address: Address, mac_address: Address,
tray_handle: Option<Handle<MyTray>>, tray_handle: Option<Handle<MyTray>>,
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>, ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
stem_control: Arc<AtomicBool>,
) -> Self { ) -> Self {
info!("Creating new AirPodsDevice for {}", mac_address); info!("Creating new AirPodsDevice for {}", mac_address);
let mut aacp_manager = AACPManager::new(); let mut aacp_manager = AACPManager::new();
@@ -82,7 +81,17 @@ impl AirPodsDevice {
error!("Failed to request proximity keys: {}", e); error!("Failed to request proximity keys: {}", e);
} }
if stem_control.load(Ordering::Relaxed) { let app_settings_path = get_app_settings_path();
let settings = std::fs::read_to_string(&app_settings_path)
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
let stem_control = settings
.clone()
.and_then(|v| v.get("stem_control").cloned())
.and_then(|s| serde_json::from_value(s).ok())
.unwrap_or(false);
if stem_control {
// Enable stem press detection (double and triple tap) // Enable stem press detection (double and triple tap)
// StemConfig bitmask for the control command: single=0x01, double=0x02, triple=0x04, long=0x08 // StemConfig bitmask for the control command: single=0x01, double=0x02, triple=0x04, long=0x08
// We want double and triple: 0x02 | 0x04 = 0x06 // We want double and triple: 0x02 | 0x04 = 0x06
@@ -222,7 +231,6 @@ impl AirPodsDevice {
let local_mac_events = local_mac.clone(); let local_mac_events = local_mac.clone();
let ui_tx_clone = ui_tx.clone(); let ui_tx_clone = ui_tx.clone();
let command_tx_clone = command_tx.clone(); let command_tx_clone = command_tx.clone();
let stem_control_clone = stem_control.clone();
tokio::spawn(async move { tokio::spawn(async move {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
let event_clone = event.clone(); let event_clone = event.clone();
@@ -348,7 +356,7 @@ impl AirPodsDevice {
"Received Stem Press: {:?} on {:?}", "Received Stem Press: {:?} on {:?}",
press_type, bud_type press_type, bud_type
); );
if stem_control_clone.load(Ordering::Relaxed) { if stem_control {
let controller = mc_clone.lock().await; let controller = mc_clone.lock().await;
match press_type { match press_type {
StemPressType::DoublePress => { StemPressType::DoublePress => {

View File

@@ -19,7 +19,7 @@ use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
use dbus::message::MatchRule; use dbus::message::MatchRule;
use devices::airpods::AirPodsDevice; use devices::airpods::AirPodsDevice;
use ksni::TrayMethods; use ksni::TrayMethods;
use log::{info, warn}; use log::{debug, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::sync::atomic::{AtomicBool}; use std::sync::atomic::{AtomicBool};
@@ -44,12 +44,7 @@ struct Args {
)] )]
le_debug: bool, le_debug: bool,
#[arg(long, short = 'v', help = "Show application version and exit")] #[arg(long, short = 'v', help = "Show application version and exit")]
version: bool, version: bool
#[arg(
long,
help = "Disable stem press track control (use this if your environment already handles AirPods AVRCP commands natively)"
)]
no_stem_control: bool,
} }
fn main() -> iced::Result { fn main() -> iced::Result {
@@ -88,40 +83,28 @@ fn main() -> iced::Result {
Arc::new(RwLock::new(HashMap::new())); Arc::new(RwLock::new(HashMap::new()));
// Load stem_control initial value from settings JSON, then apply CLI override. // Load stem_control initial value from settings JSON, then apply CLI override.
let app_settings_path = get_app_settings_path();
let saved_stem_control = std::fs::read_to_string(&app_settings_path)
.ok()
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
.and_then(|v| v.get("stem_control").and_then(|b| b.as_bool()))
.unwrap_or(true);
// CLI --no-stem-control overrides the saved setting.
let stem_control_initial = if args.no_stem_control { false } else { saved_stem_control };
let stem_control: Arc<AtomicBool> = Arc::new(AtomicBool::new(stem_control_initial));
if args.no_tray { if args.no_tray {
// Run headless without UI // Run headless without UI
info!("Running in headless mode (no GUI)"); info!("Running in headless mode (no GUI)");
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async_main(ui_tx, device_managers, stem_control)).unwrap(); rt.block_on(async_main(ui_tx, device_managers)).unwrap();
Ok(()) Ok(())
} else { } else {
// Run with UI // Run with UI
let device_managers_clone = device_managers.clone(); let device_managers_clone = device_managers.clone();
let stem_control_clone = stem_control.clone();
std::thread::spawn(|| { std::thread::spawn(|| {
let rt = tokio::runtime::Runtime::new().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async_main(ui_tx, device_managers_clone, stem_control_clone)) rt.block_on(async_main(ui_tx, device_managers_clone))
.unwrap(); .unwrap();
}); });
ui::window::start_ui(ui_rx, args.start_minimized, device_managers, stem_control) ui::window::start_ui(ui_rx, args.start_minimized, device_managers)
} }
} }
async fn async_main( async fn async_main(
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>, ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>, device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
stem_control: Arc<AtomicBool>,
) -> bluer::Result<()> { ) -> bluer::Result<()> {
let args = Args::parse(); let args = Args::parse();
@@ -189,7 +172,7 @@ async fn async_main(
.unwrap_or_else(|| "Unknown".to_string()); .unwrap_or_else(|| "Unknown".to_string());
info!("Found connected AirPods: {}, initializing.", name); info!("Found connected AirPods: {}, initializing.", name);
let airpods_device = let airpods_device =
AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone(), stem_control.clone()).await; AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await;
let mut managers = device_managers.write().await; let mut managers = device_managers.write().await;
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
@@ -278,9 +261,6 @@ async fn async_main(
let Some(is_connected) = connected_var.0.as_ref().as_u64() else { let Some(is_connected) = connected_var.0.as_ref().as_u64() else {
return true; return true;
}; };
if is_connected == 0 {
return true;
}
let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000)); let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000));
let Ok(uuids) = proxy.get::<Vec<String>>("org.bluez.Device1", "UUIDs") else { let Ok(uuids) = proxy.get::<Vec<String>>("org.bluez.Device1", "UUIDs") else {
return true; return true;
@@ -293,7 +273,12 @@ async fn async_main(
let Ok(addr) = addr_str.parse::<Address>() else { let Ok(addr) = addr_str.parse::<Address>() else {
return true; return true;
}; };
if is_connected==0 {
if let Err(e) = ui_tx.send(BluetoothUIMessage::DeviceDisconnected(addr_str.clone())) {
warn!("Failed to send DeviceConnected UI message: {:?}", e);
}
return true
}
if managed_devices_mac.contains(&addr_str) { if managed_devices_mac.contains(&addr_str) {
info!("Managed device connected: {}, initializing", addr_str); info!("Managed device connected: {}, initializing", addr_str);
let type_ = devices_list.get(&addr_str).unwrap().type_.clone(); let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
@@ -327,9 +312,8 @@ async fn async_main(
let handle_clone = tray_handle.clone(); let handle_clone = tray_handle.clone();
let ui_tx_clone = ui_tx.clone(); let ui_tx_clone = ui_tx.clone();
let device_managers = device_managers.clone(); let device_managers = device_managers.clone();
let stem_control_arc = stem_control.clone();
tokio::spawn(async move { tokio::spawn(async move {
let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone.clone(), stem_control_arc.clone()).await; let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone.clone()).await;
let mut managers = device_managers.write().await; let mut managers = device_managers.write().await;
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone()); let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());

View File

@@ -10,7 +10,7 @@ use crate::ui::airpods::airpods_view;
use crate::ui::messages::BluetoothUIMessage; use crate::ui::messages::BluetoothUIMessage;
use crate::ui::nothing::nothing_view; use crate::ui::nothing::nothing_view;
use crate::utils::{MyTheme, get_app_settings_path, get_devices_path}; use crate::utils::{MyTheme, get_app_settings_path, get_devices_path};
use bluer::{Address, Session}; use bluer::{Address};
use iced::border::Radius; use iced::border::Radius;
use iced::overlay::menu; use iced::overlay::menu;
use iced::widget::button::Style; use iced::widget::button::Style;
@@ -19,7 +19,7 @@ use iced::widget::{
Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text, Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text,
text_input, toggler text_input, toggler
}; };
use iced::{Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, daemon, window, Settings}; use iced::{Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, daemon, window, Settings, Program};
use log::{debug, error}; use log::{debug, error};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -31,7 +31,7 @@ pub fn start_ui(
ui_rx: UnboundedReceiver<BluetoothUIMessage>, ui_rx: UnboundedReceiver<BluetoothUIMessage>,
start_minimized: bool, start_minimized: bool,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>, device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
stem_control: Arc<AtomicBool>, // stem_control: Arc<AtomicBool>,
) -> iced::Result { ) -> iced::Result {
let ui_rx = Arc::new(Mutex::new(ui_rx)); let ui_rx = Arc::new(Mutex::new(ui_rx));
@@ -42,7 +42,7 @@ pub fn start_ui(
Arc::clone(&ui_rx), Arc::clone(&ui_rx),
start_minimized, start_minimized,
Arc::clone(&device_managers), Arc::clone(&device_managers),
Arc::clone(&stem_control), // Arc::clone(&stem_control),
) )
}, },
App::update, App::update,
@@ -50,6 +50,7 @@ pub fn start_ui(
) )
.subscription(App::subscription) .subscription(App::subscription)
.theme(App::theme) .theme(App::theme)
.title(App::title)
.settings(Settings { .settings(Settings {
id: Some("librepods".to_string()), id: Some("librepods".to_string()),
fonts: vec![include_bytes!("../../assets/font/sf_pro.otf").as_slice().into()], fonts: vec![include_bytes!("../../assets/font/sf_pro.otf").as_slice().into()],
@@ -74,7 +75,7 @@ pub struct App {
device_type_state: combo_box::State<DeviceType>, device_type_state: combo_box::State<DeviceType>,
selected_device_type: Option<DeviceType>, selected_device_type: Option<DeviceType>,
tray_text_mode: bool, tray_text_mode: bool,
stem_control: Arc<AtomicBool>, stem_control: bool,
} }
pub struct BluetoothState { pub struct BluetoothState {
@@ -98,7 +99,7 @@ pub enum Message {
ThemeSelected(MyTheme), ThemeSelected(MyTheme),
CopyToClipboard(String), CopyToClipboard(String),
BluetoothMessage(BluetoothUIMessage), BluetoothMessage(BluetoothUIMessage),
ShowNewDialogTab, // ShowNewDialogTab,
GotPairedDevices(HashMap<String, Address>), GotPairedDevices(HashMap<String, Address>),
StartAddDevice(String, Address), StartAddDevice(String, Address),
SelectDeviceType(DeviceType), SelectDeviceType(DeviceType),
@@ -127,7 +128,7 @@ impl App {
ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>, ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
start_minimized: bool, start_minimized: bool,
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>, device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
stem_control: Arc<AtomicBool>, // stem_control: Arc<AtomicBool>,
) -> (Self, Task<Message>) { ) -> (Self, Task<Message>) {
let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar); let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar);
let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content); let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content);
@@ -160,6 +161,11 @@ impl App {
.and_then(|v| v.get("tray_text_mode").cloned()) .and_then(|v| v.get("tray_text_mode").cloned())
.and_then(|ttm| serde_json::from_value(ttm).ok()) .and_then(|ttm| serde_json::from_value(ttm).ok())
.unwrap_or(false); .unwrap_or(false);
let stem_control = settings
.clone()
.and_then(|v| v.get("stem_control").cloned())
.and_then(|s| serde_json::from_value(s).ok())
.unwrap_or(false);
let bluetooth_state = BluetoothState::new(); let bluetooth_state = BluetoothState::new();
@@ -246,7 +252,7 @@ impl App {
let settings = serde_json::json!({ let settings = serde_json::json!({
"theme": self.selected_theme, "theme": self.selected_theme,
"tray_text_mode": self.tray_text_mode, "tray_text_mode": self.tray_text_mode,
"stem_control": self.stem_control.load(Ordering::Relaxed), "stem_control": self.stem_control,
}); });
debug!( debug!(
"Writing settings to {}: {}", "Writing settings to {}: {}",
@@ -404,7 +410,16 @@ impl App {
let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
debug!("Device disconnected: {}", mac); debug!("Device disconnected: {}", mac);
self.bluetooth_state
.connected_devices
.retain(|device| device != &mac);
self.device_states.remove(&mac); self.device_states.remove(&mac);
if matches!(&self.selected_tab, Tab::Device(selected_mac) if selected_mac == &mac) {
self.selected_tab = Tab::Device("none".to_string());
}
Task::batch(vec![wait_task]) Task::batch(vec![wait_task])
} }
BluetoothUIMessage::AACPUIEvent(mac, event) => { BluetoothUIMessage::AACPUIEvent(mac, event) => {
@@ -520,11 +535,11 @@ impl App {
} }
} }
} }
Message::ShowNewDialogTab => { // Message::ShowNewDialogTab => {
debug!("switching to Add Device tab"); // debug!("switching to Add Device tab");
self.selected_tab = Tab::AddDevice; // self.selected_tab = Tab::AddDevice;
Task::perform(load_paired_devices(), Message::GotPairedDevices) // Task::perform(load_paired_devices(), Message::GotPairedDevices)
} // }
Message::GotPairedDevices(map) => { Message::GotPairedDevices(map) => {
self.paired_devices = map; self.paired_devices = map;
Task::none() Task::none()
@@ -615,7 +630,7 @@ impl App {
let settings = serde_json::json!({ let settings = serde_json::json!({
"theme": self.selected_theme, "theme": self.selected_theme,
"tray_text_mode": self.tray_text_mode, "tray_text_mode": self.tray_text_mode,
"stem_control": self.stem_control.load(Ordering::Relaxed), "stem_control": self.stem_control,
}); });
debug!( debug!(
"Writing settings to {}: {}", "Writing settings to {}: {}",
@@ -626,12 +641,12 @@ impl App {
Task::none() Task::none()
} }
Message::StemControlChanged(is_enabled) => { Message::StemControlChanged(is_enabled) => {
self.stem_control.store(is_enabled, Ordering::Relaxed); self.stem_control = is_enabled;
let app_settings_path = get_app_settings_path(); let app_settings_path = get_app_settings_path();
let settings = serde_json::json!({ let settings = serde_json::json!({
"theme": self.selected_theme, "theme": self.selected_theme,
"tray_text_mode": self.tray_text_mode, "tray_text_mode": self.tray_text_mode,
"stem_control": self.stem_control.load(Ordering::Relaxed), "stem_control": self.stem_control,
}); });
debug!( debug!(
"Writing settings to {}: {}", "Writing settings to {}: {}",
@@ -1040,7 +1055,7 @@ impl App {
] ]
.spacing(12); .spacing(12);
let stem_control_value = self.stem_control.load(Ordering::Relaxed); let stem_control_value = self.stem_control;
let stem_control_toggle = container( let stem_control_toggle = container(
row![ row![
column![ column![
@@ -1300,25 +1315,26 @@ async fn wait_for_message(ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>
} }
} }
} }
async fn load_paired_devices() -> HashMap<String, Address> {
let mut devices = HashMap::new();
let session = Session::new().await.ok().unwrap(); // async fn load_paired_devices() -> HashMap<String, Address> {
let adapter = session.default_adapter().await.ok().unwrap(); // let mut devices = HashMap::new();
let addresses = adapter.device_addresses().await.ok().unwrap(); //
for addr in addresses { // let session = Session::new().await.ok().unwrap();
let device = adapter.device(addr).ok().unwrap(); // let adapter = session.default_adapter().await.ok().unwrap();
let paired = device.is_paired().await.ok().unwrap(); // let addresses = adapter.device_addresses().await.ok().unwrap();
if paired { // for addr in addresses {
let name = device // let device = adapter.device(addr).ok().unwrap();
.name() // let paired = device.is_paired().await.ok().unwrap();
.await // if paired {
.ok() // let name = device
.flatten() // .name()
.unwrap_or_else(|| "Unknown".to_string()); // .await
devices.insert(name, addr); // .ok()
} // .flatten()
} // .unwrap_or_else(|| "Unknown".to_string());
// devices.insert(name, addr);
devices // }
} // }
//
// devices
// }

View File

@@ -15,18 +15,40 @@ pub fn get_devices_path() -> PathBuf {
pub fn get_preferences_path() -> PathBuf { pub fn get_preferences_path() -> PathBuf {
let config_dir = std::env::var("XDG_CONFIG_HOME") let config_dir = std::env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.config", std::env::var("HOME").unwrap_or_default()));
PathBuf::from(config_dir) PathBuf::from(config_dir)
.join("librepods") .join("librepods")
.join("preferences.json") .join("preferences.json")
} }
pub fn get_app_settings_path() -> PathBuf { pub fn get_app_settings_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_default();
let config_dir = std::env::var("XDG_CONFIG_HOME") let config_dir = std::env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); .unwrap_or_else(|_| format!("{}/.config", home));
PathBuf::from(config_dir)
let data_dir = std::env::var("XDG_DATA_HOME")
.unwrap_or_else(|_| format!("{}/.local/share", home));
let new_path = PathBuf::from(&config_dir)
.join("librepods") .join("librepods")
.join("app_settings.json") .join("app_settings.json");
let old_path = PathBuf::from(&data_dir)
.join("app_settings.json");
// migrate if needed
if old_path.exists() && !new_path.exists() {
if let Some(parent) = new_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if std::fs::copy(&old_path, &new_path).is_ok() {
let _ = std::fs::remove_file(&old_path);
}
}
new_path
} }
fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {