[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:
@@ -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(...);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+124
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
+31
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user