// Copyright (C) 2025 Huawei Device Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Cache download functionality for animation resources.
//!
//! This module provides functions for downloading, canceling, and configuring
//! cache settings for animation resources. It serves as a bridge between the
//! ETS interface and the native cache download service.
use std::sync::OnceLock;
use ani_rs::business_error::BusinessError;
use ani_rs::objects::AniFnObject;
use ani_rs::AniEnv;
use preload_native_rlib::{CacheDownloadService, DownloadRequest, Downloader, PreloadCallback};
use preload_permission_verify::permission_check;
use crate::bridge::{CacheDownloadOptions, CacheStrategy, DownloadInfo, SslType};
use crate::callback::{self, CallbackManager, CallbackWrapper};
const MAX_FILE_SIZE: i64 = 4294967296;
const MAX_MEM_SIZE: i64 = 1073741824;
const MAX_URL_LENGTH: usize = 8192;
const MAX_INFO_LIST_SIZE: u16 = 8192;
// Retry constants
const DEFAULT_MAX_RETRY_COUNT: i32 = 1;
const MIN_RETRY_COUNT: i32 = 0;
const MAX_RETRY_COUNT: i32 = 10;
// Timeout constants
const DEFAULT_NETWORK_CHECK_TIMEOUT: i32 = 20;
const MIN_NETWORK_CHECK_TIMEOUT: i32 = 0;
const MAX_NETWORK_CHECK_TIMEOUT: i32 = 20;
const DEFAULT_HTTP_TOTAL_TIMEOUT: i32 = 60;
const MIN_HTTP_TOTAL_TIMEOUT: i32 = 1;
const MAX_HTTP_TOTAL_TIMEOUT: i32 = (u32::MAX / 1000) as i32;
static HAS_INTERNET_PERM: OnceLock<bool> = OnceLock::new();
static HAS_GET_NETWORK_INFO_PERM: OnceLock<bool> = OnceLock::new();
fn has_internet_permission() -> bool {
*HAS_INTERNET_PERM.get_or_init(|| permission_check::CheckInternetPermission())
}
fn has_get_network_info_permission() -> bool {
*HAS_GET_NETWORK_INFO_PERM.get_or_init(|| permission_check::CheckGetNetworkInfoPermission())
}
/// Initiates a download of a resource with the specified URL and options.
///
/// Creates a new download request, configures it with any provided headers, and
/// submits it to the cache download service for preloading.
///
/// # Arguments
///
/// * `url` - The URL of the resource to download
/// * `options` - Configuration options for the download, including optional
/// headers
///
/// # Returns
///
/// * `Ok(())` if the download was successfully initiated
/// * `Err(BusinessError)` if there was an error initiating the download
///
/// # Examples
///
/// ```rust
/// use ani_cache_download::cache_download::{download, CacheDownloadOptions};
/// use ani_rs::business_error::BusinessError;
///
/// // Basic download
/// let result: Result<(), BusinessError> = download(
/// "https://example.com/resource.mp4".to_string(),
/// CacheDownloadOptions { header: None },
/// );
///
/// // Download with headers
/// let mut headers = std::collections::HashMap::new();
/// headers.insert("Authorization".to_string(), "Bearer token123".to_string());
/// let result: Result<(), BusinessError> = download(
/// "https://example.com/resource.mp4".to_string(),
/// CacheDownloadOptions {
/// header: Some(headers),
/// },
/// );
/// ```
#[ani_rs::native]
pub fn download(url: String, options: CacheDownloadOptions) -> Result<(), BusinessError> {
if !has_internet_permission() {
return Err(BusinessError::new(
201,
"internet permission denied".to_string(),
));
}
if url.len() > MAX_URL_LENGTH {
return Err(BusinessError::new(
401,
"url exceeds the maximum length".to_string(),
));
}
let mut request = DownloadRequest::new(&url);
let headers = options.headers.unwrap_or_default();
let headers_vec: Vec<(String, String)> = headers.into_iter().collect();
let borrowed: Vec<(&str, &str)> = headers_vec
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
if !borrowed.is_empty() {
request.headers(borrowed);
}
if let Some(ssl_type) = options.ssl_type {
match ssl_type {
SslType::TLS => request.ssl_type("TLS"),
SslType::TLCP => request.ssl_type("TLCP"),
};
}
if let Some(ref ca_path) = options.ca_path {
if !ca_path.is_empty() {
request.ca_path(ca_path.as_str());
}
}
let is_update = options.cache_strategy != Some(CacheStrategy::LAZY);
if let Some(ref retry) = options.retry {
if let Some(max_retry_count) = retry.max_retry_count {
if max_retry_count < MIN_RETRY_COUNT || max_retry_count > MAX_RETRY_COUNT {
return Err(BusinessError::new(
401,
"maxRetryCount out of range".to_string(),
));
}
request.max_retry(max_retry_count as usize);
}
}
if let Some(ref timeout) = options.timeout {
if let Some(network_check_timeout) = timeout.network_check_timeout {
if network_check_timeout < MIN_NETWORK_CHECK_TIMEOUT
|| network_check_timeout > MAX_NETWORK_CHECK_TIMEOUT
{
return Err(BusinessError::new(
401,
"networkCheckTimeout out of range".to_string(),
));
}
request.network_check_timeout(network_check_timeout as u32);
}
if let Some(http_total_timeout) = timeout.http_total_timeout {
if http_total_timeout < MIN_HTTP_TOTAL_TIMEOUT
|| http_total_timeout > MAX_HTTP_TOTAL_TIMEOUT
{
return Err(BusinessError::new(
401,
"httpTotalTimeout out of range".to_string(),
));
}
request.http_total_timeout(http_total_timeout as u32);
}
}
CacheDownloadService::get_instance().preload(
request,
Box::new(CallbackWrapper::new(url.clone())),
is_update,
Downloader::Netstack,
);
Ok(())
}
/// Cancels a previously initiated download by URL.
///
/// Sends a cancel request to the cache download service for the specified URL.
///
/// # Arguments
///
/// * `url` - The URL of the resource download to cancel
///
/// # Returns
///
/// * `Ok(())` if the cancel request was successfully submitted
/// * `Err(BusinessError)` if there was an error submitting the cancel request
///
/// # Examples
///
/// ```rust
/// use ani_cache_download::cache_download::cancel;
/// use ani_rs::business_error::BusinessError;
///
/// // Cancel a download
/// let result: Result<(), BusinessError> = cancel("https://example.com/resource.mp4".to_string());
/// ```
#[ani_rs::native]
pub fn cancel(url: String) -> Result<(), BusinessError> {
if (url.len() > MAX_URL_LENGTH as usize) {
return Err(BusinessError::new(
401,
"url exceeds the maximum length".to_string(),
));
}
CacheDownloadService::get_instance().cancel(&url);
Ok(())
}
/// Sets the maximum memory (RAM) cache size in bytes.
///
/// Configures the RAM cache size for the cache download service.
///
/// # Arguments
///
/// * `size` - The maximum size of the memory cache in bytes
///
/// # Returns
///
/// * `Ok(())` if the cache size was successfully updated
/// * `Err(BusinessError)` if there was an error updating the cache size
///
/// # Examples
///
/// ```rust
/// use ani_cache_download::cache_download::set_memory_cache_size;
/// use ani_rs::business_error::BusinessError;
///
/// // Set memory cache size to 50MB
/// let result: Result<(), BusinessError> = set_memory_cache_size(50 * 1024 * 1024);
/// ```
#[ani_rs::native]
pub fn set_memory_cache_size(size: i64) -> Result<(), BusinessError> {
if (size > MAX_MEM_SIZE) {
return Err(BusinessError::new(
401,
"memory cache size exceeds the maximum value".to_string(),
));
}
// Convert signed i64 to unsigned u64 for cache size
CacheDownloadService::get_instance().set_ram_cache_size(size as u64);
Ok(())
}
/// Sets the maximum file cache size in bytes.
///
/// Configures the file system cache size for the cache download service.
///
/// # Arguments
///
/// * `size` - The maximum size of the file cache in bytes
///
/// # Returns
///
/// * `Ok(())` if the cache size was successfully updated
/// * `Err(BusinessError)` if there was an error updating the cache size
///
/// # Examples
///
/// ```rust
/// use ani_cache_download::cache_download::set_file_cache_size;
/// use ani_rs::business_error::BusinessError;
///
/// // Set file cache size to 500MB
/// let result: Result<(), BusinessError> = set_file_cache_size(500 * 1024 * 1024);
/// ```
#[ani_rs::native]
pub fn set_file_cache_size(size: i64) -> Result<(), BusinessError> {
if (size > MAX_FILE_SIZE) {
return Err(BusinessError::new(
401,
"file cache size exceeds the maximum value".to_string(),
));
}
// Convert signed i64 to unsigned u64 for cache size
CacheDownloadService::get_instance().set_file_cache_size(size as u64);
Ok(())
}
/// Retrieves download information for the resource at the given URL.
///
/// Returns `None` when no cached entry exists for the URL. Requires the
/// `GET_NETWORK_INFO` permission.
///
/// # Returns
///
/// * `Ok(Some(DownloadInfo))` with the collected download information
/// * `Ok(None)` if no information is available for the URL
/// * `Err(BusinessError)` on permission denial or an invalid URL
#[ani_rs::native]
pub fn get_download_info(url: String) -> Result<Option<DownloadInfo>, BusinessError> {
if !has_get_network_info_permission() {
return Err(BusinessError::new(
201,
"GET_NETWORK_INFO permission denied".to_string(),
));
}
if (url.len() > MAX_URL_LENGTH as usize) {
return Err(BusinessError::new(
401,
"url exceeds the maximum length".to_string(),
));
}
let info = CacheDownloadService::get_instance()
.get_download_info(&url)
.map(|info| {
DownloadInfo::from_native(
preload_native_rlib::info::RustDownloadInfo::from_download_info(info),
)
});
Ok(info)
}
/// Sets the maximum number of download info entries retained.
///
/// # Returns
///
/// * `Ok(())` if the list size was successfully updated
/// * `Err(BusinessError)` if the size is negative or exceeds the maximum
#[ani_rs::native]
pub fn set_download_info_list_size(size: i64) -> Result<(), BusinessError> {
if (size > MAX_INFO_LIST_SIZE as i64) {
return Err(BusinessError::new(
401,
"info list size exceeds the maximum value".to_string(),
));
}
if (size < 0) {
return Err(BusinessError::new(
401,
"info list size is negative".to_string(),
));
}
CacheDownloadService::get_instance().set_info_list_size(size as u16);
Ok(())
}
/// Clears all entries from the memory (RAM) cache.
///
/// Removes all cached resources from the in-memory cache.
///
/// # Examples
///
/// ```rust
/// use ani_cache_download::cache_download::clear_memory_cache;
/// use ani_rs::business_error::BusinessError;
///
/// // Clear memory cache
/// let result: Result<(), BusinessError> = clear_memory_cache();
/// ```
#[ani_rs::native]
pub fn clear_memory_cache() -> Result<(), BusinessError> {
CacheDownloadService::get_instance().clear_memory_cache();
Ok(())
}
/// Clears all entries from the file system cache.
///
/// Removes all cached resources from the file system cache.
///
/// # Examples
///
/// ```rust
/// use ani_cache_download::cache_download::clear_file_cache;
/// use ani_rs::business_error::BusinessError;
///
/// // Clear file cache
/// let result: Result<(), BusinessError> = clear_file_cache();
/// ```
#[ani_rs::native]
pub fn clear_file_cache() -> Result<(), BusinessError> {
CacheDownloadService::get_instance().clear_file_cache();
Ok(())
}
/// Registers a callback invoked when the download for `url` succeeds.
#[ani_rs::native]
pub fn on_download_success(
env: &AniEnv,
url: String,
callback: AniFnObject,
) -> Result<(), BusinessError> {
check_url_length(url.as_str())?;
let callback = callback.into_global_callback(env)?;
CallbackManager::get_instance().register_success_callback(&url.as_str(), callback);
Ok(())
}
/// Registers a callback invoked when the download for `url` fails.
#[ani_rs::native]
pub fn on_download_error(
env: &AniEnv,
url: String,
callback: AniFnObject,
) -> Result<(), BusinessError> {
check_url_length(url.as_str())?;
let callback = callback.into_global_callback(env)?;
CallbackManager::get_instance().register_error_callback(&url.as_str(), callback);
Ok(())
}
/// Unregisters a success callback for the given URL.
///
/// When `success_callback` is `None`, all success callbacks for the URL are
/// removed.
#[ani_rs::native]
pub fn off_download_success(
env: &AniEnv,
url: String,
success_callback: Option<AniFnObject>,
) -> Result<(), BusinessError> {
check_url_length(url.as_str())?;
let callback = success_callback
.map(|cb| cb.into_global_callback(env))
.transpose()?;
CallbackManager::get_instance().unregister_success_callback(&url.as_str(), callback);
Ok(())
}
/// Unregisters an error callback for the given URL.
///
/// When `err_callback` is `None`, all error callbacks for the URL are removed.
#[ani_rs::native]
pub fn off_download_error(
env: &AniEnv,
url: String,
err_callback: Option<AniFnObject>,
) -> Result<(), BusinessError> {
check_url_length(url.as_str())?;
let callback = err_callback
.map(|cb| cb.into_global_callback(env))
.transpose()?;
CallbackManager::get_instance().unregister_error_callback(&url.as_str(), callback);
Ok(())
}
fn check_url_length(url: &str) -> Result<(), BusinessError> {
let url_len = url.len();
if url_len == 0 {
return Err(BusinessError::new(401, "url is empty".to_string()));
}
if url_len > MAX_URL_LENGTH {
return Err(BusinessError::new(
401,
"url exceeds the maximum length".to_string(),
));
}
Ok(())
}
/// Sets global retry options for all subsequent cache downloads.
///
/// Falls back to a default retry count when `options` is `None`.
///
/// # Returns
///
/// * `Ok(())` if the options were applied
/// * `Err(BusinessError)` if `maxRetryCount` is out of the allowed range
#[ani_rs::native]
pub fn set_global_retry_options(
options: Option<crate::bridge::RetryOptions>,
) -> Result<(), BusinessError> {
// Use default value when the parameter is missing
let max_retry_count = match options {
None => DEFAULT_MAX_RETRY_COUNT,
Some(o) => match o.max_retry_count {
None => DEFAULT_MAX_RETRY_COUNT,
Some(v) => {
// Validate parameter range
if v < MIN_RETRY_COUNT || v > MAX_RETRY_COUNT {
return Err(BusinessError::new(
401,
"maxRetryCount out of range".to_string(),
));
}
v
}
},
};
CacheDownloadService::get_instance().set_global_retry_options(max_retry_count as usize);
Ok(())
}
/// Sets global timeout options for all subsequent cache downloads.
///
/// Falls back to default timeout values when `options` or its fields are
/// `None`.
///
/// # Returns
///
/// * `Ok(())` if the options were applied
/// * `Err(BusinessError)` if a timeout value is out of the allowed range
#[ani_rs::native]
pub fn set_global_timeout_options(
options: Option<crate::bridge::TimeoutOptions>,
) -> Result<(), BusinessError> {
// Use default value when the parameter is missing
let network_check_timeout = match options {
None => DEFAULT_NETWORK_CHECK_TIMEOUT,
Some(ref o) => match o.network_check_timeout {
None => DEFAULT_NETWORK_CHECK_TIMEOUT,
Some(v) => {
// Validate parameter range
if v < MIN_NETWORK_CHECK_TIMEOUT || v > MAX_NETWORK_CHECK_TIMEOUT {
return Err(BusinessError::new(
401,
"networkCheckTimeout out of range".to_string(),
));
}
v
}
},
};
let http_total_timeout = match options {
None => DEFAULT_HTTP_TOTAL_TIMEOUT,
Some(ref o) => match o.http_total_timeout {
None => DEFAULT_HTTP_TOTAL_TIMEOUT,
Some(v) => {
if v < MIN_HTTP_TOTAL_TIMEOUT || v > MAX_HTTP_TOTAL_TIMEOUT {
return Err(BusinessError::new(
401,
"httpTotalTimeout out of range".to_string(),
));
}
v
}
},
};
CacheDownloadService::get_instance()
.set_global_timeout_options(network_check_timeout as u32, http_total_timeout as u32);
Ok(())
}