Adding instrumentation to locate the source of jankiness
[chromium-blink-merge.git] / chrome / browser / file_select_helper.cc
blob4675d6481e20dc411008f4bbbce39e7134590cf8
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/file_select_helper.h"
7 #include <string>
8 #include <utility>
10 #include "base/bind.h"
11 #include "base/files/file_enumerator.h"
12 #include "base/files/file_util.h"
13 #include "base/strings/string_split.h"
14 #include "base/strings/string_util.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "chrome/browser/platform_util.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/ui/browser.h"
19 #include "chrome/browser/ui/browser_list.h"
20 #include "chrome/browser/ui/chrome_select_file_policy.h"
21 #include "chrome/grit/generated_resources.h"
22 #include "content/public/browser/browser_thread.h"
23 #include "content/public/browser/notification_details.h"
24 #include "content/public/browser/notification_source.h"
25 #include "content/public/browser/notification_types.h"
26 #include "content/public/browser/render_view_host.h"
27 #include "content/public/browser/render_widget_host_view.h"
28 #include "content/public/browser/web_contents.h"
29 #include "content/public/common/file_chooser_file_info.h"
30 #include "content/public/common/file_chooser_params.h"
31 #include "net/base/mime_util.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/shell_dialogs/selected_file_info.h"
35 using content::BrowserThread;
36 using content::FileChooserParams;
37 using content::RenderViewHost;
38 using content::RenderWidgetHost;
39 using content::WebContents;
41 namespace {
43 // There is only one file-selection happening at any given time,
44 // so we allocate an enumeration ID for that purpose. All IDs from
45 // the renderer must start at 0 and increase.
46 const int kFileSelectEnumerationId = -1;
48 void NotifyRenderViewHost(
49 RenderViewHost* render_view_host,
50 const std::vector<ui::SelectedFileInfo>& files,
51 FileChooserParams::Mode dialog_mode) {
52 std::vector<content::FileChooserFileInfo> chooser_files;
53 for (size_t i = 0; i < files.size(); ++i) {
54 content::FileChooserFileInfo chooser_file;
55 chooser_file.file_path = files[i].local_path;
56 chooser_file.display_name = files[i].display_name;
57 chooser_files.push_back(chooser_file);
59 render_view_host->FilesSelectedInChooser(chooser_files, dialog_mode);
62 // Converts a list of FilePaths to a list of ui::SelectedFileInfo.
63 std::vector<ui::SelectedFileInfo> FilePathListToSelectedFileInfoList(
64 const std::vector<base::FilePath>& paths) {
65 std::vector<ui::SelectedFileInfo> selected_files;
66 for (size_t i = 0; i < paths.size(); ++i) {
67 selected_files.push_back(
68 ui::SelectedFileInfo(paths[i], paths[i]));
70 return selected_files;
73 void DeleteFiles(const std::vector<base::FilePath>& paths) {
74 for (auto& file_path : paths)
75 base::DeleteFile(file_path, false);
78 } // namespace
80 struct FileSelectHelper::ActiveDirectoryEnumeration {
81 ActiveDirectoryEnumeration() : rvh_(NULL) {}
83 scoped_ptr<DirectoryListerDispatchDelegate> delegate_;
84 scoped_ptr<net::DirectoryLister> lister_;
85 RenderViewHost* rvh_;
86 std::vector<base::FilePath> results_;
89 FileSelectHelper::FileSelectHelper(Profile* profile)
90 : profile_(profile),
91 render_view_host_(NULL),
92 web_contents_(NULL),
93 select_file_dialog_(),
94 select_file_types_(),
95 dialog_type_(ui::SelectFileDialog::SELECT_OPEN_FILE),
96 dialog_mode_(FileChooserParams::Open) {
99 FileSelectHelper::~FileSelectHelper() {
100 // There may be pending file dialogs, we need to tell them that we've gone
101 // away so they don't try and call back to us.
102 if (select_file_dialog_.get())
103 select_file_dialog_->ListenerDestroyed();
105 // Stop any pending directory enumeration, prevent a callback, and free
106 // allocated memory.
107 std::map<int, ActiveDirectoryEnumeration*>::iterator iter;
108 for (iter = directory_enumerations_.begin();
109 iter != directory_enumerations_.end();
110 ++iter) {
111 iter->second->lister_.reset();
112 delete iter->second;
116 void FileSelectHelper::DirectoryListerDispatchDelegate::OnListFile(
117 const net::DirectoryLister::DirectoryListerData& data) {
118 parent_->OnListFile(id_, data);
121 void FileSelectHelper::DirectoryListerDispatchDelegate::OnListDone(int error) {
122 parent_->OnListDone(id_, error);
125 void FileSelectHelper::FileSelected(const base::FilePath& path,
126 int index, void* params) {
127 FileSelectedWithExtraInfo(ui::SelectedFileInfo(path, path), index, params);
130 void FileSelectHelper::FileSelectedWithExtraInfo(
131 const ui::SelectedFileInfo& file,
132 int index,
133 void* params) {
134 profile_->set_last_selected_directory(file.file_path.DirName());
136 if (!render_view_host_) {
137 RunFileChooserEnd();
138 return;
141 const base::FilePath& path = file.local_path;
142 if (dialog_type_ == ui::SelectFileDialog::SELECT_UPLOAD_FOLDER) {
143 StartNewEnumeration(path, kFileSelectEnumerationId, render_view_host_);
144 return;
147 std::vector<ui::SelectedFileInfo> files;
148 files.push_back(file);
150 #if defined(OS_MACOSX) && !defined(OS_IOS)
151 content::BrowserThread::PostTask(
152 content::BrowserThread::FILE_USER_BLOCKING,
153 FROM_HERE,
154 base::Bind(&FileSelectHelper::ProcessSelectedFilesMac, this, files));
155 #else
156 NotifyRenderViewHostAndEnd(files);
157 #endif // defined(OS_MACOSX) && !defined(OS_IOS)
160 void FileSelectHelper::MultiFilesSelected(
161 const std::vector<base::FilePath>& files,
162 void* params) {
163 std::vector<ui::SelectedFileInfo> selected_files =
164 FilePathListToSelectedFileInfoList(files);
166 MultiFilesSelectedWithExtraInfo(selected_files, params);
169 void FileSelectHelper::MultiFilesSelectedWithExtraInfo(
170 const std::vector<ui::SelectedFileInfo>& files,
171 void* params) {
172 if (!files.empty())
173 profile_->set_last_selected_directory(files[0].file_path.DirName());
175 #if defined(OS_MACOSX) && !defined(OS_IOS)
176 content::BrowserThread::PostTask(
177 content::BrowserThread::FILE_USER_BLOCKING,
178 FROM_HERE,
179 base::Bind(&FileSelectHelper::ProcessSelectedFilesMac, this, files));
180 #else
181 NotifyRenderViewHostAndEnd(files);
182 #endif // defined(OS_MACOSX) && !defined(OS_IOS)
185 void FileSelectHelper::FileSelectionCanceled(void* params) {
186 NotifyRenderViewHostAndEnd(std::vector<ui::SelectedFileInfo>());
189 void FileSelectHelper::StartNewEnumeration(const base::FilePath& path,
190 int request_id,
191 RenderViewHost* render_view_host) {
192 scoped_ptr<ActiveDirectoryEnumeration> entry(new ActiveDirectoryEnumeration);
193 entry->rvh_ = render_view_host;
194 entry->delegate_.reset(new DirectoryListerDispatchDelegate(this, request_id));
195 entry->lister_.reset(new net::DirectoryLister(path,
196 true,
197 net::DirectoryLister::NO_SORT,
198 entry->delegate_.get()));
199 if (!entry->lister_->Start()) {
200 if (request_id == kFileSelectEnumerationId)
201 FileSelectionCanceled(NULL);
202 else
203 render_view_host->DirectoryEnumerationFinished(request_id,
204 entry->results_);
205 } else {
206 directory_enumerations_[request_id] = entry.release();
210 void FileSelectHelper::OnListFile(
211 int id,
212 const net::DirectoryLister::DirectoryListerData& data) {
213 ActiveDirectoryEnumeration* entry = directory_enumerations_[id];
215 // Directory upload only cares about files.
216 if (data.info.IsDirectory())
217 return;
219 entry->results_.push_back(data.path);
222 void FileSelectHelper::OnListDone(int id, int error) {
223 // This entry needs to be cleaned up when this function is done.
224 scoped_ptr<ActiveDirectoryEnumeration> entry(directory_enumerations_[id]);
225 directory_enumerations_.erase(id);
226 if (!entry->rvh_)
227 return;
228 if (error) {
229 FileSelectionCanceled(NULL);
230 return;
233 std::vector<ui::SelectedFileInfo> selected_files =
234 FilePathListToSelectedFileInfoList(entry->results_);
236 if (id == kFileSelectEnumerationId)
237 NotifyRenderViewHost(entry->rvh_, selected_files, dialog_mode_);
238 else
239 entry->rvh_->DirectoryEnumerationFinished(id, entry->results_);
241 EnumerateDirectoryEnd();
244 void FileSelectHelper::NotifyRenderViewHostAndEnd(
245 const std::vector<ui::SelectedFileInfo>& files) {
246 if (render_view_host_)
247 NotifyRenderViewHost(render_view_host_, files, dialog_mode_);
249 // No members should be accessed from here on.
250 RunFileChooserEnd();
253 void FileSelectHelper::DeleteTemporaryFiles() {
254 BrowserThread::PostTask(BrowserThread::FILE,
255 FROM_HERE,
256 base::Bind(&DeleteFiles, temporary_files_));
257 temporary_files_.clear();
260 scoped_ptr<ui::SelectFileDialog::FileTypeInfo>
261 FileSelectHelper::GetFileTypesFromAcceptType(
262 const std::vector<base::string16>& accept_types) {
263 scoped_ptr<ui::SelectFileDialog::FileTypeInfo> base_file_type(
264 new ui::SelectFileDialog::FileTypeInfo());
265 if (accept_types.empty())
266 return base_file_type.Pass();
268 // Create FileTypeInfo and pre-allocate for the first extension list.
269 scoped_ptr<ui::SelectFileDialog::FileTypeInfo> file_type(
270 new ui::SelectFileDialog::FileTypeInfo(*base_file_type));
271 file_type->include_all_files = true;
272 file_type->extensions.resize(1);
273 std::vector<base::FilePath::StringType>* extensions =
274 &file_type->extensions.back();
276 // Find the corresponding extensions.
277 int valid_type_count = 0;
278 int description_id = 0;
279 for (size_t i = 0; i < accept_types.size(); ++i) {
280 std::string ascii_type = base::UTF16ToASCII(accept_types[i]);
281 if (!IsAcceptTypeValid(ascii_type))
282 continue;
284 size_t old_extension_size = extensions->size();
285 if (ascii_type[0] == '.') {
286 // If the type starts with a period it is assumed to be a file extension
287 // so we just have to add it to the list.
288 base::FilePath::StringType ext(ascii_type.begin(), ascii_type.end());
289 extensions->push_back(ext.substr(1));
290 } else {
291 if (ascii_type == "image/*")
292 description_id = IDS_IMAGE_FILES;
293 else if (ascii_type == "audio/*")
294 description_id = IDS_AUDIO_FILES;
295 else if (ascii_type == "video/*")
296 description_id = IDS_VIDEO_FILES;
298 net::GetExtensionsForMimeType(ascii_type, extensions);
301 if (extensions->size() > old_extension_size)
302 valid_type_count++;
305 // If no valid extension is added, bail out.
306 if (valid_type_count == 0)
307 return base_file_type.Pass();
309 // Use a generic description "Custom Files" if either of the following is
310 // true:
311 // 1) There're multiple types specified, like "audio/*,video/*"
312 // 2) There're multiple extensions for a MIME type without parameter, like
313 // "ehtml,shtml,htm,html" for "text/html". On Windows, the select file
314 // dialog uses the first extension in the list to form the description,
315 // like "EHTML Files". This is not what we want.
316 if (valid_type_count > 1 ||
317 (valid_type_count == 1 && description_id == 0 && extensions->size() > 1))
318 description_id = IDS_CUSTOM_FILES;
320 if (description_id) {
321 file_type->extension_description_overrides.push_back(
322 l10n_util::GetStringUTF16(description_id));
325 return file_type.Pass();
328 // static
329 void FileSelectHelper::RunFileChooser(content::WebContents* tab,
330 const FileChooserParams& params) {
331 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
332 // FileSelectHelper will keep itself alive until it sends the result message.
333 scoped_refptr<FileSelectHelper> file_select_helper(
334 new FileSelectHelper(profile));
335 file_select_helper->RunFileChooser(tab->GetRenderViewHost(), tab, params);
338 // static
339 void FileSelectHelper::EnumerateDirectory(content::WebContents* tab,
340 int request_id,
341 const base::FilePath& path) {
342 Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
343 // FileSelectHelper will keep itself alive until it sends the result message.
344 scoped_refptr<FileSelectHelper> file_select_helper(
345 new FileSelectHelper(profile));
346 file_select_helper->EnumerateDirectory(
347 request_id, tab->GetRenderViewHost(), path);
350 void FileSelectHelper::RunFileChooser(RenderViewHost* render_view_host,
351 content::WebContents* web_contents,
352 const FileChooserParams& params) {
353 DCHECK(!render_view_host_);
354 DCHECK(!web_contents_);
355 render_view_host_ = render_view_host;
356 web_contents_ = web_contents;
357 notification_registrar_.RemoveAll();
358 notification_registrar_.Add(this,
359 content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED,
360 content::Source<WebContents>(web_contents_));
361 notification_registrar_.Add(
362 this,
363 content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED,
364 content::Source<RenderWidgetHost>(render_view_host_));
365 notification_registrar_.Add(
366 this, content::NOTIFICATION_WEB_CONTENTS_DESTROYED,
367 content::Source<WebContents>(web_contents_));
369 BrowserThread::PostTask(
370 BrowserThread::FILE, FROM_HERE,
371 base::Bind(&FileSelectHelper::RunFileChooserOnFileThread, this, params));
373 // Because this class returns notifications to the RenderViewHost, it is
374 // difficult for callers to know how long to keep a reference to this
375 // instance. We AddRef() here to keep the instance alive after we return
376 // to the caller, until the last callback is received from the file dialog.
377 // At that point, we must call RunFileChooserEnd().
378 AddRef();
381 void FileSelectHelper::RunFileChooserOnFileThread(
382 const FileChooserParams& params) {
383 select_file_types_ = GetFileTypesFromAcceptType(params.accept_types);
385 BrowserThread::PostTask(
386 BrowserThread::UI, FROM_HERE,
387 base::Bind(&FileSelectHelper::RunFileChooserOnUIThread, this, params));
390 void FileSelectHelper::RunFileChooserOnUIThread(
391 const FileChooserParams& params) {
392 if (!render_view_host_ || !web_contents_) {
393 // If the renderer was destroyed before we started, just cancel the
394 // operation.
395 RunFileChooserEnd();
396 return;
399 select_file_dialog_ = ui::SelectFileDialog::Create(
400 this, new ChromeSelectFilePolicy(web_contents_));
401 if (!select_file_dialog_.get())
402 return;
404 dialog_mode_ = params.mode;
405 switch (params.mode) {
406 case FileChooserParams::Open:
407 dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
408 break;
409 case FileChooserParams::OpenMultiple:
410 dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_MULTI_FILE;
411 break;
412 case FileChooserParams::UploadFolder:
413 dialog_type_ = ui::SelectFileDialog::SELECT_UPLOAD_FOLDER;
414 break;
415 case FileChooserParams::Save:
416 dialog_type_ = ui::SelectFileDialog::SELECT_SAVEAS_FILE;
417 break;
418 default:
419 // Prevent warning.
420 dialog_type_ = ui::SelectFileDialog::SELECT_OPEN_FILE;
421 NOTREACHED();
424 base::FilePath default_file_name = params.default_file_name.IsAbsolute() ?
425 params.default_file_name :
426 profile_->last_selected_directory().Append(params.default_file_name);
428 gfx::NativeWindow owning_window =
429 platform_util::GetTopLevel(render_view_host_->GetView()->GetNativeView());
431 #if defined(OS_ANDROID)
432 // Android needs the original MIME types and an additional capture value.
433 std::pair<std::vector<base::string16>, bool> accept_types =
434 std::make_pair(params.accept_types, params.capture);
435 #endif
437 select_file_dialog_->SelectFile(
438 dialog_type_,
439 params.title,
440 default_file_name,
441 select_file_types_.get(),
442 select_file_types_.get() && !select_file_types_->extensions.empty()
444 : 0, // 1-based index of default extension to show.
445 base::FilePath::StringType(),
446 owning_window,
447 #if defined(OS_ANDROID)
448 &accept_types);
449 #else
450 NULL);
451 #endif
453 select_file_types_.reset();
456 // This method is called when we receive the last callback from the file
457 // chooser dialog. Perform any cleanup and release the reference we added
458 // in RunFileChooser().
459 void FileSelectHelper::RunFileChooserEnd() {
460 // If there are temporary files, then this instance needs to stick around
461 // until web_contents_ is destroyed, so that this instance can delete the
462 // temporary files.
463 if (!temporary_files_.empty())
464 return;
466 render_view_host_ = NULL;
467 web_contents_ = NULL;
468 Release();
471 void FileSelectHelper::EnumerateDirectory(int request_id,
472 RenderViewHost* render_view_host,
473 const base::FilePath& path) {
475 // Because this class returns notifications to the RenderViewHost, it is
476 // difficult for callers to know how long to keep a reference to this
477 // instance. We AddRef() here to keep the instance alive after we return
478 // to the caller, until the last callback is received from the enumeration
479 // code. At that point, we must call EnumerateDirectoryEnd().
480 AddRef();
481 StartNewEnumeration(path, request_id, render_view_host);
484 // This method is called when we receive the last callback from the enumeration
485 // code. Perform any cleanup and release the reference we added in
486 // EnumerateDirectory().
487 void FileSelectHelper::EnumerateDirectoryEnd() {
488 Release();
491 void FileSelectHelper::Observe(int type,
492 const content::NotificationSource& source,
493 const content::NotificationDetails& details) {
494 switch (type) {
495 case content::NOTIFICATION_RENDER_WIDGET_HOST_DESTROYED: {
496 DCHECK(content::Source<RenderWidgetHost>(source).ptr() ==
497 render_view_host_);
498 render_view_host_ = NULL;
499 break;
502 case content::NOTIFICATION_WEB_CONTENTS_DESTROYED: {
503 DCHECK(content::Source<WebContents>(source).ptr() == web_contents_);
504 web_contents_ = NULL;
507 // Intentional fall through.
508 case content::NOTIFICATION_RENDER_VIEW_HOST_CHANGED:
509 if (!temporary_files_.empty()) {
510 DeleteTemporaryFiles();
512 // Now that the temporary files have been scheduled for deletion, there
513 // is no longer any reason to keep this instance around.
514 Release();
517 break;
519 default:
520 NOTREACHED();
524 // static
525 bool FileSelectHelper::IsAcceptTypeValid(const std::string& accept_type) {
526 // TODO(raymes): This only does some basic checks, extend to test more cases.
527 // A 1 character accept type will always be invalid (either a "." in the case
528 // of an extension or a "/" in the case of a MIME type).
529 std::string unused;
530 if (accept_type.length() <= 1 ||
531 base::StringToLowerASCII(accept_type) != accept_type ||
532 base::TrimWhitespaceASCII(accept_type, base::TRIM_ALL, &unused) !=
533 base::TRIM_NONE) {
534 return false;
536 return true;