4040import android .net .Uri ;
4141import android .os .Build ;
4242import android .os .Bundle ;
43+ import android .os .Message ;
4344import android .view .LayoutInflater ;
4445import android .view .MotionEvent ;
4546import android .view .View ;
4647import android .view .ViewGroup ;
4748import android .webkit .PermissionRequest ;
4849import android .webkit .WebChromeClient ;
50+ import android .webkit .WebResourceRequest ;
4951import android .webkit .WebSettings ;
5052import android .webkit .WebView ;
53+ import android .webkit .WebViewClient ;
5154import android .widget .ProgressBar ;
5255
5356import androidx .activity .result .ActivityResultLauncher ;
7982import com .microsoft .identity .common .java .util .StringUtil ;
8083import com .microsoft .identity .common .logging .Logger ;
8184
85+ import com .microsoft .identity .common .java .opentelemetry .AttributeName ;
8286import java .io .UnsupportedEncodingException ;
8387import java .net .URLEncoder ;
8488import java .util .Arrays ;
8791
8892import static com .microsoft .identity .common .java .AuthenticationConstants .OAuth2 .UTID ;
8993
94+ import com .microsoft .identity .common .java .opentelemetry .OTelUtility ;
95+ import com .microsoft .identity .common .java .opentelemetry .SpanExtension ;
96+ import com .microsoft .identity .common .java .opentelemetry .SpanName ;
9097
98+ import io .opentelemetry .api .trace .Span ;
9199import io .opentelemetry .api .trace .SpanContext ;
100+ import io .opentelemetry .api .trace .StatusCode ;
101+ import io .opentelemetry .context .Scope ;
92102
93103/**
94104 * Authorization fragment with embedded webview.
@@ -147,6 +157,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
147157 if (activity != null ) {
148158 WebViewUtil .setDataDirectorySuffix (activity .getApplicationContext ());
149159 }
160+
150161 if (CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_LEGACY_FIDO_SECURITY_KEY_LOGIC )
151162 && Build .VERSION .SDK_INT < Build .VERSION_CODES .UPSIDE_DOWN_CAKE ) {
152163 mFidoLauncher = registerForActivityResult (
@@ -264,6 +275,14 @@ public void onPageLoaded(final String url) {
264275 if (!mAuthResultSent && !StringExtensions .isNullOrBlank (javascriptToExecute [0 ])) {
265276 mWebView .evaluateJavascript (javascriptToExecute [0 ], null );
266277 }
278+
279+ // Dynamically toggle multiple-windows support so that target="_blank"
280+ // interception is active ONLY on the TLR start page. On all other
281+ // pages the WebView behaves exactly as before.
282+ if (CommonFlightsManager .INSTANCE .getFlightsProvider ()
283+ .isFlightEnabled (CommonFlight .ENABLE_WEBVIEW_MULTIPLE_WINDOWS )) {
284+ mWebView .getSettings ().setSupportMultipleWindows (isTlrUrl (url ));
285+ }
267286 }
268287 },
269288 mRedirectUri ,
@@ -352,6 +371,7 @@ public boolean onTouch(final View view, final MotionEvent event) {
352371 mWebView .getSettings ().setUseWideViewPort (true );
353372 mWebView .getSettings ().setBuiltInZoomControls (webViewZoomControlsEnabled );
354373 mWebView .getSettings ().setSupportZoom (webViewZoomEnabled );
374+
355375 mWebView .setVisibility (View .INVISIBLE );
356376 mWebView .setWebViewClient (webViewClient );
357377 mWebView .setWebChromeClient (new WebChromeClient () {
@@ -376,10 +396,125 @@ public Bitmap getDefaultVideoPoster() {
376396 // We will return a 10x10 empty image, instead of the default grey playback image. #2424
377397 return Bitmap .createBitmap (10 , 10 , Bitmap .Config .ARGB_8888 );
378398 }
399+
400+ @ Override
401+ public boolean onCreateWindow (final WebView view , boolean isDialog ,
402+ boolean isUserGesture , final Message resultMsg ) {
403+ if (resultMsg .obj == null ) {
404+ Logger .error (methodTag , "onCreateWindow: resultMsg.obj is null, cannot set up transport." , null );
405+ return false ;
406+ }
407+
408+ final SpanContext parentSpanContext = requireActivity () instanceof AuthorizationActivity
409+ ? ((AuthorizationActivity ) requireActivity ()).getSpanContext () : null ;
410+ final Span span = OTelUtility .createSpanFromParent (
411+ SpanName .WebViewTargetBlankNavigation .name (), parentSpanContext );
412+ boolean windowHandled = false ;
413+ try (final Scope scope = SpanExtension .makeCurrentSpan (span )) {
414+ Logger .info (methodTag , "onCreateWindow: intercepting target=_blank navigation." );
415+ final WebView interceptorWebView = new WebView (view .getContext ());
416+ interceptorWebView .setWebViewClient (new WebViewClient () {
417+ @ Override
418+ public boolean shouldOverrideUrlLoading (WebView v , WebResourceRequest request ) {
419+ handleInterceptedUrlFromNewWindow (view , v , request , span , isUserGesture );
420+ return true ;
421+ }
422+ });
423+ final WebView .WebViewTransport transport = (WebView .WebViewTransport ) resultMsg .obj ;
424+ transport .setWebView (interceptorWebView );
425+ resultMsg .sendToTarget ();
426+ // Span status and end are handled in handleInterceptedUrlFromNewWindow,
427+ // which fires asynchronously when shouldOverrideUrlLoading is called.
428+ windowHandled = true ;
429+ } catch (@ NonNull final Exception e ) {
430+ Logger .error (methodTag , "Error handling target=_blank navigation." , e );
431+ span .recordException (e );
432+ span .setStatus (StatusCode .ERROR );
433+ span .end ();
434+ }
435+ return windowHandled ;
436+ }
379437 });
380438 setupPasskeyWebListener (mWebView , webViewClient );
381439 }
382440
441+ /**
442+ * Handles the URL intercepted from a target=_blank navigation (onCreateWindow).
443+ * Routes the URL based on whether the main WebView is currently on a TLR page:
444+ * - TLR page: opens the URL in an external browser.
445+ * - Non-TLR page: loads the URL inline in the main WebView.
446+ *
447+ * @param mainWebView The main authentication WebView.
448+ * @param interceptorWebView The temporary interceptor WebView (will be destroyed after handling).
449+ * @param request The intercepted URL request.
450+ * @param span The telemetry span to record which routing path is taken.
451+ * @param isUserGesture Whether the popup was initiated by a user gesture (e.g. a click).
452+ */
453+ @ VisibleForTesting
454+ void handleInterceptedUrlFromNewWindow (@ NonNull final WebView mainWebView ,
455+ @ NonNull final WebView interceptorWebView ,
456+ @ NonNull final WebResourceRequest request ,
457+ @ NonNull final Span span ,
458+ final boolean isUserGesture ) {
459+ final String methodTag = TAG + ":handleInterceptedUrlFromNewWindow" ;
460+ try {
461+ final String targetUrl = request .getUrl ().toString ();
462+ final String currentPageUrl = mainWebView .getUrl ();
463+
464+ if (targetUrl == null ) {
465+ span .setAttribute (AttributeName .target_blank_navigation_route .name (), AuthenticationConstants .Broker .WEBVIEW_TARGET_BLANK_ROUTE_NULL_URL );
466+ Logger .warn (methodTag , "onCreateWindow: target URL is null, ignoring." );
467+ } else if (!isUserGesture ) {
468+ // Not initiated by user gesture: load inline as a safe fallback instead of
469+ // opening an external browser, to prevent programmatic/scripted popups.
470+ span .setAttribute (AttributeName .target_blank_navigation_route .name (), AuthenticationConstants .Broker .WEBVIEW_TARGET_BLANK_ROUTE_NO_USER_GESTURE );
471+ Logger .warn (methodTag , "onCreateWindow: popup not initiated by user gesture, loading URL inline." );
472+ mainWebView .loadUrl (targetUrl );
473+ } else if (!targetUrl .toLowerCase ().startsWith (AuthenticationConstants .Broker .REDIRECT_SSL_PREFIX )) {
474+ // Non-SSL URL: refuse to open, matching AzureActiveDirectoryWebViewClient behavior.
475+ span .setAttribute (AttributeName .target_blank_navigation_route .name (), AuthenticationConstants .Broker .WEBVIEW_TARGET_BLANK_ROUTE_NON_SSL );
476+ Logger .error (methodTag , "onCreateWindow: URL is not SSL protected, refusing to open." , null );
477+ } else if (!isTlrUrl (currentPageUrl )) {
478+ // Non-TLR page: load inline, same as WebView default behavior.
479+ span .setAttribute (AttributeName .target_blank_navigation_route .name (), AuthenticationConstants .Broker .WEBVIEW_TARGET_BLANK_ROUTE_NON_TLR );
480+ Logger .warn (methodTag , "onCreateWindow: non-TLR page, loading URL inline as fallback." );
481+ mainWebView .loadUrl (targetUrl );
482+ } else {
483+ // TLR page: delegate to system browser so user can view terms externally.
484+ span .setAttribute (AttributeName .target_blank_navigation_route .name (), AuthenticationConstants .Broker .WEBVIEW_TARGET_BLANK_ROUTE_TLR );
485+ Logger .info (methodTag , "onCreateWindow: TLR page, delegating URL to system browser." );
486+ final Intent browserIntent = new Intent (Intent .ACTION_VIEW , Uri .parse (targetUrl ));
487+ mainWebView .getContext ().startActivity (browserIntent );
488+ }
489+ span .setStatus (StatusCode .OK );
490+ } catch (final Exception e ) {
491+ span .recordException (e );
492+ span .setStatus (StatusCode .ERROR );
493+ Logger .error (methodTag , "Error handling target=_blank URL." , e );
494+ } finally {
495+ span .end ();
496+ // Destroy the interceptor WebView after it has served its purpose
497+ interceptorWebView .post (interceptorWebView ::destroy );
498+ }
499+ }
500+
501+ /**
502+ * Checks whether the given URL corresponds to a TLR (Terms, License, and Restrictions)
503+ * start page.
504+ *
505+ * @param url The URL to check.
506+ * @return {@code true} if the URL is a TLR start page, {@code false} otherwise.
507+ */
508+ @ VisibleForTesting
509+ boolean isTlrUrl (@ Nullable final String url ) {
510+ if (url == null ) {
511+ return false ;
512+ }
513+ final String lowerUrl = url .toLowerCase ();
514+ return lowerUrl .startsWith (AuthenticationConstants .Broker .REDIRECT_SSL_PREFIX )
515+ && lowerUrl .contains (AuthenticationConstants .Broker .TLR_START_PATH );
516+ }
517+
383518 /**
384519 * Loads starting authorization request url into WebView.
385520 */
0 commit comments