4646import android .view .View ;
4747import android .view .ViewGroup ;
4848import android .webkit .PermissionRequest ;
49+ import android .webkit .ValueCallback ;
4950import android .webkit .WebChromeClient ;
5051import android .webkit .WebResourceRequest ;
5152import android .webkit .WebSettings ;
5455import android .widget .ProgressBar ;
5556
5657import androidx .activity .result .ActivityResultLauncher ;
58+ import androidx .activity .result .contract .ActivityResultContracts ;
5759import androidx .annotation .NonNull ;
5860import androidx .annotation .Nullable ;
5961import androidx .annotation .VisibleForTesting ;
@@ -139,6 +141,19 @@ public class WebViewAuthorizationFragment extends AuthorizationFragment {
139141
140142 private final CameraPermissionRequestHandler mCameraPermissionRequestHandler = new CameraPermissionRequestHandler (this );
141143
144+ /**
145+ * Callback for file chooser requests from the WebView.
146+ * This is set when {@link WebChromeClient#onShowFileChooser} is invoked and
147+ * must be called back with the selected file URI(s) or null if cancelled.
148+ */
149+ private ValueCallback <Uri []> mFileUploadCallback ;
150+
151+ /**
152+ * Launcher for the file chooser activity, registered in {@link #onCreate}.
153+ * Handles the result of the file selection and passes it back to the WebView.
154+ */
155+ private ActivityResultLauncher <Intent > mFileChooserLauncher ;
156+
142157 // This is used by LegacyFido2ApiManager to launch a PendingIntent received by the legacy API.
143158 private ActivityResultLauncher <LegacyFido2ApiObject > mFidoLauncher ;
144159 // This is used by the switch browser protocol to handle the resume of the flow.
@@ -158,6 +173,39 @@ public void onCreate(@Nullable Bundle savedInstanceState) {
158173 WebViewUtil .setDataDirectorySuffix (activity .getApplicationContext ());
159174 }
160175
176+ // Register file chooser launcher for WebView file upload support.
177+ if (CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_WEBVIEW_FILE_UPLOAD )) {
178+ mFileChooserLauncher = registerForActivityResult (
179+ new ActivityResultContracts .StartActivityForResult (),
180+ result -> {
181+ if (mFileUploadCallback == null ) {
182+ Logger .warn (methodTag , "File upload callback is null, ignoring result." );
183+ return ;
184+ }
185+ Uri [] resultUris = null ;
186+ if (result .getResultCode () == FragmentActivity .RESULT_OK && result .getData () != null ) {
187+ final Intent data = result .getData ();
188+ if (data .getClipData () != null ) {
189+ // Multiple files selected
190+ final int count = data .getClipData ().getItemCount ();
191+ resultUris = new Uri [count ];
192+ for (int i = 0 ; i < count ; i ++) {
193+ resultUris [i ] = data .getClipData ().getItemAt (i ).getUri ();
194+ }
195+ } else if (data .getData () != null ) {
196+ // Single file selected
197+ resultUris = new Uri []{data .getData ()};
198+ }
199+ Logger .info (methodTag , "File chooser returned "
200+ + (resultUris != null ? resultUris .length : 0 ) + " file(s)." );
201+ } else {
202+ Logger .info (methodTag , "File chooser cancelled or returned no data." );
203+ }
204+ mFileUploadCallback .onReceiveValue (resultUris );
205+ mFileUploadCallback = null ;
206+ }
207+ );
208+ }
161209 if (CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_LEGACY_FIDO_SECURITY_KEY_LOGIC )
162210 && Build .VERSION .SDK_INT < Build .VERSION_CODES .UPSIDE_DOWN_CAKE ) {
163211 mFidoLauncher = registerForActivityResult (
@@ -387,6 +435,17 @@ public void onPermissionRequest(final PermissionRequest request) {
387435 });
388436 }
389437
438+ @ Override
439+ public boolean onShowFileChooser (
440+ final WebView webView ,
441+ final ValueCallback <Uri []> filePathCallback ,
442+ final FileChooserParams fileChooserParams ) {
443+ final FragmentActivity host = getActivity ();
444+ final SpanContext parentSpanContext = host instanceof AuthorizationActivity
445+ ? ((AuthorizationActivity ) host ).getSpanContext () : null ;
446+ return handleFileUploadRequest (filePathCallback , fileChooserParams , parentSpanContext );
447+ }
448+
390449 @ Override
391450 public Bitmap getDefaultVideoPoster () {
392451 // When not playing, video elements are represented by a 'poster' image.
@@ -515,6 +574,87 @@ boolean isTlrUrl(@Nullable final String url) {
515574 && lowerUrl .contains (AuthenticationConstants .Broker .TLR_START_PATH );
516575 }
517576
577+ /**
578+ * Handles a file chooser request from the WebView. Creates a telemetry span,
579+ * manages the file upload callback, and launches the system file picker.
580+ *
581+ * @param filePathCallback The callback to deliver file selection results to the WebView.
582+ * @param fileChooserParams Parameters describing the file chooser request.
583+ * @param parentSpanContext The parent span context for telemetry, or null.
584+ * @return {@code true} if the file chooser was launched, {@code false} otherwise.
585+ */
586+ @ VisibleForTesting
587+ boolean handleFileUploadRequest (
588+ @ NonNull final ValueCallback <Uri []> filePathCallback ,
589+ @ NonNull final WebChromeClient .FileChooserParams fileChooserParams ,
590+ @ Nullable final SpanContext parentSpanContext ) {
591+ final String methodTag = TAG + ":handleFileUploadRequest" ;
592+
593+ if (!CommonFlightsManager .INSTANCE .getFlightsProvider ()
594+ .isFlightEnabled (CommonFlight .ENABLE_WEBVIEW_FILE_UPLOAD )) {
595+ Logger .info (methodTag , "ENABLE_WEBVIEW_FILE_UPLOAD flight is disabled." );
596+ return false ;
597+ }
598+
599+ final Span span = OTelUtility .createSpanFromParent (
600+ SpanName .WebViewFileUpload .name (), parentSpanContext );
601+
602+ try (final Scope scope = SpanExtension .makeCurrentSpan (span )) {
603+ // Cancel any existing callback to avoid a dangling reference.
604+ if (mFileUploadCallback != null ) {
605+ mFileUploadCallback .onReceiveValue (null );
606+ }
607+ // Clear any previous callback reference before handling the new request.
608+ mFileUploadCallback = null ;
609+
610+ // Ensure the file chooser launcher is initialized before attempting to launch.
611+ if (mFileChooserLauncher == null ) {
612+ Logger .error (methodTag ,
613+ "File chooser launcher is not initialized. Cannot handle file upload request." ,
614+ null );
615+ // Notify the caller that no file was selected/returned.
616+ filePathCallback .onReceiveValue (null );
617+ span .setStatus (StatusCode .ERROR );
618+ return false ;
619+ }
620+
621+ // At this point we have a valid launcher; store the callback for the result.
622+ mFileUploadCallback = filePathCallback ;
623+
624+ final Intent intent = fileChooserParams .createIntent ();
625+ Logger .info (methodTag , "Launching file chooser for WebView file upload." );
626+ mFileChooserLauncher .launch (intent );
627+ span .setStatus (StatusCode .OK );
628+ return true ;
629+ } catch (final Exception e ) {
630+ Logger .error (methodTag , "Failed to launch file chooser." , e );
631+ span .recordException (e );
632+ span .setStatus (StatusCode .ERROR );
633+ if (mFileUploadCallback != null ) {
634+ mFileUploadCallback .onReceiveValue (null );
635+ mFileUploadCallback = null ;
636+ }
637+ return false ;
638+ } finally {
639+ span .end ();
640+ }
641+ }
642+
643+ @ VisibleForTesting
644+ void setFileUploadCallback (@ Nullable final ValueCallback <Uri []> callback ) {
645+ mFileUploadCallback = callback ;
646+ }
647+
648+ @ VisibleForTesting
649+ ValueCallback <Uri []> getFileUploadCallback () {
650+ return mFileUploadCallback ;
651+ }
652+
653+ @ VisibleForTesting
654+ void setFileChooserLauncher (@ Nullable final ActivityResultLauncher <Intent > launcher ) {
655+ mFileChooserLauncher = launcher ;
656+ }
657+
518658 /**
519659 * Loads starting authorization request url into WebView.
520660 */
@@ -557,6 +697,14 @@ public void onDestroy() {
557697 // but we should still have a check here just to be safe.
558698 mFidoLauncher .unregister ();
559699 }
700+ // Clean up file upload callback to prevent memory leaks.
701+ if (mFileUploadCallback != null ) {
702+ mFileUploadCallback .onReceiveValue (null );
703+ mFileUploadCallback = null ;
704+ }
705+ if (mFileChooserLauncher != null ) {
706+ mFileChooserLauncher .unregister ();
707+ }
560708 }
561709
562710 /**
0 commit comments