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_service.h"
15 #include "chrome/browser/background/background_contents_service_factory.h"
16 #include "chrome/browser/chrome_notification_types.h"
17 #include "chrome/browser/extensions/chrome_extension_web_contents_observer.h"
18 #include "chrome/browser/extensions/extension_service.h"
19 #include "chrome/browser/tab_contents/background_contents.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 virtual void Display() override
{}
89 virtual void Error() override
{}
90 virtual void Close(bool by_user
) override
{}
91 virtual void Click() override
{}
92 virtual void ButtonClick(int button_index
) override
;
95 virtual ~DriveOfflineNotificationDelegate() {}
100 DISALLOW_COPY_AND_ASSIGN(DriveOfflineNotificationDelegate
);
103 void DriveOfflineNotificationDelegate::ButtonClick(int button_index
) {
104 DCHECK_EQ(0, button_index
);
106 // The support page will be localized based on the user's GAIA account.
107 const GURL url
= GURL(kDriveOfflineSupportUrl
);
109 chrome::ScopedTabbedBrowserDisplayer
displayer(
111 chrome::HOST_DESKTOP_TYPE_ASH
);
112 chrome::ShowSingletonTabOverwritingNTP(
114 chrome::GetSingletonTabNavigateParams(displayer
.browser(), url
));
117 ////////////////////////////////////////////////////////////////////////////////
118 // DriveWebContentsManager
120 // Manages web contents that initializes Google Drive offline mode. We create
121 // a background WebContents that loads a Drive endpoint to initialize offline
122 // mode. If successful, a background page will be opened to sync the user's
123 // files for offline use.
124 class DriveWebContentsManager
: public content::WebContentsObserver
,
125 public content::WebContentsDelegate
,
126 public content::NotificationObserver
{
128 typedef base::Callback
<
129 void(bool, DriveFirstRunController::UMAOutcome
)> CompletionCallback
;
131 DriveWebContentsManager(Profile
* profile
,
132 const std::string
& app_id
,
133 const std::string
& endpoint_url
,
134 const CompletionCallback
& completion_callback
);
135 virtual ~DriveWebContentsManager();
137 // Start loading the WebContents for the endpoint in the context of the Drive
138 // hosted app that will initialize offline mode and open a background page.
141 // Stop loading the endpoint. The |completion_callback| will not be called.
145 // Called when when offline initialization succeeds or fails and schedules
146 // |RunCompletionCallback|.
147 void OnOfflineInit(bool success
,
148 DriveFirstRunController::UMAOutcome outcome
);
150 // Runs |completion_callback|.
151 void RunCompletionCallback(bool success
,
152 DriveFirstRunController::UMAOutcome outcome
);
154 // content::WebContentsObserver overrides:
155 virtual void DidFailProvisionalLoad(
156 content::RenderFrameHost
* render_frame_host
,
157 const GURL
& validated_url
,
159 const base::string16
& error_description
) override
;
161 virtual void DidFailLoad(content::RenderFrameHost
* render_frame_host
,
162 const GURL
& validated_url
,
164 const base::string16
& error_description
) override
;
166 // content::WebContentsDelegate overrides:
167 virtual bool ShouldCreateWebContents(
168 content::WebContents
* web_contents
,
170 WindowContainerType window_container_type
,
171 const base::string16
& frame_name
,
172 const GURL
& target_url
,
173 const std::string
& partition_id
,
174 content::SessionStorageNamespace
* session_storage_namespace
) override
;
176 // content::NotificationObserver overrides:
177 virtual void Observe(int type
,
178 const content::NotificationSource
& source
,
179 const content::NotificationDetails
& details
) override
;
182 const std::string app_id_
;
183 const std::string endpoint_url_
;
184 scoped_ptr
<content::WebContents
> web_contents_
;
185 content::NotificationRegistrar registrar_
;
187 CompletionCallback completion_callback_
;
188 base::WeakPtrFactory
<DriveWebContentsManager
> weak_ptr_factory_
;
190 DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager
);
193 DriveWebContentsManager::DriveWebContentsManager(
195 const std::string
& app_id
,
196 const std::string
& endpoint_url
,
197 const CompletionCallback
& completion_callback
)
200 endpoint_url_(endpoint_url
),
202 completion_callback_(completion_callback
),
203 weak_ptr_factory_(this) {
204 DCHECK(!completion_callback_
.is_null());
205 registrar_
.Add(this, chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED
,
206 content::Source
<Profile
>(profile_
));
209 DriveWebContentsManager::~DriveWebContentsManager() {
212 void DriveWebContentsManager::StartLoad() {
214 const GURL
url(endpoint_url_
);
215 content::WebContents::CreateParams
create_params(
216 profile_
, content::SiteInstance::CreateForURL(profile_
, url
));
218 web_contents_
.reset(content::WebContents::Create(create_params
));
219 web_contents_
->SetDelegate(this);
220 extensions::ChromeExtensionWebContentsObserver::CreateForWebContents(
221 web_contents_
.get());
223 content::NavigationController::LoadURLParams
load_params(url
);
224 load_params
.transition_type
= ui::PAGE_TRANSITION_GENERATED
;
225 web_contents_
->GetController().LoadURLWithParams(load_params
);
227 content::WebContentsObserver::Observe(web_contents_
.get());
230 void DriveWebContentsManager::StopLoad() {
234 void DriveWebContentsManager::OnOfflineInit(
236 DriveFirstRunController::UMAOutcome outcome
) {
238 // We postpone notifying the controller as we may be in the middle
239 // of a call stack for some routine of the contained WebContents.
240 base::MessageLoop::current()->PostTask(
242 base::Bind(&DriveWebContentsManager::RunCompletionCallback
,
243 weak_ptr_factory_
.GetWeakPtr(),
250 void DriveWebContentsManager::RunCompletionCallback(
252 DriveFirstRunController::UMAOutcome outcome
) {
253 completion_callback_
.Run(success
, outcome
);
256 void DriveWebContentsManager::DidFailProvisionalLoad(
257 content::RenderFrameHost
* render_frame_host
,
258 const GURL
& validated_url
,
260 const base::string16
& error_description
) {
261 if (!render_frame_host
->GetParent()) {
262 LOG(WARNING
) << "Failed to load WebContents to enable offline mode.";
264 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED
);
268 void DriveWebContentsManager::DidFailLoad(
269 content::RenderFrameHost
* render_frame_host
,
270 const GURL
& validated_url
,
272 const base::string16
& error_description
) {
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 WindowContainerType window_container_type
,
284 const base::string16
& frame_name
,
285 const GURL
& target_url
,
286 const std::string
& partition_id
,
287 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_
),
314 base::ASCIIToUTF16(app_id_
),
316 session_storage_namespace
);
318 contents
->web_contents()->GetController().LoadURL(
321 ui::PAGE_TRANSITION_LINK
,
324 // Return false as we already created the WebContents here.
328 void DriveWebContentsManager::Observe(
330 const content::NotificationSource
& source
,
331 const content::NotificationDetails
& details
) {
332 if (type
== chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED
) {
333 const std::string app_id
= base::UTF16ToUTF8(
334 content::Details
<BackgroundContentsOpenedDetails
>(details
)
336 if (app_id
== app_id_
)
337 OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED
);
341 ////////////////////////////////////////////////////////////////////////////////
342 // DriveFirstRunController
344 DriveFirstRunController::DriveFirstRunController(Profile
* profile
)
347 initial_delay_secs_(kInitialDelaySeconds
),
348 web_contents_timeout_secs_(kWebContentsTimeoutSeconds
),
349 drive_offline_endpoint_url_(kDriveOfflineEndpointUrl
),
350 drive_hosted_app_id_(kDriveHostedAppId
) {
353 DriveFirstRunController::~DriveFirstRunController() {
356 void DriveFirstRunController::EnableOfflineMode() {
359 initial_delay_timer_
.Start(
361 base::TimeDelta::FromSeconds(initial_delay_secs_
),
363 &DriveFirstRunController::EnableOfflineMode
);
367 if (!user_manager::UserManager::Get()->IsLoggedInAsRegularUser()) {
368 LOG(ERROR
) << "Attempting to enable offline access "
369 "but not logged in a regular user.";
370 OnOfflineInit(false, OUTCOME_WRONG_USER_TYPE
);
374 ExtensionService
* extension_service
=
375 extensions::ExtensionSystem::Get(profile_
)->extension_service();
376 if (!extension_service
->GetExtensionById(drive_hosted_app_id_
, false)) {
377 LOG(WARNING
) << "Drive app is not installed.";
378 OnOfflineInit(false, OUTCOME_APP_NOT_INSTALLED
);
382 BackgroundContentsService
* background_contents_service
=
383 BackgroundContentsServiceFactory::GetForProfile(profile_
);
384 if (background_contents_service
->GetAppBackgroundContents(
385 base::UTF8ToUTF16(drive_hosted_app_id_
))) {
386 LOG(WARNING
) << "Background page for Drive app already exists";
387 OnOfflineInit(false, OUTCOME_BACKGROUND_PAGE_EXISTS
);
391 web_contents_manager_
.reset(new DriveWebContentsManager(
393 drive_hosted_app_id_
,
394 drive_offline_endpoint_url_
,
395 base::Bind(&DriveFirstRunController::OnOfflineInit
,
396 base::Unretained(this))));
397 web_contents_manager_
->StartLoad();
398 web_contents_timer_
.Start(
400 base::TimeDelta::FromSeconds(web_contents_timeout_secs_
),
402 &DriveFirstRunController::OnWebContentsTimedOut
);
405 void DriveFirstRunController::AddObserver(Observer
* observer
) {
406 observer_list_
.AddObserver(observer
);
409 void DriveFirstRunController::RemoveObserver(Observer
* observer
) {
410 observer_list_
.RemoveObserver(observer
);
413 void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs
,
416 initial_delay_secs_
= initial_delay_secs
;
417 web_contents_timeout_secs_
= timeout_secs
;
420 void DriveFirstRunController::SetAppInfoForTest(
421 const std::string
& app_id
,
422 const std::string
& endpoint_url
) {
424 drive_hosted_app_id_
= app_id
;
425 drive_offline_endpoint_url_
= endpoint_url
;
428 void DriveFirstRunController::OnWebContentsTimedOut() {
429 LOG(WARNING
) << "Timed out waiting for web contents.";
430 FOR_EACH_OBSERVER(Observer
, observer_list_
, OnTimedOut());
431 OnOfflineInit(false, OUTCOME_WEB_CONTENTS_TIMED_OUT
);
434 void DriveFirstRunController::CleanUp() {
435 if (web_contents_manager_
)
436 web_contents_manager_
->StopLoad();
437 web_contents_timer_
.Stop();
438 base::MessageLoop::current()->DeleteSoon(FROM_HERE
, this);
441 void DriveFirstRunController::OnOfflineInit(bool success
, UMAOutcome outcome
) {
442 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
445 UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
446 outcome
, OUTCOME_MAX
);
447 FOR_EACH_OBSERVER(Observer
, observer_list_
, OnCompletion(success
));
451 void DriveFirstRunController::ShowNotification() {
452 ExtensionService
* service
=
453 extensions::ExtensionSystem::Get(profile_
)->extension_service();
455 const extensions::Extension
* extension
=
456 service
->GetExtensionById(drive_hosted_app_id_
, false);
459 message_center::RichNotificationData data
;
460 data
.buttons
.push_back(message_center::ButtonInfo(
461 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_BUTTON
)));
462 ui::ResourceBundle
& resource_bundle
= ui::ResourceBundle::GetSharedInstance();
463 scoped_ptr
<message_center::Notification
> notification(
464 new message_center::Notification(
465 message_center::NOTIFICATION_TYPE_SIMPLE
,
466 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()),
471 message_center::NotifierId(message_center::NotifierId::APPLICATION
,
474 new DriveOfflineNotificationDelegate(profile_
)));
475 notification
->set_priority(message_center::LOW_PRIORITY
);
476 message_center::MessageCenter::Get()->AddNotification(notification
.Pass());
479 } // namespace chromeos