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/chromeos/login/user_manager.h"
18 #include "chrome/browser/extensions/extension_service.h"
19 #include "chrome/browser/extensions/extension_web_contents_observer.h"
20 #include "chrome/browser/tab_contents/background_contents.h"
21 #include "chrome/browser/ui/browser_navigator.h"
22 #include "chrome/browser/ui/host_desktop.h"
23 #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
24 #include "chrome/browser/ui/singleton_tabs.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "content/public/browser/navigation_controller.h"
27 #include "content/public/browser/notification_details.h"
28 #include "content/public/browser/notification_observer.h"
29 #include "content/public/browser/notification_registrar.h"
30 #include "content/public/browser/notification_source.h"
31 #include "content/public/browser/notification_types.h"
32 #include "content/public/browser/site_instance.h"
33 #include "content/public/browser/web_contents.h"
34 #include "content/public/browser/web_contents_observer.h"
35 #include "extensions/browser/extension_system.h"
36 #include "extensions/common/extension.h"
37 #include "grit/generated_resources.h"
38 #include "grit/theme_resources.h"
39 #include "ui/base/l10n/l10n_util.h"
40 #include "ui/base/resource/resource_bundle.h"
41 #include "ui/message_center/message_center.h"
42 #include "ui/message_center/notification.h"
43 #include "ui/message_center/notification_delegate.h"
50 // The initial time to wait in seconds before enabling offline mode.
51 int kInitialDelaySeconds = 180;
53 // Time to wait for Drive app background page to come up before giving up.
54 int kWebContentsTimeoutSeconds = 15;
56 // Google Drive enable offline endpoint.
57 const char kDriveOfflineEndpointUrl[] =
58 "https://docs.google.com/offline/autoenable";
60 // Google Drive app id.
61 const char kDriveHostedAppId[] = "apdfllckaahabafndbhieahigkjlhalf";
63 // Id of the notification shown when offline mode is enabled.
64 const char kDriveOfflineNotificationId[] = "chrome://drive/enable-offline";
66 // The URL of the support page opened when the notification button is clicked.
67 const char kDriveOfflineSupportUrl[] =
68 "https://support.google.com/drive/answer/1628467";
72 ////////////////////////////////////////////////////////////////////////////////
73 // DriveOfflineNotificationDelegate
75 // NotificationDelegate for the notification that is displayed when Drive
76 // offline mode is enabled automatically. Clicking on the notification button
77 // will open the Drive settings page.
78 class DriveOfflineNotificationDelegate
79 : public message_center::NotificationDelegate {
81 explicit DriveOfflineNotificationDelegate(Profile* profile)
82 : profile_(profile) {}
84 // message_center::NotificationDelegate overrides:
85 virtual void Display() OVERRIDE {}
86 virtual void Error() OVERRIDE {}
87 virtual void Close(bool by_user) OVERRIDE {}
88 virtual void Click() OVERRIDE {}
89 virtual void ButtonClick(int button_index) OVERRIDE;
92 virtual ~DriveOfflineNotificationDelegate() {}
97 DISALLOW_COPY_AND_ASSIGN(DriveOfflineNotificationDelegate);
100 void DriveOfflineNotificationDelegate::ButtonClick(int button_index) {
101 DCHECK_EQ(0, button_index);
103 // The support page will be localized based on the user's GAIA account.
104 const GURL url = GURL(kDriveOfflineSupportUrl);
106 chrome::ScopedTabbedBrowserDisplayer displayer(
108 chrome::HOST_DESKTOP_TYPE_ASH);
109 chrome::ShowSingletonTabOverwritingNTP(
111 chrome::GetSingletonTabNavigateParams(displayer.browser(), url));
114 ////////////////////////////////////////////////////////////////////////////////
115 // DriveWebContentsManager
117 // Manages web contents that initializes Google Drive offline mode. We create
118 // a background WebContents that loads a Drive endpoint to initialize offline
119 // mode. If successful, a background page will be opened to sync the user's
120 // files for offline use.
121 class DriveWebContentsManager : public content::WebContentsObserver,
122 public content::WebContentsDelegate,
123 public content::NotificationObserver {
125 typedef base::Callback<
126 void(bool, DriveFirstRunController::UMAOutcome)> CompletionCallback;
128 DriveWebContentsManager(Profile* profile,
129 const std::string& app_id,
130 const std::string& endpoint_url,
131 const CompletionCallback& completion_callback);
132 virtual ~DriveWebContentsManager();
134 // Start loading the WebContents for the endpoint in the context of the Drive
135 // hosted app that will initialize offline mode and open a background page.
138 // Stop loading the endpoint. The |completion_callback| will not be called.
142 // Called when when offline initialization succeeds or fails and schedules
143 // |RunCompletionCallback|.
144 void OnOfflineInit(bool success,
145 DriveFirstRunController::UMAOutcome outcome);
147 // Runs |completion_callback|.
148 void RunCompletionCallback(bool success,
149 DriveFirstRunController::UMAOutcome outcome);
151 // content::WebContentsObserver overrides:
152 virtual void DidFailProvisionalLoad(
154 const base::string16& frame_unique_name,
156 const GURL& validated_url,
158 const base::string16& error_description,
159 content::RenderViewHost* render_view_host) OVERRIDE;
161 virtual void DidFailLoad(int64 frame_id,
162 const GURL& validated_url,
165 const base::string16& error_description,
166 content::RenderViewHost* render_view_host) OVERRIDE;
168 // content::WebContentsDelegate overrides:
169 virtual bool ShouldCreateWebContents(
170 content::WebContents* web_contents,
172 WindowContainerType window_container_type,
173 const base::string16& frame_name,
174 const GURL& target_url,
175 const std::string& partition_id,
176 content::SessionStorageNamespace* session_storage_namespace) OVERRIDE;
178 // content::NotificationObserver overrides:
179 virtual void Observe(int type,
180 const content::NotificationSource& source,
181 const content::NotificationDetails& details) OVERRIDE;
184 const std::string app_id_;
185 const std::string endpoint_url_;
186 scoped_ptr<content::WebContents> web_contents_;
187 content::NotificationRegistrar registrar_;
189 CompletionCallback completion_callback_;
190 base::WeakPtrFactory<DriveWebContentsManager> weak_ptr_factory_;
192 DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager);
195 DriveWebContentsManager::DriveWebContentsManager(
197 const std::string& app_id,
198 const std::string& endpoint_url,
199 const CompletionCallback& completion_callback)
202 endpoint_url_(endpoint_url),
204 completion_callback_(completion_callback),
205 weak_ptr_factory_(this) {
206 DCHECK(!completion_callback_.is_null());
207 registrar_.Add(this, chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED,
208 content::Source<Profile>(profile_));
211 DriveWebContentsManager::~DriveWebContentsManager() {
214 void DriveWebContentsManager::StartLoad() {
216 const GURL url(endpoint_url_);
217 content::WebContents::CreateParams create_params(
218 profile_, content::SiteInstance::CreateForURL(profile_, url));
220 web_contents_.reset(content::WebContents::Create(create_params));
221 web_contents_->SetDelegate(this);
222 extensions::ExtensionWebContentsObserver::CreateForWebContents(
223 web_contents_.get());
225 content::NavigationController::LoadURLParams load_params(url);
226 load_params.transition_type = content::PAGE_TRANSITION_GENERATED;
227 web_contents_->GetController().LoadURLWithParams(load_params);
229 content::WebContentsObserver::Observe(web_contents_.get());
232 void DriveWebContentsManager::StopLoad() {
236 void DriveWebContentsManager::OnOfflineInit(
238 DriveFirstRunController::UMAOutcome outcome) {
240 // We postpone notifying the controller as we may be in the middle
241 // of a call stack for some routine of the contained WebContents.
242 base::MessageLoop::current()->PostTask(
244 base::Bind(&DriveWebContentsManager::RunCompletionCallback,
245 weak_ptr_factory_.GetWeakPtr(),
252 void DriveWebContentsManager::RunCompletionCallback(
254 DriveFirstRunController::UMAOutcome outcome) {
255 completion_callback_.Run(success, outcome);
258 void DriveWebContentsManager::DidFailProvisionalLoad(
260 const base::string16& frame_unique_name,
262 const GURL& validated_url,
264 const base::string16& error_description,
265 content::RenderViewHost* render_view_host) {
267 LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
269 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
273 void DriveWebContentsManager::DidFailLoad(
275 const GURL& validated_url,
278 const base::string16& error_description,
279 content::RenderViewHost* render_view_host) {
281 LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
283 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
287 bool DriveWebContentsManager::ShouldCreateWebContents(
288 content::WebContents* web_contents,
290 WindowContainerType window_container_type,
291 const base::string16& frame_name,
292 const GURL& target_url,
293 const std::string& partition_id,
294 content::SessionStorageNamespace* session_storage_namespace) {
296 if (window_container_type == WINDOW_CONTAINER_TYPE_NORMAL)
299 // Check that the target URL is for the Drive app.
300 ExtensionService* service =
301 extensions::ExtensionSystem::Get(profile_)->extension_service();
302 const extensions::Extension *extension =
303 service->GetInstalledApp(target_url);
304 if (!extension || extension->id() != app_id_)
307 // The background contents creation is normally done in Browser, but
308 // because we're using a detached WebContents, we need to do it ourselves.
309 BackgroundContentsService* background_contents_service =
310 BackgroundContentsServiceFactory::GetForProfile(profile_);
312 // Prevent redirection if background contents already exists.
313 if (background_contents_service->GetAppBackgroundContents(
314 base::UTF8ToUTF16(app_id_))) {
317 BackgroundContents* contents = background_contents_service
318 ->CreateBackgroundContents(content::SiteInstance::Create(profile_),
322 base::ASCIIToUTF16(app_id_),
324 session_storage_namespace);
326 contents->web_contents()->GetController().LoadURL(
329 content::PAGE_TRANSITION_LINK,
332 // Return false as we already created the WebContents here.
336 void DriveWebContentsManager::Observe(
338 const content::NotificationSource& source,
339 const content::NotificationDetails& details) {
340 if (type == chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED) {
341 const std::string app_id = base::UTF16ToUTF8(
342 content::Details<BackgroundContentsOpenedDetails>(details)
344 if (app_id == app_id_)
345 OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED);
349 ////////////////////////////////////////////////////////////////////////////////
350 // DriveFirstRunController
352 DriveFirstRunController::DriveFirstRunController(Profile* profile)
355 initial_delay_secs_(kInitialDelaySeconds),
356 web_contents_timeout_secs_(kWebContentsTimeoutSeconds),
357 drive_offline_endpoint_url_(kDriveOfflineEndpointUrl),
358 drive_hosted_app_id_(kDriveHostedAppId) {
361 DriveFirstRunController::~DriveFirstRunController() {
364 void DriveFirstRunController::EnableOfflineMode() {
367 initial_delay_timer_.Start(
369 base::TimeDelta::FromSeconds(initial_delay_secs_),
371 &DriveFirstRunController::EnableOfflineMode);
375 if (!UserManager::Get()->IsLoggedInAsRegularUser()) {
376 LOG(ERROR) << "Attempting to enable offline access "
377 "but not logged in a regular user.";
378 OnOfflineInit(false, OUTCOME_WRONG_USER_TYPE);
382 ExtensionService* extension_service =
383 extensions::ExtensionSystem::Get(profile_)->extension_service();
384 if (!extension_service->GetExtensionById(drive_hosted_app_id_, false)) {
385 LOG(WARNING) << "Drive app is not installed.";
386 OnOfflineInit(false, OUTCOME_APP_NOT_INSTALLED);
390 BackgroundContentsService* background_contents_service =
391 BackgroundContentsServiceFactory::GetForProfile(profile_);
392 if (background_contents_service->GetAppBackgroundContents(
393 base::UTF8ToUTF16(drive_hosted_app_id_))) {
394 LOG(WARNING) << "Background page for Drive app already exists";
395 OnOfflineInit(false, OUTCOME_BACKGROUND_PAGE_EXISTS);
399 web_contents_manager_.reset(new DriveWebContentsManager(
401 drive_hosted_app_id_,
402 drive_offline_endpoint_url_,
403 base::Bind(&DriveFirstRunController::OnOfflineInit,
404 base::Unretained(this))));
405 web_contents_manager_->StartLoad();
406 web_contents_timer_.Start(
408 base::TimeDelta::FromSeconds(web_contents_timeout_secs_),
410 &DriveFirstRunController::OnWebContentsTimedOut);
413 void DriveFirstRunController::AddObserver(Observer* observer) {
414 observer_list_.AddObserver(observer);
417 void DriveFirstRunController::RemoveObserver(Observer* observer) {
418 observer_list_.RemoveObserver(observer);
421 void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs,
424 initial_delay_secs_ = initial_delay_secs;
425 web_contents_timeout_secs_ = timeout_secs;
428 void DriveFirstRunController::SetAppInfoForTest(
429 const std::string& app_id,
430 const std::string& endpoint_url) {
432 drive_hosted_app_id_ = app_id;
433 drive_offline_endpoint_url_ = endpoint_url;
436 void DriveFirstRunController::OnWebContentsTimedOut() {
437 LOG(WARNING) << "Timed out waiting for web contents.";
438 FOR_EACH_OBSERVER(Observer, observer_list_, OnTimedOut());
439 OnOfflineInit(false, OUTCOME_WEB_CONTENTS_TIMED_OUT);
442 void DriveFirstRunController::CleanUp() {
443 if (web_contents_manager_)
444 web_contents_manager_->StopLoad();
445 web_contents_timer_.Stop();
446 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
449 void DriveFirstRunController::OnOfflineInit(bool success, UMAOutcome outcome) {
450 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
453 UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
454 outcome, OUTCOME_MAX);
455 FOR_EACH_OBSERVER(Observer, observer_list_, OnCompletion(success));
459 void DriveFirstRunController::ShowNotification() {
460 ExtensionService* service =
461 extensions::ExtensionSystem::Get(profile_)->extension_service();
463 const extensions::Extension* extension =
464 service->GetExtensionById(drive_hosted_app_id_, false);
467 message_center::RichNotificationData data;
468 data.buttons.push_back(message_center::ButtonInfo(
469 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_BUTTON)));
470 ui::ResourceBundle& resource_bundle = ui::ResourceBundle::GetSharedInstance();
471 scoped_ptr<message_center::Notification> notification(
472 new message_center::Notification(
473 message_center::NOTIFICATION_TYPE_SIMPLE,
474 kDriveOfflineNotificationId,
475 base::string16(), // title
476 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_MESSAGE),
477 resource_bundle.GetImageNamed(IDR_NOTIFICATION_DRIVE),
478 base::UTF8ToUTF16(extension->name()),
479 message_center::NotifierId(message_center::NotifierId::APPLICATION,
482 new DriveOfflineNotificationDelegate(profile_)));
483 notification->set_priority(message_center::LOW_PRIORITY);
484 message_center::MessageCenter::Get()->AddNotification(notification.Pass());
487 } // namespace chromeos