diff options
author | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-07-15 03:40:23 +0000 |
---|---|---|
committer | oshima@chromium.org <oshima@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-07-15 03:40:23 +0000 |
commit | 6692b0d7bca9e0e3327d371c312196f3af9ed7c6 (patch) | |
tree | e589215d311709b95cdbdbae330ae86c439b46a4 /chrome | |
parent | 2c77c96e4c474ff213c4cf2ed850970c48bbced0 (diff) | |
download | chromium_src-6692b0d7bca9e0e3327d371c312196f3af9ed7c6.zip chromium_src-6692b0d7bca9e0e3327d371c312196f3af9ed7c6.tar.gz chromium_src-6692b0d7bca9e0e3327d371c312196f3af9ed7c6.tar.bz2 |
Show offline interstitial page when offline and reload when reconnected to network.
* Added OfflineResourceHandler that intercept the request and show interstitial page.
* Added OfflineLoadPage that is shown when offline.
This gets deleted when
- User pressed "Load now" btton to proceed or
- User pressed "Cancel" button to cancel loading
- Network become available.
The page first appears as blank page (a little darker than white for now. I'll update when mock is ready),
and then options become available after 3 seconds maximum.
* Added unit test for OfflineLoadPage class.
* OfflineLoadService class to control when/if a load request
should proceed regardless of network status.
The current implementation is tentative and will proceed
if if loading was requested in a given tab.
I'll revisit this class to improve the logic in separate CL
later.
Known Issue:
- thumbnail is not working yet. I'll working on this in separate Cl.
- a tab shows URL instead of title string. I'll fix this in separate CL.
- InitNavigationParams in offline_load_page_unittest is copied from safe_browsing_blocking_page_unittest. I'll move this to common place in separate CL. (hopefully before checking this in)
BUG=chromium-os:3605
TEST=unit test: offline_load_page_unittest
manual: disable wifi and ethernet, then login.
chrome will show the offline page when tab is activated. Enable network (wifi or ethernet) and all tab will start loading again.
Review URL: http://codereview.chromium.org/2931005
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@52433 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
18 files changed, 1008 insertions, 22 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 4825c04..c802f74 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -6291,6 +6291,22 @@ Keep your key file in a safe place. You will need it to create new versions of y Options </message> + <!-- Offline page --> + <if expr="pp_ifdef('chromeos')"> + <message name="IDS_OFFLINE_LOAD_HEADLINE" desc="Offline Page HTML headline"> + Network is not available. + </message> + <message name="IDS_OFFLINE_LOAD_DESCRIPTION" desc="Offline Page HTML description"> + Page will be loaded when network becomes available. Press 'Load Now' if you want to load now. + </message> + <message name="IDS_OFFLINE_LOAD_BUTTON"> + Load Now + </message> + <message name="IDS_OFFLINE_BACK_BUTTON"> + Cancel + </message> + </if> + <!-- SafeBrowsing --> <message name="IDS_SAFE_BROWSING_MALWARE_TITLE" desc="SafeBrowsing Malware HTML title"> Malware Detected! diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd index ccdbc04..7463d03 100644 --- a/chrome/browser/browser_resources.grd +++ b/chrome/browser/browser_resources.grd @@ -74,6 +74,7 @@ without changes to the corresponding grd file. eadee --> <include name="IDR_HOST_REGISTRATION_PAGE_HTML" file="resources\host_registration_page.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_MEDIAPLAYER_HTML" file="resources\mediaplayer.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_MEDIAPLAYERPLAYLIST_HTML" file="resources\playlist.html" flattenhtml="true" type="BINDATA" /> + <include name="IDR_OFFLINE_LOAD_HTML" file="resources\offline_load.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_OS_CREDITS_HTML" file="resources\about_os_credits.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_SLIDESHOW_HTML" file="resources\slideshow.html" flattenhtml="true" type="BINDATA" /> </if> diff --git a/chrome/browser/chromeos/network_state_notifier.cc b/chrome/browser/chromeos/network_state_notifier.cc index 2d13030..3ee696c 100644 --- a/chrome/browser/chromeos/network_state_notifier.cc +++ b/chrome/browser/chromeos/network_state_notifier.cc @@ -2,23 +2,38 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "chrome/browser/chromeos/network_state_notifier.h" + +#include "base/message_loop.h" +#include "base/time.h" #include "chrome/browser/chrome_thread.h" #include "chrome/browser/chromeos/cros/cros_library.h" -#include "chrome/browser/chromeos/network_state_notifier.h" #include "chrome/common/notification_service.h" #include "chrome/common/notification_type.h" -#include "base/message_loop.h" - namespace chromeos { +using base::Time; +using base::TimeDelta; + +// static NetworkStateNotifier* NetworkStateNotifier::Get() { return Singleton<NetworkStateNotifier>::get(); } +// static +TimeDelta NetworkStateNotifier::GetOfflineDuration() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + // TODO(oshima): make this instance method so that + // we can mock this for ui_tests. + // http://crbug.com/4825 . + return base::Time::Now() - Get()->offline_start_time_; +} + NetworkStateNotifier::NetworkStateNotifier() - : task_factory_(this) { - state_ = RetrieveState(); + : ALLOW_THIS_IN_INITIALIZER_LIST(task_factory_(this)), + state_(RetrieveState()), + offline_start_time_(Time::Now()) { } void NetworkStateNotifier::NetworkChanged(NetworkLibrary* cros) { @@ -37,6 +52,11 @@ void NetworkStateNotifier::UpdateNetworkState( NetworkStateDetails::State new_state) { DLOG(INFO) << "UpdateNetworkState: new=" << new_state << ", old=" << state_; + if (state_ == NetworkStateDetails::CONNECTED && + new_state != NetworkStateDetails::CONNECTED) { + offline_start_time_ = Time::Now(); + } + state_ = new_state; NetworkStateDetails details(state_); NotificationService::current()->Notify( diff --git a/chrome/browser/chromeos/network_state_notifier.h b/chrome/browser/chromeos/network_state_notifier.h index a48d60e..a64f264 100644 --- a/chrome/browser/chromeos/network_state_notifier.h +++ b/chrome/browser/chromeos/network_state_notifier.h @@ -9,6 +9,7 @@ #include "base/singleton.h" #include "base/task.h" +#include "base/time.h" namespace chromeos { @@ -48,15 +49,19 @@ class NetworkStateNotifier : public NetworkLibrary::Observer { // Returns the singleton instance of the network state notifier; static NetworkStateNotifier* Get(); - // NetworkLibrary::Observer implementation. - virtual void NetworkChanged(NetworkLibrary* cros); - virtual void NetworkTraffic(NetworkLibrary* cros, int traffic_type) {} + // The duration of being in offline. The value is undefined when + // when network is connected. + static base::TimeDelta GetOfflineDuration(); // Returns true if the network is connected. static bool is_connected() { return Get()->state_ == NetworkStateDetails::CONNECTED; } + // NetworkLibrary::Observer implementation. + virtual void NetworkChanged(NetworkLibrary* cros); + virtual void NetworkTraffic(NetworkLibrary* cros, int traffic_type) {} + private: friend struct DefaultSingletonTraits<NetworkStateNotifier>; @@ -70,14 +75,18 @@ class NetworkStateNotifier : public NetworkLibrary::Observer { // This should be invoked in UI thread. void UpdateNetworkState(NetworkStateDetails::State new_state); + // A factory to post a task in UI thread. + ScopedRunnableMethodFactory<NetworkStateNotifier> task_factory_; + // The current network state. NetworkStateDetails::State state_; - ScopedRunnableMethodFactory<NetworkStateNotifier> task_factory_; + // The start time of offline. + base::Time offline_start_time_; DISALLOW_COPY_AND_ASSIGN(NetworkStateNotifier); }; -} // chromeos +} // namespace chromeos #endif // CHROME_BROWSER_CHROMEOS_NETWORK_STATE_NOTIFIER_H_ diff --git a/chrome/browser/chromeos/network_state_notifier_browsertest.cc b/chrome/browser/chromeos/network_state_notifier_browsertest.cc index bf2019c..b917413 100644 --- a/chrome/browser/chromeos/network_state_notifier_browsertest.cc +++ b/chrome/browser/chromeos/network_state_notifier_browsertest.cc @@ -30,6 +30,8 @@ class NetworkStateNotifierTest : public CrosInProcessBrowserTest, InitStatusAreaMocks(); SetStatusAreaMocksExpectations(); // Initialize network state notifier. + ASSERT_TRUE(CrosLibrary::Get()->EnsureLoaded()); + ASSERT_TRUE(mock_network_library_); EXPECT_CALL(*mock_network_library_, Connected()) .Times(1) .WillRepeatedly((Return(true))) @@ -71,7 +73,6 @@ IN_PROC_BROWSER_TEST_F(NetworkStateNotifierTest, TestConnected) { .WillRepeatedly((Return(true))) .RetiresOnSaturation(); NetworkStateNotifier* notifier = NetworkStateNotifier::Get(); - DCHECK(CrosLibrary::Get()->EnsureLoaded()); notifier->NetworkChanged(mock_network_library_); WaitForNotification(); EXPECT_EQ(chromeos::NetworkStateDetails::CONNECTED, state_); @@ -90,7 +91,6 @@ IN_PROC_BROWSER_TEST_F(NetworkStateNotifierTest, TestConnecting) { .WillOnce((Return(true))) .RetiresOnSaturation(); NetworkStateNotifier* notifier = NetworkStateNotifier::Get(); - DCHECK(CrosLibrary::Get()->EnsureLoaded()); notifier->NetworkChanged(mock_network_library_); WaitForNotification(); EXPECT_EQ(chromeos::NetworkStateDetails::CONNECTING, state_); @@ -109,7 +109,6 @@ IN_PROC_BROWSER_TEST_F(NetworkStateNotifierTest, TestDisconnected) { .WillOnce((Return(false))) .RetiresOnSaturation(); NetworkStateNotifier* notifier = NetworkStateNotifier::Get(); - DCHECK(CrosLibrary::Get()->EnsureLoaded()); notifier->NetworkChanged(mock_network_library_); WaitForNotification(); EXPECT_EQ(chromeos::NetworkStateDetails::DISCONNECTED, state_); diff --git a/chrome/browser/chromeos/offline/offline_load_page.cc b/chrome/browser/chromeos/offline/offline_load_page.cc new file mode 100644 index 0000000..0dc98264 --- /dev/null +++ b/chrome/browser/chromeos/offline/offline_load_page.cc @@ -0,0 +1,137 @@ +// Copyright (c) 2010 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 "chrome/browser/chromeos/offline/offline_load_page.h" + +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "base/histogram.h" +#include "base/i18n/rtl.h" +#include "base/string_piece.h" +#include "base/values.h" +#include "chrome/browser/browser.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/tab_contents/navigation_controller.h" +#include "chrome/browser/tab_contents/navigation_entry.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/tab_util.h" +#include "chrome/common/jstemplate_builder.h" +#include "chrome/common/notification_type.h" +#include "grit/browser_resources.h" +#include "grit/generated_resources.h" + +namespace { + +// Maximum time to show a blank page. +const int kMaxBlankPeriod = 3000; + +// A utility function to set the dictionary's value given by |resource_id|. +void SetString(DictionaryValue* strings, const wchar_t* name, int resource_id) { + strings->SetString(name, l10n_util::GetString(resource_id)); +} + +} // namespace + +namespace chromeos { + +// static +void OfflineLoadPage::Show(int process_host_id, int render_view_id, + const GURL& url, Delegate* delegate) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + if (NetworkStateNotifier::is_connected()) { + // Check again in UI thread and proceed if it's connected. + delegate->OnBlockingPageComplete(true); + } else { + TabContents* tab_contents = + tab_util::GetTabContentsByID(process_host_id, render_view_id); + DCHECK(tab_contents); + (new OfflineLoadPage(tab_contents, url, delegate))->Show(); + } +} + +OfflineLoadPage::OfflineLoadPage(TabContents* tab_contents, + const GURL& url, + Delegate* delegate) + : InterstitialPage(tab_contents, true, url), + delegate_(delegate) { + registrar_.Add(this, NotificationType::NETWORK_STATE_CHANGED, + NotificationService::AllSources()); +} + +std::string OfflineLoadPage::GetHTMLContents() { + DictionaryValue strings; + SetString(&strings, L"headLine", IDS_OFFLINE_LOAD_HEADLINE); + SetString(&strings, L"description", IDS_OFFLINE_LOAD_DESCRIPTION); + SetString(&strings, L"load_button", IDS_OFFLINE_LOAD_BUTTON); + SetString(&strings, L"back_button", IDS_OFFLINE_BACK_BUTTON); + + // TODO(oshima): tab()->GetTitle() always return url. This has to be + // a cached title. + strings.SetString(L"title", UTF16ToWide(tab()->GetTitle())); + strings.SetString(L"textdirection", base::i18n::IsRTL() ? L"rtl" : L"ltr"); + strings.SetString(L"display_go_back", + tab()->controller().CanGoBack() ? L"inline" : L"none"); + int64 time_to_wait = std::max( + static_cast<int64>(0), + kMaxBlankPeriod - + NetworkStateNotifier::GetOfflineDuration().InMilliseconds()); + strings.SetString(L"on_load", + StringPrintf(L"startTimer(%ld)", time_to_wait)); + + // TODO(oshima): thumbnail is not working yet. fix this. + const std::string url = "chrome://thumb/" + GetURL().spec(); + strings.SetString(L"thumbnailUrl", "url(" + url + ")"); + + base::StringPiece html( + ResourceBundle::GetSharedInstance().GetRawDataResource( + IDR_OFFLINE_LOAD_HTML)); + return jstemplate_builder::GetI18nTemplateHtml(html, &strings); +} + +void OfflineLoadPage::CommandReceived(const std::string& cmd) { + std::string command(cmd); + // The Jasonified response has quotes, remove them. + if (command.length() > 1 && command[0] == '"') { + command = command.substr(1, command.length() - 2); + } + // TODO(oshima): record action for metrics. + if (command == "proceed") { + Proceed(); + } else if (command == "dontproceed") { + DontProceed(); + } else { + LOG(WARNING) << "Unknown command:" << cmd; + } +} + +void OfflineLoadPage::Proceed() { + delegate_->OnBlockingPageComplete(true); + InterstitialPage::Proceed(); +} + +void OfflineLoadPage::DontProceed() { + delegate_->OnBlockingPageComplete(false); + InterstitialPage::DontProceed(); +} + +void OfflineLoadPage::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + if (type.value == NotificationType::NETWORK_STATE_CHANGED) { + chromeos::NetworkStateDetails* state_details = + Details<chromeos::NetworkStateDetails>(details).ptr(); + DLOG(INFO) << "NetworkStateChanaged notification received: state=" + << state_details->state(); + if (state_details->state() == + chromeos::NetworkStateDetails::CONNECTED) { + registrar_.Remove(this, NotificationType::NETWORK_STATE_CHANGED, + NotificationService::AllSources()); + Proceed(); + } + } else { + InterstitialPage::Observe(type, source, details); + } +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/offline/offline_load_page.h b/chrome/browser/chromeos/offline/offline_load_page.h new file mode 100644 index 0000000..3363853 --- /dev/null +++ b/chrome/browser/chromeos/offline/offline_load_page.h @@ -0,0 +1,69 @@ +// Copyright (c) 2010 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. + +#ifndef CHROME_BROWSER_CHROMEOS_OFFLINE_OFFLINE_LOAD_PAGE_H_ +#define CHROME_BROWSER_CHROMEOS_OFFLINE_OFFLINE_LOAD_PAGE_H_ + +#include <string> + +#include "chrome/browser/chromeos/network_state_notifier.h" +#include "chrome/browser/tab_contents/interstitial_page.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_service.h" + +class TabContents; + +namespace chromeos { + +// OfflineLoadPage class shows the interstitial page that is shown +// when no network is available and hides when some network (either +// one of wifi, 3g or ethernet) becomes available. +// It deletes itself when the interstitial page is closed. +class OfflineLoadPage : public InterstitialPage { + public: + // A delegate class that is called when the interstitinal page + // is closed. + class Delegate { + public: + Delegate() {} + virtual ~Delegate() {} + // Called when a user selected to proceed or not to proceed + // with loading. + virtual void OnBlockingPageComplete(bool proceed) = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(Delegate); + }; + static void Show(int process_host_id, int render_view_id, + const GURL& url, Delegate* delegate); + // Import show here so that overloading works. + using InterstitialPage::Show; + + protected: + // Create a offline load page for the |tab_contents|. + OfflineLoadPage(TabContents* tab_contents, const GURL& url, + Delegate* delegate); + virtual ~OfflineLoadPage() {} + + private: + // InterstitialPage implementation. + virtual std::string GetHTMLContents(); + virtual void CommandReceived(const std::string& command); + virtual void Proceed(); + virtual void DontProceed(); + + // Overrides InterstitialPage's Observe. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + Delegate* delegate_; + NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(OfflineLoadPage); +}; + +} // namespace chromeos + +#endif // CHROME_BROWSER_CHROMEOS_OFFLINE_OFFLINE_LOAD_PAGE_H_ diff --git a/chrome/browser/chromeos/offline/offline_load_page_unittest.cc b/chrome/browser/chromeos/offline/offline_load_page_unittest.cc new file mode 100644 index 0000000..a619243 --- /dev/null +++ b/chrome/browser/chromeos/offline/offline_load_page_unittest.cc @@ -0,0 +1,139 @@ +// Copyright (c) 2010 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 "chrome/browser/renderer_host/test/test_render_view_host.h" + +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/chromeos/offline/offline_load_page.h" +#include "chrome/browser/tab_contents/navigation_entry.h" +#include "chrome/common/render_messages.h" + +static const char* kURL1 = "http://www.google.com/"; +static const char* kURL2 = "http://www.gmail.com/"; + +namespace { + +// An OfflineLoadPage class that does not create windows. +class TestOfflineLoadPage : public chromeos::OfflineLoadPage { + public: + TestOfflineLoadPage(TabContents* tab_contents, + const GURL& url, + Delegate* delegate) + : chromeos::OfflineLoadPage(tab_contents, url, delegate) { + } + + // Overriden from InterstitialPage. Don't create a view. + virtual TabContentsView* CreateTabContentsView() { + return NULL; + } +}; + +} // namespace + +namespace chromeos { + +class OfflineLoadPageTest : public RenderViewHostTestHarness, + public OfflineLoadPage::Delegate { + public: + // The decision the user made. + enum UserResponse { + PENDING, + OK, + CANCEL + }; + + OfflineLoadPageTest() + : ui_thread_(ChromeThread::UI, MessageLoop::current()), + io_thread_(ChromeThread::IO, MessageLoop::current()) { + } + + virtual void SetUp() { + RenderViewHostTestHarness::SetUp(); + user_response_ = PENDING; + } + + // OfflineLoadPage::Delegate implementation. + virtual void OnBlockingPageComplete(bool proceed) { + if (proceed) + user_response_ = OK; + else + user_response_ = CANCEL; + } + + void Navigate(const char* url, int page_id) { + ViewHostMsg_FrameNavigate_Params params; + InitNavigateParams(¶ms, page_id, GURL(url), PageTransition::TYPED); + contents()->TestDidNavigate(contents_->render_view_host(), params); + } + + void ShowInterstitial(const char* url) { + (new TestOfflineLoadPage(contents(), GURL(url), this))->Show(); + } + + // Returns the OfflineLoadPage currently showing or NULL if none is + // showing. + InterstitialPage* GetOfflineLoadPage() { + return InterstitialPage::GetInterstitialPage(contents()); + } + + UserResponse user_response() const { return user_response_; } + + private: + UserResponse user_response_; + ChromeThread ui_thread_; + ChromeThread io_thread_; +}; + + +TEST_F(OfflineLoadPageTest, OfflinePaeProceed) { + // Start a load. + Navigate(kURL1, 1); + // Load next page. + controller().LoadURL(GURL(kURL2), GURL(), PageTransition::TYPED); + + // Simulate the load causing an offline browsing interstitial page + // to be shown. + ShowInterstitial(kURL2); + InterstitialPage* interstitial = GetOfflineLoadPage(); + ASSERT_TRUE(interstitial); + MessageLoop::current()->RunAllPending(); + + // Simulate the user clicking "proceed". + interstitial->Proceed(); + + EXPECT_EQ(OK, user_response()); + + // The URL remains to be URL2. + EXPECT_EQ(kURL2, contents()->GetURL().spec()); + + // Ccommit navigation and the interstitial page is gone. + Navigate(kURL2, 2); + EXPECT_FALSE(GetOfflineLoadPage()); +} + +// Tests showing an offline page and not proceeding. +TEST_F(OfflineLoadPageTest, OfflinePageDontProceed) { + // Start a load. + Navigate(kURL1, 1); + controller().LoadURL(GURL(kURL2), GURL(), PageTransition::TYPED); + + // Simulate the load causing an offline interstitial page to be shown. + ShowInterstitial(kURL2); + InterstitialPage* interstitial = GetOfflineLoadPage(); + ASSERT_TRUE(interstitial); + MessageLoop::current()->RunAllPending(); + + // Simulate the user clicking "don't proceed". + interstitial->DontProceed(); + + // The interstitial should be gone. + EXPECT_EQ(CANCEL, user_response()); + EXPECT_FALSE(GetOfflineLoadPage()); + // We did not proceed, the pending entry should be gone. + EXPECT_FALSE(controller().pending_entry()); + // the URL is set back to kURL1. + EXPECT_EQ(kURL1, contents()->GetURL().spec()); +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/offline/offline_load_service.cc b/chrome/browser/chromeos/offline/offline_load_service.cc new file mode 100644 index 0000000..63602f5 --- /dev/null +++ b/chrome/browser/chromeos/offline/offline_load_service.cc @@ -0,0 +1,104 @@ +// Copyright (c) 2010 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 "chrome/browser/chromeos/offline/offline_load_service.h" + +#include "base/singleton.h" +#include "base/ref_counted.h" +#include "chrome/common/notification_service.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/tab_util.h" +#include "chrome/browser/tab_contents/navigation_controller.h" + +namespace chromeos { + +// A utility class that serves a singleton instance of OfflineLoadService. +// OfflineLoadSerivce itself cannot be a singleton as it implements +// RefCount interface. +class OfflineLoadServiceSingleton { + public: + chromeos::OfflineLoadService* offline_load_service() { + return offline_load_service_.get(); + } + + private: + friend struct DefaultSingletonTraits<OfflineLoadServiceSingleton>; + OfflineLoadServiceSingleton() + : offline_load_service_(new chromeos::OfflineLoadService()) {} + virtual ~OfflineLoadServiceSingleton() {} + + scoped_refptr<chromeos::OfflineLoadService> offline_load_service_; + + DISALLOW_COPY_AND_ASSIGN(OfflineLoadServiceSingleton); +}; + +// static +OfflineLoadService* OfflineLoadService::Get() { + return Singleton<OfflineLoadServiceSingleton>::get()->offline_load_service(); +} + +void OfflineLoadService::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + if (type.value == NotificationType::TAB_CLOSED) { + registrar_.Remove(this, NotificationType::TAB_CLOSED, source); + NavigationController* tab = Source<NavigationController>(source).ptr(); + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod(this, + &OfflineLoadService::RemoveTabContents, + tab->tab_contents())); + } +} + +bool OfflineLoadService::ShouldProceed(int process_host_id, + int render_view_id, + const GURL& url) const { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + TabContents* tab_contents = tab_util::GetTabContentsByID( + process_host_id, render_view_id); + DCHECK(tab_contents); + bool proceed = tabs_.find(tab_contents) != tabs_.end(); + DLOG(INFO) << "ShouldProceed:" << proceed << ", url=" << url.spec() + << ", tab_contents=" << tab_contents; + return proceed; +} + +void OfflineLoadService::Proceeded(int process_host_id, + int render_view_id, + const GURL& url) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + TabContents* tab_contents = tab_util::GetTabContentsByID( + process_host_id, render_view_id); + DCHECK(tab_contents); + if (tabs_.find(tab_contents) == tabs_.end()) { + DLOG(INFO) << "Proceeded: url=" << url.spec() + << ", tab_contents=" << tab_contents; + tabs_.insert(tab_contents); + ChromeThread::PostTask( + ChromeThread::UI, FROM_HERE, + NewRunnableMethod(this, + &OfflineLoadService::RegisterNotification, + &tab_contents->controller())); + } else { + DLOG(WARNING) << "Proceeded: ignoring duplicate"; + } +} + +void OfflineLoadService::RemoveTabContents(TabContents* tab_contents) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::IO)); + tabs_.erase(tabs_.find(tab_contents)); +} + +void OfflineLoadService::RegisterNotification( + NavigationController* navigation_controller) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + registrar_.Add(this, NotificationType::TAB_CLOSED, + Source<NavigationController>( + navigation_controller)); +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/offline/offline_load_service.h b/chrome/browser/chromeos/offline/offline_load_service.h new file mode 100644 index 0000000..119475e --- /dev/null +++ b/chrome/browser/chromeos/offline/offline_load_service.h @@ -0,0 +1,74 @@ +// Copyright (c) 2010 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. + +#ifndef CHROME_BROWSER_CHROMEOS_OFFLINE_OFFLINE_LOAD_SERVICE_H_ +#define CHROME_BROWSER_CHROMEOS_OFFLINE_OFFLINE_LOAD_SERVICE_H_ + +#include <tr1/unordered_set> + +#include "base/ref_counted.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_type.h" +#include "chrome/common/notification_registrar.h" + +class GURL; +class NavigationController; +class TabContents; +template<class A> class scoped_ptr; + +namespace chromeos { + +// OfflineLoadService decides whether or not the given url in the tab +// should be loaded when the network is not available. The current +// implementation simply memorize the tab that loads offline page. +// TODO(oshima): Improve the logic so that we can automatically load +// if there is valid cache content and/or it supports offline mode. +class OfflineLoadService + : public NotificationObserver, + public base::RefCountedThreadSafe<OfflineLoadService> { + public: + // Returns the singleton instance of the offline load service. + static OfflineLoadService* Get(); + + // Returns true if the tab should proceed with loading the page even if + // it's offline. + bool ShouldProceed(int process_id, int render_view_id, + const GURL& url) const; + + // Record that the user pressed "procced" button for + // the tab_contents. + void Proceeded(int proceed_id, int render_view_id, const GURL& url); + + // NotificationObserver implementation. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + private: + friend class base::RefCountedThreadSafe<OfflineLoadService>; + friend class scoped_ptr<OfflineLoadService>; + friend class OfflineLoadServiceSingleton; + + OfflineLoadService() {} + virtual ~OfflineLoadService() {} + + // A method invoked in UI thread to remove |tab_contents| from |tabs_|. + void RemoveTabContents(TabContents* tab_contents); + + // A method invoked in UI thread to register TAB_CLOSED + // notification. + void RegisterNotification( + NavigationController* navigation_controller); + + NotificationRegistrar registrar_; + + // Set of tabs that should proceed + std::tr1::unordered_set<const TabContents*> tabs_; + + DISALLOW_COPY_AND_ASSIGN(OfflineLoadService); +}; + +} // namespace chromeos + +#endif // CHROME_BROWSER_CHROMEOS_OFFLINE_OFFLINE_LOAD_SERVICE_H_ diff --git a/chrome/browser/renderer_host/offline_resource_handler.cc b/chrome/browser/renderer_host/offline_resource_handler.cc new file mode 100644 index 0000000..c270d14 --- /dev/null +++ b/chrome/browser/renderer_host/offline_resource_handler.cc @@ -0,0 +1,161 @@ +// Copyright (c) 2010 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 "chrome/browser/renderer_host/offline_resource_handler.h" + +#include <vector> + +#include "base/histogram.h" +#include "base/logging.h" +#include "base/singleton.h" +#include "base/string_util.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/chromeos/network_state_notifier.h" +#include "chrome/browser/chromeos/offline/offline_load_page.h" +#include "chrome/browser/chromeos/offline/offline_load_service.h" +#include "chrome/browser/renderer_host/resource_dispatcher_host.h" +#include "chrome/browser/renderer_host/resource_dispatcher_host_request_info.h" +#include "chrome/common/url_constants.h" + +OfflineResourceHandler::OfflineResourceHandler( + ResourceHandler* handler, + int host_id, + int route_id, + ResourceDispatcherHost* rdh, + URLRequest* request) + : next_handler_(handler), + process_host_id_(host_id), + render_view_id_(route_id), + rdh_(rdh), + request_(request), + deferred_request_id_(-1) { +} + +bool OfflineResourceHandler::OnUploadProgress(int request_id, + uint64 position, + uint64 size) { + return next_handler_->OnUploadProgress(request_id, position, size); +} + +bool OfflineResourceHandler::OnRequestRedirected(int request_id, + const GURL& new_url, + ResourceResponse* response, + bool* defer) { + return next_handler_->OnRequestRedirected( + request_id, new_url, response, defer); +} + +bool OfflineResourceHandler::OnResponseStarted(int request_id, + ResourceResponse* response) { + return next_handler_->OnResponseStarted(request_id, response); +} + +bool OfflineResourceHandler::OnResponseCompleted( + int request_id, + const URLRequestStatus& status, + const std::string& security_info) { + return next_handler_->OnResponseCompleted(request_id, status, security_info); +} + +void OfflineResourceHandler::OnRequestClosed() { + next_handler_->OnRequestClosed(); +} + +bool OfflineResourceHandler::OnWillStart(int request_id, + const GURL& url, + bool* defer) { + if (ShouldShowOfflinePage(url)) { + deferred_request_id_ = request_id; + deferred_url_ = url; + DLOG(INFO) << "WillStart: this=" << this << ", request id=" << request_id; + AddRef(); // Balanced with OnBlockingPageComplete + ChromeThread::PostTask( + ChromeThread::UI, FROM_HERE, + NewRunnableMethod(this, &OfflineResourceHandler::ShowOfflinePage)); + *defer = true; + return true; + } + return next_handler_->OnWillStart(request_id, url, defer); +} + +// We'll let the original event handler provide a buffer, and reuse it for +// subsequent reads until we're done buffering. +bool OfflineResourceHandler::OnWillRead(int request_id, net::IOBuffer** buf, + int* buf_size, int min_size) { + return next_handler_->OnWillRead(request_id, buf, buf_size, min_size); +} + +bool OfflineResourceHandler::OnReadCompleted(int request_id, int* bytes_read) { + return next_handler_->OnReadCompleted(request_id, bytes_read); +} + +void OfflineResourceHandler::OnBlockingPageComplete(bool proceed) { + if (deferred_request_id_ < 0) { + LOG(WARNING) << "OnBlockingPageComplete called after completion: " + << " this=" << this << ", request_id=" + << deferred_request_id_; + NOTREACHED(); + return; + } + if (!ChromeThread::CurrentlyOn(ChromeThread::IO)) { + ChromeThread::PostTask( + ChromeThread::IO, FROM_HERE, + NewRunnableMethod(this, + &OfflineResourceHandler::OnBlockingPageComplete, + proceed)); + return; + } + if (proceed) { + Resume(); + } else { + int request_id = deferred_request_id_; + ClearRequestInfo(); + rdh_->CancelRequest(process_host_id_, request_id, false); + } + Release(); // Balanced with OnWillStart +} + +void OfflineResourceHandler::ClearRequestInfo() { + deferred_url_ = GURL(); + deferred_request_id_ = -1; +} + +bool OfflineResourceHandler::IsRemote(const GURL& url) const { + return url.SchemeIs(chrome::kFtpScheme) || + url.SchemeIs(chrome::kHttpScheme) || + url.SchemeIs(chrome::kHttpsScheme); +} + +bool OfflineResourceHandler::ShouldShowOfflinePage(const GURL& url) const { + // Only check main frame. If the network is disconnected while + // loading other resources, we'll simply show broken link/images. + return IsRemote(url) && + !chromeos::NetworkStateNotifier::is_connected() && + ResourceDispatcherHost::InfoForRequest(request_)->resource_type() + == ResourceType::MAIN_FRAME && + !chromeos::OfflineLoadService::Get()->ShouldProceed( + process_host_id_, render_view_id_, url); +} + +void OfflineResourceHandler::Resume() { + const GURL url = deferred_url_; + int request_id = deferred_request_id_; + ClearRequestInfo(); + + chromeos::OfflineLoadService::Get()->Proceeded( + process_host_id_, render_view_id_, url); + + DCHECK_NE(request_id, -1); + bool defer = false; + DLOG(INFO) << "Resume load: this=" << this + << ", request id=" << request_id; + next_handler_->OnWillStart(request_id, url, &defer); + if (!defer) + rdh_->StartDeferredRequest(process_host_id_, request_id); +} + +void OfflineResourceHandler::ShowOfflinePage() { + chromeos::OfflineLoadPage::Show( + process_host_id_, render_view_id_, deferred_url_, this); +} diff --git a/chrome/browser/renderer_host/offline_resource_handler.h b/chrome/browser/renderer_host/offline_resource_handler.h new file mode 100644 index 0000000..567e3a6 --- /dev/null +++ b/chrome/browser/renderer_host/offline_resource_handler.h @@ -0,0 +1,74 @@ +// Copyright (c) 2010 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. + +#ifndef CHROME_BROWSER_RENDERER_HOST_OFFLINE_RESOURCE_HANDLER_H_ +#define CHROME_BROWSER_RENDERER_HOST_OFFLINE_RESOURCE_HANDLER_H_ + +#include <string> + +#include "base/ref_counted.h" +#include "chrome/browser/chromeos/offline/offline_load_page.h" +#include "chrome/browser/renderer_host/resource_handler.h" + +class MessageLoop; +class ResourceDispatcherHost; +class URLRequest; + +// Used to show an offline interstitial page when the network is not available. +class OfflineResourceHandler : public ResourceHandler, + public chromeos::OfflineLoadPage::Delegate { + public: + OfflineResourceHandler(ResourceHandler* handler, + int host_id, + int render_view_id, + ResourceDispatcherHost* rdh, + URLRequest* request); + ~OfflineResourceHandler() {} + + // ResourceHandler implementation: + virtual bool OnUploadProgress(int request_id, uint64 position, uint64 size); + virtual bool OnRequestRedirected(int request_id, const GURL& new_url, + ResourceResponse* response, bool* defer); + virtual bool OnResponseStarted(int request_id, ResourceResponse* response); + virtual bool OnWillStart(int request_id, const GURL& url, bool* defer); + virtual bool OnWillRead(int request_id, net::IOBuffer** buf, int* buf_size, + int min_size); + virtual bool OnReadCompleted(int request_id, int* bytes_read); + virtual bool OnResponseCompleted(int request_id, + const URLRequestStatus& status, + const std::string& security_info); + virtual void OnRequestClosed(); + + // chromeos::OfflineLoadPage::Delegate + virtual void OnBlockingPageComplete(bool proceed); + + private: + // Erease the state assocaited with a deferred load request. + void ClearRequestInfo(); + bool IsRemote(const GURL& url) const; + + // Resume the deferred load request. + void Resume(); + + // Tells if chrome should show the offline page. + bool ShouldShowOfflinePage(const GURL& url) const; + + // Shows the offline interstitinal page in UI thread. + void ShowOfflinePage(); + + scoped_refptr<ResourceHandler> next_handler_; + + int process_host_id_; + int render_view_id_; + ResourceDispatcherHost* rdh_; + URLRequest* request_; + + // The state for deferred load quest. + int deferred_request_id_; + GURL deferred_url_; + + DISALLOW_COPY_AND_ASSIGN(OfflineResourceHandler); +}; + +#endif // CHROME_BROWSER_RENDERER_HOST_OFFLINE_RESOURCE_HANDLER_H_ diff --git a/chrome/browser/renderer_host/resource_dispatcher_host.cc b/chrome/browser/renderer_host/resource_dispatcher_host.cc index 3f093ae..130997ac 100644 --- a/chrome/browser/renderer_host/resource_dispatcher_host.cc +++ b/chrome/browser/renderer_host/resource_dispatcher_host.cc @@ -64,6 +64,11 @@ #include "webkit/appcache/appcache_interceptor.h" #include "webkit/appcache/appcache_interfaces.h" +// TODO(oshima): Enable this for other platforms. +#if defined(OS_CHROMEOS) +#include "chrome/browser/renderer_host/offline_resource_handler.h" +#endif + // Uncomment to enable logging of request traffic. // #define LOG_RESOURCE_DISPATCHER_REQUESTS @@ -447,6 +452,13 @@ void ResourceDispatcherHost::BeginRequest( request_data.resource_type); } +#if defined(OS_CHROMEOS) + // We check offline first, then check safe browsing so that we still can block + // unsafe site after we remove offline page. + handler = + new OfflineResourceHandler(handler, child_id, route_id, this, request); +#endif + // Make extra info and read footer (contains request ID). ResourceDispatcherHostRequestInfo* extra_info = new ResourceDispatcherHostRequestInfo( diff --git a/chrome/browser/renderer_host/resource_queue.cc b/chrome/browser/renderer_host/resource_queue.cc index dca6bd2..73e96a2 100644 --- a/chrome/browser/renderer_host/resource_queue.cc +++ b/chrome/browser/renderer_host/resource_queue.cc @@ -45,7 +45,9 @@ void ResourceQueue::AddRequest( GlobalRequestID request_id(request_info.child_id(), request_info.request_id()); - DCHECK(!ContainsKey(requests_, request_id)); + DCHECK(!ContainsKey(requests_, request_id)) + << "child_id:" << request_info.child_id() + << ", request_id:" << request_info.request_id(); requests_[request_id] = request; DelegateSet interested_delegates; diff --git a/chrome/browser/resources/offline_load.html b/chrome/browser/resources/offline_load.html new file mode 100644 index 0000000..b2042a3 --- /dev/null +++ b/chrome/browser/resources/offline_load.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html id="template_root" i18n-values="dir:textdirection"> +<head> +<title i18n-content="title"></title> +<style> +body { + background-color:#E0E0E0; + font-family:Helvetica,Arial,sans-serif; + margin:0px; + visibility: hidden; +} +.background { + position:absolute; + width:100%; + height:100%; +} +.cell { + padding:40px; +} +.box { + width:80%; + background-color:white; + color:black; + font-size:10pt; + line-height:16pt; + text-align:left; + padding:20px; + position:relative; + -webkit-box-shadow:3px 3px 8px #200; + border-radius:5px; +} +html[dir='rtl'] .box { + text-align:right; +} + +.icon { + position:absolute; +} +.title { + margin:0px 87px 0px; + font-size:18pt; + line-height: 140%; + margin-bottom:6pt; + font-weight:bold; + color:#660000; +} +.main { + margin:0px 90px 0px; +} +.submission { + margin:15px 5px 15px 0px; + padding:0px; +} +input { + margin:0px; +} + +.thumbnail { + background-repeat: no-repeat; + background-position: center center; + width:400px; + height:250px; + padding:20px; + position:relative; + -webkit-box-shadow:3px 3px 8px #200; + border-radius:5px; + visibility: hidden; +} + +} +</style> + +<script> +function sendCommand(cmd) { + window.domAutomationController.setAutomationId(1); + window.domAutomationController.send(cmd); +} +// Show the offline page. +function showPage() { + document.body.style.visibility = 'visible'; +} +// Start the timer to show the page. +function startTimer(time) { + // wait 2.5 seconds before showing 'load now', 'go back' button. + setTimeout('showPage()', time); +} +</script> +</head> +<body oncontextmenu="return false;" i18n-values="onload:on_load"> +<table width="100%" cellspacing="0" cellpadding="0"> + <td class="cell" valign="middle" align="center"> + <div class="box" id="box"> + <div class="title" i18n-content="headLine"></div> + <div class="main" i18n-values=".innerHTML:description"></div> + <div class="main"> + <form class="submission"> + <input type="button" name="continue_button" i18n-values="value:load_button" onclick="sendCommand('proceed')"><br> + <input type="button" name="back_button" i18n-values="value:back_button;.style.display:display_go_back" onclick="sendCommand('dontproceed')"> + </form> + </div> + </div> + <div id="thumbnail" class="thumbnail" i18n-values=".style.background-image:thumbnailUrl"> + </div> + </td> +</table> +</body> +</html> diff --git a/chrome/browser/sessions/session_restore.cc b/chrome/browser/sessions/session_restore.cc index 9a28c71..6ec5a66 100644 --- a/chrome/browser/sessions/session_restore.cc +++ b/chrome/browser/sessions/session_restore.cc @@ -26,6 +26,10 @@ #include "chrome/common/notification_registrar.h" #include "chrome/common/notification_service.h" +#if defined(OS_CHROMEOS) +#include "chrome/browser/chromeos/network_state_notifier.h" +#endif + // Are we in the process of restoring? static bool restoring = false; @@ -127,17 +131,29 @@ void TabLoader::ScheduleLoad(NavigationController* controller) { } void TabLoader::StartLoading() { +#if defined(OS_CHROMEOS) + if (chromeos::NetworkStateNotifier::is_connected()) { + loading_ = true; + LoadNextTab(); + } else { + // Start listening to network state notification now. + registrar_.Add(this, NotificationType::NETWORK_STATE_CHANGED, + NotificationService::AllSources()); + } +#else loading_ = true; LoadNextTab(); +#endif } void TabLoader::LoadNextTab() { if (!tabs_to_load_.empty()) { NavigationController* tab = tabs_to_load_.front(); + DCHECK(tab); tabs_loading_.insert(tab); tabs_to_load_.pop_front(); tab->LoadIfNecessary(); - if (tab && tab->tab_contents()) { + if (tab->tab_contents()) { int tab_index; Browser* browser = Browser::GetBrowserForController(tab, &tab_index); if (browser && browser->selected_index() != tab_index) { @@ -169,13 +185,50 @@ void TabLoader::LoadNextTab() { void TabLoader::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { - DCHECK(type == NotificationType::TAB_CLOSED || - type == NotificationType::LOAD_STOP); - NavigationController* tab = Source<NavigationController>(source).ptr(); - RemoveTab(tab); - if (loading_) { - LoadNextTab(); - // WARNING: if there are no more tabs to load, we have been deleted. + switch (type.value) { +#if defined(OS_CHROMEOS) + case NotificationType::NETWORK_STATE_CHANGED: { + chromeos::NetworkStateDetails* state_details = + Details<chromeos::NetworkStateDetails>(details).ptr(); + switch (state_details->state()) { + case chromeos::NetworkStateDetails::CONNECTED: + if (!loading_) { + loading_ = true; + LoadNextTab(); + } + // start loading + break; + case chromeos::NetworkStateDetails::CONNECTING: + // keep it going + break; + case chromeos::NetworkStateDetails::DISCONNECTED: + // disconnected while loading. set loaing_ false so + // that it stops trying to load next tab. + loading_ = false; + break; + default: + NOTREACHED() << "Unknown nework state notification:" + << state_details->state(); + } + break; + } +#endif + case NotificationType::TAB_CLOSED: + case NotificationType::LOAD_STOP: { + NavigationController* tab = Source<NavigationController>(source).ptr(); + RemoveTab(tab); + if (loading_) { + LoadNextTab(); + // WARNING: if there are no more tabs to load, we have been deleted. + } else if (tabs_to_load_.empty()) { + tabs_loading_.clear(); + delete this; + return; + } + break; + } + default: + NOTREACHED() << "Unknown notification received:" << type.value; } } diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 441d14f..c96158d 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -517,6 +517,10 @@ 'browser/chromeos/network_state_notifier.h', 'browser/chromeos/network_message_observer.cc', 'browser/chromeos/network_message_observer.h', + 'browser/chromeos/offline/offline_load_page.cc', + 'browser/chromeos/offline/offline_load_page.h', + 'browser/chromeos/offline/offline_load_service.cc', + 'browser/chromeos/offline/offline_load_service.h', 'browser/chromeos/options/internet_page_view.cc', 'browser/chromeos/options/internet_page_view.h', 'browser/chromeos/options/ip_config_view.cc', @@ -2058,6 +2062,8 @@ 'browser/renderer_host/gtk_im_context_wrapper.h', 'browser/renderer_host/gtk_key_bindings_handler.cc', 'browser/renderer_host/gtk_key_bindings_handler.h', + 'browser/renderer_host/offline_resource_handler.cc', + 'browser/renderer_host/offline_resource_handler.h', 'browser/renderer_host/render_process_host.cc', 'browser/renderer_host/render_process_host.h', 'browser/renderer_host/render_sandbox_host_linux.h', @@ -2800,6 +2806,8 @@ ['exclude', 'browser/dom_ui/filebrowse_ui.cc'], ['exclude', 'browser/dom_ui/mediaplayer_ui.cc'], ['exclude', 'browser/dom_ui/slideshow_ui.cc'], + ['exclude', 'browser/renderer_host/offline_resource_handler.cc'], + ['exclude', 'browser/renderer_host/offline_resource_handler.h'], ], }], ['chromeos==1', { diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index ea886ca..c36ad23 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -613,6 +613,7 @@ 'browser/chromeos/login/google_authenticator_unittest.cc', 'browser/chromeos/login/mock_auth_response_handler.cc', 'browser/chromeos/notifications/desktop_notifications_unittest.cc', + 'browser/chromeos/offline/offline_load_page_unittest.cc', 'browser/chromeos/options/language_config_model_unittest.cc', 'browser/chromeos/pipe_reader_unittest.cc', 'browser/chromeos/status/language_menu_button_unittest.cc', |