apkeep 0.14.0

A command-line tool for downloading APK files from various sources
use std::path::Path;
use std::rc::Rc;

use futures_util::StreamExt;
use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::{Url, Response};
use serde_json::Value;
use tokio_dl_stream_to_disk::error::ErrorKind as TDSTDErrorKind;
use tokio::time::{sleep, Duration as TokioDuration};

fn http_headers() -> HeaderMap {
    let mut headers = HeaderMap::new();
    headers.insert("User-Agent", HeaderValue::from_static("UpdateSDK##4.0.1.300##Android##Pixel 2##com.huawei.appmarket##12.0.1.301"));
    headers
}

fn client_api_body(app_id: &str) -> String {
    format!("agVersion=12.0.1&brand=Android&buildNumber=QQ2A.200405.005.2020.04.07.17&density=420&deviceSpecParams=%7B%22abis%22%3A%22arm64-v8a%2Carmeabi-v7a%2Carmeabi%22%2C%22deviceFeatures%22%3A%22U%2CP%2CB%2C0c%2Ce%2C0J%2Cp%2Ca%2Cb%2C04%2Cm%2Candroid.hardware.wifi.rtt%2Ccom.google.hardware.camera.easel%2Ccom.google.android.feature.PIXEL_2017_EXPERIENCE%2C08%2C03%2CC%2CS%2C0G%2Cq%2CL%2C2%2C6%2CY%2CZ%2C0M%2Candroid.hardware.vr.high_performance%2Cf%2C1%2C07%2C8%2C9%2Candroid.hardware.sensor.hifi_sensors%2CO%2CH%2Ccom.google.android.feature.TURBO_PRELOAD%2Candroid.hardware.vr.headtracking%2CW%2Cx%2CG%2Co%2C06%2C0N%2Ccom.google.android.feature.PIXEL_EXPERIENCE%2C3%2CR%2Cd%2CQ%2Cn%2Candroid.hardware.telephony.carrierlock%2Cy%2CT%2Ci%2Cr%2Cu%2Ccom.google.android.feature.WELLBEING%2Cl%2C4%2C0Q%2CN%2CM%2C01%2C09%2CV%2C7%2C5%2C0H%2Cg%2Cs%2Cc%2C0l%2Ct%2C0L%2C0W%2C0X%2Ck%2C00%2Ccom.google.android.feature.GOOGLE_EXPERIENCE%2Candroid.hardware.sensor.assist%2Candroid.hardware.audio.pro%2CK%2CE%2C02%2CI%2CJ%2Cj%2CD%2Ch%2Candroid.hardware.wifi.aware%2C05%2CX%2Cv%22%2C%22dpi%22%3A420%2C%22preferLan%22%3A%22en%22%7D&emuiApiLevel=0&firmwareVersion=10&getSafeGame=1&gmsSupport=0&hardwareType=0&harmonyApiLevel=0&harmonyDeviceType=&installCheck=0&isFullUpgrade=0&isUpdateSdk=1&locale=en_US&magicApiLevel=0&magicVer=&manufacturer=Google&mapleVer=0&method=client.updateCheck&odm=0&packageName=com.huawei.appmarket&phoneType=Pixel%202&pkgInfo=%7B%22params%22%3A%5B%7B%22isPre%22%3A0%2C%22maple%22%3A0%2C%22oldVersion%22%3A%221.0%22%2C%22package%22%3A%22{}%22%2C%22pkgMode%22%3A0%2C%22shellApkVer%22%3A0%2C%22targetSdkVersion%22%3A19%2C%22versionCode%22%3A1%7D%5D%7D&resolution=1080_1794&sdkVersion=4.0.1.300&serviceCountry=IE&serviceType=0&supportMaple=0&ts=1649970862661&ver=1.2&version=12.0.1.301&versionCode=120001301", app_id)
}

pub async fn download_apps(
    apps: Vec<(String, Option<String>)>,
    parallel: usize,
    sleep_duration: u64,
    outpath: &Path,
) {
    let http_client = Rc::new(reqwest::Client::new());
    let headers = http_headers();

    futures_util::stream::iter(
        apps.into_iter().map(|app| {
            let (app_id, app_version) = app;
            let http_client = Rc::clone(&http_client);
            let headers = headers.clone();
            async move {
                if app_version.is_none() {
                    println!("Downloading {}...", app_id);
                    if sleep_duration > 0 {
                        sleep(TokioDuration::from_millis(sleep_duration)).await;
                    }
                    let client_api_url = Url::parse(crate::consts::HUAWEI_APP_GALLERY_CLIENT_API_URL).unwrap();
                    let client_api_response = http_client
                        .post(client_api_url)
                        .body(client_api_body(&app_id))
                        .headers(headers)
                        .send().await.unwrap();
                    download_from_response(client_api_response, app_id.to_string(), outpath).await;
                } else {
                    println!("Specific versions can not be downloaded from Huawei AppGallery ({}@{}). Skipping...", app_id, app_version.unwrap());
                }
            }
        })
    ).buffer_unordered(parallel).collect::<Vec<()>>().await;
}

async fn download_from_response(response: Response, app_string: String, outpath: &Path) {
    let fname = format!("{}.apk", app_string);
    match response.status() {
        reqwest::StatusCode::OK => {
            let body = response.text().await.unwrap();
            let response_value: Value = serde_json::from_str(&body).unwrap();
            if response_value.is_object() {
                let response_obj = response_value.as_object().unwrap();
                if response_obj.contains_key("list") {
                    let list_value = response_obj.get("list").unwrap();
                    if list_value.is_array() {
                        let list = list_value.as_array().unwrap();
                        if !list.is_empty() && list[0].is_object(){
                            let first_list_entry = list[0].as_object().unwrap();
                            if first_list_entry.contains_key("downurl") {
                                let downurl = first_list_entry.get("downurl").unwrap();
                                if downurl.is_string() {
                                    let download_url = downurl.as_str().unwrap();
                                    match tokio_dl_stream_to_disk::download(download_url, Path::new(outpath), &fname).await {
                                        Ok(_) => println!("{} downloaded successfully!", app_string),
                                        Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => {
                                            println!("File already exists for {}. Skipping...", app_string);
                                        },
                                        Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => {
                                            println!("Permission denied when attempting to write file for {}. Skipping...", app_string);
                                        },
                                        Err(_) => {
                                            println!("An error has occurred attempting to download {}.  Retry #1...", app_string);
                                            match tokio_dl_stream_to_disk::download(download_url, Path::new(outpath), &fname).await {
                                                Ok(_) => println!("{} downloaded successfully!", app_string),
                                                Err(_) => {
                                                    println!("An error has occurred attempting to download {}.  Retry #2...", app_string);
                                                    match tokio_dl_stream_to_disk::download(download_url, Path::new(outpath), &fname).await {
                                                        Ok(_) => println!("{} downloaded successfully!", app_string),
                                                        Err(_) => {
                                                            println!("An error has occurred attempting to download {}. Skipping...", app_string);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        _ => {
            println!("Invalid app response for {}. Skipping...", app_string);
        }
    }
}

pub async fn list_versions(apps: Vec<(String, Option<String>)>) {
    for app in apps {
        let (app_id, _) = app;
        println!("Versions available for {} on Huawei AppGallery:", app_id);
        println!("| Huawei AppGallery does not make old versions of apps available.");
    }
}