code:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="action_about_title">About FFUpdater</string>
    <string name="action_settings_title">Settings</string>
    <string name="app_icon">app icon</string>
    <string name="app_name">FFUpdater</string>
    <string name="application_installation_was_not_successful">Application installation was not successful</string>
    <string name="application_successfully_installed">Application successfully installed</string>
    <string name="available_version">Available: %1$s</string>
    <string name="available_version_error">"ERROR"</string>
    <string name="available_version_loading">Loading…</string>
    <string name="background_job_failure__notification_text">Please report this error. Click to view the error report.</string>
    <string name="background_job_failure__notification_title">Background update check failed</string>
    <string name="brave__description">Privately browse the internet, for free! The Brave Private Browser App is your fast, free, and safe private web browser with ad blocker and pop-up blocker. Privately browse and search the internet without being tracked by advertisers, malware and pop-ups.</string>
    <string name="brave__title">Brave Private Browser</string>
    <string name="brave__warning">Brave uses the technical foundation of the Google Chrome (Chromium) - some people don\'t like this. Do you want to install Brave?</string>
    <string name="bromite__description">Bromite aims at providing a no-clutter browsing experience without privacy-invasive features and with the addition of a fast ad-blocking engine. Minimal UI changes are applied to help curbing the idea of "browser as an advertisement platform".</string>
    <string name="bromite__title">Bromite</string>
    <string name="bromite__warning">Bromite uses the technical foundation of the Google Chrome (Chromium) - some people don\'t like this. Do you want to install Bromite?</string>
    <string name="but_was">But was:</string>
    <string name="crash_report__copy_crash_report_to_clipboard">Copy crash report to clipboard</string>
    <string name="crash_report__explain_text__background_job">The following error occurred during the background error check. If this error occurs frequently, feel free to report it on notabug.org, GitHub or GitLab. Please don\'t forget to add your crash report from the clipboard and the description of how you reproduce this error.</string>
    <string name="crash_report__explain_text__install_activity_fetching_url">The following error occurred when the download URL was fetched. If this error occurs frequently, feel free to report it on notabug.org, GitHub or GitLab. Please don\'t forget to add your crash report from the clipboard and the description of how you reproduce this error.</string>
    <string name="crash_report__explain_text__main_activity_update_check">The following error occurred when checking for updates in the main view. If this error occurs frequently, feel free to report it on notabug.org, GitHub or GitLab. Please don\'t forget to add your crash report from the clipboard and the description of how you reproduce this error.</string>
    <string name="crash_report__explain_text__uncaught_throwable">Unfortunately, FFUpdater has crashed. Please report this error on notabug.org, GitHub or GitLab. Add to your issue the crash report from the clipboard and a description on how to reproduce the bug.</string>
    <string name="crash_report__go_to_github">Go to GitHub</string>
    <string name="crash_report__go_to_gitlab">Go to GitLab</string>
    <string name="crash_report__got_to_notabug_org">Go to notabug.org</string>
    <string name="default_theme_preference" translatable="false">3</string>
    <string name="device_too_old_dialog__message">The application requires Android %1$s. But your device is only running Android %2$s. Either choose a different app, update your operating system or switch to a newer smartphone.</string>
    <string name="device_too_old_dialog__title">Device is too old</string>
    <string name="dialog_button__do_not_install">No, don\'t install it</string>
    <string name="dialog_button__yes">Yes</string>
    <string name="download_the_app">download the app</string>
    <string name="error_notification__channel_description">Show errors of the background update checker</string>
    <string name="error_notification__channel_name">Background errors</string>
    <string name="firefox_beta__description">Firefox Browser for Android is automatically private and incredibly fast. Thousands of online trackers are following you every day, collecting information about where you go online and slowing down your speed. Firefox blocks over 2000 of these trackers by default and there are ad blocker add-ons available if you want to customize your browser even more. With Firefox, you’ll get the security you deserve and the speed you need in a private, mobile browser.</string>
    <string name="firefox_beta__title">Firefox for Android Beta</string>
    <string name="firefox_beta__warning">This app is designed for developers and testers. It may contain various bugs that can affect your usual work with the browser. Do you want to install it?</string>
    <string name="firefox_focus__description">This is a modification of the Firefox browser aimed to increase privacy.</string>
    <string name="firefox_focus__title">Firefox Focus</string>
    <string name="firefox_klar__description">Firefox Klar is developed for Germany, Austria and Switzerland, but can be used by users in any country. It is very similar to Firefox Focus, but the name was changed in German-speaking countries to avoid confusion with the magazine Focus by Hubert Burda Media group. Unlike Firefox Focus, Firefox Klar has telemetry disabled by default due to strict German privacy laws.</string>
    <string name="firefox_klar__title">Firefox Klar</string>
    <string name="firefox_nightly__description">Please note that this app used to be titled Firefox Preview. It has now been transitioned to Firefox Nightly. \n\nFirefox Nightly is a developmental channel for new Mozilla Firefox releases. \n\nFirefox Nightly is designed to showcase the more experimental builds of Firefox. The Nightly channel allows users to experience the newest Firefox innovations in an unstable environment and provide feedback on features and performance to help determine what makes the final release.</string>
    <string name="firefox_nightly__title">Firefox Nightly</string>
    <string name="firefox_nightly__warning">This app is designed for developers and testers. It may contain various bugs that can affect your usual work with the browser. Do you want to install it?</string>
    <string name="firefox_release__description">Firefox Browser for Android is automatically private and incredibly fast. Thousands of online trackers are following you every day, collecting information about where you go online and slowing down your speed. Firefox blocks over 2000 of these trackers by default and there are ad blocker add-ons available if you want to customize your browser even more. With Firefox, you’ll get the security you deserve and the speed you need in a private, mobile browser.</string>
    <string name="firefox_release__title">Firefox Browser</string>
    <string name="github" translatable="false">GitHub</string>
    <string name="go_back">Go back</string>
    <string name="iceraven__description">Definitely not brought to you by Mozilla! \n\nIceraven Browser is a web browser for Android, based on Mozilla\'s Fenix version of Firefox, GeckoView and Mozilla Android Components. \n\nOur goal is to be a close fork of the new Firefox for Android that seeks to provide users with more options, more opportunities to customize (including a broad extension library), and more information about the pages they visit and how their browsers are interacting with those pages.</string>
    <string name="iceraven__title">Iceraven Browser</string>
    <string name="infobox">This is an updater for multiple browsers from Mozilla. Click the plus-button for installation and the down-arrow-button for updating. You can check for new updates by down-swiping or reopening the app. By default the app will check regularly for updates in the background. I did my best to secure the installation/updating process - feel free to double-check it or improve it by creating a pull-request. \n\nLast successful background update check: %1$s</string>
    <string name="information_about_the_app">information about the app</string>
    <string name="install_activity__delete_cache_button_text">clear cache</string>
    <string name="install_activity__delete_cache_explanation">If you think that FFUpdater has cached a faulty app update, click the \"clear cache\" button to delete the cache for this app.</string>
    <string name="install_activity__download_app_is_disabled">The necessary system app \"Download Manager\" is disabled. Please enable it again.</string>
    <string name="install_activity__download_application_from_with_status">Download application (%1$s)</string>
    <string name="install_activity__download_failed">Download failed</string>
    <string name="install_activity__download_status_failed">failed</string>
    <string name="install_activity__download_status_paused">paused</string>
    <string name="install_activity__download_status_pending">pending</string>
    <string name="install_activity__download_status_running">running</string>
    <string name="install_activity__download_status_success">success</string>
    <string name="install_activity__downloaded_application_from">Downloaded application from:</string>
    <string name="install_activity__downloaded_application_is_not_verified">Downloaded application is NOT verified</string>
    <string name="install_activity__downloaded_application_is_verified">Downloaded application is verified</string>
    <string name="install_activity__exception__show_button">"Show the detailed error report."</string>
    <string name="install_activity__expected_sha_256_certificate_fingerprint">Expected SHA-256 certificate fingerprint:</string>
    <string name="install_activity__external_storage_not_accessible">External storage is not available - can\'t start download. Current status is:</string>
    <string name="install_activity__fetch_url_for_download">Fetch download URL from %1$s</string>
    <string name="install_activity__fetched_url_for_download_successfully">Fetched download URL from %1$s</string>
    <string name="install_activity__github_rate_limit_exceeded">GitHub-API rate limit is exceeded. Please try the download again in the next hour.</string>
    <string name="install_activity__invalid_downloaded_certificate_fingerprint_explanation">Either an attacker has replaced the correct download with a tampered one or the certificate checker doesn\'t work anymore. You can double-check the certificate fingerprint on apkmirror.com or you can create an issue on https://notabug.org/Tobiwan/ffupdater to inform my about the bug.</string>
    <string name="install_activity__invalid_installed_certificate_fingerprint_explanation">Either a tampered application has been installed or the certificate checker doesn\'t work anymore. You can double-check the certificate fingerprint on apkmirror.com or you can create an issue on https://notabug.org/Tobiwan/ffupdater to inform my about the bug. \n\nI recommend you to uninstall the application.</string>
    <string name="install_activity__open_cache_explanation">You can open the cache folder to copy the APK file to a place with more free storage.</string>
    <string name="install_activity__open_cache_folder_button_text">open cache folder</string>
    <string name="install_activity__retrigger_installation__button_title">Retry App Installation</string>
    <string name="install_activity__sha_256_certificate_fingerprint">SHA-256 certificate fingerprint:</string>
    <string name="install_activity__temporary_network_issue">A temporary network issue occurred. Please retry later.</string>
    <string name="install_activity__too_low_memory_description">Your device is low on storage (only %1$d MB free). This will lead to download or installation failure.</string>
    <string name="install_activity__too_low_memory_title">Low Disk Space</string>
    <string name="install_activity__try_disable_miui_optimization">If you are using Xiaomi or MIUI, try disabling \"MIUI Optimization\".</string>
    <string name="install_activity__unknown_signature_of_installed_app_text1">You have to uninstall the existing app before FFUpdater can install or update the app. Backup your data because the uninstall will delete all your bookmarks, settings etc.</string>
    <string name="install_activity__unknown_signature_of_installed_app_text2">Reason: The current installed app is not signed by the original developer - maybe it\'s signed by F-Droid. Android refuses to update an app if different people signed the installed and the updated version.</string>
    <string name="install_activity__use_cached_downloaded_apk">Use already downloaded file from:</string>
    <string name="install_activity__verifying_downloaded_application_certificate">Verifying application certificate</string>
    <string name="install_activity__you_can_now_close_this_window">You can now close this window</string>
    <string name="install_activity_label">Install Application</string>
    <string name="install_app">Install app</string>
    <string name="install_new_app">Install new app</string>
    <string name="install_same_version_dialog__message">The latest version of this app is already installed. Do you want to download and install the latest version again?</string>
    <string name="install_same_version_dialog__title">Same version installed</string>
    <string name="installed_app_is_not_verified">Installed app is NOT verified</string>
    <string name="installed_application_is_verified">Installed application is verified</string>
    <string name="installed_version">Installed: %1$s</string>
    <string name="lockwise__description">PASSWORD ACCESS FOR YOU, & ONLY YOU \n\nSecurely track your Firefox passwords & access them anywhere. Quickly open any website from the app to get into your accounts</string>
    <string name="lockwise__title">Firefox Lockwise</string>
    <string name="main_activity__github_api_limit_exceeded">"GitHub-API limit exceeded. Retry next hour."</string>
    <string name="main_activity__no_internet_connection">No network connection</string>
    <string name="main_activity__subtitle">Update your browsers from Mozilla</string>
    <string name="main_activity__temporary_network_issue">"Temporary network issue. Retry later."</string>
    <string name="manage_storage_activity__delete_button">Delete cached app updates.</string>
    <string name="manage_storage_activity__explanation">FFUpdater caches already downloaded app updates and stores them in the \'Android/data/de.marmaro.krt.ffupdater/files/Download\' folder.\n\nThis saves bandwidth when an app is not successfully installed or updated. Also, the cache is needed so that updates that were downloaded in the background can be installed later.\n\nIf you decide to delete these app updates, you may have to download them again.</string>
    <string name="mozilla_ci" translatable="false">Mozilla CI</string>
    <string name="ok">OK</string>
    <string name="running_downloads_dialog__message">The system download manager is downloading other files now. Do you want to start a new download that may take longer?</string>
    <string name="running_downloads_dialog__negative">No, don\'t download it</string>
    <string name="running_downloads_dialog__title">Running downloads</string>
    <string name="running_downloads_dialog__yes">Yes</string>
    <string name="session_installer__not_enough_storage">The operation failed because not enough storage is available: %1$s</string>
    <string name="session_installer__status_failure">The operation failed in a generic way. The system will always try to provide a more specific failure reason, but in some rare cases this may be delivered.</string>
    <string name="session_installer__status_failure_aborted">The operation failed because it was actively aborted. For example, the user actively declined requested permissions, or the session was abandoned.</string>
    <string name="session_installer__status_failure_blocked">The operation failed because it was blocked. For example, a device policy may be blocking the operation, a package verifier may have blocked the operation, or the app may be required for core system operation.</string>
    <string name="session_installer__status_failure_conflict">The operation failed because it conflicts (or is inconsistent with) with another package already installed on the device. For example, an existing permission, incompatible certificates, etc. The user may be able to uninstall another app to fix the issue.</string>
    <string name="session_installer__status_failure_incompatible">The operation failed because it is fundamentally incompatible with this device. For example, the app may require a hardware feature that doesn\'t exist, it may be missing native code for the ABIs supported by the device, or it requires a newer SDK version, etc.</string>
    <string name="session_installer__status_failure_invalid">The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.</string>
    <string name="session_installer__status_failure_storage">The operation failed because of storage issues. For example, the device may be running low on space, or external media may be unavailable. The user may be able to help free space or insert different external media.</string>
    <string name="settings__activity_label">Settings</string>
    <string name="settings__automatic_check_title">Automatic in Background</string>
    <string name="settings__category_appearance_title">Appearance</string>
    <string name="settings__check_interval_summary">How often should the app check for updates?</string>
    <string name="settings__check_interval_title">Check Interval</string>
    <string name="settings__disabled_apps_summary">Only installed apps will be automatically checked for updates. You can remove installed apps from the automatic update check by selecting them here.</string>
    <string name="settings__disabled_apps_title">Excluded applications</string>
    <string name="settings__only_unmetered_network_summary">If this option is disabled, FFUpdater can use your mobile internet to check for app updates in the background. This will reduce your monthly data limit. When this option is enabled, only unmetered network connections, like the majority of WiFis, are used for background update checking.</string>
    <string name="settings__ony_unmetered_network_title">Only unmetered network</string>
    <string name="settings__theme_battery_saver">Dark when Battery Saver active</string>
    <string name="settings__theme_dark">Dark</string>
    <string name="settings__theme_light">Light</string>
    <string name="settings__theme_preference_summary">Choose between light and dark theme</string>
    <string name="settings__theme_preference_title">Choose theme</string>
    <string name="settings__theme_system_default">Use system default</string>
    <string name="settings__title">Application Update Check</string>
    <string name="show_warning_before_installation_dialog__title">Warning</string>
    <string name="ungoogled_chromium__description">Google Chromium, sans integration with Google.\n\nWithout signing in to a Google Account, Chromium does pretty well in terms of security and privacy. However, Chromium still has some dependency on Google web services and binaries. In addition, Google designed Chromium to be easy and intuitive for users, which means they compromise on transparency and control of internal operations.</string>
    <string name="ungoogled_chromium__title">Ungoogled Chromium</string>
    <string name="ungoogled_chromium__warning">Ungoogled Chromium uses the technical foundation of the Google Chrome (Chromium) - some people don\'t like this. Do you want to install Ungoogled Chromium?</string>
    <string name="unsupported_abi_dialog__message">The selected browser is incompatible with your smartphone\'s processor. Please select a different browser.</string>
    <string name="unsupported_abi_dialog__title">Unsupported device</string>
    <string name="update_interval__12h">12 hours</string>
    <string name="update_interval__15min">15 minutes</string>
    <string name="update_interval__18h">18 hours</string>
    <string name="update_interval__1d">1 day</string>
    <string name="update_interval__1h">1 hour</string>
    <string name="update_interval__1m">1 month</string>
    <string name="update_interval__1w">1 week</string>
    <string name="update_interval__2d">2 days</string>
    <string name="update_interval__2h">2 hours</string>
    <string name="update_interval__2w">2 weeks</string>
    <string name="update_interval__30min">30 minutes</string>
    <string name="update_interval__3d">3 days</string>
    <string name="update_interval__4d">4 days</string>
    <string name="update_interval__4h">4 hours</string>
    <string name="update_interval__5d">5 days</string>
    <string name="update_interval__6d">6 days</string>
    <string name="update_interval__6h">6 hours</string>
    <string name="update_interval__8h">8 hours</string>
    <string name="update_notification__channel_description">Notification channel for available %1$s updates.</string>
    <string name="update_notification__channel_name">"%1$s" update notification</string>
    <string name="update_notification__text">Click to install update</string>
    <string name="update_notification__title">Update for "%1$s" available</string>
    <string name="vivaldi__description">Discover a blazingly fast Internet browser! Vivaldi is a new, free, and private web browser built with flexibility in mind. It comes packed with useful features including a built-in AdBlocker (pop-up blocker), tracking protection, and private translation. Browser options like proper desktop-style tabs, dark/light Themes, and layout choices help you make it yours.</string>
    <string name="vivaldi__source" translatable="false">https://vivaldi.com/download/</string>
    <string name="vivaldi__title">Vivaldi Browser</string>
    <string name="vivaldi__warning">"Vivaldi is only partially open source (see https://vivaldi.com/blog/vivaldi-browser-open-source/ for more info). It also uses the technical foundation of the Google Chrome (Chromium).\n\nSome people don\'t like this. Do you want to install Vivaldi?"</string>
</resources>


code:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <style name="MainActivityTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>

    <style name="MainActivityTheme.MainTitle.Small" parent="@android:style/TextAppearance.Large">
        <item name="android:textColor">@android:color/white</item>
    </style>

    <style name="MainActivityTheme.MainTitle.Big" parent="MainActivityTheme.MainTitle.Small">
        <item name="android:textSize">34sp</item>
    </style>

    <style name="MainActivityTheme.SubTitle" parent="@android:style/TextAppearance">
        <item name="android:textSize">16sp</item>
        <item name="android:textColor">@android:color/white</item>
    </style>

    <style name="DefaultTextView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">center</item>
    </style>

    <style name="DefaultButton">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">center</item>
    </style>

    <style name="BoldTextView" parent="DefaultTextView">
        <item name="android:textStyle">bold</item>
    </style>

    <style name="MonospaceSelectableTextView" parent="DefaultTextView">
        <item name="android:textIsSelectable">true</item>
        <item name="android:fontFamily">monospace</item>
    </style>

    <style name="DownloadActivitySegment">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginTop">16dp</item>
        <item name="android:orientation">vertical</item>
        <item name="android:visibility">gone</item>
    </style>

    <style name="AppCard">
        <item name="cardBackgroundColor">@color/cardview_background_color</item>
        <item name="android:layout_width">fill_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_gravity">center_horizontal</item>
        <item name="android:layout_marginLeft">10dp</item>
        <item name="android:layout_marginRight">10dp</item>
    </style>

    <style name="AppCardLayout">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginLeft">10dp</item>
        <item name="android:layout_marginTop">10dp</item>
        <item name="android:layout_marginRight">10dp</item>
        <item name="android:layout_marginBottom">10dp</item>
    </style>

    <style name="AppCardInfoButton">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginEnd">8dp</item>
        <item name="android:layout_marginRight">8dp</item>
        <item name="android:background">@android:color/transparent</item>
        <item name="android:contentDescription">Info about this app</item>
    </style>

    <style name="AppCardDownloadButton">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:background">@android:color/transparent</item>
        <item name="android:contentDescription">Download the latest Fennec Release</item>
    </style>

    <style name="AppCardIcon">
        <item name="android:layout_width">32dp</item>
        <item name="android:layout_height">32dp</item>
    </style>

    <style name="AppCardVersion" parent="@style/TextAppearance.AppCompat.Small">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:singleLine">true</item>
        <item name="android:ellipsize">start</item>
    </style>

    <style name="AppCardInstalledVersion" parent="AppCardVersion">
        <item name="android:layout_marginTop">8dp</item>
    </style>

    <style name="AppCardTitle" parent="@style/TextAppearance.AppCompat.Large">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginStart">8dp</item>
        <item name="android:layout_marginLeft">8dp</item>
        <item name="android:fontFamily">sans-serif-light</item>
        <item name="android:singleLine">true</item>
    </style>

    <style name="CrashReportTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
        <item name="colorPrimary">@color/errorRed</item>
        <item name="colorPrimaryDark">@color/errorRed</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
</resources>

repo_name: BadStarOne/ffupdater

license: mit


code:
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content xmlns:tools="http://schemas.android.com/tools">
    <!-- Backup the user settings stored in the default shared preferences files.
        Device specific information (like the cached location of downloaded files) will be automatically
        invalided on the new/restored device. -->
    <include domain="sharedpref" path="."/>

    <!-- Exclude /storage/emulated/0/Android/data/de.marmaro.krt.ffupdater/cache/Download:
     - it seems that this folder is automatically excluded
     - "getCacheDir(), getCodeCacheDir(), or getNoBackupFilesDir() are always excluded even if you
            try to include them"
     - externalCacheDir doesn't belong to any domain:
            domain root: the directory where all private files belonging to this app are stored.
            domain file: directories returned by getFilesDir().
            domain database: directories returned by getDatabasePath() - that is:
                /data/user/0/de.marmaro.krt.ffupdater/databases/
            domain sharedpref: the directory where SharedPreferences are stored.
            domain external: the directory returned by getExternalFilesDir()
      - "You cannot back up files outside of these [listed domains] locations." -->

    <!-- Exclude /storage/emulated/0/Android/data/de.marmaro.krt.ffupdater/files/Download:
         - domain external: the directory returned by getExternalFilesDir() - and that is
                "/storage/emulated/0/Android/data/de.marmaro.krt.ffupdater/files/"
         - path ".": "You can use . to reference the current directory" +
                "If you specify a directory, then the rule applies to all files in the directory
                    and recursive sub-directories."
         -->
    <exclude domain="external" path="." tools:ignore="FullBackupContent" />
</full-backup-content>


code:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <!-- Trust preinstalled CAs -->
            <certificates src="system" />
            <!-- Additionally trust user added CAs -->
            <certificates src="user" tools:ignore="AcceptsUserCertificates" />
        </trust-anchors>
    </base-config>
</network-security-config>


code:
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">

    <PreferenceCategory app:title="@string/settings__title">

        <SwitchPreferenceCompat
            app:defaultValue="true"
            app:key="automaticCheck"
            app:title="@string/settings__automatic_check_title" />

        <SwitchPreferenceCompat
            app:defaultValue="false"
            app:key="onlyUnmeteredNetwork"
            app:title="@string/settings__ony_unmetered_network_title"
            app:summary="@string/settings__only_unmetered_network_summary"/>

        <ListPreference
            app:defaultValue="360"
            app:dependency="automaticCheck"
            app:entries="@array/checkIntervalEntries"
            app:entryValues="@array/checkIntervalValues"
            app:key="checkInterval"
            app:summary="@string/settings__check_interval_summary"
            app:title="@string/settings__check_interval_title"
            app:useSimpleSummaryProvider="true" />

        <MultiSelectListPreference
            app:dependency="automaticCheck"
            app:entries="@array/disabledAppEntries"
            app:entryValues="@array/disabledAppEntryValues"
            app:key="disableApps"
            app:summary="@string/settings__disabled_apps_summary"
            app:title="@string/settings__disabled_apps_title" />

    </PreferenceCategory>

    <PreferenceCategory app:title="@string/settings__category_appearance_title">

        <ListPreference
            app:defaultValue="@string/default_theme_preference"
            app:entries="@array/themeEntries"
            app:entryValues="@array/themeValues"
            app:key="themePreference"
            app:summary="@string/settings__theme_preference_summary"
            app:title="@string/settings__theme_preference_title" />

    </PreferenceCategory>

</PreferenceScreen>


code:
package de.marmaro.krt.ffupdater

import android.content.Context
import android.content.pm.PackageManager
import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder
import de.marmaro.krt.ffupdater.app.impl.*
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockkObject
import io.mockk.unmockkObject
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.net.URL
import java.time.Duration
import java.time.ZonedDateTime
import javax.net.ssl.HttpsURLConnection

/**
 * Verify that the APIs for downloading latest app updates:
 *  - still working
 *  - not downloading outdated versions
 */
class DownloadApiChecker {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var packageManager: PackageManager

    private val sharedPreferences = SPMockBuilder().createSharedPreferences()

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        every { context.getString(R.string.available_version, any()) } returns "/"
        every { context.packageName } returns "de.marmaro.krt.ffupdater"
        every {
            packageManager.getPackageInfo(any<String>(), 0)
        } throws PackageManager.NameNotFoundException()
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        every { context.getSharedPreferences(any(), any()) } returns sharedPreferences
    }

    companion object {
        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(DeviceEnvironment)
        }
    }

    @Test
    fun brave() {
        val result = runBlocking { Brave().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 2 * 7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun bromite() {
        val result = runBlocking { Bromite().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 9 * 7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun firefoxBeta() {
        val result = runBlocking { FirefoxBeta().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 3 * 7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun firefoxFocus() {
        val result = runBlocking { FirefoxFocus().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 8 * 7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun firefoxKlar() {
        val result = runBlocking { FirefoxKlar().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 2 * 30
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun firefoxNightly() {
        sharedPreferences.edit().putLong("firefox_nightly_installed_version_code", 0)
        val result = runBlocking { FirefoxNightly().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 1 * 7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun firefoxRelease() {
        val result = runBlocking { FirefoxRelease().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 6 * 7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun iceraven() {
        val result = runBlocking { Iceraven().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 3 * 30
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun lockwise() {
        val result = runBlocking { Lockwise().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 16 * 30
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    @Test
    fun vivaldi() {
        val result = runBlocking { Vivaldi().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        assertFalse(result.version.isEmpty())
    }

    @Test
    fun ungoogledChromium() {
        val result = runBlocking { UngoogledChromium().updateCheck(context) }
        verifyThatDownloadLinkAvailable(result.downloadUrl)
        val age = Duration.between(result.publishDate, ZonedDateTime.now())
        val maxDays = 5*7
        assertTrue("$age must be smaller then $maxDays days", age.toDays() < maxDays)
    }

    private fun verifyThatDownloadLinkAvailable(urlString: String) {
        val url = URL(urlString)
        val connection = url.openConnection() as HttpsURLConnection
        val status = connection.responseCode
        assertTrue("$status of connection must be >= 200", status >= 200)
        assertTrue("$status of connection must be < 300", status < 300)
    }
}


code:
package de.marmaro.krt.ffupdater.app

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class VersionCompareHelperTest {

    @Test
    fun isAvailableVersionHigher_invalidInstalledVersion_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("v1.0.0", "1.0.0"))
    }

    @Test
    fun isAvailableVersionHigher_invalidAvailableVersion_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("1.0.0", "v1.0.0"))
    }

    @Test
    fun isAvailableVersionHigher_installedVersionIsNewer_returnTrue() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("1.0.1", "1.0.0"))
    }

    @Test
    fun isAvailableVersionHigher_availableVersionIsNewer_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("1.0.0", "1.0.1"))
    }

    @Test
    fun isAvailableVersionHigher_installedVersionIsNewer_brave_returnTrue() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("1.20.103", "1.18.12"))
    }

    @Test
    fun isAvailableVersionHigher_sameVersion_brave_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("1.18.12", "1.18.12"))
    }

    @Test
    fun isAvailableVersionHigher_availableVersionIsNewer_brave_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("1.18.12", "1.20.103"))
    }

    @Test
    fun isAvailableVersionHigher_installedVersionIsNewer_bromite_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("90.0.4430.59", "90.0.4430.58"))
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("90.0.4430.59", "89.0.4389.117"))
    }

    @Test
    fun isAvailableVersionHigher_availableVersionIsNewer_bromite_returnTrue() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("89.0.4389.117", "89.0.4389.117"))
    }

    @Test
    fun isAvailableVersionHigher_sameVersion_bromite_returnFalse() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("89.0.4389.117", "89.0.4389.118"))
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("89.0.4389.117", "90.0.4430.59"))
    }

    @Test
    fun isAvailableVersionHigher_installedVersionIsNewer_firefoxBeta_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("91.0.0-beta.3", "86.0.0-beta.3"))
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("91.0.0-beta.3", "91.0.0-beta.2"))
    }

    @Test
    fun isAvailableVersionHigher_sameVersion_firefoxBeta_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("91.0.0-beta.3", "91.0.0-beta.3"))
    }

    @Test
    fun isAvailableVersionHigher_availableVersionIsNewer_firefoxBeta_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("86.0.0-beta.3", "86.0.0-beta.4"))
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("86.0.0-beta.3", "91.0.0-beta.3"))
    }

    @Test
    fun isAvailableVersionHigher_availableVersionIsNewer_firefoxFocus_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionHigher("92.1.0", "92.1.1"))
    }

    @Test
    fun isAvailableVersionHigher_sameVersion_firefoxFocus_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("92.1.0", "92.1.0"))
    }

    @Test
    fun isAvailableVersionHigher_installedVersionIsNewer_firefoxFocus_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionHigher("92.1.1", "92.1.0"))
    }

    @Test
    fun isAvailableVersionEqual_sameVersion_brave_returnFalse() {
        assertTrue(VersionCompareHelper.isAvailableVersionEqual("1.18.12", "1.18.12"))
    }

    @Test
    fun isAvailableVersionEqual_differentVersion_brave_returnTrue() {
        assertFalse(VersionCompareHelper.isAvailableVersionEqual("1.18.12", "1.20.103"))
    }

    @Test
    fun isAvailableVersionEqual_sameVersion_bromite_returnTrue() {
        assertTrue(VersionCompareHelper.isAvailableVersionEqual("89.0.4389.117", "89.0.4389.117"))
    }

    @Test
    fun isAvailableVersionEqual_differentVersion_bromite_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionEqual("89.0.4389.117", "89.0.4389.118"))
        assertFalse(VersionCompareHelper.isAvailableVersionEqual("89.0.4389.117", "90.0.4430.59"))
    }

    @Test
    fun isAvailableVersionEqual_sameVersion_firefoxBeta_returnFalse() {
        assertTrue(VersionCompareHelper.isAvailableVersionEqual("91.0.0-beta.3", "91.0.0-beta.3"))
    }

    @Test
    fun isAvailableVersionEqual_differentVersion_firefoxBeta_returnTrue() {
        assertFalse(VersionCompareHelper.isAvailableVersionEqual("86.0.0-beta.3", "86.0.0-beta.4"))
        assertFalse(VersionCompareHelper.isAvailableVersionEqual("86.0.0-beta.3", "91.0.0-beta.3"))
    }

    @Test
    fun isAvailableVersionEqual_sameVersion_firefoxFocus_returnFalse() {
        assertTrue(VersionCompareHelper.isAvailableVersionEqual("92.1.0", "92.1.0"))
    }

    @Test
    fun isAvailableVersionEqual_differentVersion_firefoxFocus_returnFalse() {
        assertFalse(VersionCompareHelper.isAvailableVersionEqual("92.1.1", "92.1.0"))
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.google.gson.Gson
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.app.impl.fetch.github.GithubConsumer.Release
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.*
import org.junit.Assert.*
import java.io.FileReader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class BraveIT {
    @MockK
    lateinit var context: Context

    @MockK
    lateinit var packageManager: PackageManager

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        every { context.getString(R.string.available_version, any()) } returns "/"
    }

    companion object {
        const val API_URl = "https://api.github.com/repos/brave/brave-browser/releases"
        const val DOWNLOAD_URL = "https://github.com/brave/brave-browser/releases/download"

        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeReleaseJsonObjectAvailableUnderUrl(fileName: String, url: String) {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/Brave/$fileName"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, Release::class)
        } returns Gson().fromJson(FileReader(path), Release::class.java)
    }

    private fun makeReleaseJsonArrayAvailableUnderUrl(fileName: String, url: String) {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/Brave/$fileName"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, Array<Release>::class)
        } returns Gson().fromJson(FileReader(path), Array<Release>::class.java)
    }

    @Test
    fun updateCheck_2releases_checkDownloadUrlForABI() {
        makeReleaseJsonObjectAvailableUnderUrl("latest_contains_NOT_release_version.json", "$API_URl/latest")
        makeReleaseJsonArrayAvailableUnderUrl("2releases_perpage_20_page_1.json", "$API_URl?per_page=20&page=1")
        val packageInfo = PackageInfo()
        every { packageManager.getPackageInfo(App.BRAVE.detail.packageName, any()) } returns packageInfo

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonoarm.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonoarm64.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonox86.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86_64)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonox64.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }
    }

    @Test
    fun updateCheck_2releases_updateCheck() {
        makeReleaseJsonObjectAvailableUnderUrl("latest_contains_NOT_release_version.json", "$API_URl/latest")
        makeReleaseJsonArrayAvailableUnderUrl("2releases_perpage_20_page_1.json", "$API_URl?per_page=20&page=1")

        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        val packageInfo = PackageInfo()
        every { packageManager.getPackageInfo(App.BRAVE.detail.packageName, any()) } returns packageInfo

        // installed app is up-to-date
        runBlocking {
            packageInfo.versionName = "1.20.103"
            val actual = Brave().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("1.20.103", actual.version)
            assertEquals(100446537L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-02-10T11:30:45Z", ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }

        // installed app is old
        runBlocking {
            packageInfo.versionName = "1.18.12"
            val actual = Brave().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("1.20.103", actual.version)
            assertEquals(100446537L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-02-10T11:30:45Z", ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }
    }

    @Test
    fun updateCheck_3releases_checkDownloadUrlForABI() {
        makeReleaseJsonObjectAvailableUnderUrl("latest_contains_NOT_release_version.json", "$API_URl/latest")
        makeReleaseJsonArrayAvailableUnderUrl("3releases_perpage_10_page_1.json", "$API_URl?per_page=20&page=1")
        makeReleaseJsonArrayAvailableUnderUrl("3releases_perpage_10_page_2.json", "$API_URl?per_page=20&page=2")

        val packageInfo = PackageInfo()
        every { packageManager.getPackageInfo(App.BRAVE.detail.packageName, any()) } returns packageInfo

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonoarm.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonoarm64.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonox86.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86_64)
            assertEquals(
                "$DOWNLOAD_URL/v1.20.103/BraveMonox64.apk",
                Brave().updateCheck(context).downloadUrl
            )
        }
    }

    @Test
    fun updateCheck_3releases_updateCheck() {
        makeReleaseJsonObjectAvailableUnderUrl("latest_contains_NOT_release_version.json", "$API_URl/latest")
        makeReleaseJsonArrayAvailableUnderUrl("3releases_perpage_10_page_1.json", "$API_URl?per_page=20&page=1")
        makeReleaseJsonArrayAvailableUnderUrl("3releases_perpage_10_page_2.json", "$API_URl?per_page=20&page=2")

        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        val packageInfo = PackageInfo()
        every { packageManager.getPackageInfo(App.BRAVE.detail.packageName, any()) } returns packageInfo

        // installed app is up-to-date
        runBlocking {
            packageInfo.versionName = "1.20.103"
            val actual = Brave().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("1.20.103", actual.version)
            assertEquals(100446537L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-02-10T11:30:45Z", ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }

        // installed app is old
        runBlocking {
            packageInfo.versionName = "1.18.12"
            val actual = Brave().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("1.20.103", actual.version)
            assertEquals(100446537L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-02-10T11:30:45Z", ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.google.gson.Gson
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.app.impl.fetch.github.GithubConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.*
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.FileReader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class BromiteIT {
    @MockK
    lateinit var context: Context

    @MockK
    lateinit var packageManager: PackageManager

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        every { context.getString(R.string.available_version, any()) } returns "/"
    }

    companion object {
        const val DOWNLOAD_URL = "https://github.com/bromite/bromite/releases/download/90.0.4430.59"

        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeReleaseJsonObjectAvailable() {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/Bromite/" +
                "latest_contains_release_version.json"
        val url = "https://api.github.com/repos/bromite/bromite/releases/latest"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, GithubConsumer.Release::class)
        } returns Gson().fromJson(FileReader(path), GithubConsumer.Release::class.java)
    }

    @Test
    fun updateCheck_latestRelease_checkDownloadUrlForABI() {
        makeReleaseJsonObjectAvailable()
        val packageInfo = PackageInfo()
        packageInfo.versionName = "90.0.4430.59"
        every {
            packageManager.getPackageInfo(
                App.BROMITE.detail.packageName,
                0
            )
        } returns packageInfo

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
            assertEquals(
                "$DOWNLOAD_URL/arm_ChromePublic.apk",
                Bromite().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
            assertEquals(
                "$DOWNLOAD_URL/arm64_ChromePublic.apk",
                Bromite().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86)
            assertEquals(
                "$DOWNLOAD_URL/x86_ChromePublic.apk",
                Bromite().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86_64, ABI.X86)
            assertEquals(
                "$DOWNLOAD_URL/x86_ChromePublic.apk",
                Bromite().updateCheck(context).downloadUrl
            )
        }
    }

    @Test
    fun updateCheck_latestRelease_updateCheck() {
        makeReleaseJsonObjectAvailable()
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        val packageInfo = PackageInfo()
        every {
            packageManager.getPackageInfo(
                App.BROMITE.detail.packageName,
                0
            )
        } returns packageInfo

        // installed app is up-to-date
        runBlocking {
            packageInfo.versionName = "90.0.4430.59"
            val actual = Bromite().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("90.0.4430.59", actual.version)
            assertEquals(91231777L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-04-07T12:06:47Z", ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }

        // installed app is old
        runBlocking {
            packageInfo.versionName = "89.0.4389.117"
            val actual = Bromite().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("90.0.4430.59", actual.version)
            assertEquals(91231777L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-04-07T12:06:47Z", ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.*
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.File
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class FirefoxBetaIT {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var packageManager: PackageManager
    private var packageInfo = PackageInfo()

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        packageInfo.versionName = ""
        every {
            packageManager.getPackageInfo(App.FIREFOX_BETA.detail.packageName, any())
        } returns packageInfo
        every { context.packageName } returns "de.marmaro.krt.ffupdater"
        every { context.getString(R.string.available_version, any()) } returns "/"
    }

    companion object {
        const val BASE_URL = "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/" +
                "mobile.v2.fenix.beta.latest"
        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeChainOfTrustTextAvailableUnderUrl(url: String) {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/FirefoxBeta/" +
                "chain_of_trust.log"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, String::class)
        } returns File(path).readText()
    }

    @Test
    fun updateCheck_armeabiv7a_upToDate() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.armeabi-v7a/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        packageInfo.versionName = "91.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertFalse(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.armeabi-v7a/artifacts/public/build/armeabi-v7a/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_armeabiv7a_updateAvailable() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.armeabi-v7a/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        packageInfo.versionName = "86.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertTrue(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.armeabi-v7a/artifacts/public/build/armeabi-v7a/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_arm64v8a_upToDate() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.arm64-v8a/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
        packageInfo.versionName = "91.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertFalse(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.arm64-v8a/artifacts/public/build/arm64-v8a/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_arm64v8a_updateAvailable() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.arm64-v8a/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
        packageInfo.versionName = "86.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertTrue(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.arm64-v8a/artifacts/public/build/arm64-v8a/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_x86_upToDate() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.x86/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.X86)
        packageInfo.versionName = "91.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertFalse(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.x86/artifacts/public/build/x86/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_x86_updateAvailable() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.x86/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.X86)
        packageInfo.versionName = "86.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertTrue(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.x86/artifacts/public/build/x86/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_x8664_upToDate() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.x86_64/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.X86_64)
        packageInfo.versionName = "91.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertFalse(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.x86_64/artifacts/public/build/x86_64/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }

    @Test
    fun updateCheck_x8664_updateAvailable() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.x86_64/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.X86_64)
        packageInfo.versionName = "86.0.0-beta.3"

        val actual = runBlocking { FirefoxBeta().updateCheck(context) }

        assertTrue(actual.isUpdateAvailable)
        assertEquals("91.0.0-beta.3", actual.version)
        val expectedUrl = "$BASE_URL.x86_64/artifacts/public/build/x86_64/target.apk"
        assertEquals(expectedUrl, actual.downloadUrl)
        val expectedDate = ZonedDateTime.parse("2021-07-22T13:29:07Z", ISO_ZONED_DATE_TIME)
        assertEquals(expectedDate, actual.publishDate)
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.google.gson.Gson
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.app.impl.fetch.github.GithubConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import junit.framework.Assert.assertFalse
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.FileReader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class FirefoxFocusIT {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var packageManager: PackageManager
    private var packageInfo = PackageInfo()

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        packageInfo.versionName = ""
        every {
            packageManager.getPackageInfo(App.FIREFOX_FOCUS.detail.packageName, any())
        } returns packageInfo
        every { context.getString(R.string.available_version, any()) } returns "/"
        every { context.packageName } returns "de.marmaro.krt.ffupdater"
    }

    companion object {
        const val DOWNLOAD_URL =
            "https://github.com/mozilla-mobile/focus-android/releases/download/v92.1.1"

        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeReleaseJsonObjectAvailable() {
        val url = "https://api.github.com/repos/mozilla-mobile/focus-android/releases/latest"
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/FirefoxFocus/latest.json"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, GithubConsumer.Release::class)
        } returns Gson().fromJson(FileReader(path), GithubConsumer.Release::class.java)
    }

    @Test
    fun updateCheck_armeabiv7a() {
        makeReleaseJsonObjectAvailable()
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        val expectedUrl = "$DOWNLOAD_URL/Focus-arm.apk"
        val expectedTime = ZonedDateTime.parse("2021-09-04T18:12:23Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "92.1.1"
            val actual = FirefoxFocus().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "92.1.0"
            val actual = FirefoxFocus().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }

    @Test
    fun updateCheck_arm64v8a() {
        makeReleaseJsonObjectAvailable()
        every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
        val expectedUrl = "$DOWNLOAD_URL/Focus-arm64.apk"
        val expectedTime = ZonedDateTime.parse("2021-09-04T18:12:23Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "92.1.1"
            val actual = FirefoxFocus().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "92.1.0"
            val actual = FirefoxFocus().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.google.gson.Gson
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.app.impl.fetch.github.GithubConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.*
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.FileReader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class FirefoxKlarIT {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var packageManager: PackageManager
    private var packageInfo = PackageInfo()

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        packageInfo.versionName = ""
        every {
            packageManager.getPackageInfo(App.FIREFOX_KLAR.detail.packageName, any())
        } returns packageInfo
        every { context.getString(R.string.available_version, any()) } returns "/"
        every { context.packageName } returns "de.marmaro.krt.ffupdater"
    }

    companion object {
        const val DOWNLOAD_URL =
            "https://github.com/mozilla-mobile/focus-android/releases/download/v92.1.1"
        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeReleaseJsonObjectAvailable() {
        val url = "https://api.github.com/repos/mozilla-mobile/focus-android/releases/latest"
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/FirefoxFocus/latest.json"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, GithubConsumer.Release::class)
        } returns Gson().fromJson(FileReader(path), GithubConsumer.Release::class.java)
    }

    @Test
    fun updateCheck_armeabiv7a() {
        makeReleaseJsonObjectAvailable()
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        val expectedUrl = "$DOWNLOAD_URL/Klar-arm.apk"
        val expectedTime = ZonedDateTime.parse("2021-09-04T18:12:23Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "92.1.1"
            val actual = FirefoxKlar().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "92.1.0"
            val actual = FirefoxKlar().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }

    @Test
    fun updateCheck_arm64v8a() {
        makeReleaseJsonObjectAvailable()
        every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
        val expectedUrl = "$DOWNLOAD_URL/Klar-arm64.apk"
        val expectedTime = ZonedDateTime.parse("2021-09-04T18:12:23Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "92.1.1"
            val actual = FirefoxKlar().updateCheck(context)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "92.1.0"
            val actual = FirefoxKlar().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("92.1.1", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder
import com.google.gson.Gson
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.app.impl.fetch.mozillaci.MozillaCiJsonConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.*
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.BufferedReader
import java.io.FileReader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class FirefoxNightlyIT {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var packageManager: PackageManager

    private var packageInfo = PackageInfo()
    private val sharedPreferences = SPMockBuilder().createSharedPreferences()

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        every { context.getString(R.string.available_version, any()) } returns "/"
        every { context.packageName } returns "de.marmaro.krt.ffupdater"
        every { context.getSharedPreferences(any(), any()) } returns sharedPreferences

        every {
            packageManager.getPackageInfo(
                App.FIREFOX_NIGHTLY.detail.packageName,
                0
            )
        } returns packageInfo
    }

    companion object {
        const val BASE_URL = "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/" +
                "mobile.v2.fenix.nightly.latest"

        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeChainOfTrustAvailableUnderUrl(url: String) {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/FirefoxNightly/" +
                "chain-of-trust.json"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, MozillaCiJsonConsumer.ChainOfTrustJson::class)
        } returns Gson().fromJson(
            BufferedReader(FileReader(path)),
            MozillaCiJsonConsumer.ChainOfTrustJson::class.java
        )
    }

    @Test
    fun updateCheck_armeabiv7a_checkMetadata() {
        checkMetadata(
            ABI.ARMEABI_V7A,
            "armeabi-v7a",
            "16342c966a2dff3561215f7bc91ea8ae0fb1902d6c533807667b224c3b87a972"
        )
    }

    @Test
    fun updateCheck_armeabiv7a_checkForUpdates_noUpdates() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("16342c966a2dff3561215f7bc91ea8ae0fb1902d6c533807667b224c3b87a972")
        assertFalse(checkForUpdates(ABI.ARMEABI_V7A, "armeabi-v7a"))
    }

    @Test
    fun updateCheck_armeabiv7a_checkForUpdates_oldVersionInstalled() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("16342c966a2dff3561215f7bc91ea8ae0fb1902d6c533807667b224c3b87a972")
        assertTrue(checkForUpdates(ABI.ARMEABI_V7A, "armeabi-v7a"))
    }

    @Test
    fun updateCheck_armeabiv7a_checkForUpdates_updateAvailable() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.ARMEABI_V7A, "armeabi-v7a"))
    }

    @Test
    fun updateCheck_armeabiv7a_checkForUpdates_oldVersionAndUpdateAvailable() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.ARMEABI_V7A, "armeabi-v7a"))
    }

    @Test
    fun updateCheck_arm64v8a_checkMetadata() {
        checkMetadata(
            ABI.ARM64_V8A, "arm64-v8a",
            "ae53fdf802bdcd2ea95a853a60f9ba9f621fb10d30dcc98dccfd80df4eba20fc"
        )
    }

    @Test
    fun updateCheck_arm64v8a_checkForUpdates_noUpdates() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("ae53fdf802bdcd2ea95a853a60f9ba9f621fb10d30dcc98dccfd80df4eba20fc")
        assertFalse(checkForUpdates(ABI.ARM64_V8A, "arm64-v8a"))
    }

    @Test
    fun updateCheck_arm64v8a_checkForUpdates_oldVersionInstalled() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("ae53fdf802bdcd2ea95a853a60f9ba9f621fb10d30dcc98dccfd80df4eba20fc")
        assertTrue(checkForUpdates(ABI.ARM64_V8A, "arm64-v8a"))
    }

    @Test
    fun updateCheck_arm64v8a_checkForUpdates_updateAvailable() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.ARM64_V8A, "arm64-v8a"))
    }

    @Test
    fun updateCheck_arm64v8a_checkForUpdates_oldVersionAndUpdateAvailable() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.ARM64_V8A, "arm64-v8a"))
    }

    @Test
    fun updateCheck_x86_checkMetadata() {
        checkMetadata(
            ABI.X86, "x86",
            "3c81530f5a89596c03421a08f5dab8dd6db0a3fcc7063e59b5fd42874f0a7499"
        )
    }

    @Test
    fun updateCheck_x86_checkForUpdates_noUpdates() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("3c81530f5a89596c03421a08f5dab8dd6db0a3fcc7063e59b5fd42874f0a7499")
        assertFalse(checkForUpdates(ABI.X86, "x86"))
    }

    @Test
    fun updateCheck_x86_checkForUpdates_oldVersionInstalled() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("3c81530f5a89596c03421a08f5dab8dd6db0a3fcc7063e59b5fd42874f0a7499")
        assertTrue(checkForUpdates(ABI.X86, "x86"))
    }

    @Test
    fun updateCheck_x86_checkForUpdates_updateAvailable() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.X86, "x86"))
    }

    @Test
    fun updateCheck_x86_checkForUpdates_oldVersionAndUpdateAvailable() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.X86, "x86"))
    }

    @Test
    fun updateCheck_x8664_checkMetadata() {
        checkMetadata(
            ABI.X86_64, "x86_64",
            "4690f2580199423822ca8323b0235cdbaac480f04bc6f21aa7f17636cd42662c"
        )
    }

    @Test
    fun updateCheck_x8664_checkForUpdates_noUpdates() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("4690f2580199423822ca8323b0235cdbaac480f04bc6f21aa7f17636cd42662c")
        assertFalse(checkForUpdates(ABI.X86_64, "x86_64"))
    }

    @Test
    fun updateCheck_x8664_checkForUpdates_oldVersionInstalled() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("4690f2580199423822ca8323b0235cdbaac480f04bc6f21aa7f17636cd42662c")
        assertTrue(checkForUpdates(ABI.X86_64, "x86_64"))
    }

    @Test
    fun updateCheck_x8664_checkForUpdates_updateAvailable() {
        setInstalledVersionCode(1000)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.X86_64, "x86_64"))
    }

    @Test
    fun updateCheck_x8664_checkForUpdates_oldVersionAndUpdateAvailable() {
        setInstalledVersionCode(999)
        setInstalledSha256Hash("0000000000000000000000000000000000000000000000000000000000000000")
        assertTrue(checkForUpdates(ABI.X86_64, "x86_64"))
    }

    private fun checkMetadata(abi: ABI, abiString: String, hash: String) {
        makeChainOfTrustAvailableUnderUrl("$BASE_URL.$abiString/artifacts/public/chain-of-trust.json")
        every { DeviceEnvironment.abis } returns listOf(abi)
        val actual = runBlocking { FirefoxNightly().updateCheck(context) }

        assertEquals("2021-05-06 17:02", actual.version)
        assertEquals(
            "$BASE_URL.$abiString/artifacts/public/build/$abiString/target.apk",
            actual.downloadUrl
        )
        assertEquals(
            ZonedDateTime.parse("2021-05-06T17:02:55.865Z", ISO_ZONED_DATE_TIME),
            actual.publishDate
        )
        assertEquals(hash, actual.fileHash?.hexValue)
    }

    private fun checkForUpdates(abi: ABI, abiString: String): Boolean {
        makeChainOfTrustAvailableUnderUrl("$BASE_URL.$abiString/artifacts/public/chain-of-trust.json")
        every { DeviceEnvironment.abis } returns listOf(abi)
        packageInfo.versionCode = 1000

        val actual = runBlocking { FirefoxNightly().updateCheck(context) }
        return actual.isUpdateAvailable
    }

    private fun setInstalledVersionCode(versionCode: Long) {
        sharedPreferences.edit().putLong(FirefoxNightly.INSTALLED_VERSION_CODE, versionCode).apply()
    }

    private fun setInstalledSha256Hash(sha256Hash: String) {
        sharedPreferences.edit().putString(FirefoxNightly.INSTALLED_SHA256_HASH, sha256Hash).apply()
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.*
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.File
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME

class FirefoxReleaseIT {
    @MockK
    private lateinit var context: Context

    @MockK
    private lateinit var packageManager: PackageManager
    private var packageInfo = PackageInfo()

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        packageInfo.versionName = ""
        every {
            packageManager.getPackageInfo(App.FIREFOX_RELEASE.detail.packageName, any())
        } returns packageInfo
        every { context.getString(R.string.available_version, any()) } returns "/"
        every { context.packageName } returns "de.marmaro.krt.ffupdater"
    }

    companion object {
        const val BASE_URL = "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/" +
                "mobile.v2.fenix.release.latest"

        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeChainOfTrustTextAvailableUnderUrl(url: String) {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/FirefoxRelease/" +
                "chain-of-trust.log"
        coEvery {
            ApiConsumer.consumeNetworkResource(url, String::class)
        } returns File(path).readText()
    }

    @Test
    fun updateCheck_armeabiv7a() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.armeabi-v7a/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
        val expectedUrl = "$BASE_URL.armeabi-v7a/artifacts/public/build/armeabi-v7a/target.apk"
        val expectedTime = ZonedDateTime.parse("2021-07-19T15:07:50.886Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "90.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "85.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }

    @Test
    fun updateCheck_arm64v8a() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.arm64-v8a/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
        val expectedUrl = "$BASE_URL.arm64-v8a/artifacts/public/build/arm64-v8a/target.apk"
        val expectedTime = ZonedDateTime.parse("2021-07-19T15:07:50.886Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "90.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "85.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }

    @Test
    fun updateCheck_x86() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.x86/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.X86)
        val expectedUrl = "$BASE_URL.x86/artifacts/public/build/x86/target.apk"
        val expectedTime = ZonedDateTime.parse("2021-07-19T15:07:50.886Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "90.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "85.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }

    @Test
    fun updateCheck_x8664() {
        makeChainOfTrustTextAvailableUnderUrl("$BASE_URL.x86_64/artifacts/public/logs/chain_of_trust.log")
        every { DeviceEnvironment.abis } returns listOf(ABI.X86_64)
        val expectedUrl = "$BASE_URL.x86_64/artifacts/public/build/x86_64/target.apk"
        val expectedTime = ZonedDateTime.parse("2021-07-19T15:07:50.886Z", ISO_ZONED_DATE_TIME)

        runBlocking {
            packageInfo.versionName = "90.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }

        runBlocking {
            packageInfo.versionName = "85.1.2"
            val actual = FirefoxRelease().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("90.1.2", actual.version)
            assertEquals(expectedUrl, actual.downloadUrl)
            assertEquals(expectedTime, actual.publishDate)
        }
    }
}


code:
package de.marmaro.krt.ffupdater.app.impl

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.google.gson.Gson
import de.marmaro.krt.ffupdater.R
import de.marmaro.krt.ffupdater.app.App
import de.marmaro.krt.ffupdater.app.impl.fetch.ApiConsumer
import de.marmaro.krt.ffupdater.app.impl.fetch.github.GithubConsumer
import de.marmaro.krt.ffupdater.device.ABI
import de.marmaro.krt.ffupdater.device.DeviceEnvironment
import io.mockk.*
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.*
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import java.io.FileReader
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter

class IceravenIT {
    @MockK
    lateinit var context: Context

    @MockK
    lateinit var packageManager: PackageManager

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxUnitFun = true)
        every { context.packageManager } returns packageManager
        every { context.getString(R.string.available_version, any()) } returns "/"
    }

    companion object {
        const val API_URL = "https://api.github.com/repos/fork-maintainers/iceraven-browser/" +
                "releases/latest"
        const val DOWNLOAD_URL = "https://github.com/fork-maintainers/iceraven-browser/releases/" +
                "download/iceraven-1.6.0"

        @JvmStatic
        @BeforeClass
        fun beforeTests() {
            mockkObject(ApiConsumer)
            mockkObject(DeviceEnvironment)
        }

        @JvmStatic
        @AfterClass
        fun afterTests() {
            unmockkObject(ApiConsumer)
            unmockkObject(DeviceEnvironment)
        }
    }

    private fun makeReleaseJsonObjectAvailable() {
        val path = "src/test/resources/de/marmaro/krt/ffupdater/app/impl/Iceraven/latest.json"
        coEvery {
            ApiConsumer.consumeNetworkResource(API_URL, GithubConsumer.Release::class)
        } returns Gson().fromJson(FileReader(path), GithubConsumer.Release::class.java)
    }

    @Test
    fun updateCheck_latestRelease_checkDownloadUrlForABI() {
        makeReleaseJsonObjectAvailable()
        val packageInfo = PackageInfo()
        packageInfo.versionName = "1.19.92"
        every {
            packageManager.getPackageInfo(App.ICERAVEN.detail.packageName, any())
        } returns packageInfo

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)
            assertEquals(
                "$DOWNLOAD_URL/iceraven-1.6.0-browser-armeabi-v7a-forkRelease.apk",
                Iceraven().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.ARM64_V8A)
            assertEquals(
                "$DOWNLOAD_URL/iceraven-1.6.0-browser-arm64-v8a-forkRelease.apk",
                Iceraven().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86)
            assertEquals(
                "$DOWNLOAD_URL/iceraven-1.6.0-browser-x86-forkRelease.apk",
                Iceraven().updateCheck(context).downloadUrl
            )
        }

        runBlocking {
            every { DeviceEnvironment.abis } returns listOf(ABI.X86_64)
            assertEquals(
                "$DOWNLOAD_URL/iceraven-1.6.0-browser-x86_64-forkRelease.apk",
                Iceraven().updateCheck(context).downloadUrl
            )
        }
    }

    @Test
    fun updateCheck_latestRelease_updateCheck() {
        makeReleaseJsonObjectAvailable()
        val packageInfo = PackageInfo()
        every {
            packageManager.getPackageInfo(App.ICERAVEN.detail.packageName, any())
        } returns packageInfo
        every { DeviceEnvironment.abis } returns listOf(ABI.ARMEABI_V7A)

        // installed app is up-to-date
        runBlocking {
            packageInfo.versionName = "iceraven-1.6.0"
            val actual = Iceraven().updateCheck(context)
            assertFalse(actual.isUpdateAvailable)
            assertEquals("1.6.0", actual.version)
            assertEquals(66150140L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-02-07T00:37:13Z", DateTimeFormatter.ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }

        // installed app is old
        runBlocking {
            packageInfo.versionName = "iceraven-1.5.0"
            val actual = Iceraven().updateCheck(context)
            assertTrue(actual.isUpdateAvailable)
            assertEquals("1.6.0", actual.version)
            assertEquals(66150140L, actual.fileSizeBytes)
            assertEquals(
                ZonedDateTime.parse("2021-02-07T00:37:13Z", DateTimeFormatter.ISO_ZONED_DATE_TIME),
                actual.publishDate
            )
        }
    }
}