| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- #include <update/update.h>
- #include <storage/storage.h>
- static bool update_is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
- static bool update_save_char(
- const char *path_name, const char *value)
- {
- if (!value)
- {
- return false;
- }
- // Create the directory for saving settings
- char directory_path[256];
- snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s", APP_ID);
- // Create the directory
- Storage *storage = furi_record_open(RECORD_STORAGE);
- storage_common_mkdir(storage, directory_path);
- snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data", APP_ID);
- storage_common_mkdir(storage, directory_path);
- // Open the settings file
- File *file = storage_file_alloc(storage);
- char file_path[256];
- snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data/%s.txt", APP_ID, path_name);
- // Open the file in write mode
- if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
- {
- FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- return false;
- }
- // Write the data to the file
- size_t data_size = strlen(value) + 1; // Include null terminator
- if (storage_file_write(file, value, data_size) != data_size)
- {
- FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
- storage_file_close(file);
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- return false;
- }
- storage_file_close(file);
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- return true;
- }
- static bool update_load_char(
- const char *path_name,
- char *value,
- size_t value_size)
- {
- if (!value)
- {
- return false;
- }
- Storage *storage = furi_record_open(RECORD_STORAGE);
- File *file = storage_file_alloc(storage);
- char file_path[256];
- snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data/%s.txt", APP_ID, path_name);
- // Open the file for reading
- if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
- {
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- return false;
- }
- // Read data into the buffer
- size_t read_count = storage_file_read(file, value, value_size);
- if (storage_file_get_error(file) != FSE_OK)
- {
- FURI_LOG_E(HTTP_TAG, "Error reading from file.");
- storage_file_close(file);
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- return false;
- }
- // Ensure null-termination
- value[read_count - 1] = '\0';
- storage_file_close(file);
- storage_file_free(file);
- furi_record_close(RECORD_STORAGE);
- return strlen(value) > 0;
- }
- static bool update_json_to_datetime(DateTime *rtc_time, FuriString *str)
- {
- if (!rtc_time || !str)
- {
- FURI_LOG_E(TAG, "rtc_time or str is NULL");
- return false;
- }
- FuriString *hour = get_json_value_furi("hour", str);
- if (hour)
- {
- rtc_time->hour = atoi(furi_string_get_cstr(hour));
- furi_string_free(hour);
- }
- FuriString *minute = get_json_value_furi("minute", str);
- if (minute)
- {
- rtc_time->minute = atoi(furi_string_get_cstr(minute));
- furi_string_free(minute);
- }
- FuriString *second = get_json_value_furi("second", str);
- if (second)
- {
- rtc_time->second = atoi(furi_string_get_cstr(second));
- furi_string_free(second);
- }
- FuriString *day = get_json_value_furi("day", str);
- if (day)
- {
- rtc_time->day = atoi(furi_string_get_cstr(day));
- furi_string_free(day);
- }
- FuriString *month = get_json_value_furi("month", str);
- if (month)
- {
- rtc_time->month = atoi(furi_string_get_cstr(month));
- furi_string_free(month);
- }
- FuriString *year = get_json_value_furi("year", str);
- if (year)
- {
- rtc_time->year = atoi(furi_string_get_cstr(year));
- furi_string_free(year);
- }
- FuriString *weekday = get_json_value_furi("weekday", str);
- if (weekday)
- {
- rtc_time->weekday = atoi(furi_string_get_cstr(weekday));
- furi_string_free(weekday);
- }
- return datetime_validate_datetime(rtc_time);
- }
- static FuriString *update_datetime_to_json(DateTime *rtc_time)
- {
- if (!rtc_time)
- {
- FURI_LOG_E(TAG, "rtc_time is NULL");
- return NULL;
- }
- char json[256];
- snprintf(
- json,
- sizeof(json),
- "{\"hour\":%d,\"minute\":%d,\"second\":%d,\"day\":%d,\"month\":%d,\"year\":%d,\"weekday\":%d}",
- rtc_time->hour,
- rtc_time->minute,
- rtc_time->second,
- rtc_time->day,
- rtc_time->month,
- rtc_time->year,
- rtc_time->weekday);
- return furi_string_alloc_set_str(json);
- }
- static bool update_save_rtc_time(DateTime *rtc_time)
- {
- if (!rtc_time)
- {
- FURI_LOG_E(TAG, "rtc_time is NULL");
- return false;
- }
- FuriString *json = update_datetime_to_json(rtc_time);
- if (!json)
- {
- FURI_LOG_E(TAG, "Failed to convert DateTime to JSON");
- return false;
- }
- update_save_char("last_checked", furi_string_get_cstr(json));
- furi_string_free(json);
- return true;
- }
- //
- // Returns true if time_current is one hour (or more) later than the stored last_checked time
- //
- static bool update_is_update_time(DateTime *time_current)
- {
- if (!time_current)
- {
- FURI_LOG_E(TAG, "time_current is NULL");
- return false;
- }
- char last_checked_old[128];
- if (!update_load_char("last_checked", last_checked_old, sizeof(last_checked_old)))
- {
- FURI_LOG_E(TAG, "Failed to load last_checked");
- FuriString *json = update_datetime_to_json(time_current);
- if (json)
- {
- update_save_char("last_checked", furi_string_get_cstr(json));
- furi_string_free(json);
- }
- return false;
- }
- DateTime last_updated_time;
- FuriString *last_updated_furi = char_to_furi_string(last_checked_old);
- if (!last_updated_furi)
- {
- FURI_LOG_E(TAG, "Failed to convert char to FuriString");
- return false;
- }
- if (!update_json_to_datetime(&last_updated_time, last_updated_furi))
- {
- FURI_LOG_E(TAG, "Failed to convert JSON to DateTime");
- furi_string_free(last_updated_furi);
- return false;
- }
- furi_string_free(last_updated_furi); // Free after usage.
- bool time_diff = false;
- // If the date is different assume more than one hour has passed.
- if (time_current->year != last_updated_time.year ||
- time_current->month != last_updated_time.month ||
- time_current->day != last_updated_time.day)
- {
- time_diff = true;
- }
- else
- {
- // For the same day, compute seconds from midnight.
- int seconds_current = time_current->hour * 3600 + time_current->minute * 60 + time_current->second;
- int seconds_last = last_updated_time.hour * 3600 + last_updated_time.minute * 60 + last_updated_time.second;
- if ((seconds_current - seconds_last) >= 3600)
- {
- time_diff = true;
- }
- }
- return time_diff;
- }
- // Sends a request to fetch the last updated date of the app.
- static bool update_last_app_update(FlipperHTTP *fhttp, bool flipper_server)
- {
- if (!fhttp)
- {
- FURI_LOG_E(TAG, "fhttp is NULL");
- return false;
- }
- char url[256];
- if (flipper_server)
- {
- // make sure folder is created
- char directory_path[256];
- snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s", APP_ID);
- // Create the directory
- Storage *storage = furi_record_open(RECORD_STORAGE);
- storage_common_mkdir(storage, directory_path);
- snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data", APP_ID);
- storage_common_mkdir(storage, directory_path);
- snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data/last_update_request.txt", APP_ID);
- storage_simply_remove_recursive(storage, directory_path); // ensure the file is empty
- furi_record_close(RECORD_STORAGE);
- fhttp->save_received_data = false;
- fhttp->is_bytes_request = true;
- snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/%s/data/last_update_request.txt", APP_ID);
- snprintf(url, sizeof(url), "https://raw.githubusercontent.com/flipperdevices/flipper-application-catalog/main/applications/%s/%s/manifest.yml", APP_FOLDER, FAP_ID);
- return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\":\"application/json\"}", NULL);
- }
- else
- {
- snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/last-updated/%s/", FAP_ID);
- return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
- }
- }
- static bool parse_yaml_version(const char *yaml, char *out_version, size_t out_len)
- {
- const char *p = strstr(yaml, "\nversion:");
- if (!p)
- {
- // maybe it's the very first line
- p = yaml;
- }
- else
- {
- // skip the “\n”
- p++;
- }
- // skip the key name and colon
- p = strstr(p, "version");
- if (!p)
- return false;
- p += strlen("version");
- // skip whitespace and colon
- while (*p == ' ' || *p == ':')
- p++;
- // handle optional quote
- bool quoted = (*p == '"');
- if (quoted)
- p++;
- // copy up until end‐quote or newline/space
- size_t i = 0;
- while (*p && i + 1 < out_len)
- {
- if ((quoted && *p == '"') ||
- (!quoted && (*p == '\n' || *p == ' ')))
- {
- break;
- }
- out_version[i++] = *p++;
- }
- out_version[i] = '\0';
- return (i > 0);
- }
- // Parses the server response and returns true if an update is available.
- static bool update_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current, bool flipper_server)
- {
- if (!fhttp)
- {
- FURI_LOG_E(TAG, "fhttp is NULL");
- return false;
- }
- if (fhttp->state == ISSUE)
- {
- FURI_LOG_E(TAG, "Failed to fetch last app update");
- return false;
- }
- char version_str[32];
- if (!flipper_server)
- {
- if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
- {
- FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
- return false;
- }
- char *app_version = get_json_value("version", fhttp->last_response);
- if (app_version)
- {
- // Save the server app version: it should save something like: 0.8
- update_save_char("server_app_version", app_version);
- snprintf(version_str, sizeof(version_str), "%s", app_version);
- free(app_version);
- }
- else
- {
- FURI_LOG_E(TAG, "Failed to get app version");
- return false;
- }
- }
- else
- {
- FuriString *manifest_data = flipper_http_load_from_file(fhttp->file_path);
- if (!manifest_data)
- {
- FURI_LOG_E(TAG, "Failed to load app data");
- return false;
- }
- // parse version out of the YAML
- if (!parse_yaml_version(furi_string_get_cstr(manifest_data), version_str, sizeof(version_str)))
- {
- FURI_LOG_E(TAG, "Failed to parse version from YAML manifest");
- return false;
- }
- update_save_char("server_app_version", version_str);
- furi_string_free(manifest_data);
- }
- // Only check for an update if an hour or more has passed.
- if (update_is_update_time(time_current))
- {
- char app_version[32];
- if (!update_load_char("app_version", app_version, sizeof(app_version)))
- {
- FURI_LOG_E(TAG, "Failed to load app version");
- return false;
- }
- // Check if the app version is different from the server version.
- if (!update_is_str(app_version, version_str))
- {
- easy_flipper_dialog("Update available", "New update available!\nPress BACK to download.");
- return true; // Update available.
- }
- FURI_LOG_I(TAG, "No update available");
- return false; // No update available.
- }
- FURI_LOG_I(TAG, "Not enough time has passed since the last update check");
- return false; // Not yet time to update.
- }
- static bool update_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
- {
- if (!fhttp)
- {
- FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
- return false;
- }
- char url[256];
- fhttp->save_received_data = false;
- fhttp->is_bytes_request = true;
- #ifndef FW_ORIGIN_Momentum
- snprintf(
- fhttp->file_path,
- sizeof(fhttp->file_path),
- STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", APP_FOLDER, FAP_ID);
- #else
- if (strlen(MOM_FOLDER) == 0)
- snprintf(
- fhttp->file_path,
- sizeof(fhttp->file_path),
- STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", APP_FOLDER, FAP_ID);
- else
- snprintf(
- fhttp->file_path,
- sizeof(fhttp->file_path),
- STORAGE_EXT_PATH_PREFIX "/apps/%s/%s/%s.fap", APP_FOLDER, MOM_FOLDER, FAP_ID);
- #endif
- if (flipper_server)
- {
- uint8_t target;
- target = furi_hal_version_get_hw_target();
- uint16_t api_major, api_minor;
- furi_hal_info_get_api_version(&api_major, &api_minor);
- snprintf(
- url,
- sizeof(url),
- "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
- BUILD_ID,
- target,
- api_major,
- api_minor);
- }
- else
- {
- snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/%s/", FAP_ID);
- }
- return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
- }
- // Updates the app. Uses the supplied current time for validating if update check should proceed.
- static bool update_update_app(FlipperHTTP *fhttp, DateTime *time_current, bool use_flipper_api)
- {
- if (!fhttp)
- {
- FURI_LOG_E(TAG, "fhttp is NULL");
- return false;
- }
- if (!update_last_app_update(fhttp, use_flipper_api))
- {
- FURI_LOG_E(TAG, "Failed to fetch last app update");
- return false;
- }
- fhttp->state = RECEIVING;
- furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS);
- while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
- {
- furi_delay_ms(100);
- }
- furi_timer_stop(fhttp->get_timeout_timer);
- if (update_parse_last_app_update(fhttp, time_current, use_flipper_api))
- {
- if (!update_get_fap_file(fhttp, false))
- {
- FURI_LOG_E(TAG, "Failed to fetch fap file 1");
- return false;
- }
- fhttp->state = RECEIVING;
- while (fhttp->state == RECEIVING)
- {
- furi_delay_ms(100);
- }
- if (fhttp->state == ISSUE)
- {
- FURI_LOG_E(TAG, "Failed to fetch fap file 2");
- easy_flipper_dialog("Update Error", "Failed to download the\nupdate file.\nPlease try again.");
- return false;
- }
- return true;
- }
- FURI_LOG_I(TAG, "No update available");
- return false; // No update available.
- }
- // Handles the app update routine. This function obtains the current RTC time,
- // checks the "last_checked" value, and if it is more than one hour old, calls for an update.
- bool update_is_ready(FlipperHTTP *fhttp, bool use_flipper_api)
- {
- if (!fhttp)
- {
- FURI_LOG_E(TAG, "fhttp is NULL");
- return false;
- }
- DateTime rtc_time;
- furi_hal_rtc_get_datetime(&rtc_time);
- char last_checked[32];
- if (!update_load_char("last_checked", last_checked, sizeof(last_checked)))
- {
- // First time – save the current time and check for an update.
- if (!update_save_rtc_time(&rtc_time))
- {
- FURI_LOG_E(TAG, "Failed to save RTC time");
- return false;
- }
- return update_update_app(fhttp, &rtc_time, use_flipper_api);
- }
- else
- {
- // Check if the current RTC time is at least one hour past the stored time.
- if (update_is_update_time(&rtc_time))
- {
- if (!update_update_app(fhttp, &rtc_time, use_flipper_api))
- {
- // save the last_checked for the next check.
- if (!update_save_rtc_time(&rtc_time))
- {
- FURI_LOG_E(TAG, "Failed to save RTC time");
- return false;
- }
- return false;
- }
- // Save the current time for the next check.
- if (!update_save_rtc_time(&rtc_time))
- {
- FURI_LOG_E(TAG, "Failed to save RTC time");
- return false;
- }
- return true;
- }
- return false; // No update necessary.
- }
- }
|