[Android] Always show download location dialog when user opts in (#34186)

Override DownloadLocationDialogCoordinator to force the download
location dialog even on single-directory devices (no SD card).
Upstream skips the dialog when only one storage directory is
available; the Brave override intercepts that skip and re-triggers
the dialog display. Also transitions SHOW_INITIAL to SHOW_PREFERENCE
so the "Don't show again" checkbox starts unchecked, preventing the
feature from silently disabling itself after the first use.

Resolves: https://github.com/brave/brave-browser/issues/52737
This commit is contained in:
Serg
2026-02-25 20:05:42 +00:00
committed by GitHub
parent ade65da50e
commit c405b65020
10 changed files with 194 additions and 7 deletions
+10
View File
@@ -475,6 +475,16 @@
*** getActionButtonTintList(...);
}
-keep class org.chromium.chrome.browser.download.dialogs.DownloadLocationDialogCoordinator {
public <init>(...);
*** mController;
*** onDirectoryOptionsRetrieved(...);
}
-keep class org.chromium.chrome.browser.download.dialogs.BraveDownloadLocationDialogCoordinator {
public <init>(...);
}
-keep class org.chromium.chrome.browser.download.BraveDownloadMessageUiControllerImpl {
*** isVisibleToUser(...);
}
@@ -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);
}
}
@@ -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",
+8 -2
View File
@@ -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",
]
}
@@ -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).
*
* <p>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<DirectoryOption> 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<DirectoryOption> dirs) -> {
// Copy to avoid mutating the provider's
// cached list, which would cause the
// directory dropdown to show duplicates.
ArrayList<DirectoryOption> 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);
}
}
@@ -1709,9 +1709,6 @@ Are you sure you want to do this?
<message name="IDS_DOWNLOAD_PROGRESS_NOTIFICATION_TITLE" desc="Title for preference that allows the user to enable disable download progress notifications bubble.">
Show download progress notifications
</message>
<message name="IDS_BRAVE_DOWNLOAD_LOCATION_PROMPT_ENABLED_TITLE" desc="Title for preference that allows the user to indicate whether they want to show a change downloads location prompt every time they download.">
Ask where to save files (if SD card is detected)
</message>
<message name="IDS_ENABLE_PARALLEL_DOWNLOADING_TITLE" desc="Title for preference that allows the user to enable parallel downloading.">
Parallel Downloading
</message>
+1
View File
@@ -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",
@@ -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",
@@ -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);
@@ -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");
}
}