diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni index da227de40599..9a8ed66f7280 100644 --- a/android/brave_java_sources.gni +++ b/android/brave_java_sources.gni @@ -259,11 +259,13 @@ 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/SponsoredBackgroundWebView.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", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/NTPImage.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/SponsoredLogo.java", + "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/SponsoredRichMedia.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/SponsoredTab.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/TopSite.java", "../../brave/android/java/org/chromium/chrome/browser/ntp_background_images/model/Wallpaper.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..4440d054160e 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; @@ -76,6 +78,7 @@ import org.chromium.chrome.browser.logo.LogoCoordinator; import org.chromium.chrome.browser.ntp_background_images.NTPBackgroundImagesBridge; import org.chromium.chrome.browser.ntp_background_images.model.NTPImage; +import org.chromium.chrome.browser.ntp_background_images.model.SponsoredRichMedia; import org.chromium.chrome.browser.ntp_background_images.model.SponsoredTab; import org.chromium.chrome.browser.ntp_background_images.model.TopSite; import org.chromium.chrome.browser.ntp_background_images.model.Wallpaper; @@ -132,7 +135,13 @@ public class BraveNewTabPageLayout private Integer mInitialTileNum; // Own members. + private WindowAndroid mWindowAndroid; + private ImageView mBgImageView; + private SponsoredBackgroundWebView mSponsoredBackgroundWebView; + private FrameLayout mBackgroundSponsoredContentView; + private SponsoredRichMedia mSponsoredRichMedia; + private Profile mProfile; private SponsoredTab mSponsoredTab; private boolean mIsTablet; @@ -441,6 +450,31 @@ 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 && mSponsoredBackgroundWebView != null) { + mSponsoredBackgroundWebView.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 @@ -931,6 +965,11 @@ public void onConfigurationChanged(Configuration newConfig) { if (mWallpaper == null) { mSponsoredTab.setNTPImage(SponsoredImageUtil.getBackgroundImage()); } + } else if (ntpImage instanceof SponsoredRichMedia) { + mSponsoredRichMedia = (SponsoredRichMedia) ntpImage; + if (mSponsoredRichMedia == null) { + mSponsoredTab.setNTPImage(SponsoredImageUtil.getBackgroundImage()); + } } super.onConfigurationChanged(newConfig); showNTPImage(ntpImage); @@ -1181,6 +1220,8 @@ public void initialize( tabStripHeightSupplier); mIsTablet = isTablet; + mWindowAndroid = windowAndroid; + assert mMvTilesContainerLayout != null : "Something has changed in the upstream!"; @@ -1222,6 +1263,9 @@ private void showNTPImage(NTPImage ntpImage) { && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { setBackgroundImage(ntpImage); + } else if (ntpImage instanceof SponsoredRichMedia) { + final SponsoredRichMedia sponsoredRichMedia = (SponsoredRichMedia) ntpImage; + setupSponsoredBackgroundContent(sponsoredRichMedia); } else if (UserPrefs.get(ProfileManager.getLastUsedRegularProfile()) .getBoolean(BravePref.NEW_TAB_PAGE_SHOW_BACKGROUND_IMAGE) && mSponsoredTab != null @@ -1230,6 +1274,24 @@ private void showNTPImage(NTPImage ntpImage) { } } + private void createSponsoredBackgroundWebContentsIfNeeded() { + if (mSponsoredBackgroundWebView != null) { + return; + } + + mSponsoredBackgroundWebView = new SponsoredBackgroundWebView(mActivity, mWindowAndroid, mProfile); + + mBackgroundSponsoredContentView = findViewById(R.id.bg_background_view); + mBackgroundSponsoredContentView.setVisibility(View.VISIBLE); + mBackgroundSponsoredContentView.addView(mSponsoredBackgroundWebView.getView()); + } + + private void setupSponsoredBackgroundContent(SponsoredRichMedia sponsoredRichMedia) { + createSponsoredBackgroundWebContentsIfNeeded(); + + mSponsoredBackgroundWebView.loadSponsoredRichMedia(sponsoredRichMedia); + } + private void setBackgroundImage(NTPImage ntpImage) { mBgImageView = (ImageView) findViewById(R.id.bg_image_view); mBgImageView.setScaleType(ImageView.ScaleType.MATRIX); @@ -1261,6 +1323,11 @@ private void checkAndShowNTPImage(boolean isReset) { if (mWallpaper == null) { mSponsoredTab.setNTPImage(SponsoredImageUtil.getBackgroundImage()); } + } else if (ntpImage instanceof SponsoredRichMedia) { + mSponsoredRichMedia = (SponsoredRichMedia) ntpImage; + if (mSponsoredRichMedia == null) { + mSponsoredTab.setNTPImage(SponsoredImageUtil.getBackgroundImage()); + } } showNTPImage(ntpImage); } diff --git a/android/java/org/chromium/chrome/browser/ntp/SponsoredBackgroundWebView.java b/android/java/org/chromium/chrome/browser/ntp/SponsoredBackgroundWebView.java new file mode 100644 index 000000000000..eba62d3a8499 --- /dev/null +++ b/android/java/org/chromium/chrome/browser/ntp/SponsoredBackgroundWebView.java @@ -0,0 +1,85 @@ +/* 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.net.Uri; +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.ntp_background_images.model.SponsoredRichMedia; +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.IntentRequestTracker; +import org.chromium.ui.base.ViewAndroidDelegate; +import org.chromium.ui.base.WindowAndroid; + +public class SponsoredBackgroundWebView { + private WebContents mSponsoredBackgroundWebContents; + private ThinWebView mSponsoredBackgroundWebView; + + private static final String NEW_TAB_TAKEOVER_URL = "chrome://new-tab-takeover"; + private static final String WALLPAPER_URL = "wallpaperUrl"; + private static final String CREATIVE_INSTANCE_ID = "creativeInstanceId"; + private static final String PLACEMENT_ID = "placementId"; + private static final String TARGET_URL = "targetUrl"; + + public SponsoredBackgroundWebView(Activity activity, WindowAndroid windowAndroid, Profile profile) { + mSponsoredBackgroundWebContents = + WebContentsFactory.createWebContentsWithWarmRenderer( + profile, + /* initiallyHidden= */ false, + /* targetNetwork= */ NetId.INVALID); + + final ContentView webContentView = + ContentView.createContentView(activity, mSponsoredBackgroundWebContents); + final ViewAndroidDelegate delegate = + ViewAndroidDelegate.createBasicDelegate(webContentView); + mSponsoredBackgroundWebContents.setDelegates( + VersionInfo.getProductVersion(), + delegate, + webContentView, + windowAndroid, + WebContents.createDefaultInternalsHolder()); + + final IntentRequestTracker intentRequestTracker = windowAndroid.getIntentRequestTracker(); + mSponsoredBackgroundWebView = + ThinWebViewFactory.create( + activity, new ThinWebViewConstraints(), intentRequestTracker); + mSponsoredBackgroundWebView + .getView() + .setLayoutParams( + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mSponsoredBackgroundWebView.attachWebContents(mSponsoredBackgroundWebContents, + webContentView, + null); + } + + public void loadSponsoredRichMedia(SponsoredRichMedia sponsoredRichMedia) { + Uri.Builder builder = Uri.parse(NEW_TAB_TAKEOVER_URL).buildUpon(); + builder.appendQueryParameter(WALLPAPER_URL, sponsoredRichMedia.getWallpaperUrl()); + builder.appendQueryParameter(CREATIVE_INSTANCE_ID, sponsoredRichMedia.getCreativeInstanceId()); + builder.appendQueryParameter(PLACEMENT_ID, sponsoredRichMedia.getPlacementId()); + builder.appendQueryParameter(TARGET_URL, sponsoredRichMedia.getTargetUrl()); + + mSponsoredBackgroundWebContents.getNavigationController().loadUrl( + new LoadUrlParams(builder.build().toString())); + } + + public View getView() { + return mSponsoredBackgroundWebView.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..5fa86fc36c99 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 @@ -17,6 +17,7 @@ import org.chromium.chrome.browser.ntp_background_images.model.ImageCredit; import org.chromium.chrome.browser.ntp_background_images.model.NTPImage; import org.chromium.chrome.browser.ntp_background_images.model.TopSite; +import org.chromium.chrome.browser.ntp_background_images.model.SponsoredRichMedia; import org.chromium.chrome.browser.ntp_background_images.model.Wallpaper; import org.chromium.chrome.browser.ntp_background_images.util.NewTabPageListener; import org.chromium.chrome.browser.profiles.Profile; @@ -155,6 +156,11 @@ public static Wallpaper createBrandedWallpaper(String imagePath, int focalPointX wallpaperId); } + @CalledByNative + public static SponsoredRichMedia createSponsoredRichMedia(String wallpaperUrl, String creativeInstanceId, String placementId, String targetUrl) { + return new SponsoredRichMedia(wallpaperUrl, creativeInstanceId, placementId, targetUrl); + } + @CalledByNative public void onUpdated() { for (NTPBackgroundImageServiceObserver observer : mObservers) { diff --git a/android/java/org/chromium/chrome/browser/ntp_background_images/model/SponsoredRichMedia.java b/android/java/org/chromium/chrome/browser/ntp_background_images/model/SponsoredRichMedia.java new file mode 100644 index 000000000000..bb3ad370d7ca --- /dev/null +++ b/android/java/org/chromium/chrome/browser/ntp_background_images/model/SponsoredRichMedia.java @@ -0,0 +1,52 @@ +/* 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_background_images.model; + +public class SponsoredRichMedia extends NTPImage { + private String mWallpaperUrl; + private String mCreativeInstanceId; + private String mPlacementId; + private String mTargetUrl; + + public SponsoredRichMedia(String wallpaperUrl, String creativeInstanceId, String placementId, String targetUrl) { + mWallpaperUrl = wallpaperUrl; + mCreativeInstanceId = creativeInstanceId; + mPlacementId = placementId; + mTargetUrl = targetUrl; + } + + public String getWallpaperUrl() { + return mWallpaperUrl; + } + + public void setWallpaperUrl(String wallpaperUrl) { + mWallpaperUrl = wallpaperUrl; + } + + public String getCreativeInstanceId() { + return mCreativeInstanceId; + } + + public void setCreativeInstanceId(String creativeInstanceId) { + mCreativeInstanceId = creativeInstanceId; + } + + public String getPlacementId() { + return mPlacementId; + } + + public void setPlacementId(String placementId) { + mPlacementId = placementId; + } + + public String getTargetUrl() { + return mTargetUrl; + } + + public void setTargetUrl(String targetUrl) { + mTargetUrl = targetUrl; + } +} diff --git a/android/java/org/chromium/chrome/browser/util/TabUtils.java b/android/java/org/chromium/chrome/browser/util/TabUtils.java index 19c79a8ef61a..7b94896c887e 100644 --- a/android/java/org/chromium/chrome/browser/util/TabUtils.java +++ b/android/java/org/chromium/chrome/browser/util/TabUtils.java @@ -46,6 +46,8 @@ import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.ui.util.ColorUtils; +import org.jni_zero.CalledByNative; + import java.lang.reflect.Field; public class TabUtils { @@ -236,6 +238,7 @@ public static void openUrlInNewTabInBackground(boolean isIncognito, String url) } } + @CalledByNative public static void openUrlInSameTab(String url) { try { BraveActivity braveActivity = BraveActivity.getBraveActivity(); diff --git a/android/java/res/layout/new_tab_page_layout.xml b/android/java/res/layout/new_tab_page_layout.xml index 6f9694282743..bdadb6683937 100644 --- a/android/java/res/layout/new_tab_page_layout.xml +++ b/android/java/res/layout/new_tab_page_layout.xml @@ -25,6 +25,12 @@ android:layout_height="match_parent" android:contentDescription="@null"/> + + + () .Add(); } -#endif +#else + 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..97299bf9aaa6 100644 --- a/browser/ntp_background/android/ntp_background_images_bridge.cc +++ b/browser/ntp_background/android/ntp_background_images_bridge.cc @@ -193,6 +193,42 @@ NTPBackgroundImagesBridge::CreateBrandedWallpaper( ConvertUTF8ToJavaString(env, wallpaper_id ? *wallpaper_id : "")); } +base::android::ScopedJavaLocalRef +NTPBackgroundImagesBridge::CreateSponsoredRichMedia( + const base::Value::Dict& data) { + JNIEnv* env = AttachCurrentThread(); + + const std::string* wallpaper_url = + data.FindString(ntp_background_images::kWallpaperURLKey); + if (!wallpaper_url) { + return base::android::ScopedJavaLocalRef(); + } + + const std::string* creative_instance_id = + data.FindString(ntp_background_images::kCreativeInstanceIDKey); + if (!creative_instance_id) { + return base::android::ScopedJavaLocalRef(); + } + + const std::string* placement_id = + data.FindString(ntp_background_images::kWallpaperIDKey); + if (!placement_id) { + return base::android::ScopedJavaLocalRef(); + } + + const std::string* target_url = data.FindStringByDottedPath( + ntp_background_images::kLogoDestinationURLPath); + if (!target_url) { + return base::android::ScopedJavaLocalRef(); + } + + return Java_NTPBackgroundImagesBridge_createSponsoredRichMedia( + env, ConvertUTF8ToJavaString(env, *wallpaper_url), + ConvertUTF8ToJavaString(env, *creative_instance_id), + ConvertUTF8ToJavaString(env, *placement_id), + ConvertUTF8ToJavaString(env, *target_url)); +} + void NTPBackgroundImagesBridge::GetTopSites(JNIEnv* env, const JavaParamRef& obj) { std::vector top_sites = @@ -257,12 +293,17 @@ NTPBackgroundImagesBridge::GetCurrentWallpaper( if (!data) return base::android::ScopedJavaLocalRef(); - bool is_background = + const bool is_background = data->FindBool(ntp_background_images::kIsBackgroundKey).value_or(false); - if (!is_background) { - return CreateBrandedWallpaper(*data); - } else { + const std::string* sponsored_rich_media_type = + data->FindString(ntp_background_images::kWallpaperTypeKey); + if (is_background) { return CreateWallpaper(*data); + } else if (sponsored_rich_media_type && + *sponsored_rich_media_type == "richMedia") { + return CreateSponsoredRichMedia(*data); + } else { + return CreateBrandedWallpaper(*data); } } diff --git a/browser/ntp_background/android/ntp_background_images_bridge.h b/browser/ntp_background/android/ntp_background_images_bridge.h index e420790e530d..7be54d14b4d9 100644 --- a/browser/ntp_background/android/ntp_background_images_bridge.h +++ b/browser/ntp_background/android/ntp_background_images_bridge.h @@ -77,6 +77,8 @@ class NTPBackgroundImagesBridge : public NTPBackgroundImagesService::Observer, const base::Value::Dict& data); base::android::ScopedJavaLocalRef CreateBrandedWallpaper( const base::Value::Dict& data); + base::android::ScopedJavaLocalRef CreateSponsoredRichMedia( + const base::Value::Dict& data); raw_ptr profile_ = nullptr; raw_ptr view_counter_service_ = nullptr; 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/brave_web_ui_controller_factory.cc b/browser/ui/webui/brave_web_ui_controller_factory.cc index aea3eb8d530f..62c8d10820da 100644 --- a/browser/ui/webui/brave_web_ui_controller_factory.cc +++ b/browser/ui/webui/brave_web_ui_controller_factory.cc @@ -56,6 +56,7 @@ #if BUILDFLAG(IS_ANDROID) #include "brave/browser/brave_wallet/brave_wallet_service_factory.h" #include "brave/browser/ui/webui/brave_wallet/android/android_wallet_page_ui.h" +#include "brave/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.h" #include "brave/components/brave_wallet/browser/brave_wallet_service.h" #include "brave/components/brave_wallet/browser/keyring_service.h" #endif @@ -166,6 +167,14 @@ WebUIController* NewWebUI(WebUI* web_ui, const GURL& url) { #if BUILDFLAG(IS_ANDROID) } else if (url.is_valid() && url.host() == kWalletPageHost) { return new AndroidWalletPageUI(web_ui, url); + } else if (host == kNewTabTakeoverHost) { + return new NewTabTakeoverUI( + web_ui, url.host(), + brave_ads::AdsServiceFactory::GetForProfile(profile), + g_browser_process->local_state(), + g_brave_browser_process->p3a_service(), + g_brave_browser_process->ntp_background_images_service(), + profile->GetPrefs()); #endif #if BUILDFLAG(ENABLE_AI_REWRITER) } else if (host == kRewriterUIHost) { @@ -197,6 +206,7 @@ WebUIFactoryFunction GetWebUIFactoryFunction(WebUI* web_ui, base::FeatureList::IsEnabled(skus::features::kSkusFeature)) || #if BUILDFLAG(IS_ANDROID) (url.is_valid() && url.host_piece() == kWalletPageHost) || + url.host_piece() == kNewTabTakeoverHost || #else (base::FeatureList::IsEnabled( brave_news::features::kBraveNewsFeedUpdate) && 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..df0a70556493 --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/BUILD.gn @@ -0,0 +1,35 @@ +# 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", + ] + + deps = [ + "//base", + "//brave/browser/ntp_background", + "//brave/build/android:jni_headers", + "//brave/components/brave_new_tab_ui:new_tab_takeover_generated_resources", + "//brave/components/constants", + "//brave/components/ntp_background_images/browser", + "//brave/components/p3a", + "//chrome/browser:browser_public_dependencies", + "//content/public/browser", + "//content/public/common", + "//ui/webui", + ] + + 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..ea6efa3111ad --- /dev/null +++ b/browser/ui/webui/new_tab_takeover/android/new_tab_takeover_ui.cc @@ -0,0 +1,85 @@ +// 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 "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "brave/browser/ntp_background/ntp_p3a_helper_impl.h" +#include "brave/browser/ui/webui/brave_webui_source.h" +#include "brave/build/android/jni_headers/TabUtils_jni.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_p3a_helper.h" +#include "brave/components/ntp_background_images/browser/ntp_sponsored_rich_media_ad_event_handler.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" + +NewTabTakeoverUI::NewTabTakeoverUI( + content::WebUI* const web_ui, + const std::string& name, + brave_ads::AdsService* ads_service, + PrefService* local_state, + p3a::P3AService* p3a_service, + ntp_background_images::NTPBackgroundImagesService* + ntp_background_images_service, + PrefService* profile_prefs) + : ui::MojoWebUIController(web_ui) { + content::WebUIDataSource* source = CreateAndAddWebUIDataSource( + web_ui, name, 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); + + auto ntp_p3a_helper = + std::make_unique( + local_state, p3a_service, ntp_background_images_service, + profile_prefs); + + rich_media_ad_event_handler_ = std::make_unique< + ntp_background_images::NTPSponsoredRichMediaAdEventHandler>( + ads_service, std::move(ntp_p3a_helper)); +} + +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::NavigateToUrl(const GURL& url) { + Java_TabUtils_openUrlInSameTab( + base::android::AttachCurrentThread(), + base::android::ConvertUTF8ToJavaString( + base::android::AttachCurrentThread(), url.spec())); +} + +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..4505fa83c256 --- /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 "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" + +class PrefService; + +namespace brave_ads { +class AdsService; +} // namespace brave_ads + +namespace ntp_background_images { +class NTPBackgroundImagesService; +class NTPSponsoredRichMediaAdEventHandler; +} // namespace ntp_background_images + +namespace p3a { +class P3AService; +} // namespace p3a + +class NewTabTakeoverUI : public ui::MojoWebUIController, + public new_tab_takeover::mojom::NewTabTakeover { + public: + NewTabTakeoverUI(content::WebUI* const web_ui, + const std::string& name, + brave_ads::AdsService* ads_service, + PrefService* local_state, + p3a::P3AService* p3a_service, + ntp_background_images::NTPBackgroundImagesService* + ntp_background_images_service, + PrefService* profile_prefs); + + 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 NavigateToUrl(const GURL& url) override; + + mojo::Receiver + new_tab_takeover_receiver_{this}; + + 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/build/android/BUILD.gn b/build/android/BUILD.gn index 3f98c4155052..d7b9f1a445e4 100644 --- a/build/android/BUILD.gn +++ b/build/android/BUILD.gn @@ -240,6 +240,7 @@ generate_jni("jni_headers") { "//brave/android/java/org/chromium/chrome/browser/signin/BraveSigninManager.java", "//brave/android/java/org/chromium/chrome/browser/speedreader/BraveSpeedReaderUtils.java", "//brave/android/java/org/chromium/chrome/browser/sync/BraveSyncDevices.java", + "//brave/android/java/org/chromium/chrome/browser/util/TabUtils.java", "//brave/android/java/org/chromium/chrome/browser/vpn/BraveVpnNativeWorker.java", "//brave/android/java/org/chromium/chrome/browser/vpn/BraveVpnServiceFactoryAndroid.java", "//brave/android/java/org/chromium/chrome/browser/webcompat_reporter/WebcompatReporterServiceFactory.java", 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..f2f35458b072 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/App.tsx @@ -0,0 +1,78 @@ +// 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) + + React.useEffect(() => { + const params = new URLSearchParams(window.location.search) + const wallpaperUrl = params.get('wallpaperUrl') + const creativeInstanceId = params.get('creativeInstanceId') + const placementId = params.get('placementId') + const targetUrl = params.get('targetUrl') + + if (!wallpaperUrl || !creativeInstanceId || !placementId) { + return + } + + const sponsoredRichMediaBackgroundInfo: SponsoredRichMediaBackgroundInfo = { + url: wallpaperUrl, + creativeInstanceId: creativeInstanceId, + placementId: placementId, + targetUrl: targetUrl ?? '' + } + setSponsoredRichMediaBackgroundInfo(sponsoredRichMediaBackgroundInfo) + + 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) + } + }, []) + + if (!sponsoredRichMediaBackgroundInfo || !sponsoredRichMediaAdEventHandler || !newTabTakeover) { + return null + } + + return ( + { 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..52b222d195b2 --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/mojom/new_tab_takeover.mojom @@ -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/. + +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); + + 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..8c628d49814d --- /dev/null +++ b/components/brave_new_tab_ui/new_tab_takeover/new_tab_takeover.html @@ -0,0 +1,17 @@ + + + + + + New Tab Takeover Page + + + + + + + +
+ + + 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_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/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. }