1616#include " cuttlefish/host/libs/web/android_build_api.h"
1717
1818#include < dirent.h>
19+ #include < time.h>
1920#include < unistd.h>
2021
22+ #include < algorithm>
2123#include < chrono>
24+ #include < iomanip>
2225#include < memory>
2326#include < optional>
27+ #include < ranges>
2428#include < set>
29+ #include < sstream>
2530#include < string>
2631#include < string_view>
2732#include < thread>
@@ -261,8 +266,24 @@ Result<std::vector<std::string>> AndroidBuildApi::Headers() {
261266 return headers;
262267}
263268
269+ static Result<std::chrono::system_clock::time_point> ParseTime (
270+ std::string_view time_str) {
271+ std::stringstream stream = std::stringstream (std::string (time_str));
272+ tm time_tm;
273+ stream >> std::get_time (&time_tm, " %Y-%m-%dT%H:%M:%S." );
274+ CF_EXPECTF (!!stream, " Failed to parse time '{}'" , time_str);
275+
276+ return std::chrono::system_clock::from_time_t (mktime (&time_tm));
277+ }
278+
264279Result<std::optional<std::string>> AndroidBuildApi::LatestBuildId (
265280 const std::string& branch, const std::string& target) {
281+ struct CandidateBuild {
282+ std::string build_id;
283+ std::chrono::system_clock::time_point creation_time;
284+ };
285+ // Find the latest build at every safe level
286+ std::vector<CandidateBuild> candidates;
266287 for (const SafeLevel safe_level : kAllSafeLevels ) {
267288 VLOG (0 ) << " Attempting to download build at safe level '" << safe_level
268289 << " ' for branch '" << branch << " ' and target '" << target << " '" ;
@@ -276,7 +297,7 @@ Result<std::optional<std::string>> AndroidBuildApi::LatestBuildId(
276297 if (!json_res.ok ()) {
277298 continue ;
278299 }
279- const Json::Value json = *json_res;
300+ const Json::Value& json = *json_res;
280301
281302 if (!json.isMember (" builds" )) {
282303 continue ;
@@ -287,9 +308,30 @@ Result<std::optional<std::string>> AndroidBuildApi::LatestBuildId(
287308 " target \" {}\" in the response array, "
288309 " but found {}" ,
289310 branch, target, json[" builds" ].size ());
290- return CF_EXPECT (GetValue<std::string>(json[" builds" ][0 ], {" buildId" }));
311+
312+ const Json::Value& build = json[" builds" ][0 ];
313+ const std::string& completion = build[" completionTimestamp" ].asString ();
314+ candidates.emplace_back (CandidateBuild{
315+ .build_id = build[" buildId" ].asString (),
316+ .creation_time = CF_EXPECT (ParseTime (completion)),
317+ });
318+ }
319+ if (candidates.empty ()) {
320+ return std::nullopt ;
291321 }
292- return std::nullopt ;
322+ // Drop candidate builds older than 1 week
323+ auto creation = [](const auto & cd) { return cd.creation_time ; };
324+ std::chrono::system_clock::time_point latest =
325+ std::ranges::max (std::views::transform (candidates, creation));
326+ // NOLINTNEXTLINE(misc-include-cleaner): <chrono> provides std::chrono::weeks
327+ static constexpr std::chrono::weeks kWeek = std::chrono::weeks (1 );
328+ auto recent = [latest](const auto & cd) {
329+ return latest - cd.creation_time < kWeek ;
330+ };
331+ auto recent_candidates = std::views::filter (candidates, recent);
332+ CF_EXPECT (!std::ranges::empty (recent_candidates));
333+ // Return the first valid build (which will have the highest safe level)
334+ return recent_candidates.begin ()->build_id ;
293335}
294336
295337Result<std::unordered_set<std::string>> AndroidBuildApi::Artifacts (
0 commit comments