[email protected] | a82af39 | 2012-02-24 04:40:20 | [diff] [blame] | 1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "chrome/browser/platform_util.h" |
| 6 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 7 | #include <fcntl.h> |
| 8 | |
| 9 | #include <string> |
| 10 | #include <vector> |
| 11 | |
[email protected] | 7c3228a | 2011-11-11 21:35:22 | [diff] [blame] | 12 | #include "base/bind.h" |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 13 | #include "base/containers/contains.h" |
Lei Zhang | 46a1cad | 2022-01-17 10:40:31 | [diff] [blame] | 14 | #include "base/logging.h" |
Alex Clarke | 112c512 | 2019-11-22 12:26:42 | [diff] [blame] | 15 | #include "base/no_destructor.h" |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 16 | #include "base/posix/eintr_wrapper.h" |
[email protected] | d09a4ce1c | 2013-07-24 17:37:02 | [diff] [blame] | 17 | #include "base/process/kill.h" |
| 18 | #include "base/process/launch.h" |
maksim.sisov | 4e6a58a6 | 2016-05-02 19:34:47 | [diff] [blame] | 19 | #include "base/strings/string_util.h" |
[email protected] | e309f31 | 2013-06-07 21:50:08 | [diff] [blame] | 20 | #include "base/strings/utf_string_conversions.h" |
Gabriel Charette | 40182e6 | 2019-07-25 19:36:26 | [diff] [blame] | 21 | #include "base/threading/scoped_blocking_call.h" |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 22 | #include "chrome/browser/chrome_notification_types.h" |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 23 | #include "chrome/browser/platform_util_internal.h" |
Colin Blundell | 980e85d2 | 2021-06-14 16:25:11 | [diff] [blame] | 24 | // This file gets pulled in in Chromecast builds, which causes "gn check" to |
| 25 | // complain as Chromecast doesn't use (or depend on) //components/dbus. |
| 26 | // TODO(crbug.com/1215474): Eliminate //chrome being visible in the GN structure |
| 27 | // on Chromecast and remove the nogncheck below. |
| 28 | #include "components/dbus/thread_linux/dbus_thread_linux.h" // nogncheck |
[email protected] | 7c3228a | 2011-11-11 21:35:22 | [diff] [blame] | 29 | #include "content/public/browser/browser_thread.h" |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 30 | #include "content/public/browser/notification_observer.h" |
| 31 | #include "content/public/browser/notification_registrar.h" |
| 32 | #include "content/public/browser/notification_service.h" |
| 33 | #include "dbus/bus.h" |
| 34 | #include "dbus/message.h" |
| 35 | #include "dbus/object_proxy.h" |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 36 | #include "third_party/abseil-cpp/absl/types/optional.h" |
[email protected] | 761fa470 | 2013-07-02 15:25:15 | [diff] [blame] | 37 | #include "url/gurl.h" |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 38 | |
[email protected] | 7c3228a | 2011-11-11 21:35:22 | [diff] [blame] | 39 | using content::BrowserThread; |
| 40 | |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 41 | namespace platform_util { |
| 42 | |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 43 | namespace { |
| 44 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 45 | const char kMethodListActivatableNames[] = "ListActivatableNames"; |
| 46 | const char kMethodNameHasOwner[] = "NameHasOwner"; |
| 47 | |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 48 | const char kFreedesktopFileManagerName[] = "org.freedesktop.FileManager1"; |
| 49 | const char kFreedesktopFileManagerPath[] = "/org/freedesktop/FileManager1"; |
| 50 | |
| 51 | const char kMethodShowItems[] = "ShowItems"; |
| 52 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 53 | const char kFreedesktopPortalName[] = "org.freedesktop.portal.Desktop"; |
| 54 | const char kFreedesktopPortalPath[] = "/org/freedesktop/portal/desktop"; |
| 55 | const char kFreedesktopPortalOpenURI[] = "org.freedesktop.portal.OpenURI"; |
| 56 | |
| 57 | const char kMethodOpenDirectory[] = "OpenDirectory"; |
| 58 | |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 59 | class ShowItemHelper : public content::NotificationObserver { |
| 60 | public: |
| 61 | static ShowItemHelper& GetInstance() { |
| 62 | static base::NoDestructor<ShowItemHelper> instance; |
| 63 | return *instance; |
| 64 | } |
| 65 | |
| 66 | ShowItemHelper() { |
| 67 | registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING, |
| 68 | content::NotificationService::AllSources()); |
| 69 | } |
| 70 | |
| 71 | ShowItemHelper(const ShowItemHelper&) = delete; |
| 72 | ShowItemHelper& operator=(const ShowItemHelper&) = delete; |
| 73 | |
| 74 | void Observe(int type, |
| 75 | const content::NotificationSource& source, |
| 76 | const content::NotificationDetails& details) override { |
| 77 | DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| 78 | DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type); |
| 79 | // The browser process is about to exit. Clean up while we still can. |
| 80 | if (bus_) |
| 81 | bus_->ShutdownOnDBusThreadAndBlock(); |
| 82 | bus_.reset(); |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 83 | object_proxy_ = nullptr; |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 84 | } |
| 85 | |
| 86 | void ShowItemInFolder(Profile* profile, const base::FilePath& full_path) { |
| 87 | if (!bus_) { |
| 88 | // Sets up the D-Bus connection. |
| 89 | dbus::Bus::Options bus_options; |
| 90 | bus_options.bus_type = dbus::Bus::SESSION; |
| 91 | bus_options.connection_type = dbus::Bus::PRIVATE; |
| 92 | bus_options.dbus_task_runner = dbus_thread_linux::GetTaskRunner(); |
| 93 | bus_ = base::MakeRefCounted<dbus::Bus>(bus_options); |
| 94 | } |
| 95 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 96 | if (!dbus_proxy_) { |
| 97 | dbus_proxy_ = bus_->GetObjectProxy(DBUS_SERVICE_DBUS, |
| 98 | dbus::ObjectPath(DBUS_PATH_DBUS)); |
| 99 | } |
| 100 | |
| 101 | if (prefer_filemanager_interface_.has_value()) { |
| 102 | if (prefer_filemanager_interface_.value()) { |
| 103 | VLOG(1) << "Using FileManager1 to show folder"; |
| 104 | ShowItemUsingFileManager(profile, full_path); |
| 105 | } else { |
| 106 | VLOG(1) << "Using OpenURI to show folder"; |
| 107 | ShowItemUsingFreedesktopPortal(profile, full_path); |
| 108 | } |
| 109 | } else { |
| 110 | CheckFileManagerRunning(profile, full_path); |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | private: |
| 115 | void CheckFileManagerRunning(Profile* profile, |
| 116 | const base::FilePath& full_path) { |
| 117 | dbus::MethodCall method_call(DBUS_INTERFACE_DBUS, kMethodNameHasOwner); |
| 118 | dbus::MessageWriter writer(&method_call); |
| 119 | writer.AppendString(kFreedesktopFileManagerName); |
| 120 | |
| 121 | dbus_proxy_->CallMethod( |
| 122 | &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, |
| 123 | base::BindOnce(&ShowItemHelper::CheckFileManagerRunningResponse, |
| 124 | weak_ptr_factory_.GetWeakPtr(), profile, full_path)); |
| 125 | } |
| 126 | |
| 127 | void CheckFileManagerRunningResponse(Profile* profile, |
| 128 | const base::FilePath& full_path, |
| 129 | dbus::Response* response) { |
| 130 | if (prefer_filemanager_interface_.has_value()) { |
| 131 | ShowItemInFolder(profile, full_path); |
| 132 | return; |
| 133 | } |
| 134 | |
| 135 | bool is_running = false; |
| 136 | |
| 137 | if (!response) { |
| 138 | LOG(ERROR) << "Failed to call " << kMethodNameHasOwner; |
| 139 | } else { |
| 140 | dbus::MessageReader reader(response); |
| 141 | bool owned = false; |
| 142 | |
| 143 | if (!reader.PopBool(&owned)) { |
| 144 | LOG(ERROR) << "Failed to read " << kMethodNameHasOwner << " resposne"; |
| 145 | } else if (owned) { |
| 146 | is_running = true; |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | if (is_running) { |
| 151 | prefer_filemanager_interface_ = true; |
| 152 | ShowItemInFolder(profile, full_path); |
| 153 | } else { |
| 154 | CheckFileManagerActivatable(profile, full_path); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | void CheckFileManagerActivatable(Profile* profile, |
| 159 | const base::FilePath& full_path) { |
| 160 | dbus::MethodCall method_call(DBUS_INTERFACE_DBUS, |
| 161 | kMethodListActivatableNames); |
| 162 | dbus_proxy_->CallMethod( |
| 163 | &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, |
| 164 | base::BindOnce(&ShowItemHelper::CheckFileManagerActivatableResponse, |
| 165 | weak_ptr_factory_.GetWeakPtr(), profile, full_path)); |
| 166 | } |
| 167 | |
| 168 | void CheckFileManagerActivatableResponse(Profile* profile, |
| 169 | const base::FilePath& full_path, |
| 170 | dbus::Response* response) { |
| 171 | if (prefer_filemanager_interface_.has_value()) { |
| 172 | ShowItemInFolder(profile, full_path); |
| 173 | return; |
| 174 | } |
| 175 | |
| 176 | bool is_activatable = false; |
| 177 | |
| 178 | if (!response) { |
| 179 | LOG(ERROR) << "Failed to call " << kMethodListActivatableNames; |
| 180 | } else { |
| 181 | dbus::MessageReader reader(response); |
| 182 | std::vector<std::string> names; |
| 183 | if (!reader.PopArrayOfStrings(&names)) { |
| 184 | LOG(ERROR) << "Failed to read " << kMethodListActivatableNames |
| 185 | << " response"; |
| 186 | } else if (base::Contains(names, kFreedesktopFileManagerName)) { |
| 187 | is_activatable = true; |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | prefer_filemanager_interface_ = is_activatable; |
| 192 | ShowItemInFolder(profile, full_path); |
| 193 | } |
| 194 | |
| 195 | void ShowItemUsingFreedesktopPortal(Profile* profile, |
| 196 | const base::FilePath& full_path) { |
| 197 | if (!object_proxy_) { |
| 198 | object_proxy_ = bus_->GetObjectProxy( |
| 199 | kFreedesktopPortalName, dbus::ObjectPath(kFreedesktopPortalPath)); |
| 200 | } |
| 201 | |
| 202 | base::ScopedFD fd( |
| 203 | HANDLE_EINTR(open(full_path.value().c_str(), O_RDONLY | O_CLOEXEC))); |
| 204 | if (!fd.is_valid()) { |
| 205 | PLOG(ERROR) << "Failed to open " << full_path << " for URI portal"; |
| 206 | |
| 207 | // At least open the parent folder, as long as we're not in the unit |
| 208 | // tests. |
| 209 | if (internal::AreShellOperationsAllowed()) { |
| 210 | OpenItem(profile, full_path.DirName(), OPEN_FOLDER, |
| 211 | OpenOperationCallback()); |
| 212 | } |
| 213 | |
| 214 | return; |
| 215 | } |
| 216 | |
| 217 | dbus::MethodCall open_directory_call(kFreedesktopPortalOpenURI, |
| 218 | kMethodOpenDirectory); |
| 219 | dbus::MessageWriter writer(&open_directory_call); |
| 220 | |
| 221 | writer.AppendString(""); |
| 222 | |
| 223 | // Note that AppendFileDescriptor() duplicates the fd, so we shouldn't |
| 224 | // release ownership of it here. |
| 225 | writer.AppendFileDescriptor(fd.get()); |
| 226 | |
| 227 | dbus::MessageWriter options_writer(nullptr); |
| 228 | writer.OpenArray("{sv}", &options_writer); |
| 229 | writer.CloseContainer(&options_writer); |
| 230 | |
| 231 | ShowItemUsingBusCall(&open_directory_call, profile, full_path); |
| 232 | } |
| 233 | |
| 234 | void ShowItemUsingFileManager(Profile* profile, |
| 235 | const base::FilePath& full_path) { |
| 236 | if (!object_proxy_) { |
| 237 | object_proxy_ = |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 238 | bus_->GetObjectProxy(kFreedesktopFileManagerName, |
| 239 | dbus::ObjectPath(kFreedesktopFileManagerPath)); |
| 240 | } |
| 241 | |
| 242 | dbus::MethodCall show_items_call(kFreedesktopFileManagerName, |
| 243 | kMethodShowItems); |
| 244 | dbus::MessageWriter writer(&show_items_call); |
| 245 | |
| 246 | writer.AppendArrayOfStrings( |
| 247 | {"file://" + full_path.value()}); // List of file(s) to highlight. |
| 248 | writer.AppendString({}); // startup-id |
| 249 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 250 | ShowItemUsingBusCall(&show_items_call, profile, full_path); |
| 251 | } |
| 252 | |
| 253 | void ShowItemUsingBusCall(dbus::MethodCall* call, |
| 254 | Profile* profile, |
| 255 | const base::FilePath& full_path) { |
Jesse McKenna | 14bd4f1a | 2020-08-10 23:54:29 | [diff] [blame] | 256 | // Skip opening the folder during browser tests, to avoid leaving an open |
| 257 | // file explorer window behind. |
| 258 | if (!internal::AreShellOperationsAllowed()) |
| 259 | return; |
| 260 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 261 | object_proxy_->CallMethod( |
| 262 | call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT, |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 263 | base::BindOnce(&ShowItemHelper::ShowItemInFolderResponse, |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 264 | weak_ptr_factory_.GetWeakPtr(), profile, full_path, |
| 265 | call->GetMember())); |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 266 | } |
| 267 | |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 268 | void ShowItemInFolderResponse(Profile* profile, |
| 269 | const base::FilePath& full_path, |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 270 | const std::string& method, |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 271 | dbus::Response* response) { |
| 272 | if (response) |
| 273 | return; |
| 274 | |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 275 | LOG(ERROR) << "Error calling " << method; |
| 276 | // If the bus call fails, at least open the parent folder. |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 277 | OpenItem(profile, full_path.DirName(), OPEN_FOLDER, |
| 278 | OpenOperationCallback()); |
| 279 | } |
| 280 | |
| 281 | content::NotificationRegistrar registrar_; |
| 282 | |
| 283 | scoped_refptr<dbus::Bus> bus_; |
Ryan Gonzalez | cdc98cbb | 2021-07-08 04:49:32 | [diff] [blame] | 284 | dbus::ObjectProxy* dbus_proxy_ = nullptr; |
| 285 | dbus::ObjectProxy* object_proxy_ = nullptr; |
| 286 | |
| 287 | absl::optional<bool> prefer_filemanager_interface_; |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 288 | |
| 289 | base::WeakPtrFactory<ShowItemHelper> weak_ptr_factory_{this}; |
| 290 | }; |
maksim.sisov | 4e6a58a6 | 2016-05-02 19:34:47 | [diff] [blame] | 291 | |
| 292 | void RunCommand(const std::string& command, |
| 293 | const base::FilePath& working_directory, |
| 294 | const std::string& arg) { |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 295 | std::vector<std::string> argv; |
maksim.sisov | 4e6a58a6 | 2016-05-02 19:34:47 | [diff] [blame] | 296 | argv.push_back(command); |
[email protected] | e9ce67a1 | 2010-11-11 20:58:49 | [diff] [blame] | 297 | argv.push_back(arg); |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 298 | |
[email protected] | b345c48 | 2013-08-30 18:00:39 | [diff] [blame] | 299 | base::LaunchOptions options; |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 300 | options.current_directory = working_directory; |
[email protected] | 720397c5 | 2014-04-22 14:06:12 | [diff] [blame] | 301 | options.allow_new_privs = true; |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 302 | // xdg-open can fall back on mailcap which eventually might plumb through |
| 303 | // to a command that needs a terminal. Set the environment variable telling |
| 304 | // it that we definitely don't have a terminal available and that it should |
| 305 | // bring up a new terminal if necessary. See "man mailcap". |
David Benjamin | 76ee79eb | 2019-03-15 17:02:09 | [diff] [blame] | 306 | options.environment["MM_NOTTTY"] = "1"; |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 307 | |
| 308 | // In Google Chrome, we do not let GNOME's bug-buddy intercept our crashes. |
| 309 | // However, we do not want this environment variable to propagate to external |
| 310 | // applications. See http://crbug.com/24120 |
| 311 | char* disable_gnome_bug_buddy = getenv("GNOME_DISABLE_CRASH_DIALOG"); |
| 312 | if (disable_gnome_bug_buddy && |
[email protected] | b345c48 | 2013-08-30 18:00:39 | [diff] [blame] | 313 | disable_gnome_bug_buddy == std::string("SET_BY_GOOGLE_CHROME")) |
David Benjamin | 76ee79eb | 2019-03-15 17:02:09 | [diff] [blame] | 314 | options.environment["GNOME_DISABLE_CRASH_DIALOG"] = std::string(); |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 315 | |
rvargas | c7b523fa | 2015-01-14 01:13:23 | [diff] [blame] | 316 | base::Process process = base::LaunchProcess(argv, options); |
| 317 | if (process.IsValid()) |
Wez | c18a57c | 2018-04-02 20:20:14 | [diff] [blame] | 318 | base::EnsureProcessGetsReaped(std::move(process)); |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 319 | } |
| 320 | |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 321 | void XDGOpen(const base::FilePath& working_directory, const std::string& path) { |
maksim.sisov | 4e6a58a6 | 2016-05-02 19:34:47 | [diff] [blame] | 322 | RunCommand("xdg-open", working_directory, path); |
[email protected] | e9ce67a1 | 2010-11-11 20:58:49 | [diff] [blame] | 323 | } |
| 324 | |
| 325 | void XDGEmail(const std::string& email) { |
maksim.sisov | 4e6a58a6 | 2016-05-02 19:34:47 | [diff] [blame] | 326 | RunCommand("xdg-email", base::FilePath(), email); |
| 327 | } |
| 328 | |
[email protected] | 7c3228a | 2011-11-11 21:35:22 | [diff] [blame] | 329 | } // namespace |
| 330 | |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 331 | namespace internal { |
| 332 | |
| 333 | void PlatformOpenVerifiedItem(const base::FilePath& path, OpenItemType type) { |
Gabriel Charette | 40182e6 | 2019-07-25 19:36:26 | [diff] [blame] | 334 | // May result in an interactive dialog. |
| 335 | base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| 336 | base::BlockingType::MAY_BLOCK); |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 337 | switch (type) { |
| 338 | case OPEN_FILE: |
| 339 | XDGOpen(path.DirName(), path.value()); |
| 340 | break; |
| 341 | case OPEN_FOLDER: |
| 342 | // The utility process checks the working directory prior to the |
| 343 | // invocation of xdg-open by changing the current directory into it. This |
| 344 | // operation only succeeds if |path| is a directory. Opening "." from |
| 345 | // there ensures that the target of the operation is a directory. Note |
| 346 | // that there remains a TOCTOU race where the directory could be unlinked |
| 347 | // between the time the utility process changes into the directory and the |
| 348 | // time the application invoked by xdg-open inspects the path by name. |
| 349 | XDGOpen(path, "."); |
| 350 | break; |
| 351 | } |
| 352 | } |
maksim.sisov | 4e6a58a6 | 2016-05-02 19:34:47 | [diff] [blame] | 353 | |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 354 | } // namespace internal |
[email protected] | 7c3228a | 2011-11-11 21:35:22 | [diff] [blame] | 355 | |
[email protected] | 4d225de1 | 2013-12-13 09:29:17 | [diff] [blame] | 356 | void ShowItemInFolder(Profile* profile, const base::FilePath& full_path) { |
thestig | 00844cea | 2015-09-08 21:44:52 | [diff] [blame] | 357 | DCHECK_CURRENTLY_ON(BrowserThread::UI); |
Kai Uwe Broulik | da1cd32 | 2019-11-18 21:55:06 | [diff] [blame] | 358 | ShowItemHelper::GetInstance().ShowItemInFolder(profile, full_path); |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 359 | } |
| 360 | |
[email protected] | 7f0a3efa | 2013-12-12 17:16:12 | [diff] [blame] | 361 | void OpenExternal(Profile* profile, const GURL& url) { |
thestig | 00844cea | 2015-09-08 21:44:52 | [diff] [blame] | 362 | DCHECK_CURRENTLY_ON(BrowserThread::UI); |
[email protected] | e9ce67a1 | 2010-11-11 20:58:49 | [diff] [blame] | 363 | if (url.SchemeIs("mailto")) |
| 364 | XDGEmail(url.spec()); |
| 365 | else |
asanka | 655d111 | 2015-03-07 05:33:41 | [diff] [blame] | 366 | XDGOpen(base::FilePath(), url.spec()); |
[email protected] | 14a000d | 2010-04-29 21:44:24 | [diff] [blame] | 367 | } |
| 368 | |
| 369 | } // namespace platform_util |