[go: nahoru, domu]

Add birch weather provider

Adds a birch item struct for weather, with some updates to the generic
BirchItem - adds an icon image model, and changes the title to u16
string.

Introduces birch weather provider, that uses ambient mode backend to
query current weather state - not using the ambient mode weather model
at this point so the network requests tags when downloading icon are
different, and eventually, the backend call will be updated to use a
different weather client ID. Also, birch currently does not use polling
for weather changes.

Still todo - check whether geolocation is enabled before requesting
weather.

BUG= b/323229328

Change-Id: I5494f0afe697be5d053b201b494a0e745d114fd6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5255822
Reviewed-by: Matthew Mourgos <mmourgos@chromium.org>
Reviewed-by: Eric Sum <esum@google.com>
Commit-Queue: Toni Barzic <tbarzic@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1256514}
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index a063051..9b6c3be 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -333,6 +333,8 @@
     "birch/birch_item.h",
     "birch/birch_model.cc",
     "birch/birch_model.h",
+    "birch/birch_weather_provider.cc",
+    "birch/birch_weather_provider.h",
     "bluetooth_devices_observer.cc",
     "bluetooth_devices_observer.h",
     "booting/booting_animation_controller.cc",
@@ -3314,6 +3316,7 @@
     "assistant/util/deep_link_util_unittest.cc",
     "assistant/util/resource_util_unittest.cc",
     "birch/birch_model_unittest.cc",
+    "birch/birch_weather_provider_unittest.cc",
     "bubble/bubble_event_filter_unittest.cc",
     "bubble/bubble_utils_unittest.cc",
     "capture_mode/capture_audio_mixing_unittests.cc",
diff --git a/ash/ambient/backdrop/ambient_backend_controller_impl.cc b/ash/ambient/backdrop/ambient_backend_controller_impl.cc
index 3268872..06be7bc 100644
--- a/ash/ambient/backdrop/ambient_backend_controller_impl.cc
+++ b/ash/ambient/backdrop/ambient_backend_controller_impl.cc
@@ -219,6 +219,8 @@
   WeatherInfo weather_info;
   const auto& list_result = result.GetList();
 
+  weather_info.condition_description = GetStringValue(
+      list_result, backdrop::WeatherInfo::kConditionDescriptionFieldNumber);
   weather_info.condition_icon_url = GetStringValue(
       list_result, backdrop::WeatherInfo::kConditionIconUrlFieldNumber);
   weather_info.temp_f =
diff --git a/ash/birch/birch_item.cc b/ash/birch/birch_item.cc
index d45c5cf..91b668b 100644
--- a/ash/birch/birch_item.cc
+++ b/ash/birch/birch_item.cc
@@ -12,13 +12,15 @@
 
 namespace ash {
 
-BirchItem::BirchItem(const std::string& title) : title(title) {}
+BirchItem::BirchItem(const std::u16string& title, ui::ImageModel icon)
+    : title(title), icon(std::move(icon)) {}
 
 BirchItem::~BirchItem() = default;
 
 BirchFileItem::BirchFileItem(const base::FilePath& file_path,
                              const std::optional<base::Time>& timestamp)
-    : BirchItem(file_path.BaseName().value()),
+    : BirchItem(base::UTF8ToUTF16(file_path.BaseName().value()),
+                ui::ImageModel()),
       file_path(file_path),
       timestamp(timestamp) {}
 
@@ -26,21 +28,38 @@
 
 std::string BirchFileItem::ToString() const {
   std::stringstream ss;
-  ss << "title: " << title << ", file_path:" << file_path;
+  ss << "File item : {title: " << base::UTF16ToUTF8(title)
+     << ", file_path:" << file_path;
   if (timestamp.has_value()) {
     ss << ", timestamp: "
        << base::UTF16ToUTF8(
               base::TimeFormatShortDateAndTime(timestamp.value()));
   }
+  ss << "}";
   return ss.str();
 }
 
-BirchTabItem::BirchTabItem(const std::string& title,
+BirchWeatherItem::BirchWeatherItem(const std::u16string& weather_description,
+                                   const std::u16string& temperature,
+                                   ui::ImageModel icon)
+    : BirchItem(weather_description, std::move(icon)),
+      temperature(temperature) {}
+
+BirchWeatherItem::~BirchWeatherItem() = default;
+
+std::string BirchWeatherItem::ToString() const {
+  std::stringstream ss;
+  ss << "Weather item: {title: " << base::UTF16ToUTF8(title)
+     << ", temperature:" << base::UTF16ToUTF8(temperature) << "}";
+  return ss.str();
+}
+
+BirchTabItem::BirchTabItem(const std::u16string& title,
                            const GURL& url,
                            const base::Time& timestamp,
                            const GURL& favicon_url,
                            const std::string& session_name)
-    : BirchItem(title),
+    : BirchItem(title, ui::ImageModel()),
       url(url),
       timestamp(timestamp),
       favicon_url(favicon_url),
@@ -52,8 +71,9 @@
 
 std::string BirchTabItem::ToString() const {
   std::stringstream ss;
-  ss << "title: " << title << ", url:" << url << ", timestamp:" << timestamp
-     << ", favicon_url:" << favicon_url << ", session_name:" << session_name;
+  ss << "title: " << base::UTF16ToUTF8(title) << ", url:" << url
+     << ", timestamp:" << timestamp << ", favicon_url:" << favicon_url
+     << ", session_name:" << session_name;
   return ss.str();
 }
 
diff --git a/ash/birch/birch_item.h b/ash/birch/birch_item.h
index fb87c24..23d54d0 100644
--- a/ash/birch/birch_item.h
+++ b/ash/birch/birch_item.h
@@ -11,20 +11,22 @@
 #include "ash/ash_export.h"
 #include "base/files/file_path.h"
 #include "base/time/time.h"
+#include "ui/base/models/image_model.h"
 #include "url/gurl.h"
 
 namespace ash {
 
 // The base item which is stored by the birch model.
 struct ASH_EXPORT BirchItem {
-  explicit BirchItem(const std::string& title);
+  BirchItem(const std::u16string& title, const ui::ImageModel icon);
   BirchItem(BirchItem&&) = default;
   BirchItem(const BirchItem&);
   BirchItem& operator=(const BirchItem&);
   ~BirchItem();
   bool operator==(const BirchItem& rhs) const = default;
 
-  const std::string title;
+  const std::u16string title;
+  const ui::ImageModel icon;
 };
 
 // A birch item which contains file path and time information.
@@ -32,8 +34,8 @@
   BirchFileItem(const base::FilePath& file_path,
                 const std::optional<base::Time>& timestamp);
   BirchFileItem(BirchFileItem&&) = default;
-  BirchFileItem(const BirchFileItem&);
-  BirchFileItem& operator=(const BirchFileItem&);
+  BirchFileItem(const BirchFileItem&) = delete;
+  BirchFileItem& operator=(const BirchFileItem&) = delete;
   bool operator==(const BirchFileItem& rhs) const = default;
   ~BirchFileItem();
 
@@ -46,7 +48,7 @@
 
 // A birch item which contains tab and session information.
 struct ASH_EXPORT BirchTabItem : public BirchItem {
-  BirchTabItem(const std::string& title,
+  BirchTabItem(const std::u16string& title,
                const GURL& url,
                const base::Time& timestamp,
                const GURL& favicon_url,
@@ -66,6 +68,22 @@
   std::string ToString() const;
 };
 
+struct ASH_EXPORT BirchWeatherItem : public BirchItem {
+  BirchWeatherItem(const std::u16string& weather_description,
+                   const std::u16string& temperature,
+                   ui::ImageModel icon);
+  BirchWeatherItem(BirchWeatherItem&&) = default;
+  BirchWeatherItem(const BirchWeatherItem&) = delete;
+  BirchWeatherItem& operator=(const BirchWeatherItem&) = delete;
+  bool operator==(const BirchWeatherItem& rhs) const = default;
+  ~BirchWeatherItem();
+
+  const std::u16string temperature;
+
+  // Intended for debugging.
+  std::string ToString() const;
+};
+
 }  // namespace ash
 
 #endif  // ASH_BIRCH_BIRCH_ITEM_H_
diff --git a/ash/birch/birch_model.cc b/ash/birch/birch_model.cc
index b388f63..9f9ed535 100644
--- a/ash/birch/birch_model.cc
+++ b/ash/birch/birch_model.cc
@@ -4,9 +4,16 @@
 
 #include "ash/birch/birch_model.h"
 
+#include "ash/birch/birch_weather_provider.h"
+#include "ash/constants/ash_features.h"
+
 namespace ash {
 
-BirchModel::BirchModel() = default;
+BirchModel::BirchModel() {
+  if (features::IsBirchWeatherEnabled()) {
+    weather_provider_ = std::make_unique<BirchWeatherProvider>(this);
+  }
+}
 
 BirchModel::~BirchModel() = default;
 
@@ -39,6 +46,13 @@
   }
 
   recent_tab_items_ = std::move(recent_tab_items);
+}
+
+void BirchModel::SetWeatherItems(std::vector<BirchWeatherItem> weather_items) {
+  if (weather_items == weather_items_) {
+    return;
+  }
+  weather_items_ = std::move(weather_items);
 
   for (auto& observer : observers_) {
     observer.OnItemsChanged();
@@ -50,6 +64,9 @@
   if (birch_client_) {
     birch_client_->RequestBirchDataFetch();
   }
+  if (weather_provider_) {
+    weather_provider_->RequestDataFetch();
+  }
 }
 
 }  // namespace ash
diff --git a/ash/birch/birch_model.h b/ash/birch/birch_model.h
index 9589e36..de6c9fd 100644
--- a/ash/birch/birch_model.h
+++ b/ash/birch/birch_model.h
@@ -5,6 +5,9 @@
 #ifndef ASH_BIRCH_BIRCH_MODEL_H_
 #define ASH_BIRCH_BIRCH_MODEL_H_
 
+#include <optional>
+#include <vector>
+
 #include "ash/ash_export.h"
 #include "ash/birch/birch_client.h"
 #include "ash/birch/birch_item.h"
@@ -12,6 +15,8 @@
 
 namespace ash {
 
+class BirchWeatherProvider;
+
 // Birch model, which is used to aggregate and store relevant information from
 // different providers.
 class ASH_EXPORT BirchModel {
@@ -39,8 +44,8 @@
   void RequestBirchDataFetch();
 
   void SetFileSuggestItems(std::vector<BirchFileItem> file_suggest_items);
-
   void SetRecentTabItems(std::vector<BirchTabItem> recent_tab_items);
+  void SetWeatherItems(std::vector<BirchWeatherItem> weather_items);
 
   void SetClient(BirchClient* client) { birch_client_ = client; }
 
@@ -51,6 +56,10 @@
     return recent_tab_items_;
   }
 
+  const std::vector<BirchWeatherItem>& GetWeatherForTest() const {
+    return weather_items_;
+  }
+
  private:
   // A type-specific list of items for all file suggestion items.
   std::vector<BirchFileItem> file_suggest_items_;
@@ -58,8 +67,13 @@
   // A type-specific list of items for all tab items.
   std::vector<BirchTabItem> recent_tab_items_;
 
+  // A type-specific list of weather items.
+  std::vector<BirchWeatherItem> weather_items_;
+
   raw_ptr<BirchClient> birch_client_ = nullptr;
 
+  std::unique_ptr<BirchWeatherProvider> weather_provider_;
+
   base::ObserverList<Observer> observers_;
 };
 
diff --git a/ash/birch/birch_weather_provider.cc b/ash/birch/birch_weather_provider.cc
new file mode 100644
index 0000000..db89a19
--- /dev/null
+++ b/ash/birch/birch_weather_provider.cc
@@ -0,0 +1,124 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/birch/birch_weather_provider.h"
+
+#include <string>
+
+#include "ash/ambient/ambient_controller.h"
+#include "ash/birch/birch_item.h"
+#include "ash/birch/birch_model.h"
+#include "ash/public/cpp/ambient/ambient_backend_controller.h"
+#include "ash/public/cpp/image_downloader.h"
+#include "ash/public/cpp/session/session_types.h"
+#include "ash/session/session_controller_impl.h"
+#include "ash/shell.h"
+#include "ash/strings/grit/ash_strings.h"
+#include "base/check.h"
+#include "base/functional/bind.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+
+namespace ash {
+
+namespace {
+
+constexpr net::NetworkTrafficAnnotationTag kWeatherIconTag =
+    net::DefineNetworkTrafficAnnotation("weather_icon", R"(
+        semantics {
+          sender: "Birch feature"
+          description:
+            "Download weather icon image from Google."
+          trigger:
+            "The user opens an UI surface associated with birch feature."
+          data: "None."
+          destination: GOOGLE_OWNED_SERVICE
+        }
+        policy {
+         cookies_allowed: NO
+         setting:
+           "This feature is off by default."
+         policy_exception_justification:
+           "Policy is planned, but not yet implemented."
+        })");
+
+void DownloadImageFromUrl(const std::string& url_string,
+                          ImageDownloader::DownloadCallback callback) {
+  GURL url(url_string);
+  if (!url.is_valid()) {
+    std::move(callback).Run(gfx::ImageSkia());
+    return;
+  }
+
+  const UserSession* active_user_session =
+      Shell::Get()->session_controller()->GetUserSession(0);
+  DCHECK(active_user_session);
+
+  ImageDownloader::Get()->Download(url, kWeatherIconTag,
+                                   active_user_session->user_info.account_id,
+                                   std::move(callback));
+}
+
+}  // namespace
+
+BirchWeatherProvider::BirchWeatherProvider(BirchModel* birch_model)
+    : birch_model_(birch_model) {}
+
+BirchWeatherProvider::~BirchWeatherProvider() = default;
+
+void BirchWeatherProvider::RequestDataFetch() {
+  Shell::Get()
+      ->ambient_controller()
+      ->ambient_backend_controller()
+      ->FetchWeather(base::BindOnce(&BirchWeatherProvider::OnWeatherInfoFetched,
+                                    weak_factory_.GetWeakPtr()));
+}
+
+void BirchWeatherProvider::OnWeatherInfoFetched(
+    const std::optional<WeatherInfo>& weather_info) {
+  if (!weather_info || !weather_info->temp_f.has_value() ||
+      !weather_info->condition_icon_url ||
+      !weather_info->condition_description ||
+      weather_info->condition_icon_url->empty()) {
+    birch_model_->SetWeatherItems({});
+    return;
+  }
+
+  // Ideally we should avoid downloading from the same url again to reduce the
+  // overhead, as it's unlikely that the weather condition is changing
+  // frequently during the day.
+  DownloadImageFromUrl(
+      *weather_info->condition_icon_url,
+      base::BindOnce(&BirchWeatherProvider::OnWeatherConditionIconDownloaded,
+                     weak_factory_.GetWeakPtr(),
+                     base::UTF8ToUTF16(*weather_info->condition_description),
+                     *weather_info->temp_f, weather_info->show_celsius));
+}
+
+void BirchWeatherProvider::OnWeatherConditionIconDownloaded(
+    const std::u16string& weather_description,
+    float temp_f,
+    bool show_celsius,
+    const gfx::ImageSkia& icon) {
+  if (icon.isNull()) {
+    birch_model_->SetWeatherItems({});
+    return;
+  }
+
+  std::u16string temperature_string =
+      show_celsius ? l10n_util::GetStringFUTF16Int(
+                         IDS_ASH_AMBIENT_MODE_WEATHER_TEMPERATURE_IN_CELSIUS,
+                         static_cast<int>((temp_f - 32) * 5 / 9))
+                   : l10n_util::GetStringFUTF16Int(
+                         IDS_ASH_AMBIENT_MODE_WEATHER_TEMPERATURE_IN_FAHRENHEIT,
+                         static_cast<int>(temp_f));
+
+  std::vector<BirchWeatherItem> items;
+  items.emplace_back(weather_description, temperature_string,
+                     ui::ImageModel::FromImageSkia(icon));
+  birch_model_->SetWeatherItems(std::move(items));
+}
+
+}  // namespace ash
diff --git a/ash/birch/birch_weather_provider.h b/ash/birch/birch_weather_provider.h
new file mode 100644
index 0000000..712e0e2
--- /dev/null
+++ b/ash/birch/birch_weather_provider.h
@@ -0,0 +1,54 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_BIRCH_BIRCH_WEATHER_PROVIDER_H_
+#define ASH_BIRCH_BIRCH_WEATHER_PROVIDER_H_
+
+#include <optional>
+
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+
+namespace gfx {
+class ImageSkia;
+}
+
+namespace ash {
+
+class BirchModel;
+
+struct WeatherInfo;
+
+class BirchWeatherProvider {
+ public:
+  explicit BirchWeatherProvider(BirchModel* birch_model);
+  BirchWeatherProvider(const BirchWeatherProvider&) = delete;
+  BirchWeatherProvider& operator=(const BirchWeatherProvider&) = delete;
+  ~BirchWeatherProvider();
+
+  // Called from birch model to request weather information to be displayed in
+  // UI.
+  void RequestDataFetch();
+
+ private:
+  // Called in response to a weather info request. It initiates icon fetch from
+  // the URL provided in the weather info.
+  void OnWeatherInfoFetched(const std::optional<WeatherInfo>& weather_info);
+
+  // Callback to weather info icon request. It will update birch model with the
+  // fetched weather info (including the downloaded weather icon).
+  void OnWeatherConditionIconDownloaded(
+      const std::u16string& weather_description,
+      float temp_f,
+      bool show_celsius,
+      const gfx::ImageSkia& icon);
+
+  const raw_ptr<BirchModel> birch_model_;
+
+  base::WeakPtrFactory<BirchWeatherProvider> weak_factory_{this};
+};
+
+}  // namespace ash
+
+#endif  // ASH_BIRCH_BIRCH_WEATHER_PROVIDER_H_
diff --git a/ash/birch/birch_weather_provider_unittest.cc b/ash/birch/birch_weather_provider_unittest.cc
new file mode 100644
index 0000000..d780a98e
--- /dev/null
+++ b/ash/birch/birch_weather_provider_unittest.cc
@@ -0,0 +1,278 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/birch/birch_weather_provider.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "ash/ambient/ambient_controller.h"
+#include "ash/birch/birch_model.h"
+#include "ash/constants/ash_features.h"
+#include "ash/constants/ash_switches.h"
+#include "ash/public/cpp/ambient/ambient_backend_controller.h"
+#include "ash/public/cpp/ambient/fake_ambient_backend_controller_impl.h"
+#include "ash/public/cpp/test/test_image_downloader.h"
+#include "ash/shell.h"
+#include "ash/test/ash_test_base.h"
+#include "base/memory/raw_ptr.h"
+#include "base/test/scoped_feature_list.h"
+
+namespace ash {
+
+class BirchWeatherProviderTest : public AshTestBase {
+ public:
+  BirchWeatherProviderTest() {
+    switches::SetIgnoreForestSecretKeyForTest(true);
+    feature_list_.InitWithFeatures(
+        {features::kForestFeature, features::kBirchWeather}, {});
+  }
+  ~BirchWeatherProviderTest() override {
+    switches::SetIgnoreForestSecretKeyForTest(false);
+  }
+
+  // AshTestBase:
+  void SetUp() override {
+    AshTestBase::SetUp();
+
+    image_downloader_ = std::make_unique<ash::TestImageDownloader>();
+
+    Shell::Get()->ambient_controller()->set_backend_controller_for_testing(
+        nullptr);
+    auto ambient_backend_controller =
+        std::make_unique<FakeAmbientBackendControllerImpl>();
+    ambient_backend_controller_ = ambient_backend_controller.get();
+    Shell::Get()->ambient_controller()->set_backend_controller_for_testing(
+        std::move(ambient_backend_controller));
+  }
+  void TearDown() override {
+    ambient_backend_controller_ = nullptr;
+    image_downloader_.reset();
+    AshTestBase::TearDown();
+  }
+
+  raw_ptr<FakeAmbientBackendControllerImpl> ambient_backend_controller_;
+  std::unique_ptr<TestImageDownloader> image_downloader_;
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(BirchWeatherProviderTest, GetWeather) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_description = "Cloudy";
+  info.condition_icon_url = "https://fake-icon-url";
+  info.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(info);
+
+  birch_model->RequestBirchDataFetch();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  auto& weather_items = birch_model->GetWeatherForTest();
+  ASSERT_EQ(1u, weather_items.size());
+  EXPECT_EQ(u"Cloudy", weather_items[0].title);
+  EXPECT_EQ(u"70\xB0 F", weather_items[0].temperature);
+  EXPECT_FALSE(weather_items[0].icon.IsEmpty());
+}
+
+TEST_F(BirchWeatherProviderTest, GetWeatherInCelsius) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_description = "Cloudy";
+  info.condition_icon_url = "https://fake-icon-url";
+  info.temp_f = 70.0f;
+  info.show_celsius = true;
+  ambient_backend_controller_->SetWeatherInfo(std::move(info));
+
+  birch_model->RequestBirchDataFetch();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  auto& weather_items = birch_model->GetWeatherForTest();
+  ASSERT_EQ(1u, weather_items.size());
+  EXPECT_EQ(u"Cloudy", weather_items[0].title);
+  EXPECT_EQ(u"21\xB0 C", weather_items[0].temperature);
+  EXPECT_FALSE(weather_items[0].icon.IsEmpty());
+}
+
+TEST_F(BirchWeatherProviderTest, NoWeatherInfo) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+TEST_F(BirchWeatherProviderTest, WeatherWithNoIcon) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_description = "Cloudy";
+  info.show_celsius = false;
+  info.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(std::move(info));
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+TEST_F(BirchWeatherProviderTest, WeatherWithInvalidIcon) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_description = "Cloudy";
+  info.condition_icon_url = "<invalid url>";
+  info.show_celsius = false;
+  info.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(std::move(info));
+
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+TEST_F(BirchWeatherProviderTest, WeatherIconDownloadFailure) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_description = "Cloudy";
+  info.condition_icon_url = "https://fake_icon_url";
+  info.show_celsius = false;
+  info.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(std::move(info));
+
+  image_downloader_->set_should_fail(true);
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+TEST_F(BirchWeatherProviderTest, WeatherWithNoTemperature) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_description = "Cloudy";
+  info.condition_icon_url = "https://fake_icon_url";
+  info.show_celsius = false;
+  ambient_backend_controller_->SetWeatherInfo(std::move(info));
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+TEST_F(BirchWeatherProviderTest, WeatherWithNoDecription) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info;
+  info.condition_icon_url = "https://fake_icon_url";
+  info.show_celsius = false;
+  info.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(std::move(info));
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+TEST_F(BirchWeatherProviderTest, RefetchWeather) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info1;
+  info1.condition_description = "Cloudy";
+  info1.condition_icon_url = "https://fake-icon-url";
+  info1.show_celsius = false;
+  info1.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(info1);
+
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  auto& weather_items = birch_model->GetWeatherForTest();
+  ASSERT_EQ(1u, weather_items.size());
+  EXPECT_EQ(u"Cloudy", weather_items[0].title);
+  EXPECT_EQ(u"70\xB0 F", weather_items[0].temperature);
+  EXPECT_FALSE(weather_items[0].icon.IsEmpty());
+
+  WeatherInfo info2;
+  info2.condition_description = "Sunny";
+  info2.condition_icon_url = "https://fake-icon-url";
+  info2.show_celsius = false;
+  info2.temp_f = 73.0f;
+  ambient_backend_controller_->SetWeatherInfo(info2);
+
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  auto& updated_weather_items = birch_model->GetWeatherForTest();
+  ASSERT_EQ(1u, updated_weather_items.size());
+  EXPECT_EQ(u"Sunny", updated_weather_items[0].title);
+  EXPECT_EQ(u"73\xB0 F", updated_weather_items[0].temperature);
+  EXPECT_FALSE(updated_weather_items[0].icon.IsEmpty());
+}
+
+TEST_F(BirchWeatherProviderTest, RefetchInvalidWeather) {
+  auto* birch_model = Shell::Get()->birch_model();
+
+  WeatherInfo info1;
+  info1.condition_description = "Cloudy";
+  info1.condition_icon_url = "https://fake-icon-url";
+  info1.show_celsius = false;
+  info1.temp_f = 70.0f;
+  ambient_backend_controller_->SetWeatherInfo(info1);
+
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+
+  auto& weather_items = birch_model->GetWeatherForTest();
+  ASSERT_EQ(1u, weather_items.size());
+  EXPECT_EQ(u"Cloudy", weather_items[0].title);
+  EXPECT_EQ(u"70\xB0 F", weather_items[0].temperature);
+  EXPECT_FALSE(weather_items[0].icon.IsEmpty());
+
+  WeatherInfo info2;
+  info2.show_celsius = false;
+  ambient_backend_controller_->SetWeatherInfo(info2);
+
+  birch_model->RequestBirchDataFetch();
+
+  // The fake image downloader post a task to respond with an image.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(birch_model->GetWeatherForTest().empty());
+}
+
+}  // namespace ash
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 509217fb..7b0089d2 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -286,6 +286,9 @@
              "CrosBatterySaverAlwaysOn",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Display weather information in birch UI.
+BASE_FEATURE(kBirchWeather, "BirchWeather", base::FEATURE_DISABLED_BY_DEFAULT);
+
 // Enables or disables the usage of fixed Bluetooth A2DP packet size to improve
 // audio performance in noisy environment.
 BASE_FEATURE(kBluetoothFixA2dpPacketSize,
@@ -3192,6 +3195,10 @@
   return base::FeatureList::IsEnabled(kBatterySaverAlwaysOn);
 }
 
+bool IsBirchWeatherEnabled() {
+  return base::FeatureList::IsEnabled(kBirchWeather);
+}
+
 bool IsBluetoothDisconnectWarningEnabled() {
   return base::FeatureList::IsEnabled(kBluetoothDisconnectWarning);
 }
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 7f84678..3f92e95 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -75,6 +75,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kAvatarsCloudMigration);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBackgroundListening);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBatterySaver);
+COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kBirchWeather);
 enum BatterySaverNotificationBehavior { kBSMAutoEnable, kBSMOptIn };
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::FeatureParam<BatterySaverNotificationBehavior>
@@ -934,6 +935,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBackgroundListeningEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBatterySaverAvailable();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBatterySaverAlwaysOn();
+COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBirchWeatherEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBluetoothQualityReportEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsBluetoothDisconnectWarningEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsCaptureModeAudioMixingEnabled();
diff --git a/ash/public/cpp/ambient/ambient_backend_controller.h b/ash/public/cpp/ambient/ambient_backend_controller.h
index b9f3ba52..70102ea 100644
--- a/ash/public/cpp/ambient/ambient_backend_controller.h
+++ b/ash/public/cpp/ambient/ambient_backend_controller.h
@@ -58,6 +58,9 @@
   WeatherInfo& operator=(const WeatherInfo&);
   ~WeatherInfo();
 
+  // The description of the weather condition.
+  std::optional<std::string> condition_description;
+
   // The url of the weather condition icon image.
   std::optional<std::string> condition_icon_url;
 
diff --git a/chrome/browser/ui/ash/birch/birch_keyed_service_unittest.cc b/chrome/browser/ui/ash/birch/birch_keyed_service_unittest.cc
index 29c4e90..a26788c 100644
--- a/chrome/browser/ui/ash/birch/birch_keyed_service_unittest.cc
+++ b/chrome/browser/ui/ash/birch/birch_keyed_service_unittest.cc
@@ -329,11 +329,11 @@
   auto& tabs = Shell::Get()->birch_model()->GetTabsForTest();
   ASSERT_EQ(tabs.size(), 2u);
 
-  EXPECT_EQ(tabs[0].title, base::UTF16ToUTF8(kTabTitle1));
+  EXPECT_EQ(tabs[0].title, kTabTitle1);
   EXPECT_EQ(tabs[0].url, GURL(kExampleURL1));
   EXPECT_EQ(tabs[0].session_name, kSessionName1);
 
-  EXPECT_EQ(tabs[1].title, base::UTF16ToUTF8(kTabTitle2));
+  EXPECT_EQ(tabs[1].title, kTabTitle2);
   EXPECT_EQ(tabs[1].url, GURL(kExampleURL2));
   EXPECT_EQ(tabs[1].session_name, kSessionName2);
 }
diff --git a/chrome/browser/ui/ash/birch/birch_recent_tabs_provider.cc b/chrome/browser/ui/ash/birch/birch_recent_tabs_provider.cc
index ca7074c..7d868c7 100644
--- a/chrome/browser/ui/ash/birch/birch_recent_tabs_provider.cc
+++ b/chrome/browser/ui/ash/birch/birch_recent_tabs_provider.cc
@@ -44,9 +44,9 @@
         const sessions::SerializedNavigationEntry& current_navigation =
             tab->navigations.at(tab->normalized_navigation_index());
         items.emplace_back(
-            base::UTF16ToUTF8(current_navigation.title()),
-            current_navigation.virtual_url(), current_navigation.timestamp(),
-            current_navigation.favicon_url(), session->GetSessionName());
+            current_navigation.title(), current_navigation.virtual_url(),
+            current_navigation.timestamp(), current_navigation.favicon_url(),
+            session->GetSessionName());
       }
     }
   }