1 // Copyright 2013 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/chromeos/first_run/drive_first_run_controller.h"
8 #include "ash/system/tray/system_tray_delegate.h"
9 #include "base/callback.h"
10 #include "base/memory/weak_ptr.h"
11 #include "base/message_loop/message_loop.h"
12 #include "base/metrics/histogram.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/background/background_contents.h"
15 #include "chrome/browser/background/background_contents_service.h"
16 #include "chrome/browser/background/background_contents_service_factory.h"
17 #include "chrome/browser/chrome_notification_types.h"
18 #include "chrome/browser/extensions/chrome_extension_web_contents_observer.h"
19 #include "chrome/browser/extensions/extension_service.h"
20 #include "chrome/browser/ui/browser_navigator.h"
21 #include "chrome/browser/ui/host_desktop.h"
22 #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
23 #include "chrome/browser/ui/singleton_tabs.h"
24 #include "chrome/grit/generated_resources.h"
25 #include "chrome/grit/theme_resources.h"
26 #include "components/user_manager/user_manager.h"
27 #include "content/public/browser/browser_thread.h"
28 #include "content/public/browser/navigation_controller.h"
29 #include "content/public/browser/notification_details.h"
30 #include "content/public/browser/notification_observer.h"
31 #include "content/public/browser/notification_registrar.h"
32 #include "content/public/browser/notification_source.h"
33 #include "content/public/browser/notification_types.h"
34 #include "content/public/browser/render_frame_host.h"
35 #include "content/public/browser/site_instance.h"
36 #include "content/public/browser/web_contents.h"
37 #include "content/public/browser/web_contents_observer.h"
38 #include "extensions/browser/extension_registry.h"
39 #include "extensions/browser/extension_system.h"
40 #include "extensions/common/extension.h"
41 #include "extensions/common/extension_set.h"
42 #include "ui/base/l10n/l10n_util.h"
43 #include "ui/base/resource/resource_bundle.h"
44 #include "ui/message_center/message_center.h"
45 #include "ui/message_center/notification.h"
46 #include "ui/message_center/notification_delegate.h"
53 // The initial time to wait in seconds before enabling offline mode.
54 int kInitialDelaySeconds
= 180;
56 // Time to wait for Drive app background page to come up before giving up.
57 int kWebContentsTimeoutSeconds
= 15;
59 // Google Drive enable offline endpoint.
60 const char kDriveOfflineEndpointUrl
[] =
61 "https://docs.google.com/offline/autoenable";
63 // Google Drive app id.
64 const char kDriveHostedAppId
[] = "apdfllckaahabafndbhieahigkjlhalf";
66 // Id of the notification shown when offline mode is enabled.
67 const char kDriveOfflineNotificationId
[] = "chrome://drive/enable-offline";
69 // The URL of the support page opened when the notification button is clicked.
70 const char kDriveOfflineSupportUrl
[] =
71 "https://support.google.com/drive/answer/1628467";
75 ////////////////////////////////////////////////////////////////////////////////
76 // DriveOfflineNotificationDelegate
78 // NotificationDelegate for the notification that is displayed when Drive
79 // offline mode is enabled automatically. Clicking on the notification button
80 // will open the Drive settings page.
81 class DriveOfflineNotificationDelegate
82 : public message_center::NotificationDelegate
{
84 explicit DriveOfflineNotificationDelegate(Profile
* profile
)
85 : profile_(profile
) {}
87 // message_center::NotificationDelegate overrides:
88 void ButtonClick(int button_index
) override
;
91 ~DriveOfflineNotificationDelegate() override
{}
96 DISALLOW_COPY_AND_ASSIGN(DriveOfflineNotificationDelegate
);
99 void DriveOfflineNotificationDelegate::ButtonClick(int button_index
) {
100 DCHECK_EQ(0, button_index
);
102 // The support page will be localized based on the user's GAIA account.
103 const GURL url
= GURL(kDriveOfflineSupportUrl
);
105 chrome::ScopedTabbedBrowserDisplayer
displayer(
107 chrome::HOST_DESKTOP_TYPE_ASH
);
108 chrome::ShowSingletonTabOverwritingNTP(
110 chrome::GetSingletonTabNavigateParams(displayer
.browser(), url
));
113 ////////////////////////////////////////////////////////////////////////////////
114 // DriveWebContentsManager
116 // Manages web contents that initializes Google Drive offline mode. We create
117 // a background WebContents that loads a Drive endpoint to initialize offline
118 // mode. If successful, a background page will be opened to sync the user's
119 // files for offline use.
120 class DriveWebContentsManager
: public content::WebContentsObserver
,
121 public content::WebContentsDelegate
,
122 public content::NotificationObserver
{
124 typedef base::Callback
<
125 void(bool, DriveFirstRunController::UMAOutcome
)> CompletionCallback
;
127 DriveWebContentsManager(Profile
* profile
,
128 const std::string
& app_id
,
129 const std::string
& endpoint_url
,
130 const CompletionCallback
& completion_callback
);
131 ~DriveWebContentsManager() override
;
133 // Start loading the WebContents for the endpoint in the context of the Drive
134 // hosted app that will initialize offline mode and open a background page.
137 // Stop loading the endpoint. The |completion_callback| will not be called.
141 // Called when when offline initialization succeeds or fails and schedules
142 // |RunCompletionCallback|.
143 void OnOfflineInit(bool success
,
144 DriveFirstRunController::UMAOutcome outcome
);
146 // Runs |completion_callback|.
147 void RunCompletionCallback(bool success
,
148 DriveFirstRunController::UMAOutcome outcome
);
150 // content::WebContentsObserver overrides:
151 void DidFailProvisionalLoad(content::RenderFrameHost
* render_frame_host
,
152 const GURL
& validated_url
,
154 const base::string16
& error_description
,
155 bool was_ignored_by_handler
) override
;
157 void DidFailLoad(content::RenderFrameHost
* render_frame_host
,
158 const GURL
& validated_url
,
160 const base::string16
& error_description
,
161 bool was_ignored_by_handler
) override
;
163 // content::WebContentsDelegate overrides:
164 bool ShouldCreateWebContents(
165 content::WebContents
* web_contents
,
167 int main_frame_route_id
,
168 WindowContainerType window_container_type
,
169 const std::string
& frame_name
,
170 const GURL
& target_url
,
171 const std::string
& partition_id
,
172 content::SessionStorageNamespace
* session_storage_namespace
) override
;
174 // content::NotificationObserver overrides:
175 void Observe(int type
,
176 const content::NotificationSource
& source
,
177 const content::NotificationDetails
& details
) override
;
180 const std::string app_id_
;
181 const std::string endpoint_url_
;
182 scoped_ptr
<content::WebContents
> web_contents_
;
183 content::NotificationRegistrar registrar_
;
185 CompletionCallback completion_callback_
;
186 base::WeakPtrFactory
<DriveWebContentsManager
> weak_ptr_factory_
;
188 DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager
);
191 DriveWebContentsManager::DriveWebContentsManager(
193 const std::string
& app_id
,
194 const std::string
& endpoint_url
,
195 const CompletionCallback
& completion_callback
)
198 endpoint_url_(endpoint_url
),
200 completion_callback_(completion_callback
),
201 weak_ptr_factory_(this) {
202 DCHECK(!completion_callback_
.is_null());
203 registrar_
.Add(this, chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED
,
204 content::Source
<Profile
>(profile_
));
207 DriveWebContentsManager::~DriveWebContentsManager() {
210 void DriveWebContentsManager::StartLoad() {
212 const GURL
url(endpoint_url_
);
213 content::WebContents::CreateParams
create_params(
214 profile_
, content::SiteInstance::CreateForURL(profile_
, url
));
216 web_contents_
.reset(content::WebContents::Create(create_params
));
217 web_contents_
->SetDelegate(this);
218 extensions::ChromeExtensionWebContentsObserver::CreateForWebContents(
219 web_contents_
.get());
221 content::NavigationController::LoadURLParams
load_params(url
);
222 load_params
.transition_type
= ui::PAGE_TRANSITION_GENERATED
;
223 web_contents_
->GetController().LoadURLWithParams(load_params
);
225 content::WebContentsObserver::Observe(web_contents_
.get());
228 void DriveWebContentsManager::StopLoad() {
232 void DriveWebContentsManager::OnOfflineInit(
234 DriveFirstRunController::UMAOutcome outcome
) {
236 // We postpone notifying the controller as we may be in the middle
237 // of a call stack for some routine of the contained WebContents.
238 base::MessageLoop::current()->PostTask(
240 base::Bind(&DriveWebContentsManager::RunCompletionCallback
,
241 weak_ptr_factory_
.GetWeakPtr(),
248 void DriveWebContentsManager::RunCompletionCallback(
250 DriveFirstRunController::UMAOutcome outcome
) {
251 completion_callback_
.Run(success
, outcome
);
254 void DriveWebContentsManager::DidFailProvisionalLoad(
255 content::RenderFrameHost
* render_frame_host
,
256 const GURL
& validated_url
,
258 const base::string16
& error_description
,
259 bool was_ignored_by_handler
) {
260 if (!render_frame_host
->GetParent()) {
261 LOG(WARNING
) << "Failed to load WebContents to enable offline mode.";
263 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED
);
267 void DriveWebContentsManager::DidFailLoad(
268 content::RenderFrameHost
* render_frame_host
,
269 const GURL
& validated_url
,
271 const base::string16
& error_description
,
272 bool was_ignored_by_handler
) {
273 if (!render_frame_host
->GetParent()) {
274 LOG(WARNING
) << "Failed to load WebContents to enable offline mode.";
276 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED
);
280 bool DriveWebContentsManager::ShouldCreateWebContents(
281 content::WebContents
* web_contents
,
283 int main_frame_route_id
,
284 WindowContainerType window_container_type
,
285 const std::string
& frame_name
,
286 const GURL
& target_url
,
287 const std::string
& partition_id
,
288 content::SessionStorageNamespace
* session_storage_namespace
) {
289 if (window_container_type
== WINDOW_CONTAINER_TYPE_NORMAL
)
292 // Check that the target URL is for the Drive app.
293 const extensions::Extension
* extension
=
294 extensions::ExtensionRegistry::Get(profile_
)
295 ->enabled_extensions().GetAppByURL(target_url
);
296 if (!extension
|| extension
->id() != app_id_
)
299 // The background contents creation is normally done in Browser, but
300 // because we're using a detached WebContents, we need to do it ourselves.
301 BackgroundContentsService
* background_contents_service
=
302 BackgroundContentsServiceFactory::GetForProfile(profile_
);
304 // Prevent redirection if background contents already exists.
305 if (background_contents_service
->GetAppBackgroundContents(
306 base::UTF8ToUTF16(app_id_
))) {
309 BackgroundContents
* contents
= background_contents_service
310 ->CreateBackgroundContents(content::SiteInstance::Create(profile_
),
315 base::ASCIIToUTF16(app_id_
),
317 session_storage_namespace
);
319 contents
->web_contents()->GetController().LoadURL(
322 ui::PAGE_TRANSITION_LINK
,
325 // Return false as we already created the WebContents here.
329 void DriveWebContentsManager::Observe(
331 const content::NotificationSource
& source
,
332 const content::NotificationDetails
& details
) {
333 if (type
== chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED
) {
334 const std::string app_id
= base::UTF16ToUTF8(
335 content::Details
<BackgroundContentsOpenedDetails
>(details
)
337 if (app_id
== app_id_
)
338 OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED
);
342 ////////////////////////////////////////////////////////////////////////////////
343 // DriveFirstRunController
345 DriveFirstRunController::DriveFirstRunController(Profile
* profile
)
348 initial_delay_secs_(kInitialDelaySeconds
),
349 web_contents_timeout_secs_(kWebContentsTimeoutSeconds
),
350 drive_offline_endpoint_url_(kDriveOfflineEndpointUrl
),
351 drive_hosted_app_id_(kDriveHostedAppId
) {
354 DriveFirstRunController::~DriveFirstRunController() {
357 void DriveFirstRunController::EnableOfflineMode() {
360 initial_delay_timer_
.Start(
362 base::TimeDelta::FromSeconds(initial_delay_secs_
),
364 &DriveFirstRunController::EnableOfflineMode
);
368 if (!user_manager::UserManager::Get()->IsLoggedInAsUserWithGaiaAccount()) {
369 LOG(ERROR
) << "Attempting to enable offline access "
370 "but not logged in a regular user.";
371 OnOfflineInit(false, OUTCOME_WRONG_USER_TYPE
);
375 ExtensionService
* extension_service
=
376 extensions::ExtensionSystem::Get(profile_
)->extension_service();
377 if (!extension_service
->GetExtensionById(drive_hosted_app_id_
, false)) {
378 LOG(WARNING
) << "Drive app is not installed.";
379 OnOfflineInit(false, OUTCOME_APP_NOT_INSTALLED
);
383 BackgroundContentsService
* background_contents_service
=
384 BackgroundContentsServiceFactory::GetForProfile(profile_
);
385 if (background_contents_service
->GetAppBackgroundContents(
386 base::UTF8ToUTF16(drive_hosted_app_id_
))) {
387 LOG(WARNING
) << "Background page for Drive app already exists";
388 OnOfflineInit(false, OUTCOME_BACKGROUND_PAGE_EXISTS
);
392 web_contents_manager_
.reset(new DriveWebContentsManager(
394 drive_hosted_app_id_
,
395 drive_offline_endpoint_url_
,
396 base::Bind(&DriveFirstRunController::OnOfflineInit
,
397 base::Unretained(this))));
398 web_contents_manager_
->StartLoad();
399 web_contents_timer_
.Start(
401 base::TimeDelta::FromSeconds(web_contents_timeout_secs_
),
403 &DriveFirstRunController::OnWebContentsTimedOut
);
406 void DriveFirstRunController::AddObserver(Observer
* observer
) {
407 observer_list_
.AddObserver(observer
);
410 void DriveFirstRunController::RemoveObserver(Observer
* observer
) {
411 observer_list_
.RemoveObserver(observer
);
414 void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs
,
417 initial_delay_secs_
= initial_delay_secs
;
418 web_contents_timeout_secs_
= timeout_secs
;
421 void DriveFirstRunController::SetAppInfoForTest(
422 const std::string
& app_id
,
423 const std::string
& endpoint_url
) {
425 drive_hosted_app_id_
= app_id
;
426 drive_offline_endpoint_url_
= endpoint_url
;
429 void DriveFirstRunController::OnWebContentsTimedOut() {
430 LOG(WARNING
) << "Timed out waiting for web contents.";
431 FOR_EACH_OBSERVER(Observer
, observer_list_
, OnTimedOut());
432 OnOfflineInit(false, OUTCOME_WEB_CONTENTS_TIMED_OUT
);
435 void DriveFirstRunController::CleanUp() {
436 if (web_contents_manager_
)
437 web_contents_manager_
->StopLoad();
438 web_contents_timer_
.Stop();
439 base::MessageLoop::current()->DeleteSoon(FROM_HERE
, this);
442 void DriveFirstRunController::OnOfflineInit(bool success
, UMAOutcome outcome
) {
443 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
446 UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
447 outcome
, OUTCOME_MAX
);
448 FOR_EACH_OBSERVER(Observer
, observer_list_
, OnCompletion(success
));
452 void DriveFirstRunController::ShowNotification() {
453 ExtensionService
* service
=
454 extensions::ExtensionSystem::Get(profile_
)->extension_service();
456 const extensions::Extension
* extension
=
457 service
->GetExtensionById(drive_hosted_app_id_
, false);
460 message_center::RichNotificationData data
;
461 data
.buttons
.push_back(message_center::ButtonInfo(
462 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_BUTTON
)));
463 ui::ResourceBundle
& resource_bundle
= ui::ResourceBundle::GetSharedInstance();
464 scoped_ptr
<message_center::Notification
> notification(
465 new message_center::Notification(
466 message_center::NOTIFICATION_TYPE_SIMPLE
, kDriveOfflineNotificationId
,
467 base::string16(), // title
468 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_MESSAGE
),
469 resource_bundle
.GetImageNamed(IDR_NOTIFICATION_DRIVE
),
470 base::UTF8ToUTF16(extension
->name()), GURL(),
471 message_center::NotifierId(message_center::NotifierId::APPLICATION
,
473 data
, new DriveOfflineNotificationDelegate(profile_
)));
474 notification
->set_priority(message_center::LOW_PRIORITY
);
475 message_center::MessageCenter::Get()->AddNotification(notification
.Pass());
478 } // namespace chromeos