From 4a589a5c8e9c6b91605aa3ac58840441a1ab295e Mon Sep 17 00:00:00 2001 From: Aleksei Seren Date: Tue, 25 Feb 2025 09:21:06 -0500 Subject: [PATCH] [ads] RichNTT: Android --- android/brave_java_sources.gni | 1 + .../browser/ntp/BraveNewTabPageLayout.java | 48 ++++- .../ntp/SponsoredRichMediaWebView.java | 66 +++++++ .../NTPBackgroundImagesBridge.java | 29 ++- .../model/Wallpaper.java | 20 +- .../java/res/layout/new_tab_page_layout.xml | 7 + browser/brave_content_browser_client.cc | 6 +- .../android/ntp_background_images_bridge.cc | 10 +- browser/sources.gni | 1 + browser/ui/BUILD.gn | 5 +- browser/ui/config.gni | 4 + .../webui/new_tab_takeover/android/BUILD.gn | 40 ++++ .../android/new_tab_takeover_ui.cc | 109 +++++++++++ .../android/new_tab_takeover_ui.h | 71 +++++++ .../android/new_tab_takeover_ui_config.cc | 49 +++++ .../android/new_tab_takeover_ui_config.h | 30 +++ .../browser/ui/webui/chrome_web_ui_configs.cc | 5 +- .../chrome/common/webui_url_constants.cc | 3 +- components/brave_new_tab_ui/BUILD.gn | 21 ++ .../brave_new_tab_ui/new_tab_takeover/App.tsx | 88 +++++++++ .../new_tab_takeover/mojom/BUILD.gn | 16 ++ .../mojom/new_tab_takeover.mojom | 21 ++ .../new_tab_takeover/new_tab_takeover.html | 18 ++ .../new_tab_takeover/new_tab_takeover.tsx | 15 ++ .../new_tab_takeover_resources.grdp | 4 + components/constants/webui_url_constants.h | 4 + .../browser/ntp_sponsored_images_data.cc | 6 +- .../browser/ntp_sponsored_images_data.h | 3 + .../ntp_sponsored_images_data_unittest.cc | 180 ++++++++++++++++++ .../ntp_sponsored_rich_media_source.cc | 4 +- ...tp_sponsored_rich_media_source_unittest.cc | 5 +- .../browser/view_counter_service.cc | 48 +++++ .../browser/view_counter_service.h | 8 + .../browser/view_counter_service_unittest.cc | 57 ++++++ components/resources/BUILD.gn | 5 + .../resources/brave_components_resources.grd | 4 + resources/resource_ids.spec | 4 + test/BUILD.gn | 1 + 38 files changed, 993 insertions(+), 23 deletions(-) create mode 100644 android/java/org/chromium/chrome/browser/ntp/SponsoredRichMediaWebView.java create mode 100644 browser/ui/webui/new_tab_takeover/android/BUILD.gn create mode 100644 browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.cc create mode 100644 browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h create mode 100644 browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.cc create mode 100644 browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.h create mode 100644 components/brave_new_tab_ui/new_tab_takeover/App.tsx create mode 100644 components/brave_new_tab_ui/new_tab_takeover/mojom/BUILD.gn create mode 100644 components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom create mode 100644 components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.html create mode 100644 components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.tsx create mode 100644 components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover_resources.grdp create mode 100644 components/ntp_background_images/browser/ntp_sponsored_images_data_unittest.cc diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni index da227de40599..44c0e9b2a6f3 100644 --- a/android/brave_java_sources.gni +++ b/android/brave_java_sources.gni @@ -259,6 +259,7 @@ brave_java_sources = [ "../../brave/android/java/org/chromium/chrome/browser/ntp/IncognitoNewTabPageView.java", "../../brave/android/java/org/chromium/chrome/browser/ntp/NtpUtil.java", "../../brave/android/java/org/chromium/chrome/browser/ntp/OnBraveNtpListener.java", + "../../brave/android/java/org/chromium/chrome/browser/ntp/SponsoredRichMediaWebView.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/NTPBackgroundImagesBridge.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/BackgroundImage.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/ImageCredit.java", diff --git a/android/java/org/chromium/chrome/browser/ntp/BraveNewTabPageLayout.java b/android/java/org/chromium/chrome/browser/ntp/BraveNewTabPageLayout.java index 3f7775fa5955..743163f58771 100644 --- a/android/java/org/chromium/chrome/browser/ntp/BraveNewTabPageLayout.java +++ b/android/java/org/chromium/chrome/browser/ntp/BraveNewTabPageLayout.java @@ -31,6 +31,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; @@ -39,6 +40,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; import androidx.recyclerview.widget.SimpleItemAnimator; import com.airbnb.lottie.LottieAnimationView; @@ -132,7 +134,12 @@ public class BraveNewTabPageLayout private Integer mInitialTileNum; // Own members. + private WindowAndroid mWindowAndroid; + private ImageView mBgImageView; + private SponsoredRichMediaWebView mSponsoredRichMediaWebView; + private FrameLayout mBackgroundSponsoredRichMediaView; + private Profile mProfile; private SponsoredTab mSponsoredTab; private boolean mIsTablet; @@ -441,6 +448,27 @@ private void setNtpRecyclerView(LinearLayoutManager linearLayoutManager) { } mPrevVisibleNewsCardPosition = firstNewsFeedPosition() - 1; + + mRecyclerView.addOnItemTouchListener( + new OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent( + RecyclerView recyclerView, MotionEvent event) { + final View childView = + recyclerView.findChildViewUnder(event.getX(), event.getY()); + if (childView == null && mSponsoredRichMediaWebView != null) { + mSponsoredRichMediaWebView.getView().dispatchTouchEvent(event); + } + return false; + } + + @Override + public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {} + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {} + }); + mRecyclerView.addOnScrollListener( new RecyclerView.OnScrollListener() { @Override @@ -1181,6 +1209,7 @@ public void initialize( tabStripHeightSupplier); mIsTablet = isTablet; + mWindowAndroid = windowAndroid; assert mMvTilesContainerLayout != null : "Something has changed in the upstream!"; @@ -1217,7 +1246,9 @@ private void showNTPImage(NTPImage ntpImage) { if (mNtpAdapter != null) { mNtpAdapter.setNtpImage(ntpImage); } - if (ntpImage instanceof Wallpaper + if (ntpImage instanceof Wallpaper && ((Wallpaper) ntpImage).isRichMedia()) { + setupSponsoredBackgroundContent(); + } else if (ntpImage instanceof Wallpaper && NTPImageUtil.isReferralEnabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { setBackgroundImage(ntpImage); @@ -1230,6 +1261,21 @@ private void showNTPImage(NTPImage ntpImage) { } } + private void setupSponsoredBackgroundContent() { + if (mSponsoredRichMediaWebView != null) { + return; + } + + mSponsoredRichMediaWebView = + new SponsoredRichMediaWebView(mActivity, mWindowAndroid, mProfile); + + mBackgroundSponsoredRichMediaView = findViewById(R.id.bg_sponsored_rich_media_view); + mBackgroundSponsoredRichMediaView.setVisibility(View.VISIBLE); + mBackgroundSponsoredRichMediaView.addView(mSponsoredRichMediaWebView.getView()); + + mSponsoredRichMediaWebView.loadSponsoredRichMedia(); + } + private void setBackgroundImage(NTPImage ntpImage) { mBgImageView = (ImageView) findViewById(R.id.bg_image_view); mBgImageView.setScaleType(ImageView.ScaleType.MATRIX); diff --git a/android/java/org/chromium/chrome/browser/ntp/SponsoredRichMediaWebView.java b/android/java/org/chromium/chrome/browser/ntp/SponsoredRichMediaWebView.java new file mode 100644 index 000000000000..e0fc966a752d --- /dev/null +++ b/android/java/org/chromium/chrome/browser/ntp/SponsoredRichMediaWebView.java @@ -0,0 +1,66 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +package org.chromium.chrome.browser.ntp; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.chromium.base.version_info.VersionInfo; +import org.chromium.chrome.browser.content.WebContentsFactory; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.components.embedder_support.view.ContentView; +import org.chromium.components.thinwebview.ThinWebView; +import org.chromium.components.thinwebview.ThinWebViewConstraints; +import org.chromium.components.thinwebview.ThinWebViewFactory; +import org.chromium.content_public.browser.LoadUrlParams; +import org.chromium.content_public.browser.WebContents; +import org.chromium.net.NetId; +import org.chromium.ui.base.ViewAndroidDelegate; +import org.chromium.ui.base.WindowAndroid; + +public class SponsoredRichMediaWebView { + private static final String NEW_TAB_TAKEOVER_URL = "chrome://new-tab-takeover"; + + private WebContents mWebContents; + private ThinWebView mWebView; + + public SponsoredRichMediaWebView( + Activity activity, WindowAndroid windowAndroid, Profile profile) { + mWebContents = + WebContentsFactory.createWebContentsWithWarmRenderer( + profile, /* initiallyHidden= */ false, /* targetNetwork= */ NetId.INVALID); + + final ContentView webContentView = ContentView.createContentView(activity, mWebContents); + mWebContents.setDelegates( + VersionInfo.getProductVersion(), + ViewAndroidDelegate.createBasicDelegate(webContentView), + webContentView, + windowAndroid, + WebContents.createDefaultInternalsHolder()); + + mWebView = + ThinWebViewFactory.create( + activity, + new ThinWebViewConstraints(), + windowAndroid.getIntentRequestTracker()); + mWebView.getView() + .setLayoutParams( + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mWebView.attachWebContents(mWebContents, webContentView, null); + } + + public void loadSponsoredRichMedia() { + mWebContents.getNavigationController().loadUrl(new LoadUrlParams(NEW_TAB_TAKEOVER_URL)); + } + + public View getView() { + return mWebView.getView(); + } +} diff --git a/android/java/org/chromium/chrome/browser/ntp_background_images/NTPBackgroundImagesBridge.java b/android/java/org/chromium/chrome/browser/ntp_background_images/NTPBackgroundImagesBridge.java index c3bba02081fd..8ef8ecfd5b2c 100644 --- a/android/java/org/chromium/chrome/browser/ntp_background_images/NTPBackgroundImagesBridge.java +++ b/android/java/org/chromium/chrome/browser/ntp_background_images/NTPBackgroundImagesBridge.java @@ -146,13 +146,28 @@ public static BackgroundImage createWallpaper(String imagePath, String author, S } @CalledByNative - public static Wallpaper createBrandedWallpaper(String imagePath, int focalPointX, - int focalPointY, String logoPath, String logoDestinationUrl, String themeName, - boolean isSponsored, String creativeInstanceId, String wallpaperId) { - return new Wallpaper(imagePath, focalPointX, focalPointY, - logoPath, logoDestinationUrl, - themeName, isSponsored, creativeInstanceId, - wallpaperId); + public static Wallpaper createBrandedWallpaper( + String imagePath, + int focalPointX, + int focalPointY, + String logoPath, + String logoDestinationUrl, + String themeName, + boolean isSponsored, + String creativeInstanceId, + String wallpaperId, + boolean isRichMedia) { + return new Wallpaper( + imagePath, + focalPointX, + focalPointY, + logoPath, + logoDestinationUrl, + themeName, + isSponsored, + creativeInstanceId, + wallpaperId, + isRichMedia); } @CalledByNative diff --git a/android/java/org/chromium/chrome/browser/ntp_background_images/model/Wallpaper.java b/android/java/org/chromium/chrome/browser/ntp_background_images/model/Wallpaper.java index bdc315b67c40..9e14714a3aea 100644 --- a/android/java/org/chromium/chrome/browser/ntp_background_images/model/Wallpaper.java +++ b/android/java/org/chromium/chrome/browser/ntp_background_images/model/Wallpaper.java @@ -15,10 +15,19 @@ public class Wallpaper extends NTPImage { private boolean mIsSponsored; private String mCreativeInstanceId; private String mWallpaperId; + private boolean mIsRichMedia; - public Wallpaper(String imagePath, int focalPointX, int focalPointY, String logoPath, - String logoDestinationUrl, String themeName, boolean isSponsored, - String creativeInstanceId, String wallpaperId) { + public Wallpaper( + String imagePath, + int focalPointX, + int focalPointY, + String logoPath, + String logoDestinationUrl, + String themeName, + boolean isSponsored, + String creativeInstanceId, + String wallpaperId, + boolean isRichMedia) { mImagePath = imagePath; mFocalPointX = focalPointX; mFocalPointY = focalPointY; @@ -28,6 +37,7 @@ public Wallpaper(String imagePath, int focalPointX, int focalPointY, String logo mIsSponsored = isSponsored; mCreativeInstanceId = creativeInstanceId; mWallpaperId = wallpaperId; + mIsRichMedia = isRichMedia; } public String getImagePath() { @@ -65,4 +75,8 @@ public String getCreativeInstanceId() { public String getWallpaperId() { return mWallpaperId; } + + public boolean isRichMedia() { + return mIsRichMedia; + } } diff --git a/android/java/res/layout/new_tab_page_layout.xml b/android/java/res/layout/new_tab_page_layout.xml index 6f9694282743..e12a035bb56d 100644 --- a/android/java/res/layout/new_tab_page_layout.xml +++ b/android/java/res/layout/new_tab_page_layout.xml @@ -25,6 +25,13 @@ android:layout_height="match_parent" android:contentDescription="@null"/> + + + () .Add(); } -#endif +#else // !BUILDFLAG(IS_ANDROID) + registry.ForWebUI() + .Add(); +#endif // !BUILDFLAG(IS_ANDROID) } std::optional diff --git a/browser/ntp_background/android/ntp_background_images_bridge.cc b/browser/ntp_background/android/ntp_background_images_bridge.cc index 720f75b1399f..48eeb74cf186 100644 --- a/browser/ntp_background/android/ntp_background_images_bridge.cc +++ b/browser/ntp_background/android/ntp_background_images_bridge.cc @@ -177,6 +177,13 @@ NTPBackgroundImagesBridge::CreateBrandedWallpaper( const std::string* wallpaper_id = data.FindString(ntp_background_images::kWallpaperIDKey); + bool is_rich_media = false; + if (const std::string* sponsored_rich_media_type = + data.FindString(ntp_background_images::kWallpaperTypeKey)) { + is_rich_media = *sponsored_rich_media_type == + ntp_background_images::kRichMediaWallpaperType; + } + view_counter_service_->BrandedWallpaperWillBeDisplayed( wallpaper_id ? *wallpaper_id : "", creative_instance_id ? *creative_instance_id : "", @@ -190,7 +197,8 @@ NTPBackgroundImagesBridge::CreateBrandedWallpaper( ConvertUTF8ToJavaString(env, *theme_name), is_sponsored, ConvertUTF8ToJavaString( env, creative_instance_id ? *creative_instance_id : ""), - ConvertUTF8ToJavaString(env, wallpaper_id ? *wallpaper_id : "")); + ConvertUTF8ToJavaString(env, wallpaper_id ? *wallpaper_id : ""), + is_rich_media); } void NTPBackgroundImagesBridge::GetTopSites(JNIEnv* env, diff --git a/browser/sources.gni b/browser/sources.gni index 7f44b6f3c631..18eba7999b7c 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -412,6 +412,7 @@ if (is_android) { "//brave/browser/brave_ads/android", "//brave/browser/download/android:jni_headers", "//brave/browser/ntp_background/android", + "//brave/browser/ui/webui/new_tab_takeover/android:new_tab_takeover", "//brave/build/android:jni_headers", "//brave/components/brave_sync:sync_service_impl_helper", "//chrome/android:jni_headers", diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 015abf26273d..6f866c0328af 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -149,7 +149,10 @@ source_set("ui") { "android/ai_chat/brave_leo_settings_launcher_helper.h", ] - deps += [ "//brave/build/android:jni_headers" ] + deps += [ + "//brave/browser/ui/webui/new_tab_takeover/android:new_tab_takeover", + "//brave/build/android:jni_headers", + ] } if (enable_ai_rewriter) { diff --git a/browser/ui/config.gni b/browser/ui/config.gni index c436a93345c9..586f7e8dacfd 100644 --- a/browser/ui/config.gni +++ b/browser/ui/config.gni @@ -13,6 +13,10 @@ if (!is_android) { "//brave/browser/ui/webui/brave_news_internals", ] } +if (is_android) { + brave_ui_allow_circular_includes_from += + [ "//brave/browser/ui/webui/new_tab_takeover/android:new_tab_takeover" ] +} if (toolkit_views) { brave_ui_allow_circular_includes_from += [ "//brave/browser/ui/views/split_view" ] diff --git a/browser/ui/webui/new_tab_takeover/android/BUILD.gn b/browser/ui/webui/new_tab_takeover/android/BUILD.gn new file mode 100644 index 000000000000..09f7f30dd6ea --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/BUILD.gn @@ -0,0 +1,40 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//brave/build/config.gni") + +assert(is_android) + +source_set("new_tab_takeover") { + sources = [ + "new_tab_takeover_ui.cc", + "new_tab_takeover_ui.h", + "new_tab_takeover_ui_config.cc", + "new_tab_takeover_ui_config.h", + ] + + deps = [ + "//base", + "//brave/browser:browser_process", + "//brave/browser/brave_ads:brave_ads", + "//brave/browser/ntp_background", + "//brave/components/brave_new_tab_ui:new_tab_takeover_generated_resources", + "//brave/components/constants", + "//brave/components/ntp_background_images/browser", + "//chrome/browser:browser_process", + "//chrome/browser:browser_public_dependencies", + "//chrome/browser/profiles:profile", + "//content/public/browser", + "//content/public/common", + "//ui/webui", + "//url", + ] + + public_deps = [ + "//brave/components/brave_new_tab_ui/new_tab_takeover/mojom", + "//brave/components/ntp_background_images/browser/mojom", + "//mojo/public/cpp/bindings", + ] +} diff --git a/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.cc b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.cc new file mode 100644 index 000000000000..a8fd3cbb367f --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.cc @@ -0,0 +1,109 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h" + +#include +#include +#include +#include + +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "brave/browser/ui/webui/brave_webui_source.h" +#include "brave/components/brave_new_tab_ui/new_tab_takeover/grit/new_tab_takeover_generated_map.h" +#include "brave/components/constants/webui_url_constants.h" +#include "brave/components/ntp_background_images/browser/ntp_sponsored_rich_media_ad_event_handler.h" +#include "brave/components/ntp_background_images/browser/url_constants.h" +#include "brave/components/ntp_background_images/browser/view_counter_service.h" +#include "chrome/browser/ui/android/tab_model/tab_model.h" +#include "chrome/browser/ui/android/tab_model/tab_model_list.h" +#include "components/grit/brave_components_resources.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_ui.h" +#include "content/public/browser/web_ui_data_source.h" +#include "content/public/common/url_constants.h" + +namespace { + +content::WebContents* GetActiveWebContents() { + const TabModelList::TabModelVector& tab_models = TabModelList::models(); + const auto iter = std::ranges::find_if( + tab_models, [](const auto& model) { return model->IsActiveModel(); }); + if (iter == tab_models.cend()) { + return nullptr; + } + + return (*iter)->GetActiveWebContents(); +} + +} // namespace + +NewTabTakeoverUI::NewTabTakeoverUI( + content::WebUI* const web_ui, + ntp_background_images::ViewCounterService* view_counter_service, + std::unique_ptr + rich_media_ad_event_handler) + : ui::MojoWebUIController(web_ui), + view_counter_service_(view_counter_service), + rich_media_ad_event_handler_(std::move(rich_media_ad_event_handler)) { + content::WebUIDataSource* source = CreateAndAddWebUIDataSource( + web_ui, kNewTabTakeoverHost, kNewTabTakeoverGenerated, + IDR_NEW_TAB_TAKEOVER_HTML); + + web_ui->AddRequestableScheme(content::kChromeUIUntrustedScheme); + + source->OverrideContentSecurityPolicy( + network::mojom::CSPDirectiveName::FrameSrc, + base::StringPrintf("frame-src %s;", kNTPNewTabTakeoverRichMediaUrl)); + source->AddString("ntpNewTabTakeoverRichMediaUrl", + kNTPNewTabTakeoverRichMediaUrl); +} + +NewTabTakeoverUI::~NewTabTakeoverUI() = default; + +void NewTabTakeoverUI::BindInterface( + mojo::PendingReceiver + pending_receiver) { + if (new_tab_takeover_receiver_.is_bound()) { + new_tab_takeover_receiver_.reset(); + } + + new_tab_takeover_receiver_.Bind(std::move(pending_receiver)); +} + +/////////////////////////////////////////////////////////////////////////////// + +void NewTabTakeoverUI::SetSponsoredRichMediaAdEventHandler( + mojo::PendingReceiver< + ntp_background_images::mojom::SponsoredRichMediaAdEventHandler> + event_handler) { + rich_media_ad_event_handler_->Bind(std::move(event_handler)); +} + +void NewTabTakeoverUI::GetCurrentWallpaper( + GetCurrentWallpaperCallback callback) { + if (view_counter_service_) { + view_counter_service_->GetCurrentBrandedWallpaper(std::move(callback)); + } +} + +void NewTabTakeoverUI::NavigateToUrl(const GURL& url) { + // The current New Tab Takeover web contents is displayed in the ThinWebView + // so it is not connected to the Android Tab, i.e. WebContents::GetDelegate() + // returns nullptr. Therefore to do a Tab navigation, we need to locate + // the current Android Tab and open the URL in it. + content::WebContents* web_contents = GetActiveWebContents(); + if (!web_contents) { + return; + } + + const content::OpenURLParams params( + url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB, + ui::PAGE_TRANSITION_AUTO_TOPLEVEL, /*is_renderer_initiated=*/false); + web_contents->OpenURL(params, /*navigation_handle_callback=*/{}); +} + +WEB_UI_CONTROLLER_TYPE_IMPL(NewTabTakeoverUI) diff --git a/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h new file mode 100644 index 000000000000..b43f31d73454 --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h @@ -0,0 +1,71 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_NEW_TAB_TAKEOVER_ANDROID_NEW_TAB_TAKEOVER_UI_H_ +#define BRAVE_BROWSER_UI_WEBUI_NEW_TAB_TAKEOVER_ANDROID_NEW_TAB_TAKEOVER_UI_H_ + +#include +#include + +#include "base/memory/raw_ptr.h" +#include "brave/components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom.h" +#include "brave/components/ntp_background_images/browser/mojom/ntp_background_images.mojom.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "ui/webui/mojo_web_ui_controller.h" + +namespace ntp_background_images { +class NTPSponsoredRichMediaAdEventHandler; +class ViewCounterService; +} // namespace ntp_background_images + +// On desktop, we use a Web UI to display new tab pages. On Android, however, +// there is no Web UI implementation. Instead, Android overlays a native view +// over a web contents view. The native view displays the background image, +// Brave Stats, and Brave News. When the user navigates to a URL, the native +// view is hidden, revealing the web contents view and its HTML content. To +// display rich media HTML alongside Brave Stats and Brave News, we use a +// `ThinWebView` to render the HTML behind these overlays. +class NewTabTakeoverUI : public ui::MojoWebUIController, + public new_tab_takeover::mojom::NewTabTakeover { + public: + NewTabTakeoverUI( + content::WebUI* const web_ui, + ntp_background_images::ViewCounterService* view_counter_service, + std::unique_ptr< + ntp_background_images::NTPSponsoredRichMediaAdEventHandler> + rich_media_ad_event_handler); + + NewTabTakeoverUI(const NewTabTakeoverUI&) = delete; + NewTabTakeoverUI& operator=(const NewTabTakeoverUI&) = delete; + + ~NewTabTakeoverUI() override; + + void BindInterface( + mojo::PendingReceiver + pending_receiver); + + private: + // new_tab_takeover::mojom::NewTabTakeover: + void SetSponsoredRichMediaAdEventHandler( + mojo::PendingReceiver< + ntp_background_images::mojom::SponsoredRichMediaAdEventHandler> + event_handler) override; + void GetCurrentWallpaper(GetCurrentWallpaperCallback callback) override; + void NavigateToUrl(const GURL& url) override; + + mojo::Receiver + new_tab_takeover_receiver_{this}; + + raw_ptr + view_counter_service_; // Not owned. + + std::unique_ptr + rich_media_ad_event_handler_; + + WEB_UI_CONTROLLER_TYPE_DECL(); +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_NEW_TAB_TAKEOVER_ANDROID_NEW_TAB_TAKEOVER_UI_H_ diff --git a/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.cc b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.cc new file mode 100644 index 000000000000..3aa75b18351c --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.cc @@ -0,0 +1,49 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.h" + +#include +#include + +#include "brave/browser/brave_ads/ads_service_factory.h" +#include "brave/browser/brave_browser_process.h" +#include "brave/browser/ntp_background/ntp_p3a_helper_impl.h" +#include "brave/browser/ntp_background/view_counter_service_factory.h" +#include "brave/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h" +#include "brave/components/constants/webui_url_constants.h" +#include "brave/components/ntp_background_images/browser/ntp_p3a_helper.h" +#include "brave/components/ntp_background_images/browser/ntp_sponsored_rich_media_ad_event_handler.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "content/public/browser/web_ui.h" +#include "content/public/common/url_constants.h" +#include "url/gurl.h" + +NewTabTakeoverUIConfig::NewTabTakeoverUIConfig() + : WebUIConfig(content::kChromeUIScheme, kNewTabTakeoverHost) {} + +std::unique_ptr +NewTabTakeoverUIConfig::CreateWebUIController(content::WebUI* web_ui, + const GURL& url) { + Profile* profile = Profile::FromWebUI(web_ui); + + auto ntp_p3a_helper = + std::make_unique( + g_browser_process->local_state(), + g_brave_browser_process->p3a_service(), + g_brave_browser_process->ntp_background_images_service(), + profile->GetPrefs()); + + auto rich_media_ad_event_handler = std::make_unique< + ntp_background_images::NTPSponsoredRichMediaAdEventHandler>( + brave_ads::AdsServiceFactory::GetForProfile(profile), + std::move(ntp_p3a_helper)); + + return std::make_unique( + web_ui, + ntp_background_images::ViewCounterServiceFactory::GetForProfile(profile), + std::move(rich_media_ad_event_handler)); +} diff --git a/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.h b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.h new file mode 100644 index 000000000000..d88bbc187850 --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_UI_WEBUI_NEW_TAB_TAKEOVER_ANDROID_NEW_TAB_TAKEOVER_UI_CONFIG_H_ +#define BRAVE_BROWSER_UI_WEBUI_NEW_TAB_TAKEOVER_ANDROID_NEW_TAB_TAKEOVER_UI_CONFIG_H_ + +#include + +#include "content/public/browser/webui_config.h" + +class GURL; + +namespace content { +class WebUI; +class WebUIController; +} // namespace content + +class NewTabTakeoverUIConfig : public content::WebUIConfig { + public: + NewTabTakeoverUIConfig(); + + // content::WebUIConfig: + std::unique_ptr CreateWebUIController( + content::WebUI* web_ui, + const GURL& url) override; +}; + +#endif // BRAVE_BROWSER_UI_WEBUI_NEW_TAB_TAKEOVER_ANDROID_NEW_TAB_TAKEOVER_UI_CONFIG_H_ diff --git a/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc b/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc index de8e31aa3051..4ca6a1c80c1d 100644 --- a/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc +++ b/chromium_src/chrome/browser/ui/webui/chrome_web_ui_configs.cc @@ -26,6 +26,8 @@ #include "brave/browser/ui/webui/private_new_tab_page/brave_private_new_tab_ui.h" #include "brave/browser/ui/webui/speedreader/speedreader_toolbar_ui.h" #include "brave/browser/ui/webui/webcompat_reporter/webcompat_reporter_ui.h" +#else // !BUILDFLAG(IS_ANDROID) +#include "brave/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui_config.h" #endif // !BUILDFLAG(IS_ANDROID) #include "brave/browser/ui/webui/brave_adblock_internals_ui.h" @@ -84,7 +86,8 @@ void RegisterChromeWebUIConfigs() { map.AddWebUIConfig(std::make_unique()); map.AddWebUIConfig( std::make_unique()); - +#else // !BUILDFLAG(IS_ANDROID) + map.AddWebUIConfig(std::make_unique()); #endif // !BUILDFLAG(IS_ANDROID) map.AddWebUIConfig(std::make_unique()); map.AddWebUIConfig(std::make_unique()); diff --git a/chromium_src/chrome/common/webui_url_constants.cc b/chromium_src/chrome/common/webui_url_constants.cc index abe7946d8a11..a560046399e6 100644 --- a/chromium_src/chrome/common/webui_url_constants.cc +++ b/chromium_src/chrome/common/webui_url_constants.cc @@ -9,7 +9,8 @@ #define kChromeUIAttributionInternalsHost \ kChromeUIAttributionInternalsHost, kAdblockHost, kAdblockInternalsHost, \ kRewardsPageHost, kRewardsInternalsHost, kWelcomeHost, kWalletPageHost, \ - kTorInternalsHost, kSkusInternalsHost, kAdsInternalsHost + kTorInternalsHost, kSkusInternalsHost, kAdsInternalsHost, \ + kNewTabTakeoverHost #include "src/chrome/common/webui_url_constants.cc" diff --git a/components/brave_new_tab_ui/BUILD.gn b/components/brave_new_tab_ui/BUILD.gn index 04f7f5c04bb4..6e586ef586ac 100644 --- a/components/brave_new_tab_ui/BUILD.gn +++ b/components/brave_new_tab_ui/BUILD.gn @@ -43,3 +43,24 @@ mojom("mojom") { enabled_features = [ "enable_brave_vpn" ] } } + +transpile_web_ui("new_tab_takeover_ui") { + resource_name = "new_tab_takeover" + entry_points = [ [ + "new_tab_takeover", + rebase_path("new_tab_takeover/new_tab_takeover.tsx"), + ] ] + public_deps = [ + "//brave/components/brave_ads/core/mojom:mojom_js", + "//brave/components/brave_new_tab_ui/new_tab_takeover/mojom:mojom_js", + "//brave/components/ntp_background_images/browser/mojom:mojom_js", + "//mojo/public/mojom/base", + ] +} + +pack_web_resources("new_tab_takeover_generated_resources") { + resource_name = "new_tab_takeover" + output_dir = + "$root_gen_dir/brave/components/brave_new_tab_ui/new_tab_takeover" + deps = [ ":new_tab_takeover_ui" ] +} diff --git a/components/brave_new_tab_ui/new_tab_takeover/App.tsx b/components/brave_new_tab_ui/new_tab_takeover/App.tsx new file mode 100644 index 000000000000..d232736c8f55 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/App.tsx @@ -0,0 +1,88 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import * as React from 'react'; +import * as NTPBackgroundMediaMojom from 'gen/brave/components/ntp_background_images/browser/mojom/ntp_background_images.mojom.m.js' +import * as NewTabTakeoverMojom from 'gen/brave/components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom.m.js' +import * as BraveAdsMojom from 'gen/brave/components/brave_ads/core/mojom/brave_ads.mojom.m.js' +import { Url } from 'gen/url/mojom/url.mojom.m.js' + +import { + SponsoredRichMediaBackgroundInfo, SponsoredRichMediaBackground +} from '../containers/newTab/sponsored_rich_media_background' + +export default function App(props: React.PropsWithChildren) { + const [sponsoredRichMediaBackgroundInfo, setSponsoredRichMediaBackgroundInfo] = React.useState(null) + const [sponsoredRichMediaAdEventHandler, setSponsoredRichMediaAdEventHandler] = React.useState(null) + const [newTabTakeover, setNewTabTakeover] = React.useState(null) + const [richMediaHasLoaded, setRichMediaHasLoaded] = React.useState(false) + + const getCurrentWallpaper = React.useCallback(async () => { + if (!newTabTakeover) { + return + } + try { + const response = await newTabTakeover.getCurrentWallpaper(); + if (!response || !response.url || !response.placementId || + !response.creativeInstanceId || !response.targetUrl) { + return + } + + const sponsoredRichMediaBackgroundInfo: SponsoredRichMediaBackgroundInfo = { + url: response.url.url, + creativeInstanceId: response.creativeInstanceId, + placementId: response.placementId, + targetUrl: response.targetUrl.url + } + setSponsoredRichMediaBackgroundInfo(sponsoredRichMediaBackgroundInfo) + } catch (error) { + console.error('Failed to get last displayed branded wallpaper:', error); + } + }, [newTabTakeover]); + + React.useEffect(() => { + const newTabTakeover = NewTabTakeoverMojom.NewTabTakeover.getRemote(); + setNewTabTakeover(newTabTakeover) + + const sponsoredRichMediaAdEventHandler = new NTPBackgroundMediaMojom.SponsoredRichMediaAdEventHandlerRemote() + newTabTakeover.setSponsoredRichMediaAdEventHandler(sponsoredRichMediaAdEventHandler.$.bindNewPipeAndPassReceiver()) + setSponsoredRichMediaAdEventHandler(sponsoredRichMediaAdEventHandler) + + return () => { + setSponsoredRichMediaBackgroundInfo(null) + setNewTabTakeover(null) + setSponsoredRichMediaAdEventHandler(null) + } + }, []) + + React.useEffect(() => { + getCurrentWallpaper() + }, [getCurrentWallpaper, newTabTakeover]) + + return ( + + {sponsoredRichMediaBackgroundInfo && sponsoredRichMediaAdEventHandler && newTabTakeover && ( + setRichMediaHasLoaded(true)} + onEventReported={(adEventType) => { + sponsoredRichMediaAdEventHandler.reportRichMediaAdEvent( + sponsoredRichMediaBackgroundInfo.creativeInstanceId, + sponsoredRichMediaBackgroundInfo.placementId, + adEventType + ); + + if (adEventType === BraveAdsMojom.NewTabPageAdEventType.kClicked) { + const mojomUrl = new Url(); + mojomUrl.url = sponsoredRichMediaBackgroundInfo.targetUrl; + newTabTakeover.navigateToUrl(mojomUrl); + } + }} + /> + )} + + ) +} diff --git a/components/brave_new_tab_ui/new_tab_takeover/mojom/BUILD.gn b/components/brave_new_tab_ui/new_tab_takeover/mojom/BUILD.gn new file mode 100644 index 000000000000..1555904b59ff --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/mojom/BUILD.gn @@ -0,0 +1,16 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//mojo/public/tools/bindings/mojom.gni") + +mojom("mojom") { + generate_java = true + sources = [ "new_tab_takeover.mojom" ] + + public_deps = [ + "//brave/components/ntp_background_images/browser/mojom", + "//mojo/public/mojom/base", + ] +} diff --git a/components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom b/components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom new file mode 100644 index 000000000000..9fa2868264d0 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom @@ -0,0 +1,21 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +module new_tab_takeover.mojom; + +import "url/mojom/url.mojom"; +import "brave/components/ntp_background_images/browser/mojom/ntp_background_images.mojom"; + +interface NewTabTakeover { + SetSponsoredRichMediaAdEventHandler( + pending_receiver event_handler); + + GetCurrentWallpaper() => (url.mojom.Url? url, + string? placement_id, + string? creative_instance_id, + url.mojom.Url? target_url); + + NavigateToUrl(url.mojom.Url url); +}; diff --git a/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.html b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.html new file mode 100644 index 000000000000..9d571c9692a0 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.html @@ -0,0 +1,18 @@ + + + + + + + + New Tab Takeover + + + + + + +
+ + + diff --git a/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.tsx b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.tsx new file mode 100644 index 000000000000..9052936991a9 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.tsx @@ -0,0 +1,15 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import * as React from 'react' +import { createRoot } from 'react-dom/client' +import { setIconBasePath } from '@brave/leo/react/icon' +import App from './App' + +setIconBasePath('//resources/brave-icons') + +createRoot(document.querySelector('#root')!).render( + +) diff --git a/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover_resources.grdp b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover_resources.grdp new file mode 100644 index 000000000000..5f224dc39e11 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover_resources.grdp @@ -0,0 +1,4 @@ + + + + diff --git a/components/constants/webui_url_constants.h b/components/constants/webui_url_constants.h index 69ed9e31e814..8e78ad02d0a3 100644 --- a/components/constants/webui_url_constants.h +++ b/components/constants/webui_url_constants.h @@ -16,6 +16,7 @@ inline constexpr char kAdblockInternalsHost[] = "adblock-internals"; inline constexpr char kAdblockJS[] = "brave_adblock.js"; inline constexpr char kSkusInternalsHost[] = "skus-internals"; inline constexpr char kAdsInternalsHost[] = "ads-internals"; +inline constexpr char kNewTabTakeoverHost[] = "new-tab-takeover"; inline constexpr char kWebcompatReporterHost[] = "webcompat"; inline constexpr char kRewardsPageHost[] = "rewards"; inline constexpr char kRewardsPageURL[] = "chrome://rewards/"; @@ -100,6 +101,9 @@ inline constexpr char kNTPNewTabTakeoverRichMediaUrl[] = inline constexpr char kBraveUINewTabURL[] = "chrome://newtab/"; +inline constexpr char kBraveUINewTabTakeoverURL[] = + "chrome://new-tab-takeover/"; + // Hosts that are allowed to be installed as PWAs, which is usually // a blocked action for WebUIs. In Chromium, the "password-manager" host // is already allowed. diff --git a/components/ntp_background_images/browser/ntp_sponsored_images_data.cc b/components/ntp_background_images/browser/ntp_sponsored_images_data.cc index 2809e1c15757..45482a331aa4 100644 --- a/components/ntp_background_images/browser/ntp_sponsored_images_data.cc +++ b/components/ntp_background_images/browser/ntp_sponsored_images_data.cc @@ -36,7 +36,6 @@ constexpr char kCreativeConditionMatchersKey[] = "conditionMatchers"; constexpr char kCreativeConditionMatcherConditionKey[] = "condition"; constexpr char kCreativeConditionMatcherPrefPathKey[] = "prefPath"; constexpr char kWallpaperKey[] = "wallpaper"; -constexpr char kImageWallpaperType[] = "image"; constexpr char kImageWallpaperRelativeUrlKey[] = "relativeUrl"; constexpr char kImageWallpaperFocalPointXKey[] = "focalPoint.x"; constexpr char kImageWallpaperFocalPointYKey[] = "focalPoint.y"; @@ -47,17 +46,16 @@ constexpr char kImageWallpaperViewBoxHeightKey[] = "viewBox.height"; constexpr char kImageWallpaperBackgroundColorKey[] = "backgroundColor"; constexpr char kImageWallpaperButtonImageRelativeUrlKey[] = "button.image.relativeUrl"; -constexpr char kRichMediaWallpaperType[] = "richMedia"; constexpr char kRichMediaWallpaperRelativeUrlKey[] = "relativeUrl"; std::optional ToString(WallpaperType wallpaper_type) { switch (wallpaper_type) { case WallpaperType::kImage: { - return "image"; + return kImageWallpaperType; } case WallpaperType::kRichMedia: { - return "richMedia"; + return kRichMediaWallpaperType; } } diff --git a/components/ntp_background_images/browser/ntp_sponsored_images_data.h b/components/ntp_background_images/browser/ntp_sponsored_images_data.h index 6372c09e0d5d..285e36401767 100644 --- a/components/ntp_background_images/browser/ntp_sponsored_images_data.h +++ b/components/ntp_background_images/browser/ntp_sponsored_images_data.h @@ -23,6 +23,9 @@ struct NewTabPageAdInfo; namespace ntp_background_images { +inline constexpr char kImageWallpaperType[] = "image"; +inline constexpr char kRichMediaWallpaperType[] = "richMedia"; + struct TopSite { TopSite(); diff --git a/components/ntp_background_images/browser/ntp_sponsored_images_data_unittest.cc b/components/ntp_background_images/browser/ntp_sponsored_images_data_unittest.cc new file mode 100644 index 000000000000..7b0ca723038a --- /dev/null +++ b/components/ntp_background_images/browser/ntp_sponsored_images_data_unittest.cc @@ -0,0 +1,180 @@ +/* Copyright (c) 2025 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/components/ntp_background_images/browser/ntp_sponsored_images_data.h" + +#include "base/files/file_path.h" +#include "base/test/values_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/gfx/geometry/point.h" +#include "ui/gfx/geometry/rect.h" +#include "url/gurl.h" + +namespace ntp_background_images { + +constexpr char kTestEmptyCampaigns[] = R"( + { + "schemaVersion": 2, + "campaigns": [ + ] + })"; + +constexpr char kTestSponsoredImagesCampaign[] = R"( + { + "schemaVersion": 2, + "campaigns": [ + { + "version": 1, + "campaignId": "65933e82-6b21-440b-9956-c0f675ca7435", + "creativeSets": [ + { + "creativeSetId": "6690ad47-d0af-4dbb-a2dd-c7a678b2b83b", + "creatives": [ + { + "creativeInstanceId": "30244a36-561a-48f0-8d7a-780e9035c57a", + "companyName": "Image NTT Creative", + "alt": "Some content", + "targetUrl": "https://basicattentiontoken.org", + "wallpaper": { + "type": "image", + "relativeUrl": "30244a36-561a-48f0-8d7a-780e9035c57a/background-1.jpg", + "focalPoint": { + "x": 25, + "y": 50 + }, + "button": { + "image": { + "relativeUrl": "30244a36-561a-48f0-8d7a-780e9035c57a/button-1.png" + } + } + } + } + ] + } + ] + } + ] + })"; + +constexpr char kTestSponsoredRichMediaCampaign[] = R"( + { + "schemaVersion": 2, + "campaigns": [ + { + "version": 1, + "campaignId": "c27a3fae-ee9e-48a2-b3a7-f4675744e6ec", + "creativeSets": [ + { + "creativeSetId": "a245e3b9-2df4-47f5-aaab-67b61c528b6f", + "creatives": [ + { + "creativeInstanceId": "39d78863-327d-4b64-9952-cd0e5e330eb6", + "alt": "Some more rich content", + "companyName": "Another Rich Media NTT Creative", + "targetUrl": "https://basicattentiontoken.org", + "wallpaper": { + "type": "richMedia", + "relativeUrl": "39d78863-327d-4b64-9952-cd0e5e330eb6/index.html" + } + } + ] + } + ] + } + ] + })"; + +TEST(NTPSponsoredImagesDataTest, EmptyJson) { + base::Value::Dict dict; + base::FilePath installed_dir(FILE_PATH_LITERAL("ntp_sponsored_images_data")); + NTPSponsoredImagesData data(dict, installed_dir); + EXPECT_THAT(data.IsValid(), testing::IsFalse()); +} + +TEST(NTPSponsoredImagesDataTest, EmptyCampaigns) { + base::Value::Dict dict = base::test::ParseJsonDict(kTestEmptyCampaigns); + base::FilePath installed_dir(FILE_PATH_LITERAL("ntp_sponsored_images_data")); + NTPSponsoredImagesData data(dict, installed_dir); + EXPECT_THAT(data.IsValid(), testing::IsFalse()); +} + +TEST(NTPSponsoredImagesDataTest, ParseSponsoredImageCampaign) { + base::Value::Dict dict = + base::test::ParseJsonDict(kTestSponsoredImagesCampaign); + base::FilePath installed_dir(FILE_PATH_LITERAL("ntp_sponsored_images_data")); + NTPSponsoredImagesData data(dict, installed_dir); + EXPECT_THAT(data.IsValid(), testing::IsTrue()); + + ASSERT_EQ(data.campaigns.size(), 1u); + const auto& campaign = data.campaigns[0]; + EXPECT_EQ(campaign.campaign_id, "65933e82-6b21-440b-9956-c0f675ca7435"); + ASSERT_EQ(campaign.creatives.size(), 1u); + const auto& creative = campaign.creatives[0]; + EXPECT_EQ(creative.wallpaper_type, WallpaperType::kImage); + EXPECT_EQ(creative.creative_instance_id, + "30244a36-561a-48f0-8d7a-780e9035c57a"); + EXPECT_EQ(creative.url, + GURL("chrome://branded-wallpaper/sponsored-images/" + "30244a36-561a-48f0-8d7a-780e9035c57a/background-1.jpg")); + EXPECT_EQ(creative.file_path, + base::FilePath() + .AppendASCII("ntp_sponsored_images_data") + .AppendASCII("30244a36-561a-48f0-8d7a-780e9035c57a") + .AppendASCII("background-1.jpg")); + EXPECT_EQ(creative.focal_point, gfx::Point(25, 50)); + EXPECT_EQ(creative.background_color, ""); + EXPECT_EQ(creative.condition_matchers, brave_ads::ConditionMatcherMap()); + EXPECT_EQ(creative.viewbox, gfx::Rect{}); + + EXPECT_EQ(creative.logo.company_name, "Image NTT Creative"); + EXPECT_EQ(creative.logo.alt_text, "Some content"); + EXPECT_EQ(creative.logo.destination_url, "https://basicattentiontoken.org"); + EXPECT_EQ(creative.logo.image_file, + base::FilePath() + .AppendASCII("ntp_sponsored_images_data") + .AppendASCII("30244a36-561a-48f0-8d7a-780e9035c57a") + .AppendASCII("button-1.png")); + EXPECT_EQ(creative.logo.image_url, + "chrome://branded-wallpaper/sponsored-images/" + "30244a36-561a-48f0-8d7a-780e9035c57a/button-1.png"); +} + +TEST(NTPSponsoredImagesDataTest, ParseSponsoredRichMediaCampaign) { + base::Value::Dict dict = + base::test::ParseJsonDict(kTestSponsoredRichMediaCampaign); + base::FilePath installed_dir(FILE_PATH_LITERAL("ntp_sponsored_images_data")); + NTPSponsoredImagesData data(dict, installed_dir); + EXPECT_THAT(data.IsValid(), testing::IsTrue()); + + ASSERT_EQ(data.campaigns.size(), 1u); + const auto& campaign = data.campaigns[0]; + EXPECT_EQ(campaign.campaign_id, "c27a3fae-ee9e-48a2-b3a7-f4675744e6ec"); + ASSERT_EQ(campaign.creatives.size(), 1u); + const auto& creative = campaign.creatives[0]; + EXPECT_EQ(creative.wallpaper_type, WallpaperType::kRichMedia); + EXPECT_EQ(creative.creative_instance_id, + "39d78863-327d-4b64-9952-cd0e5e330eb6"); + EXPECT_EQ(creative.url, + GURL("chrome-untrusted://new-tab-takeover/" + "39d78863-327d-4b64-9952-cd0e5e330eb6/index.html")); + EXPECT_EQ(creative.file_path, + base::FilePath() + .AppendASCII("ntp_sponsored_images_data") + .AppendASCII("39d78863-327d-4b64-9952-cd0e5e330eb6") + .AppendASCII("index.html")); + EXPECT_EQ(creative.focal_point, gfx::Point(0, 0)); + EXPECT_EQ(creative.background_color, ""); + EXPECT_EQ(creative.condition_matchers, brave_ads::ConditionMatcherMap()); + EXPECT_EQ(creative.viewbox, std::nullopt); + + EXPECT_EQ(creative.logo.company_name, "Another Rich Media NTT Creative"); + EXPECT_EQ(creative.logo.alt_text, "Some more rich content"); + EXPECT_EQ(creative.logo.destination_url, "https://basicattentiontoken.org"); + EXPECT_THAT(creative.logo.image_file.empty(), testing::IsTrue()); + EXPECT_THAT(creative.logo.image_url, testing::IsEmpty()); +} + +} // namespace ntp_background_images diff --git a/components/ntp_background_images/browser/ntp_sponsored_rich_media_source.cc b/components/ntp_background_images/browser/ntp_sponsored_rich_media_source.cc index ca93782d73ba..05d9302487a4 100644 --- a/components/ntp_background_images/browser/ntp_sponsored_rich_media_source.cc +++ b/components/ntp_background_images/browser/ntp_sponsored_rich_media_source.cc @@ -12,6 +12,7 @@ #include "base/files/file_util.h" #include "base/functional/bind.h" #include "base/memory/ref_counted_memory.h" +#include "base/strings/stringprintf.h" #include "base/task/thread_pool.h" #include "brave/components/constants/webui_url_constants.h" #include "brave/components/ntp_background_images/browser/ntp_background_images_service.h" @@ -98,7 +99,8 @@ std::string NTPSponsoredRichMediaSource::GetContentSecurityPolicy( network::mojom::CSPDirectiveName directive) { switch (directive) { case network::mojom::CSPDirectiveName::FrameAncestors: - return std::string("frame-ancestors ") + kBraveUINewTabURL + ";"; + return base::StringPrintf("frame-ancestors %s %s;", kBraveUINewTabURL, + kBraveUINewTabTakeoverURL); case network::mojom::CSPDirectiveName::Sandbox: return "sandbox allow-scripts;"; case network::mojom::CSPDirectiveName::DefaultSrc: diff --git a/components/ntp_background_images/browser/ntp_sponsored_rich_media_source_unittest.cc b/components/ntp_background_images/browser/ntp_sponsored_rich_media_source_unittest.cc index efe338b59842..e54fcd720897 100644 --- a/components/ntp_background_images/browser/ntp_sponsored_rich_media_source_unittest.cc +++ b/components/ntp_background_images/browser/ntp_sponsored_rich_media_source_unittest.cc @@ -143,8 +143,9 @@ TEST_F(NTPSponsoredRichMediaSourceTest, GetContentSecurityPolicy) { const auto directive = static_cast(i); switch (directive) { case network::mojom::CSPDirectiveName::FrameAncestors: { - EXPECT_EQ("frame-ancestors chrome://newtab/;", - url_data_source()->GetContentSecurityPolicy(directive)); + EXPECT_EQ( + "frame-ancestors chrome://newtab/ chrome://new-tab-takeover/;", + url_data_source()->GetContentSecurityPolicy(directive)); break; } diff --git a/components/ntp_background_images/browser/view_counter_service.cc b/components/ntp_background_images/browser/view_counter_service.cc index 2ef4c509314b..7d5b68f01624 100644 --- a/components/ntp_background_images/browser/view_counter_service.cc +++ b/components/ntp_background_images/browser/view_counter_service.cc @@ -189,6 +189,7 @@ ViewCounterService::GetCurrentWallpaperForDisplay() { if (std::optional branded_wallpaper = GetCurrentBrandedWallpaper()) { + current_wallpaper_ = branded_wallpaper->Clone(); return branded_wallpaper; } @@ -240,6 +241,53 @@ ViewCounterService::GetCurrentBrandedWallpaper() { return GetNextBrandedWallpaperWhichMatchesConditions(); } +void ViewCounterService::GetCurrentBrandedWallpaper( + base::OnceCallback< + void(const std::optional& url, + const std::optional& placement_id, + const std::optional& creative_instance_id, + const std::optional& target_url)> callback) const { + auto failed = [&callback]() { + std::move(callback).Run(/*url=*/std::nullopt, + /*placement_id=*/std::nullopt, + /*creative_instance_id=*/std::nullopt, + /*target_url=*/std::nullopt); + }; + + if (!current_wallpaper_) { + return failed(); + } + + const std::string* const url = + current_wallpaper_->FindString(ntp_background_images::kWallpaperURLKey); + if (!url) { + return failed(); + } + + const std::string* const creative_instance_id = + current_wallpaper_->FindString( + ntp_background_images::kCreativeInstanceIDKey); + if (!creative_instance_id) { + return failed(); + } + + const std::string* const placement_id = + current_wallpaper_->FindString(ntp_background_images::kWallpaperIDKey); + if (!placement_id) { + return failed(); + } + + const std::string* const target_url = + current_wallpaper_->FindStringByDottedPath( + ntp_background_images::kLogoDestinationURLPath); + if (!target_url) { + return failed(); + } + + std::move(callback).Run(GURL(*url), *placement_id, *creative_instance_id, + GURL(*target_url)); +} + std::optional ViewCounterService::GetConditionMatchers(const base::Value::Dict& dict) { const base::Value::List* const list = diff --git a/components/ntp_background_images/browser/view_counter_service.h b/components/ntp_background_images/browser/view_counter_service.h index 13e8739ceff3..33c83fd64920 100644 --- a/components/ntp_background_images/browser/view_counter_service.h +++ b/components/ntp_background_images/browser/view_counter_service.h @@ -11,6 +11,7 @@ #include #include +#include "base/functional/callback_forward.h" #include "base/gtest_prod_util.h" #include "base/memory/raw_ptr.h" #include "base/scoped_observation.h" @@ -90,6 +91,12 @@ class ViewCounterService : public KeyedService, std::optional GetCurrentWallpaperForDisplay(); std::optional GetCurrentWallpaper() const; std::optional GetCurrentBrandedWallpaper(); + void GetCurrentBrandedWallpaper( + base::OnceCallback< + void(const std::optional& url, + const std::optional& placement_id, + const std::optional& creative_instance_id, + const std::optional& target_url)> callback) const; std::optional GetConditionMatchers( const base::Value::Dict& dict); std::optional @@ -187,6 +194,7 @@ class ViewCounterService : public KeyedService, PrefChangeRegistrar pref_change_registrar_; ViewCounterModel model_; base::WallClockTimer p3a_update_timer_; + std::optional current_wallpaper_; // Can be null if custom background is not supported. raw_ptr custom_background_service_ = nullptr; diff --git a/components/ntp_background_images/browser/view_counter_service_unittest.cc b/components/ntp_background_images/browser/view_counter_service_unittest.cc index 34a9df12181d..5cbce0904c79 100644 --- a/components/ntp_background_images/browser/view_counter_service_unittest.cc +++ b/components/ntp_background_images/browser/view_counter_service_unittest.cc @@ -12,6 +12,7 @@ #include "base/files/file_path.h" #include "base/memory/raw_ptr.h" +#include "base/test/mock_callback.h" #include "base/test/task_environment.h" #include "brave/components/brave_ads/browser/ads_service_mock.h" #include "brave/components/brave_ads/core/public/ad_units/new_tab_page_ad/new_tab_page_ad_info.h" @@ -515,4 +516,60 @@ TEST_F(NTPBackgroundImagesViewCounterTest, WrongSponsoredImageAdServed) { EXPECT_FALSE(si_wallpaper->FindString(kWallpaperIDKey)); } +TEST_F(NTPBackgroundImagesViewCounterTest, + GetCurrentBrandedWallpaperIfNotDisplayed) { + InitBackgroundAndSponsoredImageWallpapers(); + + prefs()->SetBoolean(brave_rewards::prefs::kEnabled, false); + + auto background_wallpaper = view_counter_->GetCurrentWallpaperForDisplay(); + EXPECT_TRUE(background_wallpaper->FindBool(kIsBackgroundKey).value_or(false)); + + base::MockCallback&, const std::optional&, + const std::optional&, const std::optional&)>> + callback; + EXPECT_CALL(callback, + Run(/*url=*/std::optional(), + /*placement_id=*/std::optional(), + /*creative_instance_id=*/std::optional(), + /*target_url=*/std::optional())); + + view_counter_->GetCurrentBrandedWallpaper(callback.Get()); +} + +TEST_F(NTPBackgroundImagesViewCounterTest, GetCurrentBrandedWallpaper) { + InitBackgroundAndSponsoredImageWallpapers(); + + prefs()->SetBoolean(brave_rewards::prefs::kEnabled, false); + + auto sponsored_wallpaper = TryGetFirstSponsoredImageWallpaper(); + + const std::string* url = sponsored_wallpaper->FindString(kWallpaperURLKey); + ASSERT_TRUE(url); + + const std::string* placement_id = + sponsored_wallpaper->FindString(kWallpaperIDKey); + ASSERT_TRUE(placement_id); + + const std::string* creative_instance_id = + sponsored_wallpaper->FindString(kCreativeInstanceIDKey); + ASSERT_TRUE(creative_instance_id); + + const std::string* target_url = + sponsored_wallpaper->FindStringByDottedPath(kLogoDestinationURLPath); + ASSERT_TRUE(target_url); + + base::MockCallback&, const std::optional&, + const std::optional&, const std::optional&)>> + callback; + EXPECT_CALL(callback, Run(std::optional(*url), + std::optional(*placement_id), + std::optional(*creative_instance_id), + std::optional(*target_url))); + + view_counter_->GetCurrentBrandedWallpaper(callback.Get()); +} + } // namespace ntp_background_images diff --git a/components/resources/BUILD.gn b/components/resources/BUILD.gn index 9d4b0d4b2e75..263e3842c1b0 100644 --- a/components/resources/BUILD.gn +++ b/components/resources/BUILD.gn @@ -69,6 +69,11 @@ repack("resources") { "$root_gen_dir/components/brave_components_static.pak", ] + if (is_android) { + deps += [ "//brave/components/brave_new_tab_ui:new_tab_takeover_generated_resources" ] + sources += [ "$root_gen_dir/brave/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover_generated.pak" ] + } + if (!is_ios) { deps += [ "//brave/components/brave_adblock_ui:generated_resources", diff --git a/components/resources/brave_components_resources.grd b/components/resources/brave_components_resources.grd index 71509dd54ba6..7022c227a93a 100644 --- a/components/resources/brave_components_resources.grd +++ b/components/resources/brave_components_resources.grd @@ -104,6 +104,10 @@ + + + + diff --git a/resources/resource_ids.spec b/resources/resource_ids.spec index 58e0454fa138..d24c52c73520 100644 --- a/resources/resource_ids.spec +++ b/resources/resource_ids.spec @@ -241,6 +241,10 @@ "META": {"sizes": {"includes": [10]}}, "includes": [34770], }, + "<(SHARED_INTERMEDIATE_DIR)/brave/web-ui-new_tab_takeover/new_tab_takeover.grd": { + "META": {"sizes": {"includes": [10]}}, + "includes": [34780], + }, # WARNING: The upstream ChromeOS/Ash strings currently run through 36930. We # must be careful not to exceed that maximum when adding new strings here. } diff --git a/test/BUILD.gn b/test/BUILD.gn index 7c4afaf9503b..f2b735d597e5 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -125,6 +125,7 @@ test("brave_unit_tests") { "//brave/components/content_settings/core/browser/brave_content_settings_utils_unittest.cc", "//brave/components/ntp_background_images/browser/ntp_background_images_service_unittest.cc", "//brave/components/ntp_background_images/browser/ntp_background_images_source_unittest.cc", + "//brave/components/ntp_background_images/browser/ntp_sponsored_images_data_unittest.cc", "//brave/components/ntp_background_images/browser/ntp_sponsored_rich_media_ad_event_handler_unittest.cc", "//brave/components/ntp_background_images/browser/ntp_sponsored_rich_media_source_unittest.cc", "//brave/components/ntp_background_images/browser/ntp_sponsored_source_util_unittest.cc",