// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "stdafx.h" #include "win8/metro_driver/file_picker.h" #include #include "base/bind.h" #include "base/files/file_path.h" #include "base/logging.h" #include "base/message_loop/message_loop.h" #include "base/strings/string_util.h" #include "base/synchronization/waitable_event.h" #include "base/win/metro.h" #include "base/win/scoped_comptr.h" #include "win8/metro_driver/chrome_app_view.h" #include "win8/metro_driver/winrt_utils.h" namespace { namespace winstorage = ABI::Windows::Storage; typedef winfoundtn::Collections::IVector StringVectorItf; // TODO(siggi): Complete this implementation and move it to a common place. class StringVectorImpl : public mswr::RuntimeClass { public: ~StringVectorImpl() { std::for_each(strings_.begin(), strings_.end(), ::WindowsDeleteString); } HRESULT RuntimeClassInitialize(const std::vector& list) { for (size_t i = 0; i < list.size(); ++i) strings_.push_back(MakeHString(list[i])); return S_OK; } // IVector implementation. STDMETHOD(GetAt)(unsigned index, HSTRING* item) { if (index >= strings_.size()) return E_INVALIDARG; return ::WindowsDuplicateString(strings_[index], item); } STDMETHOD(get_Size)(unsigned *size) { if (strings_.size() > UINT_MAX) return E_UNEXPECTED; *size = static_cast(strings_.size()); return S_OK; } STDMETHOD(GetView)(winfoundtn::Collections::IVectorView **view) { return E_NOTIMPL; } STDMETHOD(IndexOf)(HSTRING value, unsigned *index, boolean *found) { return E_NOTIMPL; } // write methods STDMETHOD(SetAt)(unsigned index, HSTRING item) { return E_NOTIMPL; } STDMETHOD(InsertAt)(unsigned index, HSTRING item) { return E_NOTIMPL; } STDMETHOD(RemoveAt)(unsigned index) { return E_NOTIMPL; } STDMETHOD(Append)(HSTRING item) { return E_NOTIMPL; } STDMETHOD(RemoveAtEnd)() { return E_NOTIMPL; } STDMETHOD(Clear)() { return E_NOTIMPL; } private: std::vector strings_; }; class FilePickerSessionBase { public: // Creates a file picker for open_file_name. explicit FilePickerSessionBase(OPENFILENAME* open_file_name); // Runs the picker, returns true on success. bool Run(); protected: // Creates, configures and starts a file picker. // If the HRESULT returned is a failure code the file picker has not started, // so no callbacks should be expected. virtual HRESULT StartFilePicker() = 0; // The parameters to our picker. OPENFILENAME* open_file_name_; // The event Run waits on. base::WaitableEvent event_; // True iff a file picker has successfully finished. bool success_; private: // Initiate a file picker, must be called on the metro dispatcher's thread. void DoFilePicker(); DISALLOW_COPY_AND_ASSIGN(FilePickerSessionBase); }; class OpenFilePickerSession : public FilePickerSessionBase { public: explicit OpenFilePickerSession(OPENFILENAME* open_file_name); private: virtual HRESULT StartFilePicker() OVERRIDE; typedef winfoundtn::IAsyncOperation SingleFileAsyncOp; typedef winfoundtn::Collections::IVectorView< winstorage::StorageFile*> StorageFileVectorCollection; typedef winfoundtn::IAsyncOperation MultiFileAsyncOp; // Called asynchronously when a single file picker is done. HRESULT SinglePickerDone(SingleFileAsyncOp* async, AsyncStatus status); // Called asynchronously when a multi file picker is done. HRESULT MultiPickerDone(MultiFileAsyncOp* async, AsyncStatus status); // Composes a multi-file result string suitable for returning to a // from a storage file collection. static HRESULT ComposeMultiFileResult(StorageFileVectorCollection* files, string16* result); private: DISALLOW_COPY_AND_ASSIGN(OpenFilePickerSession); }; class SaveFilePickerSession : public FilePickerSessionBase { public: explicit SaveFilePickerSession(OPENFILENAME* open_file_name); private: virtual HRESULT StartFilePicker() OVERRIDE; typedef winfoundtn::IAsyncOperation SaveFileAsyncOp; // Called asynchronously when the save file picker is done. HRESULT FilePickerDone(SaveFileAsyncOp* async, AsyncStatus status); }; FilePickerSessionBase::FilePickerSessionBase(OPENFILENAME* open_file_name) : open_file_name_(open_file_name), event_(true, false), success_(false) { } bool FilePickerSessionBase::Run() { DCHECK(globals.appview_msg_loop != NULL); // Post the picker request over to the metro thread. bool posted = globals.appview_msg_loop->PostTask(FROM_HERE, base::Bind(&FilePickerSessionBase::DoFilePicker, base::Unretained(this))); if (!posted) return false; // Wait for the file picker to complete. event_.Wait(); return success_; } void FilePickerSessionBase::DoFilePicker() { // The file picker will fail if spawned from a snapped application, // so let's attempt to unsnap first if we're in that state. HRESULT hr = ChromeAppView::Unsnap(); if (FAILED(hr)) { LOG(ERROR) << "Failed to unsnap for file picker, error 0x" << hr; } if (SUCCEEDED(hr)) hr = StartFilePicker(); if (FAILED(hr)) { LOG(ERROR) << "Failed to start file picker, error 0x" << std::hex << hr; event_.Signal(); } } OpenFilePickerSession::OpenFilePickerSession(OPENFILENAME* open_file_name) : FilePickerSessionBase(open_file_name) { } HRESULT OpenFilePickerSession::SinglePickerDone(SingleFileAsyncOp* async, AsyncStatus status) { if (status == Completed) { mswr::ComPtr file; HRESULT hr = async->GetResults(file.GetAddressOf()); if (file) { mswr::ComPtr storage_item; if (SUCCEEDED(hr)) hr = file.As(&storage_item); mswrw::HString file_path; if (SUCCEEDED(hr)) hr = storage_item->get_Path(file_path.GetAddressOf()); if (SUCCEEDED(hr)) { UINT32 path_len = 0; const wchar_t* path_str = ::WindowsGetStringRawBuffer(file_path.Get(), &path_len); // If the selected file name is longer than the supplied buffer, // we return false as per GetOpenFileName documentation. if (path_len < open_file_name_->nMaxFile) { base::wcslcpy(open_file_name_->lpstrFile, path_str, open_file_name_->nMaxFile); success_ = true; } } } else { LOG(ERROR) << "NULL IStorageItem"; } } else { LOG(ERROR) << "Unexpected async status " << static_cast(status); } event_.Signal(); return S_OK; } HRESULT OpenFilePickerSession::MultiPickerDone(MultiFileAsyncOp* async, AsyncStatus status) { if (status == Completed) { mswr::ComPtr files; HRESULT hr = async->GetResults(files.GetAddressOf()); if (files) { string16 result; if (SUCCEEDED(hr)) hr = ComposeMultiFileResult(files.Get(), &result); if (SUCCEEDED(hr)) { if (result.size() + 1 < open_file_name_->nMaxFile) { // Because the result has embedded nulls, we must memcpy. memcpy(open_file_name_->lpstrFile, result.c_str(), (result.size() + 1) * sizeof(result[0])); success_ = true; } } } else { LOG(ERROR) << "NULL StorageFileVectorCollection"; } } else { LOG(ERROR) << "Unexpected async status " << static_cast(status); } event_.Signal(); return S_OK; } HRESULT OpenFilePickerSession::StartFilePicker() { DCHECK(globals.appview_msg_loop->BelongsToCurrentThread()); DCHECK(open_file_name_ != NULL); mswrw::HStringReference class_name( RuntimeClass_Windows_Storage_Pickers_FileOpenPicker); // Create the file picker. mswr::ComPtr picker; HRESULT hr = ::Windows::Foundation::ActivateInstance( class_name.Get(), picker.GetAddressOf()); CheckHR(hr); // Set the file type filter mswr::ComPtr> filter; hr = picker->get_FileTypeFilter(filter.GetAddressOf()); if (FAILED(hr)) return hr; if (open_file_name_->lpstrFilter == NULL) { hr = filter->Append(mswrw::HStringReference(L"*").Get()); if (FAILED(hr)) return hr; } else { // The filter is a concatenation of zero terminated string pairs, // where each pair is {description, extension}. The concatenation ends // with a zero length string - e.g. a double zero terminator. const wchar_t* walk = open_file_name_->lpstrFilter; while (*walk != L'\0') { // Walk past the description. walk += wcslen(walk) + 1; // We should have an extension, but bail on malformed filters. if (*walk == L'\0') break; // There can be a single extension, or a list of semicolon-separated ones. std::vector extensions_win32_style; size_t extension_count = Tokenize(walk, L";", &extensions_win32_style); DCHECK_EQ(extension_count, extensions_win32_style.size()); // Metro wants suffixes only, not patterns. mswrw::HString extension; for (size_t i = 0; i < extensions_win32_style.size(); ++i) { if (extensions_win32_style[i] == L"*.*") { // The wildcard filter is "*" for Metro. The string "*.*" produces // an "invalid parameter" error. hr = extension.Set(L"*"); } else { // Metro wants suffixes only, not patterns. string16 ext = base::FilePath(extensions_win32_style[i]).Extension(); if ((ext.size() < 2) || (ext.find_first_of(L"*?") != string16::npos)) { continue; } hr = extension.Set(ext.c_str()); } if (SUCCEEDED(hr)) hr = filter->Append(extension.Get()); if (FAILED(hr)) return hr; } // Walk past the extension. walk += wcslen(walk) + 1; } } // Spin up a single or multi picker as appropriate. if (open_file_name_->Flags & OFN_ALLOWMULTISELECT) { mswr::ComPtr completion; hr = picker->PickMultipleFilesAsync(&completion); if (FAILED(hr)) return hr; // Create the callback method. typedef winfoundtn::IAsyncOperationCompletedHandler< StorageFileVectorCollection*> HandlerDoneType; mswr::ComPtr handler(mswr::Callback( this, &OpenFilePickerSession::MultiPickerDone)); DCHECK(handler.Get() != NULL); hr = completion->put_Completed(handler.Get()); return hr; } else { mswr::ComPtr completion; hr = picker->PickSingleFileAsync(&completion); if (FAILED(hr)) return hr; // Create the callback method. typedef winfoundtn::IAsyncOperationCompletedHandler< winstorage::StorageFile*> HandlerDoneType; mswr::ComPtr handler(mswr::Callback( this, &OpenFilePickerSession::SinglePickerDone)); DCHECK(handler.Get() != NULL); hr = completion->put_Completed(handler.Get()); return hr; } } HRESULT OpenFilePickerSession::ComposeMultiFileResult( StorageFileVectorCollection* files, string16* result) { DCHECK(files != NULL); DCHECK(result != NULL); // Empty the output string. result->clear(); unsigned int num_files = 0; HRESULT hr = files->get_Size(&num_files); if (FAILED(hr)) return hr; // Make sure we return an error on an empty collection. if (num_files == 0) { DLOG(ERROR) << "Empty collection on input."; return E_UNEXPECTED; } // This stores the base path that should be the parent of all the files. base::FilePath base_path; // Iterate through the collection and append the file paths to the result. for (unsigned int i = 0; i < num_files; ++i) { mswr::ComPtr file; hr = files->GetAt(i, file.GetAddressOf()); if (FAILED(hr)) return hr; mswr::ComPtr storage_item; hr = file.As(&storage_item); if (FAILED(hr)) return hr; mswrw::HString file_path_str; hr = storage_item->get_Path(file_path_str.GetAddressOf()); if (FAILED(hr)) return hr; base::FilePath file_path(MakeStdWString(file_path_str.Get())); if (base_path.empty()) { DCHECK(result->empty()); base_path = file_path.DirName(); // Append the path, including the terminating zero. // We do this only for the first file. result->append(base_path.value().c_str(), base_path.value().size() + 1); } DCHECK(!result->empty()); DCHECK(!base_path.empty()); DCHECK(base_path == file_path.DirName()); // Append the base name, including the terminating zero. base::FilePath base_name = file_path.BaseName(); result->append(base_name.value().c_str(), base_name.value().size() + 1); } DCHECK(!result->empty()); return S_OK; } SaveFilePickerSession::SaveFilePickerSession(OPENFILENAME* open_file_name) : FilePickerSessionBase(open_file_name) { } HRESULT SaveFilePickerSession::StartFilePicker() { DCHECK(globals.appview_msg_loop->BelongsToCurrentThread()); DCHECK(open_file_name_ != NULL); mswrw::HStringReference class_name( RuntimeClass_Windows_Storage_Pickers_FileSavePicker); // Create the file picker. mswr::ComPtr picker; HRESULT hr = ::Windows::Foundation::ActivateInstance( class_name.Get(), picker.GetAddressOf()); CheckHR(hr); typedef winfoundtn::Collections::IMap StringVectorMap; mswr::ComPtr choices; hr = picker->get_FileTypeChoices(choices.GetAddressOf()); if (FAILED(hr)) return hr; if (open_file_name_->lpstrFilter) { // The filter is a concatenation of zero terminated string pairs, // where each pair is {description, extension list}. The concatenation ends // with a zero length string - e.g. a double zero terminator. const wchar_t* walk = open_file_name_->lpstrFilter; while (*walk != L'\0') { mswrw::HString description; hr = description.Set(walk); if (FAILED(hr)) return hr; // Walk past the description. walk += wcslen(walk) + 1; // We should have an extension, but bail on malformed filters. if (*walk == L'\0') break; // There can be a single extension, or a list of semicolon-separated ones. std::vector extensions_win32_style; size_t extension_count = Tokenize(walk, L";", &extensions_win32_style); DCHECK_EQ(extension_count, extensions_win32_style.size()); // Metro wants suffixes only, not patterns. Also, metro does not support // the all files ("*") pattern in the save picker. std::vector extensions; for (size_t i = 0; i < extensions_win32_style.size(); ++i) { string16 ext = base::FilePath(extensions_win32_style[i]).Extension(); if ((ext.size() < 2) || (ext.find_first_of(L"*?") != string16::npos)) continue; extensions.push_back(ext); } if (!extensions.empty()) { // Convert to a Metro collection class. mswr::ComPtr list; hr = mswr::MakeAndInitialize( list.GetAddressOf(), extensions); if (FAILED(hr)) return hr; // Finally set the filter. boolean replaced = FALSE; hr = choices->Insert(description.Get(), list.Get(), &replaced); if (FAILED(hr)) return hr; DCHECK_EQ(FALSE, replaced); } // Walk past the extension(s). walk += wcslen(walk) + 1; } } // The save picker requires at least one choice. Callers are strongly advised // to provide sensible choices. If none were given, fallback to .dat. uint32 num_choices = 0; hr = choices->get_Size(&num_choices); if (FAILED(hr)) return hr; if (num_choices == 0) { mswrw::HString description; // TODO(grt): Get a properly translated string. This can't be done from // within metro_driver. Consider preprocessing the filter list in Chrome // land to ensure it has this entry if all others are patterns. In that // case, this whole block of code can be removed. hr = description.Set(L"Data File"); if (FAILED(hr)) return hr; mswr::ComPtr list; hr = mswr::MakeAndInitialize( list.GetAddressOf(), std::vector(1, L".dat")); if (FAILED(hr)) return hr; boolean replaced = FALSE; hr = choices->Insert(description.Get(), list.Get(), &replaced); if (FAILED(hr)) return hr; DCHECK_EQ(FALSE, replaced); } if (open_file_name_->lpstrFile != NULL) { hr = picker->put_SuggestedFileName( mswrw::HStringReference( const_cast(open_file_name_->lpstrFile)).Get()); if (FAILED(hr)) return hr; } mswr::ComPtr completion; hr = picker->PickSaveFileAsync(&completion); if (FAILED(hr)) return hr; // Create the callback method. typedef winfoundtn::IAsyncOperationCompletedHandler< winstorage::StorageFile*> HandlerDoneType; mswr::ComPtr handler(mswr::Callback( this, &SaveFilePickerSession::FilePickerDone)); DCHECK(handler.Get() != NULL); hr = completion->put_Completed(handler.Get()); return hr; } HRESULT SaveFilePickerSession::FilePickerDone(SaveFileAsyncOp* async, AsyncStatus status) { if (status == Completed) { mswr::ComPtr file; HRESULT hr = async->GetResults(file.GetAddressOf()); if (file) { mswr::ComPtr storage_item; if (SUCCEEDED(hr)) hr = file.As(&storage_item); mswrw::HString file_path; if (SUCCEEDED(hr)) hr = storage_item->get_Path(file_path.GetAddressOf()); if (SUCCEEDED(hr)) { string16 path_str = MakeStdWString(file_path.Get()); // If the selected file name is longer than the supplied buffer, // we return false as per GetOpenFileName documentation. if (path_str.size() < open_file_name_->nMaxFile) { base::wcslcpy(open_file_name_->lpstrFile, path_str.c_str(), open_file_name_->nMaxFile); success_ = true; } } } else { LOG(ERROR) << "NULL IStorageItem"; } } else { LOG(ERROR) << "Unexpected async status " << static_cast(status); } event_.Signal(); return S_OK; } } // namespace BOOL MetroGetOpenFileName(OPENFILENAME* open_file_name) { OpenFilePickerSession session(open_file_name); return session.Run(); } BOOL MetroGetSaveFileName(OPENFILENAME* open_file_name) { SaveFilePickerSession session(open_file_name); return session.Run(); }