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 ButtonClick(int button_index) override;
91 virtual ~DriveOfflineNotificationDelegate() {}
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 virtual ~DriveWebContentsManager();
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 virtual void DidFailProvisionalLoad(
152 content::RenderFrameHost* render_frame_host,
153 const GURL& validated_url,
155 const base::string16& error_description) override;
157 virtual void DidFailLoad(content::RenderFrameHost* render_frame_host,
158 const GURL& validated_url,
160 const base::string16& error_description) override;
162 // content::WebContentsDelegate overrides:
163 virtual bool ShouldCreateWebContents(
164 content::WebContents* web_contents,
166 WindowContainerType window_container_type,
167 const base::string16& frame_name,
168 const GURL& target_url,
169 const std::string& partition_id,
170 content::SessionStorageNamespace* session_storage_namespace) override;
172 // content::NotificationObserver overrides:
173 virtual void Observe(int type,
174 const content::NotificationSource& source,
175 const content::NotificationDetails& details) override;
178 const std::string app_id_;
179 const std::string endpoint_url_;
180 scoped_ptr<content::WebContents> web_contents_;
181 content::NotificationRegistrar registrar_;
183 CompletionCallback completion_callback_;
184 base::WeakPtrFactory<DriveWebContentsManager> weak_ptr_factory_;
186 DISALLOW_COPY_AND_ASSIGN(DriveWebContentsManager);
189 DriveWebContentsManager::DriveWebContentsManager(
191 const std::string& app_id,
192 const std::string& endpoint_url,
193 const CompletionCallback& completion_callback)
196 endpoint_url_(endpoint_url),
198 completion_callback_(completion_callback),
199 weak_ptr_factory_(this) {
200 DCHECK(!completion_callback_.is_null());
201 registrar_.Add(this, chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED,
202 content::Source<Profile>(profile_));
205 DriveWebContentsManager::~DriveWebContentsManager() {
208 void DriveWebContentsManager::StartLoad() {
210 const GURL url(endpoint_url_);
211 content::WebContents::CreateParams create_params(
212 profile_, content::SiteInstance::CreateForURL(profile_, url));
214 web_contents_.reset(content::WebContents::Create(create_params));
215 web_contents_->SetDelegate(this);
216 extensions::ChromeExtensionWebContentsObserver::CreateForWebContents(
217 web_contents_.get());
219 content::NavigationController::LoadURLParams load_params(url);
220 load_params.transition_type = ui::PAGE_TRANSITION_GENERATED;
221 web_contents_->GetController().LoadURLWithParams(load_params);
223 content::WebContentsObserver::Observe(web_contents_.get());
226 void DriveWebContentsManager::StopLoad() {
230 void DriveWebContentsManager::OnOfflineInit(
232 DriveFirstRunController::UMAOutcome outcome) {
234 // We postpone notifying the controller as we may be in the middle
235 // of a call stack for some routine of the contained WebContents.
236 base::MessageLoop::current()->PostTask(
238 base::Bind(&DriveWebContentsManager::RunCompletionCallback,
239 weak_ptr_factory_.GetWeakPtr(),
246 void DriveWebContentsManager::RunCompletionCallback(
248 DriveFirstRunController::UMAOutcome outcome) {
249 completion_callback_.Run(success, outcome);
252 void DriveWebContentsManager::DidFailProvisionalLoad(
253 content::RenderFrameHost* render_frame_host,
254 const GURL& validated_url,
256 const base::string16& error_description) {
257 if (!render_frame_host->GetParent()) {
258 LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
260 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
264 void DriveWebContentsManager::DidFailLoad(
265 content::RenderFrameHost* render_frame_host,
266 const GURL& validated_url,
268 const base::string16& error_description) {
269 if (!render_frame_host->GetParent()) {
270 LOG(WARNING) << "Failed to load WebContents to enable offline mode.";
272 DriveFirstRunController::OUTCOME_WEB_CONTENTS_LOAD_FAILED);
276 bool DriveWebContentsManager::ShouldCreateWebContents(
277 content::WebContents* web_contents,
279 WindowContainerType window_container_type,
280 const base::string16& frame_name,
281 const GURL& target_url,
282 const std::string& partition_id,
283 content::SessionStorageNamespace* session_storage_namespace) {
285 if (window_container_type == WINDOW_CONTAINER_TYPE_NORMAL)
288 // Check that the target URL is for the Drive app.
289 const extensions::Extension* extension =
290 extensions::ExtensionRegistry::Get(profile_)
291 ->enabled_extensions().GetAppByURL(target_url);
292 if (!extension || extension->id() != app_id_)
295 // The background contents creation is normally done in Browser, but
296 // because we're using a detached WebContents, we need to do it ourselves.
297 BackgroundContentsService* background_contents_service =
298 BackgroundContentsServiceFactory::GetForProfile(profile_);
300 // Prevent redirection if background contents already exists.
301 if (background_contents_service->GetAppBackgroundContents(
302 base::UTF8ToUTF16(app_id_))) {
305 BackgroundContents* contents = background_contents_service
306 ->CreateBackgroundContents(content::SiteInstance::Create(profile_),
310 base::ASCIIToUTF16(app_id_),
312 session_storage_namespace);
314 contents->web_contents()->GetController().LoadURL(
317 ui::PAGE_TRANSITION_LINK,
320 // Return false as we already created the WebContents here.
324 void DriveWebContentsManager::Observe(
326 const content::NotificationSource& source,
327 const content::NotificationDetails& details) {
328 if (type == chrome::NOTIFICATION_BACKGROUND_CONTENTS_OPENED) {
329 const std::string app_id = base::UTF16ToUTF8(
330 content::Details<BackgroundContentsOpenedDetails>(details)
332 if (app_id == app_id_)
333 OnOfflineInit(true, DriveFirstRunController::OUTCOME_OFFLINE_ENABLED);
337 ////////////////////////////////////////////////////////////////////////////////
338 // DriveFirstRunController
340 DriveFirstRunController::DriveFirstRunController(Profile* profile)
343 initial_delay_secs_(kInitialDelaySeconds),
344 web_contents_timeout_secs_(kWebContentsTimeoutSeconds),
345 drive_offline_endpoint_url_(kDriveOfflineEndpointUrl),
346 drive_hosted_app_id_(kDriveHostedAppId) {
349 DriveFirstRunController::~DriveFirstRunController() {
352 void DriveFirstRunController::EnableOfflineMode() {
355 initial_delay_timer_.Start(
357 base::TimeDelta::FromSeconds(initial_delay_secs_),
359 &DriveFirstRunController::EnableOfflineMode);
363 if (!user_manager::UserManager::Get()->IsLoggedInAsRegularUser()) {
364 LOG(ERROR) << "Attempting to enable offline access "
365 "but not logged in a regular user.";
366 OnOfflineInit(false, OUTCOME_WRONG_USER_TYPE);
370 ExtensionService* extension_service =
371 extensions::ExtensionSystem::Get(profile_)->extension_service();
372 if (!extension_service->GetExtensionById(drive_hosted_app_id_, false)) {
373 LOG(WARNING) << "Drive app is not installed.";
374 OnOfflineInit(false, OUTCOME_APP_NOT_INSTALLED);
378 BackgroundContentsService* background_contents_service =
379 BackgroundContentsServiceFactory::GetForProfile(profile_);
380 if (background_contents_service->GetAppBackgroundContents(
381 base::UTF8ToUTF16(drive_hosted_app_id_))) {
382 LOG(WARNING) << "Background page for Drive app already exists";
383 OnOfflineInit(false, OUTCOME_BACKGROUND_PAGE_EXISTS);
387 web_contents_manager_.reset(new DriveWebContentsManager(
389 drive_hosted_app_id_,
390 drive_offline_endpoint_url_,
391 base::Bind(&DriveFirstRunController::OnOfflineInit,
392 base::Unretained(this))));
393 web_contents_manager_->StartLoad();
394 web_contents_timer_.Start(
396 base::TimeDelta::FromSeconds(web_contents_timeout_secs_),
398 &DriveFirstRunController::OnWebContentsTimedOut);
401 void DriveFirstRunController::AddObserver(Observer* observer) {
402 observer_list_.AddObserver(observer);
405 void DriveFirstRunController::RemoveObserver(Observer* observer) {
406 observer_list_.RemoveObserver(observer);
409 void DriveFirstRunController::SetDelaysForTest(int initial_delay_secs,
412 initial_delay_secs_ = initial_delay_secs;
413 web_contents_timeout_secs_ = timeout_secs;
416 void DriveFirstRunController::SetAppInfoForTest(
417 const std::string& app_id,
418 const std::string& endpoint_url) {
420 drive_hosted_app_id_ = app_id;
421 drive_offline_endpoint_url_ = endpoint_url;
424 void DriveFirstRunController::OnWebContentsTimedOut() {
425 LOG(WARNING) << "Timed out waiting for web contents.";
426 FOR_EACH_OBSERVER(Observer, observer_list_, OnTimedOut());
427 OnOfflineInit(false, OUTCOME_WEB_CONTENTS_TIMED_OUT);
430 void DriveFirstRunController::CleanUp() {
431 if (web_contents_manager_)
432 web_contents_manager_->StopLoad();
433 web_contents_timer_.Stop();
434 base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
437 void DriveFirstRunController::OnOfflineInit(bool success, UMAOutcome outcome) {
438 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
441 UMA_HISTOGRAM_ENUMERATION("DriveOffline.CrosAutoEnableOutcome",
442 outcome, OUTCOME_MAX);
443 FOR_EACH_OBSERVER(Observer, observer_list_, OnCompletion(success));
447 void DriveFirstRunController::ShowNotification() {
448 ExtensionService* service =
449 extensions::ExtensionSystem::Get(profile_)->extension_service();
451 const extensions::Extension* extension =
452 service->GetExtensionById(drive_hosted_app_id_, false);
455 message_center::RichNotificationData data;
456 data.buttons.push_back(message_center::ButtonInfo(
457 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_BUTTON)));
458 ui::ResourceBundle& resource_bundle = ui::ResourceBundle::GetSharedInstance();
459 scoped_ptr<message_center::Notification> notification(
460 new message_center::Notification(
461 message_center::NOTIFICATION_TYPE_SIMPLE,
462 kDriveOfflineNotificationId,
463 base::string16(), // title
464 l10n_util::GetStringUTF16(IDS_DRIVE_OFFLINE_NOTIFICATION_MESSAGE),
465 resource_bundle.GetImageNamed(IDR_NOTIFICATION_DRIVE),
466 base::UTF8ToUTF16(extension->name()),
467 message_center::NotifierId(message_center::NotifierId::APPLICATION,
470 new DriveOfflineNotificationDelegate(profile_)));
471 notification->set_priority(message_center::LOW_PRIORITY);
472 message_center::MessageCenter::Get()->AddNotification(notification.Pass());
475 } // namespace chromeos