diff --git a/android/java/apk_for_test.flags b/android/java/apk_for_test.flags index 71268116454..de977c82f28 100644 --- a/android/java/apk_for_test.flags +++ b/android/java/apk_for_test.flags @@ -475,6 +475,16 @@ *** getActionButtonTintList(...); } +-keep class org.chromium.chrome.browser.download.dialogs.DownloadLocationDialogCoordinator { + public (...); + *** mController; + *** onDirectoryOptionsRetrieved(...); +} + +-keep class org.chromium.chrome.browser.download.dialogs.BraveDownloadLocationDialogCoordinator { + public (...); +} + -keep class org.chromium.chrome.browser.download.BraveDownloadMessageUiControllerImpl { *** isVisibleToUser(...); } diff --git a/android/java/org/chromium/chrome/browser/download/settings/BraveDownloadSettings.java b/android/java/org/chromium/chrome/browser/download/settings/BraveDownloadSettings.java index 63aef13acb5..1b476432896 100644 --- a/android/java/org/chromium/chrome/browser/download/settings/BraveDownloadSettings.java +++ b/android/java/org/chromium/chrome/browser/download/settings/BraveDownloadSettings.java @@ -58,8 +58,8 @@ public class BraveDownloadSettings extends DownloadSettings ChromeSwitchPreference locationPromptEnabledPref = (ChromeSwitchPreference) findPreference(PREF_LOCATION_PROMPT_ENABLED); if (locationPromptEnabledPref != null) { - locationPromptEnabledPref.setTitle( - R.string.brave_download_location_prompt_enabled_title); + locationPromptEnabledPref.setVisible(true); + locationPromptEnabledPref.setOnPreferenceChangeListener(this); } } diff --git a/android/javatests/org/chromium/chrome/browser/BytecodeTest.java b/android/javatests/org/chromium/chrome/browser/BytecodeTest.java index 92f23838e92..4de75acb963 100644 --- a/android/javatests/org/chromium/chrome/browser/BytecodeTest.java +++ b/android/javatests/org/chromium/chrome/browser/BytecodeTest.java @@ -226,6 +226,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -338,6 +339,9 @@ public class BytecodeTest { "org/chromium/chrome/browser/share/send_tab_to_self/ManageAccountDevicesLinkView")); // presubmit: ignore-long-line Assert.assertTrue( classExists("org/chromium/chrome/browser/dom_distiller/ReaderModeManager")); + Assert.assertTrue( + classExists( + "org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator")); // presubmit: ignore-long-line Assert.assertTrue( classExists( "org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl")); @@ -788,6 +792,13 @@ public class BytecodeTest { MethodModifier.STATIC, String.class, Intent.class)); + Assert.assertTrue( + methodExists( + "org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator", // presubmit: ignore-long-line + "onDirectoryOptionsRetrieved", + MethodModifier.REGULAR, + void.class, + ArrayList.class)); Assert.assertTrue( methodExists( "org/chromium/chrome/browser/download/DownloadMessageUiControllerImpl", @@ -2494,6 +2505,10 @@ public class BytecodeTest { fieldExists("org/chromium/chrome/browser/ntp/NewTabPageLayout", "mProfile")); Assert.assertTrue( fieldExists("org/chromium/chrome/browser/dom_distiller/ReaderModeManager", "mTab")); + Assert.assertTrue( + fieldExists( + "org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator", // presubmit: ignore-long-line + "mController")); Assert.assertTrue( fieldExists( "org/chromium/chrome/browser/omnibox/LocationBarCoordinator", diff --git a/browser/download/android/BUILD.gn b/browser/download/android/BUILD.gn index 0502faf94e8..d6875766c49 100644 --- a/browser/download/android/BUILD.gn +++ b/browser/download/android/BUILD.gn @@ -7,14 +7,20 @@ import("//build/config/android/rules.gni") import("//third_party/jni_zero/jni_zero.gni") android_library("java") { - sources = - [ "java/src/org/chromium/chrome/browser/download/BraveMimeUtils.java" ] + sources = [ + "java/src/org/chromium/chrome/browser/download/BraveMimeUtils.java", + "java/src/org/chromium/chrome/browser/download/dialogs/BraveDownloadLocationDialogCoordinator.java", + ] deps = [ ":jni_headers", "//base:base_java", + "//build/android:build_java", + "//chrome/browser/download/android:file_provider_java", "//chrome/browser/download/android:java", + "//chrome/browser/profiles/android:java", "//third_party/jni_zero:jni_zero_java", + "//ui/android:ui_java", ] } diff --git a/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/BraveDownloadLocationDialogCoordinator.java b/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/BraveDownloadLocationDialogCoordinator.java new file mode 100644 index 00000000000..b31cd367637 --- /dev/null +++ b/browser/download/android/java/src/org/chromium/chrome/browser/download/dialogs/BraveDownloadLocationDialogCoordinator.java @@ -0,0 +1,124 @@ +/* Copyright (c) 2026 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.download.dialogs; + +import android.content.Context; + +import org.chromium.build.annotations.NullMarked; +import org.chromium.chrome.browser.download.DirectoryOption; +import org.chromium.chrome.browser.download.DownloadDialogBridge; +import org.chromium.chrome.browser.download.DownloadDirectoryProvider; +import org.chromium.chrome.browser.download.DownloadLocationDialogType; +import org.chromium.chrome.browser.download.DownloadPromptStatus; +import org.chromium.chrome.browser.profiles.Profile; +import org.chromium.ui.modaldialog.ModalDialogManager; + +import java.util.ArrayList; + +/** + * Brave override of {@link DownloadLocationDialogCoordinator} that always shows the download + * location dialog when the user has opted in via settings, even when only one storage directory is + * available (no SD card). + * + *

Upstream skips the dialog when there's only one storage directory by calling {@code + * mController.onDownloadLocationDialogComplete(path, false)} directly. We intercept that skip by + * temporarily wrapping the controller. When the skip fires, our wrapper re-triggers {@code + * onDirectoryOptionsRetrieved} with a modified directory list ({@code dirs.size() > 1}) so the + * parent's dialog display code runs normally. The actual directory UI is populated independently by + * {@link org.chromium.chrome.browser.download.settings.DownloadLocationHelperImpl}. + */ +@NullMarked +public class BraveDownloadLocationDialogCoordinator extends DownloadLocationDialogCoordinator { + // Shadows the parent's private field. The bytecode adapter deletes this declaration and + // makes the parent's field protected, so references resolve to the parent's field at runtime. + // Suppressed because this field is removed by bytecode rewriting and never actually exists. + @SuppressWarnings("NullAway") + private DownloadLocationDialogController mController; + + // Stub matching parent's private method. The bytecode adapter deletes this and makes the + // parent's method public, so calls resolve to the parent's implementation at runtime. + void onDirectoryOptionsRetrieved(ArrayList dirs) {} + + @Override + public void showDialog( + Context context, + ModalDialogManager modalDialogManager, + long totalBytes, + @DownloadLocationDialogType int dialogType, + String suggestedPath, + Profile profile) { + @DownloadPromptStatus + int promptStatus = DownloadDialogBridge.getPromptForDownloadAndroid(profile); + boolean userWantsPrompt = + promptStatus == DownloadPromptStatus.SHOW_PREFERENCE + || promptStatus == DownloadPromptStatus.SHOW_INITIAL; + + // If user hasn't opted in, or it's not a DEFAULT dialog type, or it's incognito, + // defer to upstream behavior (which skips the dialog for single directory). + if (!userWantsPrompt + || dialogType != DownloadLocationDialogType.DEFAULT + || profile.isOffTheRecord()) { + super.showDialog( + context, modalDialogManager, totalBytes, dialogType, suggestedPath, profile); + return; + } + + // Transition SHOW_INITIAL → SHOW_PREFERENCE so the "Don't show again" checkbox + // starts unchecked. With SHOW_INITIAL the checkbox defaults to checked, causing + // the status to flip to DONT_SHOW after the first dialog — which makes the C++ + // layer (DownloadPrefs::PromptForDownload) skip the dialog entirely on subsequent + // downloads. + if (promptStatus == DownloadPromptStatus.SHOW_INITIAL) { + DownloadDialogBridge.setPromptForDownloadAndroid( + profile, DownloadPromptStatus.SHOW_PREFERENCE); + } + + // Temporarily wrap the controller to intercept the single-directory skip. + // When the parent's onDirectoryOptionsRetrieved() decides to skip the dialog + // (dirs.size() == 1), it calls mController.onDownloadLocationDialogComplete(path, false). + // Our wrapper catches didUserConfirm=false (the skip), restores the original controller, + // and re-triggers onDirectoryOptionsRetrieved with a modified list so the dialog is shown. + final DownloadLocationDialogController originalController = mController; + mController = + new DownloadLocationDialogController() { + @Override + public void onDownloadLocationDialogComplete( + String path, boolean didUserConfirm) { + mController = originalController; + if (!didUserConfirm) { + // The parent skipped the dialog. Force it to show by + // re-fetching directories and ensuring dirs.size() > 1. + DownloadDirectoryProvider.getInstance() + .getAllDirectoriesOptions( + (ArrayList dirs) -> { + // Copy to avoid mutating the provider's + // cached list, which would cause the + // directory dropdown to show duplicates. + ArrayList copy = + new ArrayList<>(dirs); + if (copy.size() == 1) { + copy.add(copy.get(0)); + } + onDirectoryOptionsRetrieved(copy); + }); + } else { + originalController.onDownloadLocationDialogComplete( + path, didUserConfirm); + } + } + + @Override + public void onDownloadLocationDialogCanceled() { + mController = originalController; + originalController.onDownloadLocationDialogCanceled(); + } + }; + + // Let upstream handle ALL showDialog logic (field setup, directory fetch, etc.). + super.showDialog( + context, modalDialogManager, totalBytes, dialogType, suggestedPath, profile); + } +} diff --git a/browser/ui/android/strings/android_brave_strings.grd b/browser/ui/android/strings/android_brave_strings.grd index 05406bd60c3..4427b3d3b3c 100644 --- a/browser/ui/android/strings/android_brave_strings.grd +++ b/browser/ui/android/strings/android_brave_strings.grd @@ -1709,9 +1709,6 @@ Are you sure you want to do this? Show download progress notifications - - Ask where to save files (if SD card is detected) - Parallel Downloading diff --git a/build/android/bytecode/BUILD.gn b/build/android/bytecode/BUILD.gn index f960f4cc0b5..d3d58b66359 100644 --- a/build/android/bytecode/BUILD.gn +++ b/build/android/bytecode/BUILD.gn @@ -49,6 +49,7 @@ java_binary("java_bytecode_rewriter") { "//brave/build/android/bytecode/java/org/brave/bytecode/BraveCustomTabActivityClassAdapter.java", "//brave/build/android/bytecode/java/org/brave/bytecode/BraveCustomizationProviderDelegateImplClassAdapter.java", "//brave/build/android/bytecode/java/org/brave/bytecode/BraveDefaultBrowserPromoUtilsClassAdapter.java", + "//brave/build/android/bytecode/java/org/brave/bytecode/BraveDownloadLocationDialogCoordinatorClassAdapter.java", "//brave/build/android/bytecode/java/org/brave/bytecode/BraveDownloadMessageUiControllerImplClassAdapter.java", "//brave/build/android/bytecode/java/org/brave/bytecode/BraveDropdownItemViewInfoListBuilderClassAdapter.java", "//brave/build/android/bytecode/java/org/brave/bytecode/BraveDropdownItemViewInfoListManagerClassAdapter.java", diff --git a/build/android/bytecode/bytecode_rewriter.gni b/build/android/bytecode/bytecode_rewriter.gni index db06dfe8b99..59757fbbbe0 100644 --- a/build/android/bytecode/bytecode_rewriter.gni +++ b/build/android/bytecode/bytecode_rewriter.gni @@ -10,6 +10,7 @@ brave_bytecode_jars = [ "obj/base/base_shared_preferences_java.javac.jar", "obj/brave/android/features/tab_ui/java.javac.jar", "obj/brave/android/java/org/chromium/chrome/browser/search_engines/java.javac.jar", + "obj/brave/browser/download/android/java.javac.jar", "obj/brave/browser/notifications/java.javac.jar", "obj/brave/browser/ui/android/favicon/java.javac.jar", "obj/brave/browser/ui/android/logo/java.javac.jar", @@ -24,6 +25,7 @@ brave_bytecode_jars = [ "obj/chrome/android/chrome_java.javac.jar", "obj/chrome/android/features/tab_ui/java.javac.jar", "obj/chrome/browser/bookmarks/android/java.javac.jar", + "obj/chrome/browser/download/android/java.javac.jar", "obj/chrome/browser/download/internal/android/java.javac.jar", "obj/chrome/browser/flags/java.javac.jar", "obj/chrome/browser/hub/internal/java.javac.jar", diff --git a/build/android/bytecode/java/org/brave/bytecode/BraveClassAdapter.java b/build/android/bytecode/java/org/brave/bytecode/BraveClassAdapter.java index a538cc33926..73bf0e3e0d3 100644 --- a/build/android/bytecode/java/org/brave/bytecode/BraveClassAdapter.java +++ b/build/android/bytecode/java/org/brave/bytecode/BraveClassAdapter.java @@ -53,6 +53,7 @@ public class BraveClassAdapter { chain = new BraveContentViewClassAdapter(chain); chain = new BraveCustomizationProviderDelegateImplClassAdapter(chain); chain = new BraveDefaultBrowserPromoUtilsClassAdapter(chain); + chain = new BraveDownloadLocationDialogCoordinatorClassAdapter(chain); chain = new BraveDownloadMessageUiControllerImplClassAdapter(chain); chain = new BraveDropdownItemViewInfoListBuilderClassAdapter(chain); chain = new BraveDropdownItemViewInfoListManagerClassAdapter(chain); diff --git a/build/android/bytecode/java/org/brave/bytecode/BraveDownloadLocationDialogCoordinatorClassAdapter.java b/build/android/bytecode/java/org/brave/bytecode/BraveDownloadLocationDialogCoordinatorClassAdapter.java new file mode 100644 index 00000000000..7c5b1b1b4d2 --- /dev/null +++ b/build/android/bytecode/java/org/brave/bytecode/BraveDownloadLocationDialogCoordinatorClassAdapter.java @@ -0,0 +1,31 @@ +/* Copyright (c) 2026 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.brave.bytecode; + +import org.objectweb.asm.ClassVisitor; + +public class BraveDownloadLocationDialogCoordinatorClassAdapter extends BraveClassVisitor { + static String sDownloadLocationDialogCoordinatorClassName = + "org/chromium/chrome/browser/download/dialogs/DownloadLocationDialogCoordinator"; + static String sBraveDownloadLocationDialogCoordinatorClassName = + "org/chromium/chrome/browser/download/dialogs/BraveDownloadLocationDialogCoordinator"; + + public BraveDownloadLocationDialogCoordinatorClassAdapter(ClassVisitor visitor) { + super(visitor); + + redirectConstructor( + sDownloadLocationDialogCoordinatorClassName, + sBraveDownloadLocationDialogCoordinatorClassName); + + deleteField(sBraveDownloadLocationDialogCoordinatorClassName, "mController"); + makeProtectedField(sDownloadLocationDialogCoordinatorClassName, "mController"); + + deleteMethod( + sBraveDownloadLocationDialogCoordinatorClassName, "onDirectoryOptionsRetrieved"); + makePublicMethod( + sDownloadLocationDialogCoordinatorClassName, "onDirectoryOptionsRetrieved"); + } +}