Home | History | Annotate | Download | only in views
      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.
      4 
      5 #include "chrome/browser/ui/views/select_file_dialog_extension.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/callback.h"
      9 #include "base/logging.h"
     10 #include "base/memory/ref_counted.h"
     11 #include "base/memory/singleton.h"
     12 #include "base/message_loop/message_loop.h"
     13 #include "base/prefs/pref_service.h"
     14 #include "chrome/browser/app_mode/app_mode_utils.h"
     15 #include "chrome/browser/apps/app_window_registry_util.h"
     16 #include "chrome/browser/chromeos/file_manager/app_id.h"
     17 #include "chrome/browser/chromeos/file_manager/fileapi_util.h"
     18 #include "chrome/browser/chromeos/file_manager/select_file_dialog_util.h"
     19 #include "chrome/browser/chromeos/file_manager/url_util.h"
     20 #include "chrome/browser/chromeos/login/ui/login_web_dialog.h"
     21 #include "chrome/browser/extensions/extension_service.h"
     22 #include "chrome/browser/extensions/extension_view_host.h"
     23 #include "chrome/browser/profiles/profile.h"
     24 #include "chrome/browser/sessions/session_tab_helper.h"
     25 #include "chrome/browser/ui/browser.h"
     26 #include "chrome/browser/ui/browser_finder.h"
     27 #include "chrome/browser/ui/browser_window.h"
     28 #include "chrome/browser/ui/chrome_select_file_policy.h"
     29 #include "chrome/browser/ui/host_desktop.h"
     30 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     31 #include "chrome/browser/ui/views/extensions/extension_dialog.h"
     32 #include "chrome/common/pref_names.h"
     33 #include "content/public/browser/browser_thread.h"
     34 #include "extensions/browser/app_window/app_window.h"
     35 #include "extensions/browser/app_window/native_app_window.h"
     36 #include "extensions/browser/extension_system.h"
     37 #include "ui/base/base_window.h"
     38 #include "ui/shell_dialogs/selected_file_info.h"
     39 #include "ui/views/widget/widget.h"
     40 
     41 #if defined(USE_ATHENA)
     42 #include "chrome/browser/ui/views/athena/athena_util.h"
     43 #endif  // USE_ATHENA
     44 
     45 using extensions::AppWindow;
     46 using content::BrowserThread;
     47 
     48 namespace {
     49 
     50 const int kFileManagerWidth = 972;  // pixels
     51 const int kFileManagerHeight = 640;  // pixels
     52 const int kFileManagerMinimumWidth = 640;  // pixels
     53 const int kFileManagerMinimumHeight = 240;  // pixels
     54 
     55 // Holds references to file manager dialogs that have callbacks pending
     56 // to their listeners.
     57 class PendingDialog {
     58  public:
     59   static PendingDialog* GetInstance();
     60   void Add(SelectFileDialogExtension::RoutingID id,
     61            scoped_refptr<SelectFileDialogExtension> dialog);
     62   void Remove(SelectFileDialogExtension::RoutingID id);
     63   scoped_refptr<SelectFileDialogExtension> Find(
     64       SelectFileDialogExtension::RoutingID id);
     65 
     66  private:
     67   friend struct DefaultSingletonTraits<PendingDialog>;
     68   typedef std::map<SelectFileDialogExtension::RoutingID,
     69                    scoped_refptr<SelectFileDialogExtension> > Map;
     70   Map map_;
     71 };
     72 
     73 // static
     74 PendingDialog* PendingDialog::GetInstance() {
     75   return Singleton<PendingDialog>::get();
     76 }
     77 
     78 void PendingDialog::Add(SelectFileDialogExtension::RoutingID id,
     79                         scoped_refptr<SelectFileDialogExtension> dialog) {
     80   DCHECK(dialog.get());
     81   if (map_.find(id) == map_.end())
     82     map_.insert(std::make_pair(id, dialog));
     83   else
     84     DLOG(WARNING) << "Duplicate pending dialog " << id;
     85 }
     86 
     87 void PendingDialog::Remove(SelectFileDialogExtension::RoutingID id) {
     88   map_.erase(id);
     89 }
     90 
     91 scoped_refptr<SelectFileDialogExtension> PendingDialog::Find(
     92     SelectFileDialogExtension::RoutingID id) {
     93   Map::const_iterator it = map_.find(id);
     94   if (it == map_.end())
     95     return NULL;
     96   return it->second;
     97 }
     98 
     99 #if defined(USE_ATHENA)
    100 void FindRuntimeContext(gfx::NativeWindow owner_window,
    101                         ui::BaseWindow** base_window,
    102                         content::WebContents** web_contents) {
    103   *base_window = NULL;
    104   *web_contents = GetWebContentsForWindow(owner_window);
    105 }
    106 #else   // USE_ATHENA
    107 // Given |owner_window| finds corresponding |base_window|, it's associated
    108 // |web_contents| and |profile|.
    109 void FindRuntimeContext(gfx::NativeWindow owner_window,
    110                         ui::BaseWindow** base_window,
    111                         content::WebContents** web_contents) {
    112   *base_window = NULL;
    113   *web_contents = NULL;
    114   // To get the base_window and web contents, either a Browser or AppWindow is
    115   // needed.
    116   Browser* owner_browser =  NULL;
    117   AppWindow* app_window = NULL;
    118 
    119   // If owner_window is supplied, use that to find a browser or a app window.
    120   if (owner_window) {
    121     owner_browser = chrome::FindBrowserWithWindow(owner_window);
    122     if (!owner_browser) {
    123       // If an owner_window was supplied but we couldn't find a browser, this
    124       // could be for a app window.
    125       app_window =
    126           AppWindowRegistryUtil::GetAppWindowForNativeWindowAnyProfile(
    127               owner_window);
    128     }
    129   }
    130 
    131   if (app_window) {
    132     DCHECK(!app_window->window_type_is_panel());
    133     *base_window = app_window->GetBaseWindow();
    134     *web_contents = app_window->web_contents();
    135   } else {
    136     // If the owning window is still unknown, this could be a background page or
    137     // and extension popup. Use the last active browser.
    138     if (!owner_browser) {
    139       owner_browser =
    140           chrome::FindLastActiveWithHostDesktopType(chrome::GetActiveDesktop());
    141     }
    142     if (owner_browser) {
    143       *base_window = owner_browser->window();
    144       *web_contents = owner_browser->tab_strip_model()->GetActiveWebContents();
    145     }
    146   }
    147 
    148   // In ChromeOS kiosk launch mode, we can still show file picker for
    149   // certificate manager dialog. There are no browser or webapp window
    150   // instances present in this case.
    151   if (chrome::IsRunningInForcedAppMode() && !(*web_contents))
    152     *web_contents = chromeos::LoginWebDialog::GetCurrentWebContents();
    153 
    154   CHECK(web_contents);
    155 }
    156 #endif  // USE_ATHENA
    157 
    158 }  // namespace
    159 
    160 /////////////////////////////////////////////////////////////////////////////
    161 
    162 // static
    163 SelectFileDialogExtension::RoutingID
    164 SelectFileDialogExtension::GetRoutingIDFromWebContents(
    165     const content::WebContents* web_contents) {
    166   // Use the raw pointer value as the identifier. Previously we have used the
    167   // tab ID for the purpose, but some web_contents, especially those of the
    168   // packaged apps, don't have tab IDs assigned.
    169   return web_contents;
    170 }
    171 
    172 // TODO(jamescook): Move this into a new file shell_dialogs_chromeos.cc
    173 // static
    174 SelectFileDialogExtension* SelectFileDialogExtension::Create(
    175     Listener* listener,
    176     ui::SelectFilePolicy* policy) {
    177   return new SelectFileDialogExtension(listener, policy);
    178 }
    179 
    180 SelectFileDialogExtension::SelectFileDialogExtension(
    181     Listener* listener,
    182     ui::SelectFilePolicy* policy)
    183     : SelectFileDialog(listener, policy),
    184       has_multiple_file_type_choices_(false),
    185       routing_id_(),
    186       profile_(NULL),
    187       owner_window_(NULL),
    188       selection_type_(CANCEL),
    189       selection_index_(0),
    190       params_(NULL) {
    191 }
    192 
    193 SelectFileDialogExtension::~SelectFileDialogExtension() {
    194   if (extension_dialog_.get())
    195     extension_dialog_->ObserverDestroyed();
    196 }
    197 
    198 bool SelectFileDialogExtension::IsRunning(
    199     gfx::NativeWindow owner_window) const {
    200   return owner_window_ == owner_window;
    201 }
    202 
    203 void SelectFileDialogExtension::ListenerDestroyed() {
    204   listener_ = NULL;
    205   params_ = NULL;
    206   PendingDialog::GetInstance()->Remove(routing_id_);
    207 }
    208 
    209 void SelectFileDialogExtension::ExtensionDialogClosing(
    210     ExtensionDialog* /*dialog*/) {
    211   profile_ = NULL;
    212   owner_window_ = NULL;
    213   // Release our reference to the underlying dialog to allow it to close.
    214   extension_dialog_ = NULL;
    215   PendingDialog::GetInstance()->Remove(routing_id_);
    216   // Actually invoke the appropriate callback on our listener.
    217   NotifyListener();
    218 }
    219 
    220 void SelectFileDialogExtension::ExtensionTerminated(
    221     ExtensionDialog* dialog) {
    222   // The extension would have been unloaded because of the termination,
    223   // reload it.
    224   std::string extension_id = dialog->host()->extension()->id();
    225   // Reload the extension after a bit; the extension may not have been unloaded
    226   // yet. We don't want to try to reload the extension only to have the Unload
    227   // code execute after us and re-unload the extension.
    228   //
    229   // TODO(rkc): This is ugly. The ideal solution is that we shouldn't need to
    230   // reload the extension at all - when we try to open the extension the next
    231   // time, the extension subsystem would automatically reload it for us. At
    232   // this time though this is broken because of some faulty wiring in
    233   // extensions::ProcessManager::CreateViewHost. Once that is fixed, remove
    234   // this.
    235   if (profile_) {
    236     base::MessageLoop::current()->PostTask(
    237         FROM_HERE,
    238         base::Bind(&ExtensionService::ReloadExtension,
    239                    base::Unretained(extensions::ExtensionSystem::Get(profile_)
    240                                         ->extension_service()),
    241                    extension_id));
    242   }
    243 
    244   dialog->GetWidget()->Close();
    245 }
    246 
    247 // static
    248 void SelectFileDialogExtension::OnFileSelected(
    249     RoutingID routing_id,
    250     const ui::SelectedFileInfo& file,
    251     int index) {
    252   scoped_refptr<SelectFileDialogExtension> dialog =
    253       PendingDialog::GetInstance()->Find(routing_id);
    254   if (!dialog.get())
    255     return;
    256   dialog->selection_type_ = SINGLE_FILE;
    257   dialog->selection_files_.clear();
    258   dialog->selection_files_.push_back(file);
    259   dialog->selection_index_ = index;
    260 }
    261 
    262 // static
    263 void SelectFileDialogExtension::OnMultiFilesSelected(
    264     RoutingID routing_id,
    265     const std::vector<ui::SelectedFileInfo>& files) {
    266   scoped_refptr<SelectFileDialogExtension> dialog =
    267       PendingDialog::GetInstance()->Find(routing_id);
    268   if (!dialog.get())
    269     return;
    270   dialog->selection_type_ = MULTIPLE_FILES;
    271   dialog->selection_files_ = files;
    272   dialog->selection_index_ = 0;
    273 }
    274 
    275 // static
    276 void SelectFileDialogExtension::OnFileSelectionCanceled(RoutingID routing_id) {
    277   scoped_refptr<SelectFileDialogExtension> dialog =
    278       PendingDialog::GetInstance()->Find(routing_id);
    279   if (!dialog.get())
    280     return;
    281   dialog->selection_type_ = CANCEL;
    282   dialog->selection_files_.clear();
    283   dialog->selection_index_ = 0;
    284 }
    285 
    286 content::RenderViewHost* SelectFileDialogExtension::GetRenderViewHost() {
    287   if (extension_dialog_.get())
    288     return extension_dialog_->host()->render_view_host();
    289   return NULL;
    290 }
    291 
    292 void SelectFileDialogExtension::NotifyListener() {
    293   if (!listener_)
    294     return;
    295   switch (selection_type_) {
    296     case CANCEL:
    297       listener_->FileSelectionCanceled(params_);
    298       break;
    299     case SINGLE_FILE:
    300       listener_->FileSelectedWithExtraInfo(selection_files_[0],
    301                                            selection_index_,
    302                                            params_);
    303       break;
    304     case MULTIPLE_FILES:
    305       listener_->MultiFilesSelectedWithExtraInfo(selection_files_, params_);
    306       break;
    307     default:
    308       NOTREACHED();
    309       break;
    310   }
    311 }
    312 
    313 void SelectFileDialogExtension::AddPending(RoutingID routing_id) {
    314   PendingDialog::GetInstance()->Add(routing_id, this);
    315 }
    316 
    317 // static
    318 bool SelectFileDialogExtension::PendingExists(RoutingID routing_id) {
    319   return PendingDialog::GetInstance()->Find(routing_id).get() != NULL;
    320 }
    321 
    322 bool SelectFileDialogExtension::HasMultipleFileTypeChoicesImpl() {
    323   return has_multiple_file_type_choices_;
    324 }
    325 
    326 void SelectFileDialogExtension::SelectFileImpl(
    327     Type type,
    328     const base::string16& title,
    329     const base::FilePath& default_path,
    330     const FileTypeInfo* file_types,
    331     int file_type_index,
    332     const base::FilePath::StringType& default_extension,
    333     gfx::NativeWindow owner_window,
    334     void* params) {
    335   if (owner_window_) {
    336     LOG(ERROR) << "File dialog already in use!";
    337     return;
    338   }
    339 
    340   // The base window to associate the dialog with.
    341   ui::BaseWindow* base_window = NULL;
    342 
    343   // The web contents to associate the dialog with.
    344   content::WebContents* web_contents = NULL;
    345   FindRuntimeContext(owner_window, &base_window, &web_contents);
    346   CHECK(web_contents);
    347   profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext());
    348   CHECK(profile_);
    349 
    350   // Check if we have another dialog opened for the contents. It's unlikely, but
    351   // possible. In such situation, discard this request.
    352   RoutingID routing_id = GetRoutingIDFromWebContents(web_contents);
    353   if (PendingExists(routing_id))
    354     return;
    355 
    356   const PrefService* pref_service = profile_->GetPrefs();
    357   DCHECK(pref_service);
    358 
    359   base::FilePath download_default_path(
    360       pref_service->GetFilePath(prefs::kDownloadDefaultDirectory));
    361 
    362   base::FilePath selection_path = default_path.IsAbsolute() ?
    363       default_path : download_default_path.Append(default_path.BaseName());
    364 
    365   base::FilePath fallback_path = profile_->last_selected_directory().empty() ?
    366       download_default_path : profile_->last_selected_directory();
    367 
    368   // Convert the above absolute paths to file system URLs.
    369   GURL selection_url;
    370   if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
    371       profile_,
    372       selection_path,
    373       file_manager::kFileManagerAppId,
    374       &selection_url)) {
    375     // Due to the current design, an invalid temporal cache file path may passed
    376     // as |default_path| (crbug.com/178013 #9-#11). In such a case, we use the
    377     // last selected directory as a workaround. Real fix is tracked at
    378     // crbug.com/110119.
    379     if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
    380         profile_,
    381         fallback_path.Append(default_path.BaseName()),
    382         file_manager::kFileManagerAppId,
    383         &selection_url)) {
    384       DVLOG(1) << "Unable to resolve the selection URL.";
    385     }
    386   }
    387 
    388   GURL current_directory_url;
    389   base::FilePath current_directory_path = selection_path.DirName();
    390   if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
    391       profile_,
    392       current_directory_path,
    393       file_manager::kFileManagerAppId,
    394       &current_directory_url)) {
    395     // Fallback if necessary, see the comment above.
    396     if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
    397             profile_,
    398             fallback_path,
    399             file_manager::kFileManagerAppId,
    400             &current_directory_url)) {
    401       DVLOG(1) << "Unable to resolve the current directory URL for: "
    402                << fallback_path.value();
    403     }
    404   }
    405 
    406   has_multiple_file_type_choices_ =
    407       !file_types || (file_types->extensions.size() > 1);
    408 
    409   GURL file_manager_url =
    410       file_manager::util::GetFileManagerMainPageUrlWithParams(
    411           type,
    412           title,
    413           current_directory_url,
    414           selection_url,
    415           default_path.BaseName().value(),
    416           file_types,
    417           file_type_index,
    418           default_extension);
    419 
    420   ExtensionDialog* dialog = ExtensionDialog::Show(
    421       file_manager_url,
    422       base_window ? base_window->GetNativeWindow() : owner_window,
    423       profile_,
    424       web_contents,
    425       kFileManagerWidth,
    426       kFileManagerHeight,
    427       kFileManagerMinimumWidth,
    428       kFileManagerMinimumHeight,
    429       file_manager::util::GetSelectFileDialogTitle(type),
    430       this /* ExtensionDialog::Observer */);
    431   if (!dialog) {
    432     LOG(ERROR) << "Unable to create extension dialog";
    433     return;
    434   }
    435 
    436   // Connect our listener to FileDialogFunction's per-tab callbacks.
    437   AddPending(routing_id);
    438 
    439   extension_dialog_ = dialog;
    440   params_ = params;
    441   routing_id_ = routing_id;
    442   owner_window_ = owner_window;
    443 }
    444